1export type WorkspaceTab = "threads" | "pullRequests";
2export type WorkspaceSearchMode = "resting" | "active" | "expanded";
3
4export interface WorkspaceTabState {
5 readonly query: string;
6 readonly searchMode: WorkspaceSearchMode;
7 readonly scrollOffset: number;
8 readonly searchEntryOffset: number;
9}
10
11export interface WorkspaceTabsState {
12 readonly selectedTab: WorkspaceTab;
13 readonly selectionRevision: number;
14 readonly tabs: Readonly<Record<WorkspaceTab, WorkspaceTabState>>;
15}
16
17function emptyTab(): WorkspaceTabState {
18 return { query: "", searchMode: "resting", scrollOffset: 0, searchEntryOffset: 0 };
19}
20
21export function initialWorkspaceTabsState(): WorkspaceTabsState {
22 return {
23 selectedTab: "threads",
24 selectionRevision: 0,
25 tabs: { threads: emptyTab(), pullRequests: emptyTab() },
26 };
27}
28
29export type WorkspaceTabsAction =
30 | { readonly type: "select"; readonly tab: WorkspaceTab }
31 | { readonly type: "search"; readonly tab?: WorkspaceTab }
32 | { readonly type: "query"; readonly query: string; readonly tab?: WorkspaceTab }
33 | { readonly type: "expand"; readonly tab?: WorkspaceTab }
34 | { readonly type: "cancel"; readonly tab?: WorkspaceTab }
35 | { readonly type: "scroll"; readonly tab: WorkspaceTab; readonly offset: number };
36
37/** Each root retains its own search; leaving search restores the unfiltered list position. */
38export function workspaceTabsReducer(
39 state: WorkspaceTabsState,
40 action: WorkspaceTabsAction,
41): WorkspaceTabsState {
42 if (action.type === "select")
43 return action.tab === state.selectedTab
44 ? state
45 : {
46 ...state,
47 selectedTab: action.tab,
48 selectionRevision: state.selectionRevision + 1,
49 };
50 const tab = action.tab ?? state.selectedTab;
51 const current = state.tabs[tab];
52 let next: WorkspaceTabState;
53 switch (action.type) {
54 case "search":
55 next = {
56 ...current,
57 searchMode: "active",
58 searchEntryOffset:
59 current.searchMode === "resting" ? current.scrollOffset : current.searchEntryOffset,
60 };
61 break;
62 case "query":
63 next = { ...current, query: action.query };
64 break;
65 case "expand":
66 if (current.searchMode === "resting") return state;
67 next = { ...current, searchMode: "expanded" };
68 break;
69 case "cancel":
70 next = {
71 ...current,
72 query: "",
73 searchMode: "resting",
74 scrollOffset: current.searchEntryOffset,
75 };
76 break;
77 case "scroll":
78 if (current.scrollOffset === action.offset) return state;
79 next = { ...current, scrollOffset: action.offset };
80 break;
81 }
82 return { ...state, tabs: { ...state.tabs, [tab]: next } };
83}
84
85export const WORKSPACE_TOOLBAR = { height: 48, expandedGap: 10, edge: 16, gap: 8 };
86export const WORKSPACE_DOCK = { height: 48, tabsWidth: 176, gap: 12, bottomGap: 6 };
87
88export function workspaceToolbarHeight(mode: WorkspaceSearchMode): number {
89 return mode === "expanded"
90 ? WORKSPACE_TOOLBAR.height * 2 + WORKSPACE_TOOLBAR.expandedGap
91 : WORKSPACE_TOOLBAR.height;
92}