1import { RegistryContext, useAtomValue } from "@effect/atom-react";
2import { AsyncResult, Atom } from "effect/reactivity";
3import { createContext, use, useState, type Dispatch, type ReactNode } from "react";
4
5import { mobilePreferencesAtom } from "../../../state/preferences";
6import {
7 navigationCustomizationAvailable,
8 resolveHiddenNavigationTabs,
9 resolveSelectedNavigationTab,
10} from "./navigation-tab-order";
11import {
12 initialWorkspaceTabsState,
13 resolveWorkspaceTabOrder,
14 workspaceTabsReducer,
15 type WorkspaceSearchTab,
16 type WorkspaceTab,
17 type WorkspaceTabsAction,
18 type WorkspaceTabsState,
19} from "./workspace-tabs-state";
20
21export const WorkspaceTabsContext = createContext<{
22 readonly state: WorkspaceTabsState;
23 readonly dispatch: Dispatch<WorkspaceTabsAction>;
24 readonly isTabActive: (tab: WorkspaceTab) => boolean;
25 readonly bottomClearance: number;
26 readonly settingsOpen: boolean;
27 readonly getScrollOffset: (tab: WorkspaceSearchTab) => number;
28 readonly recordScrollOffset: (tab: WorkspaceSearchTab, offset: number) => void;
29 /** Leaves search and returns the list to where it was when search began. */
30 readonly cancelSearch: (tab: WorkspaceSearchTab) => void;
31} | null>(null);
32
33export function useWorkspaceTabs() {
34 return use(WorkspaceTabsContext);
35}
36
37type MobilePreferencesResult = Atom.Type<typeof mobilePreferencesAtom>;
38
39/** Moves the selection off a tab the user has hidden or that no longer exists. */
40function resolveWorkspaceTabsSelection(
41 state: WorkspaceTabsState,
42 preferences: MobilePreferencesResult,
43): WorkspaceTabsState {
44 const saved = AsyncResult.isSuccess(preferences) ? preferences.value : undefined;
45 const order = resolveWorkspaceTabOrder(saved?.navigationTabOrder);
46 const hidden = navigationCustomizationAvailable()
47 ? resolveHiddenNavigationTabs(saved?.hiddenNavigationTabs)
48 : [];
49 const selected = resolveSelectedNavigationTab(order, hidden, state.selectedTab);
50 return selected && selected !== state.selectedTab
51 ? workspaceTabsReducer(state, { type: "select", tab: selected })
52 : state;
53}
54
55// One model per provider. Handlers on frozen screens keep stale closures, so
56// they read the current selection from the registry at call time.
57function createWorkspaceTabsModel(selectedTab?: WorkspaceTab) {
58 const stored = Atom.make(initialWorkspaceTabsState(selectedTab));
59 const scrollOffsets: Record<WorkspaceSearchTab, number> = {
60 threads: 0,
61 pullRequests: 0,
62 usage: 0,
63 settings: 0,
64 };
65 return {
66 stored,
67 resolved: Atom.make((get) =>
68 resolveWorkspaceTabsSelection(get(stored), get(mobilePreferencesAtom)),
69 ),
70 getScrollOffset: (tab: WorkspaceSearchTab) => scrollOffsets[tab],
71 recordScrollOffset: (tab: WorkspaceSearchTab, offset: number) => {
72 scrollOffsets[tab] = offset;
73 },
74 };
75}
76
77export function WorkspaceTabsProvider(props: {
78 readonly children: ReactNode;
79 readonly settingsOpen?: boolean;
80 readonly initialTab?: WorkspaceTab;
81}) {
82 const registry = use(RegistryContext);
83 const [model] = useState(() => createWorkspaceTabsModel(props.initialTab));
84 const state = useAtomValue(model.resolved);
85 const dispatch = (action: WorkspaceTabsAction) =>
86 registry.set(model.stored, workspaceTabsReducer(registry.get(model.resolved), action));
87 const context = {
88 state,
89 dispatch,
90 isTabActive: (tab: WorkspaceTab) => registry.get(model.resolved).selectedTab === tab,
91 bottomClearance: 0,
92 settingsOpen: props.settingsOpen ?? false,
93 getScrollOffset: model.getScrollOffset,
94 recordScrollOffset: model.recordScrollOffset,
95 cancelSearch: (tab: WorkspaceSearchTab) => {
96 model.recordScrollOffset(tab, registry.get(model.resolved).tabs[tab].searchEntryOffset);
97 dispatch({ type: "cancel", tab });
98 },
99 };
100 return <WorkspaceTabsContext value={context}>{props.children}</WorkspaceTabsContext>;
101}