dsh-better-sidebar 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/README_EN.md +139 -0
  4. package/lib/client-docx.js +7649 -0
  5. package/lib/client-editor.js +32677 -0
  6. package/lib/client-pptx.js +63438 -0
  7. package/lib/client-registry.js +8021 -0
  8. package/lib/client-terminal.js +8060 -0
  9. package/lib/client-xlsx.js +359755 -0
  10. package/lib/client.js +8021 -0
  11. package/lib/index.js +2604 -0
  12. package/lib/invariant.js +22 -0
  13. package/lib/types/agent-pty.d.ts +220 -0
  14. package/lib/types/browser-probe.d.ts +15 -0
  15. package/lib/types/bundle-route.d.ts +13 -0
  16. package/lib/types/client/BrowserView.d.ts +22 -0
  17. package/lib/types/client/DiffTab.d.ts +6 -0
  18. package/lib/types/client/DiffView.d.ts +51 -0
  19. package/lib/types/client/EditorHost.d.ts +10 -0
  20. package/lib/types/client/ExplorerView.d.ts +9 -0
  21. package/lib/types/client/GitView.d.ts +8 -0
  22. package/lib/types/client/OrphanedTab.d.ts +11 -0
  23. package/lib/types/client/PdfView.d.ts +6 -0
  24. package/lib/types/client/PptxView.d.ts +6 -0
  25. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  26. package/lib/types/client/SideCardSection.d.ts +27 -0
  27. package/lib/types/client/Sidebar.d.ts +6 -0
  28. package/lib/types/client/SubagentView.d.ts +16 -0
  29. package/lib/types/client/TabBar.d.ts +38 -0
  30. package/lib/types/client/TerminalView.d.ts +8 -0
  31. package/lib/types/client/TextEditor.d.ts +10 -0
  32. package/lib/types/client/api.d.ts +170 -0
  33. package/lib/types/client/binary-download.d.ts +12 -0
  34. package/lib/types/client/breakpoints.d.ts +12 -0
  35. package/lib/types/client/browser.d.ts +50 -0
  36. package/lib/types/client/builtins/index.d.ts +17 -0
  37. package/lib/types/client/builtins/tabs.d.ts +6 -0
  38. package/lib/types/client/builtins/viewers.d.ts +3 -0
  39. package/lib/types/client/chunk-loader.d.ts +74 -0
  40. package/lib/types/client/chunks/docx.d.ts +8 -0
  41. package/lib/types/client/chunks/editor.d.ts +9 -0
  42. package/lib/types/client/chunks/pptx.d.ts +8 -0
  43. package/lib/types/client/chunks/terminal.d.ts +9 -0
  44. package/lib/types/client/chunks/xlsx.d.ts +9 -0
  45. package/lib/types/client/cm-themes.d.ts +24 -0
  46. package/lib/types/client/conversation-draft.d.ts +14 -0
  47. package/lib/types/client/docx-view.d.ts +8 -0
  48. package/lib/types/client/editor-load.d.ts +66 -0
  49. package/lib/types/client/icons.d.ts +47 -0
  50. package/lib/types/client/image-types.d.ts +3 -0
  51. package/lib/types/client/ime-guard.d.ts +36 -0
  52. package/lib/types/client/index.d.ts +10 -0
  53. package/lib/types/client/intercept.d.ts +20 -0
  54. package/lib/types/client/lang.d.ts +12 -0
  55. package/lib/types/client/lazy-chunk.d.ts +25 -0
  56. package/lib/types/client/link-intercept.d.ts +35 -0
  57. package/lib/types/client/locales.d.ts +237 -0
  58. package/lib/types/client/office-shared.d.ts +33 -0
  59. package/lib/types/client/office-types.d.ts +36 -0
  60. package/lib/types/client/office-view.d.ts +12 -0
  61. package/lib/types/client/openpath-intercept.d.ts +41 -0
  62. package/lib/types/client/paths.d.ts +19 -0
  63. package/lib/types/client/pdf-types.d.ts +2 -0
  64. package/lib/types/client/prefs.d.ts +30 -0
  65. package/lib/types/client/produced-files.d.ts +27 -0
  66. package/lib/types/client/selection-payload.d.ts +27 -0
  67. package/lib/types/client/service.d.ts +214 -0
  68. package/lib/types/client/split-pane.d.ts +29 -0
  69. package/lib/types/client/state.d.ts +322 -0
  70. package/lib/types/client/subagent-activity.d.ts +35 -0
  71. package/lib/types/client/subagent-detect.d.ts +47 -0
  72. package/lib/types/client/subagent-jobs.d.ts +63 -0
  73. package/lib/types/client/theme.d.ts +28 -0
  74. package/lib/types/client/xlsx-to-univer.d.ts +33 -0
  75. package/lib/types/client/xlsx-view.d.ts +10 -0
  76. package/lib/types/config.d.ts +41 -0
  77. package/lib/types/context-types.d.ts +431 -0
  78. package/lib/types/fs-tree.d.ts +41 -0
  79. package/lib/types/git.d.ts +75 -0
  80. package/lib/types/html-route.d.ts +48 -0
  81. package/lib/types/index.d.ts +39 -0
  82. package/lib/types/invariant.d.ts +15 -0
  83. package/lib/types/jobs-routes.d.ts +45 -0
  84. package/lib/types/prefs-shared.d.ts +106 -0
  85. package/lib/types/pty-manager.d.ts +76 -0
  86. package/lib/types/tools.d.ts +28 -0
  87. package/lib/types/trust-fence.d.ts +24 -0
  88. package/lib/types/wire.d.ts +38 -0
  89. package/package.json +168 -0
  90. package/src/agent-pty.ts +519 -0
  91. package/src/browser-probe.ts +26 -0
  92. package/src/bundle-route.ts +130 -0
  93. package/src/client/BrowserView.tsx +253 -0
  94. package/src/client/DiffTab.tsx +110 -0
  95. package/src/client/DiffView.tsx +266 -0
  96. package/src/client/EditorHost.tsx +101 -0
  97. package/src/client/ExplorerView.tsx +283 -0
  98. package/src/client/GitView.tsx +540 -0
  99. package/src/client/OrphanedTab.tsx +27 -0
  100. package/src/client/PdfView.tsx +110 -0
  101. package/src/client/PptxView.tsx +131 -0
  102. package/src/client/SandboxStatusBar.tsx +60 -0
  103. package/src/client/SideCardSection.module.css +271 -0
  104. package/src/client/SideCardSection.tsx +426 -0
  105. package/src/client/Sidebar.tsx +861 -0
  106. package/src/client/SubagentView.module.css +547 -0
  107. package/src/client/SubagentView.tsx +866 -0
  108. package/src/client/TabBar.tsx +188 -0
  109. package/src/client/TerminalView.tsx +233 -0
  110. package/src/client/TextEditor.tsx +382 -0
  111. package/src/client/api.ts +215 -0
  112. package/src/client/binary-download.tsx +23 -0
  113. package/src/client/breakpoints.ts +51 -0
  114. package/src/client/browser.ts +119 -0
  115. package/src/client/builtins/index.ts +33 -0
  116. package/src/client/builtins/tabs.tsx +211 -0
  117. package/src/client/builtins/viewers.tsx +151 -0
  118. package/src/client/chunk-loader.ts +196 -0
  119. package/src/client/chunks/docx.tsx +8 -0
  120. package/src/client/chunks/editor.tsx +9 -0
  121. package/src/client/chunks/pptx.tsx +8 -0
  122. package/src/client/chunks/terminal.tsx +9 -0
  123. package/src/client/chunks/xlsx.tsx +9 -0
  124. package/src/client/cm-themes.ts +129 -0
  125. package/src/client/conversation-draft.ts +29 -0
  126. package/src/client/css-modules.d.ts +5 -0
  127. package/src/client/docx-view.tsx +113 -0
  128. package/src/client/editor-load.ts +92 -0
  129. package/src/client/icons.tsx +137 -0
  130. package/src/client/image-types.ts +8 -0
  131. package/src/client/ime-guard.ts +47 -0
  132. package/src/client/index.tsx +238 -0
  133. package/src/client/intercept.tsx +94 -0
  134. package/src/client/lang.ts +95 -0
  135. package/src/client/layout.css +54 -0
  136. package/src/client/lazy-chunk.tsx +89 -0
  137. package/src/client/link-intercept.ts +66 -0
  138. package/src/client/locales.ts +476 -0
  139. package/src/client/office-shared.tsx +40 -0
  140. package/src/client/office-types.ts +48 -0
  141. package/src/client/office-view.tsx +12 -0
  142. package/src/client/openpath-intercept.ts +62 -0
  143. package/src/client/paths.ts +28 -0
  144. package/src/client/pdf-types.ts +4 -0
  145. package/src/client/prefs.ts +100 -0
  146. package/src/client/produced-files.ts +87 -0
  147. package/src/client/selection-payload.ts +86 -0
  148. package/src/client/service.ts +434 -0
  149. package/src/client/sidebar.module.css +1917 -0
  150. package/src/client/split-pane.tsx +292 -0
  151. package/src/client/state.ts +1048 -0
  152. package/src/client/subagent-activity.ts +67 -0
  153. package/src/client/subagent-detect.ts +125 -0
  154. package/src/client/subagent-jobs.ts +161 -0
  155. package/src/client/theme.ts +44 -0
  156. package/src/client/xlsx-to-univer.ts +260 -0
  157. package/src/client/xlsx-view.tsx +116 -0
  158. package/src/config.ts +95 -0
  159. package/src/context-types.ts +445 -0
  160. package/src/fs-tree.ts +115 -0
  161. package/src/git.ts +236 -0
  162. package/src/html-route.ts +70 -0
  163. package/src/index.ts +897 -0
  164. package/src/invariant.ts +32 -0
  165. package/src/jobs-routes.ts +257 -0
  166. package/src/prefs-shared.ts +127 -0
  167. package/src/pty-manager.ts +199 -0
  168. package/src/tools.ts +477 -0
  169. package/src/trust-fence.ts +77 -0
  170. package/src/wire.ts +94 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * The BetterSidebar client service: a registry that external plugins use
3
+ * to contribute sidebar tab types and file previewers. The service is
4
+ * published to the cordis context as `ctx.betterSidebar` (see
5
+ * {@link ../context-types.ts}); consumers declare it in `inject` and call
6
+ * `registerTab` / `registerFileViewer`, both returning a disposer that
7
+ * cordis auto-invokes on fiber disposal (HMR-safe).
8
+ *
9
+ * Design notes:
10
+ * - The registry is synchronous-snapshot (Map + listener set) so React
11
+ * can read it through `useSyncExternalStore` without tearing.
12
+ * - `dedupeKey` unifies the three open-tab strategies the builtins used to
13
+ * hardcode: single-instance (`() => type`), per-path (`tab => tab.path`),
14
+ * and per-id (`tab => tab.id` for diff tabs whose id is change-derived).
15
+ * `single: true` is sugar for `dedupeKey: () => id`.
16
+ * - `createTab` lets a descriptor own tab instantiation (the terminal
17
+ * builtin uses it to mint `terminal:<n>` ids and bump `nextTerminal`).
18
+ * - `matchFileViewer` walks descriptors in priority order (desc, stable):
19
+ * per descriptor it tries `detect` first (when `head` bytes are given),
20
+ * then `exts`; `exts: []` is a catch-all that matches any path.
21
+ */
22
+ import type { ReactNode } from 'react';
23
+ import type { Context } from '../context-types.ts';
24
+ import { type SidebarState, type SidebarStore, type SidebarTab } from './state.ts';
25
+ import type { SessionScope } from './api.ts';
26
+ /** One declarative boolean setting of a tab/viewer, rendered as a nested
27
+ * switch row in the Side card settings page (e.g. the Subagent page's
28
+ * "auto-open when a subagent appears"). */
29
+ export interface SidebarSettingToggle {
30
+ /** The SidebarPrefs field this toggle reads and writes ('autoOpenSubagent'). */
31
+ key: string;
32
+ /** Row title (i18n friendly: string or () => string). */
33
+ title: string | (() => string);
34
+ /** Row description (i18n friendly). */
35
+ desc?: string | (() => string);
36
+ }
37
+ /** Declarative settings of one registered tab or file viewer. */
38
+ export interface SidebarSettingsDeclaration {
39
+ /**
40
+ * Extra boolean toggles rendered under the feature's own row in the
41
+ * settings page (only while the feature is enabled). Keys must be fields
42
+ * of the host's PrefsSchema (built-ins: 'autoOpenSubagent',
43
+ * 'agentTerminalTools'); unknown keys are dropped by the settings seam.
44
+ */
45
+ toggles?: readonly SidebarSettingToggle[];
46
+ }
47
+ /** Props every tab component receives (builtins and external alike). */
48
+ export interface TabComponentProps {
49
+ ctx: Context;
50
+ store: SidebarStore;
51
+ scope: SessionScope;
52
+ tab: SidebarTab;
53
+ /** Whether this tab is the active one AND the panel is open (live views pause otherwise). */
54
+ visible: boolean;
55
+ /** The explorer's expanded directory set (ExplorerView). */
56
+ expanded?: string[];
57
+ onToggleDir?: (path: string) => void;
58
+ onReferenceFile?: (path: string) => void;
59
+ onOpenFile?: (path: string) => void;
60
+ onOpenDiff?: (tab: SidebarTab) => void;
61
+ onSubagentJump?: (childSessionId: string) => void;
62
+ }
63
+ /** Describes one kind of sidebar tab (builtins register themselves too). */
64
+ export interface TabDescriptor {
65
+ /** Unique id; also the `SidebarTab.type` value (`'explorer'`, `'my-plugin:db'`). */
66
+ id: string;
67
+ title: string | (() => string);
68
+ icon?: ReactNode | ((size: number) => ReactNode);
69
+ /** + menu sort order (ascending); default 100. */
70
+ order?: number;
71
+ /** Hide from the + menu (the editor tab is opened by file-open, not by the menu). */
72
+ hidden?: boolean;
73
+ /**
74
+ * + menu disabled predicate (e.g. terminal at capacity). Receives the
75
+ * session scope and the live sidebar state (counts, expansions).
76
+ */
77
+ available?: (ctx: Context, scope: SessionScope, state: SidebarState) => boolean;
78
+ /**
79
+ * Single-instance sugar: `true` is shorthand for `dedupeKey: () => id`
80
+ * (opening the tab focuses an existing one of the same type instead of
81
+ * creating a duplicate). An explicit `dedupeKey` always wins when both
82
+ * are given. Builtins: explorer/git/subagent use `single: true`.
83
+ */
84
+ single?: boolean;
85
+ /**
86
+ * If provided, opening a tab whose `dedupeKey(tab)` matches an existing
87
+ * tab's key focuses the existing one instead of creating a new one.
88
+ * Returning `undefined` means "no dedup — always open a new tab".
89
+ * Builtins: editor uses `tab => tab.path`; diff uses `tab => tab.id`
90
+ * (openDiffTab mints change-derived ids).
91
+ */
92
+ dedupeKey?: (tab: SidebarTab) => string | undefined;
93
+ /**
94
+ * Custom tab creation (minting the `SidebarTab` and any state patches).
95
+ * Return `null` to refuse creation. The terminal builtin uses this to
96
+ * mint `terminal:<n>` ids and bump `nextTerminal`.
97
+ * When omitted, a default `{ id, type, title }` tab is created.
98
+ */
99
+ createTab?: (state: SidebarState) => {
100
+ tab: SidebarTab;
101
+ patch?: Partial<SidebarState>;
102
+ } | null;
103
+ /**
104
+ * Declarative settings shown in the Side card settings page: every
105
+ * registered tab gets an enable/disable switch (icon + title + id), and
106
+ * `settings.toggles` adds nested switches tied to SidebarPrefs fields
107
+ * (e.g. the subagent tab's 'autoOpenSubagent').
108
+ */
109
+ settings?: SidebarSettingsDeclaration;
110
+ component: (props: TabComponentProps) => ReactNode;
111
+ }
112
+ /** How the host loads a file's bytes for one viewer. */
113
+ export type FileFetchStrategy = 'none' | 'fsRead' | 'mediaUrl' | 'custom' | 'binary-download';
114
+ /** Props every file viewer component receives. */
115
+ export interface FileViewerProps {
116
+ ctx: Context;
117
+ store: SidebarStore;
118
+ scope: SessionScope;
119
+ path: string;
120
+ title: string;
121
+ /** The matching descriptor's id (`'code'`, `'my-plugin:csv'`). */
122
+ viewerId: string;
123
+ /** fsRead text content (fetchStrategy='fsRead'). */
124
+ content?: string;
125
+ truncated?: boolean;
126
+ /** mediaUrl for the path (fetchStrategy='mediaUrl'). */
127
+ mediaUrl?: string;
128
+ /** custom load() return value (fetchStrategy='custom'). */
129
+ customData?: unknown;
130
+ }
131
+ /** Describes one file previewer (builtins register themselves too). */
132
+ export interface FileViewerDescriptor {
133
+ /** Unique id (`'image'`, `'pdf'`, `'my-plugin:csv'`). */
134
+ id: string;
135
+ /** Display name for the settings inventory (falls back to `id` when absent). */
136
+ title?: string | (() => string);
137
+ /** Icon shown in the settings inventory. */
138
+ icon?: ReactNode | ((size: number) => ReactNode);
139
+ /** Lowercase extensions without leading dot (`['png','jpg']`). `[]` = match any (catch-all). */
140
+ exts: readonly string[];
141
+ /** Higher wins; default 0. Builtins use 0; the catch-all `code` viewer uses -100. */
142
+ priority?: number;
143
+ fetchStrategy: FileFetchStrategy;
144
+ /**
145
+ * Content sniff: when `head` bytes are available the descriptor's `detect`
146
+ * is consulted before its `exts` (per-descriptor, in priority order).
147
+ */
148
+ detect?: (path: string, head: Uint8Array) => boolean;
149
+ /** fetchStrategy='custom' loader. */
150
+ load?: (path: string, scope: SessionScope) => Promise<unknown>;
151
+ /**
152
+ * Declarative settings shown in the Side card settings page: every
153
+ * registered viewer gets an enable/disable switch (icon + title + exts).
154
+ */
155
+ settings?: SidebarSettingsDeclaration;
156
+ component: (props: FileViewerProps) => ReactNode;
157
+ }
158
+ /** The registry service published as `ctx.betterSidebar`. */
159
+ export interface BetterSidebarService {
160
+ registerTab(descriptor: TabDescriptor): () => void;
161
+ registerFileViewer(descriptor: FileViewerDescriptor): () => void;
162
+ getTabs(): readonly TabDescriptor[];
163
+ getFileViewers(): readonly FileViewerDescriptor[];
164
+ /** Find a tab descriptor by id (undefined if not registered). */
165
+ getTab(id: string): TabDescriptor | undefined;
166
+ /**
167
+ * Whether a tab type is enabled in the side card prefs. An absent
168
+ * `tabsEnabled[id]` entry means enabled — only an explicit `false`
169
+ * disables the type (hidden from the + menu, `openTab` refuses, and
170
+ * derived flows gate on it).
171
+ */
172
+ isTabEnabled(id: string): boolean;
173
+ /** Whether a file viewer is enabled (absent `viewersEnabled[id]` = enabled). */
174
+ isViewerEnabled(id: string): boolean;
175
+ /**
176
+ * Find a file viewer for a path (priority desc; detect first, then exts).
177
+ * Disabled viewers are skipped, so files fall through to the next match.
178
+ */
179
+ matchFileViewer(path: string, head?: Uint8Array): FileViewerDescriptor | undefined;
180
+ /**
181
+ * Open a tab (used by external tabs and the + menu). `title` overrides
182
+ * the descriptor's title when given (the editor tab shows the file name);
183
+ * when the descriptor provides `createTab` it mints the tab itself and
184
+ * `title`/`path`/`id` are ignored. `url` lands the tab with its `path`
185
+ * pre-set to the URL (the browser tab's navigation seed; the caller
186
+ * usually pairs it with a hostname `title`). A disabled tab type is a
187
+ * no-op.
188
+ *
189
+ * A CONTENT open (a `path` or `url` seed) must land in sight: when the
190
+ * panel hosting the landing pane is collapsed, it is expanded
191
+ * automatically (the right panel by default, the bottom panel when the
192
+ * active pane lives there; on narrow viewports the merged drawer opens).
193
+ * Type-only opens (the + menu, agent-terminal auto-tabs) never expand —
194
+ * the panel behavior is their caller's business.
195
+ */
196
+ openTab(seed: {
197
+ type: string;
198
+ title?: string;
199
+ path?: string;
200
+ diff?: SidebarTab['diff'];
201
+ id?: string;
202
+ url?: string;
203
+ }): void;
204
+ /** Close a tab by id. */
205
+ closeTab(tabId: string): void;
206
+ /** Subscribe to registry changes (register/dispose). */
207
+ subscribe(listener: () => void): () => void;
208
+ }
209
+ /**
210
+ * Create one BetterSidebar service bound to a store. The service owns the
211
+ * tab/viewer registries (Map + listener set) and proxies openTab/closeTab
212
+ * to the store's reducer. One instance per client plugin activation.
213
+ */
214
+ export declare function createBetterSidebarService(store: SidebarStore): BetterSidebarService;
@@ -0,0 +1,29 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { SidebarState, SidebarTab, SplitNode } from './state.ts';
3
+ import type { DropZone } from './state.ts';
4
+ import { type NewTabOption, type TabDragPayload } from './TabBar.tsx';
5
+ /** Actions the workbench needs (bound to the store by the sidebar shell). */
6
+ export interface WorkbenchActions {
7
+ closeTab: (paneId: string, tabId: string) => void;
8
+ activateTab: (paneId: string, tabId: string) => void;
9
+ /** Make a pane the target of newly opened tabs (click focus). */
10
+ focusPane: (paneId: string) => void;
11
+ /** VSCode drag gesture: edge → split the target pane, center → merge. */
12
+ moveTabToEdge: (payload: TabDragPayload, toPane: string, zone: DropZone) => void;
13
+ /** Reorder within a pane (drop onto another tab inserts before it). */
14
+ moveTabBefore: (payload: TabDragPayload, toPane: string, beforeTabId: string) => void;
15
+ resizeSplit: (splitId: string, index: number, deltaFrac: number) => void;
16
+ }
17
+ /** The workbench: the split tree filling the sidebar body. `tree` selects
18
+ * which tree renders (the right panel's by default; the bottom panel passes
19
+ * `state.bottomSplits` — the actions route by pane id, so one action set
20
+ * serves both). */
21
+ export declare function Workbench(props: {
22
+ state: SidebarState;
23
+ tree?: SplitNode;
24
+ newTabOptions: NewTabOption[];
25
+ actions: WorkbenchActions;
26
+ onNewTab: (optionId: string) => void;
27
+ renderTab: (tab: SidebarTab, active: boolean, paneId: string) => ReactNode;
28
+ getTabIcon?: (tab: SidebarTab) => ReactNode;
29
+ }): import("react").JSX.Element;
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Per-session sidebar state: the panel geometry, the split-pane workbench
3
+ * tree, open tabs, and the explorer expansion set. One state instance per
4
+ * conversation id, persisted to localStorage under `dsh-sidebar:v1:<id>` so
5
+ * a reload restores the exact layout of the session it belongs to — switching
6
+ * conversations swaps the whole state (memory + isolation).
7
+ *
8
+ * The split tree is a recursive structure: a leaf holds a tab group, a split
9
+ * divides the space row- or column-wise with fractional sizes. All tree
10
+ * operations are pure functions over the node, unit-tested in tests/state.spec.ts.
11
+ */
12
+ import { type SidebarPrefs } from '../prefs-shared.ts';
13
+ /**
14
+ * Tab type identifier. Builtins register their ids (explorer / git / editor
15
+ * / terminal / subagent / diff) through the sidebar service; external
16
+ * plugins register their own (e.g. `'my-plugin:db'`). Kept as `string` so
17
+ * the registry stays open.
18
+ */
19
+ export type TabType = string;
20
+ /** What a diff tab shows: a worktree/index change of one path, or one commit's full patch. */
21
+ export type SidebarDiffRef = {
22
+ kind: 'worktree';
23
+ path: string;
24
+ staged: boolean;
25
+ untracked?: boolean;
26
+ } | {
27
+ kind: 'commit';
28
+ hash: string;
29
+ hashFull: string;
30
+ subject: string;
31
+ };
32
+ /** One open tab. `path` carries the file (editor) or is absent (explorer/git);
33
+ * `diff` carries the change a diff tab shows. */
34
+ export interface SidebarTab {
35
+ id: string;
36
+ type: TabType;
37
+ title: string;
38
+ path?: string;
39
+ diff?: SidebarDiffRef;
40
+ }
41
+ /** A tab group. */
42
+ export interface SidebarLeaf {
43
+ kind: 'leaf';
44
+ id: string;
45
+ tabs: SidebarTab[];
46
+ active: string | null;
47
+ }
48
+ /** A recursive split between child panes (fractional sizes summing to 1). */
49
+ export interface SidebarSplit {
50
+ kind: 'split';
51
+ id: string;
52
+ dir: 'row' | 'col';
53
+ sizes: number[];
54
+ children: SplitNode[];
55
+ }
56
+ export type SplitNode = SidebarLeaf | SidebarSplit;
57
+ /** The full per-session state. */
58
+ export interface SidebarState {
59
+ panelOpen: boolean;
60
+ width: number;
61
+ /** The pane receiving newly opened tabs (last pane the user touched).
62
+ * Pane ids are globally unique across BOTH trees (shared uid counter), so
63
+ * one field resolves into either tree — see {@link treeOf}. */
64
+ activePane: string | null;
65
+ /** Monotonic terminal tab counter (ids survive reloads). */
66
+ nextTerminal: number;
67
+ /** Monotonic browser tab counter (ids survive reloads; mirrors nextTerminal). */
68
+ nextBrowser: number;
69
+ /** Explorer expansion set (absolute directory paths). */
70
+ expanded: string[];
71
+ /** The right sidebar's split tree (the original workbench). */
72
+ splits: SplitNode;
73
+ /** Whether the bottom panel (a second, independent workbench) is open. */
74
+ bottomOpen: boolean;
75
+ /** The bottom panel's height (clamped to the contract range). */
76
+ bottomHeight: number;
77
+ /**
78
+ * Whether the bottom panel has been expanded at least once in this
79
+ * session — the FIRST expansion tries to auto-open a terminal tab (gated
80
+ * on the bottomPanelAutoTerminal pref); later expansions never do.
81
+ */
82
+ bottomOpenedOnce: boolean;
83
+ /** The bottom panel's own split tree (panes/tabs live only in ONE tree;
84
+ * tabs never cross panels — the two panels only share panel-size drags). */
85
+ bottomSplits: SplitNode;
86
+ }
87
+ export declare const PANEL_MIN = 280;
88
+ export declare const PANEL_MAX = 640;
89
+ export declare const PANEL_DEFAULT = 400;
90
+ export declare const TAB_MAX_WIDTH = 160;
91
+ /** Bottom panel geometry contract (mirrors the width contract; the upper
92
+ * bound is the viewport, enforced by {@link setBottomHeight}). */
93
+ export declare const BOTTOM_MIN = 120;
94
+ export declare const BOTTOM_DEFAULT = 220;
95
+ /** A fresh default state: one explorer tab in one pane, open per the caller's
96
+ * preference. `width` is the caller's preferred panel width (default
97
+ * PANEL_DEFAULT) and `panelOpen` whether the panel starts expanded (default
98
+ * true); the store seeds new sessions from the user's side card prefs.
99
+ * `seedExplorer` places the default explorer tab — the store passes false
100
+ * when the user disabled the explorer tab type in settings, so a fresh
101
+ * session starts with an empty pane instead of a tab they turned off. */
102
+ export declare function makeDefaultState(width?: number, panelOpen?: boolean, seedExplorer?: boolean): SidebarState;
103
+ /** Which tree owns a pane/split id: 'bottomSplits' when the id lives in the
104
+ * bottom panel's tree, else 'splits' (the right panel's tree). Ids are
105
+ * globally unique (the shared uid counter), so an id in neither tree falls
106
+ * back to the right tree, where tree operations no-op on a missing node —
107
+ * the pre-bottom-panel behavior. */
108
+ export declare function treeOf(state: SidebarState, id: string): 'splits' | 'bottomSplits';
109
+ /** Walk the tree and apply `visit` to the leaf with the given id. */
110
+ export declare function mapLeaf(node: SplitNode, paneId: string, visit: (leaf: SidebarLeaf) => void): SplitNode;
111
+ /** The first leaf of the tree (fallback pane when activePane is gone). */
112
+ export declare function firstLeaf(node: SplitNode): SidebarLeaf;
113
+ /**
114
+ * Narrow-viewport migration: the bottom panel's tabs are thrown INTO the
115
+ * right sidebar — the "merged display" on mobile is the right panel alone,
116
+ * whose tab strips now carry the bottom tree's tabs (depth-first order,
117
+ * appended to the right tree's FIRST leaf). The bottom tree is emptied (its
118
+ * structure stays — the desktop bottom panel re-renders its welcome cards)
119
+ * and the panel closes. The active pane moves to the right tree's first
120
+ * leaf so every new tab lands in the visible panel.
121
+ *
122
+ * Idempotent: a bottom tree with no tabs and a closed panel returns the
123
+ * same reference. Runs when the viewport enters narrow (see the Sidebar
124
+ * shell); migrating is permanent for the session — the tabs now live in the
125
+ * right tree, exactly like the user "threw them in".
126
+ */
127
+ export declare function migrateBottomTabs(state: SidebarState): SidebarState;
128
+ /** Find the leaf containing a tab id, if any. */
129
+ export declare function leafWithTab(node: SplitNode, tabId: string): SidebarLeaf | undefined;
130
+ /** All leaves of the tree, depth-first. */
131
+ export declare function allLeaves(node: SplitNode): SidebarLeaf[];
132
+ /** Whether a tab exists anywhere in a state (either tree, any pane). */
133
+ export declare function tabOpenIn(state: SidebarState, tabId: string): boolean;
134
+ /** Replace a leaf with a split of it plus a fresh empty leaf. */
135
+ export declare function splitLeafAt(node: SplitNode, paneId: string, dir: 'row' | 'col'): SplitNode;
136
+ /**
137
+ * Split a leaf by inserting a fresh leaf holding `tab` beside it — the
138
+ * VSCode drag-to-edge gesture. `dir` is the split direction ('row' for
139
+ * left/right, 'col' for up/down); `front` places the new leaf first (left/
140
+ * up) or second (right/down).
141
+ * @returns the new tree plus the fresh leaf's id (the drop's active pane).
142
+ */
143
+ export declare function insertLeafAt(node: SplitNode, paneId: string, dir: 'row' | 'col', tab: SidebarTab, front: boolean): {
144
+ node: SplitNode;
145
+ leafId: string;
146
+ };
147
+ /** Where a tab drop lands on a pane: an edge creates a split, center merges. */
148
+ export type DropZone = 'left' | 'right' | 'up' | 'down' | 'center';
149
+ /**
150
+ * The VSCode drag gesture: move a tab out of its pane and either merge it
151
+ * into the target pane (center) or split the target pane with the tab in a
152
+ * fresh leaf (edge). The source pane collapses when it empties.
153
+ *
154
+ * The panes may live in DIFFERENT trees (dragging a tab between the two
155
+ * panels): the tab then leaves its own tree and lands in the other one.
156
+ */
157
+ export declare function moveTabToEdge(state: SidebarState, fromPane: string, tabId: string, toPane: string, zone: DropZone): SidebarState;
158
+ /**
159
+ * Remove a leaf from the tree. A split left with one child promotes that
160
+ * child; removing the last leaf yields an empty leaf.
161
+ */
162
+ export declare function removeLeafAt(node: SplitNode, paneId: string): SplitNode;
163
+ /** Close a tab; an emptied leaf is removed (unless it is the only pane). */
164
+ export declare function closeTab(state: SidebarState, paneId: string, tabId: string): SidebarState;
165
+ /** Activate a tab in its pane (the pane's own tree). */
166
+ export declare function activateTab(state: SidebarState, paneId: string, tabId: string): SidebarState;
167
+ /** Update the display fields of one open tab (title / path) without
168
+ * re-opening it. The browser tab persists its current URL and hostname
169
+ * title through this reducer so a reload restores the visited page. A
170
+ * missing tab id is a no-op. The tab may live in either tree. */
171
+ export declare function patchTab(state: SidebarState, tabId: string, patch: {
172
+ title?: string;
173
+ path?: string;
174
+ }): SidebarState;
175
+ /**
176
+ * Land a tab in the active pane (or focus its existing instance by id).
177
+ * Dedup strategies (single-instance, per-path, per-change) are owned by the
178
+ * tab descriptor through {@link BetterSidebarService.openTab} / `dedupeKey`;
179
+ * this reducer only handles the id-based safety net (reconcile and
180
+ * openDiffTab already check existence before calling) and the landing
181
+ * itself — the service's dedupe path delegates here after its dedupeKey
182
+ * check misses.
183
+ *
184
+ * The active pane may live in EITHER tree (pane ids are globally unique):
185
+ * a stale id that survives in neither tree falls back to the right tree's
186
+ * first pane instead of swallowing the open.
187
+ */
188
+ export declare function openTabInActivePane(state: SidebarState, tab: SidebarTab): SidebarState;
189
+ /** Move a tab from one pane to another (insert at index; -1 appends).
190
+ * The panes may live in DIFFERENT trees — dragging a tab between the two
191
+ * panels removes it from its own tree and lands it in the other one. */
192
+ export declare function moveTab(state: SidebarState, fromPane: string, tabId: string, toPane: string, index?: number): SidebarState;
193
+ /** Split the active pane (or the pane containing the active tab). */
194
+ export declare function splitPane(state: SidebarState, dir: 'row' | 'col'): SidebarState;
195
+ /**
196
+ * Open a diff tab the VSCode way: an existing instance of the same change is
197
+ * focused wherever it lives; otherwise the tab joins the first pane that
198
+ * already holds diff tabs (diff panes are sticky — repeated clicks stack
199
+ * there); on the FIRST diff of a layout the source pane splits vertically so
200
+ * the diff lands in a fresh pane below it ("默认在下半栏新增一个").
201
+ *
202
+ * This is split-tree placement surgery, not registry dispatch: the diff tab
203
+ * descriptor's `dedupeKey` is `(tab) => tab.id`, and the existing-instance
204
+ * check below is exactly that rule — the two agree by construction (asserted
205
+ * in tests). Diff tabs minted by the Git view carry change-derived ids, so
206
+ * the id check is the per-change dedupe.
207
+ * @returns the new state, with the diff pane active.
208
+ */
209
+ export declare function openDiffTab(state: SidebarState, sourcePaneId: string, tab: SidebarTab): SidebarState;
210
+ /** Toggle the panel open/closed (opening restores the previous layout). */
211
+ export declare function togglePanel(state: SidebarState): SidebarState;
212
+ /** Toggle the bottom panel open/closed (independent of the right panel). */
213
+ export declare function toggleBottomPanel(state: SidebarState): SidebarState;
214
+ /** Set the panel width (clamped to the contract range; the upper bound is
215
+ * the viewport so the fullscreen expansion can fill the window). */
216
+ export declare function setWidth(state: SidebarState, width: number): SidebarState;
217
+ /** Set the bottom panel height (clamped to the contract range). The upper
218
+ * bound leaves the center column (the agent output area) at least PANEL_MIN
219
+ * tall — without the cap the bottom panel could swallow the whole viewport
220
+ * and squeeze the conversation to zero height. */
221
+ export declare function setBottomHeight(state: SidebarState, height: number): SidebarState;
222
+ /** Toggle a directory in the explorer expansion set. */
223
+ export declare function toggleExpanded(state: SidebarState, path: string): SidebarState;
224
+ /** Adjust one split divider: `i` is the left/top child index, delta in fractions. */
225
+ export declare function resizeSplit(node: SplitNode, splitId: string, index: number, delta: number): SplitNode;
226
+ /** State-level {@link resizeSplit} route: the divider may live in either
227
+ * tree (split ids are globally unique). */
228
+ export declare function resizeSplitIn(state: SidebarState, splitId: string, index: number, delta: number): SidebarState;
229
+ /** Prefix marking a tab id as an agent-owned terminal (suffix is the uuid). */
230
+ export declare const AGENT_TAB_PREFIX = "agent:";
231
+ /** Whether a tab id refers to an agent-owned terminal. */
232
+ export declare function isAgentTabId(tabId: string): boolean;
233
+ /** Extract the agent terminal uuid from an `agent:<uuid>` tab id. */
234
+ export declare function agentUuidOf(tabId: string): string;
235
+ /** Build the sidebar tab id for one agent terminal uuid. */
236
+ export declare function agentTabId(uuid: string): string;
237
+ /**
238
+ * Reconcile the sidebar's agent-terminal tabs with the host's live list.
239
+ * The host pushes the current list of agent terminals (created by the model
240
+ * through the `terminal_create` tool) over a dedicated WebSocket; this
241
+ * reducer mirrors that list into tabs: new uuids get a tab, vanished uuids
242
+ * lose theirs. The agent owns the lifetime — the user closing a tab sends a
243
+ * WS close frame that kills the pty, which fires a change, which converges
244
+ * the view. Idempotent: a no-op when the lists already match.
245
+ * @param state - the current per-session sidebar state.
246
+ * @param agentTerminals - the live agent terminal snapshots from the host.
247
+ * @returns the next state (or the same reference if no change was needed).
248
+ */
249
+ export declare function reconcileAgentTerminals(state: SidebarState, agentTerminals: ReadonlyArray<{
250
+ uuid: string;
251
+ title: string;
252
+ }>): SidebarState;
253
+ /** Immutable snapshot handed to React (replaced only on real changes). */
254
+ export interface SidebarSnapshot {
255
+ sessionId: string | undefined;
256
+ state: SidebarState | undefined;
257
+ /**
258
+ * The current side card prefs. Carried IN the snapshot (not a separate
259
+ * subscription) so prefs changes re-render the consumers that gate on
260
+ * them — the + menu hides a tab type the moment its switch flips.
261
+ */
262
+ prefs: SidebarPrefs;
263
+ }
264
+ /** Default panel width for one viewport: the prefs percent of the window,
265
+ * clamped to the panel floor (a tiny percent must stay usable) and to the
266
+ * viewport (a large one must never cover the whole window). */
267
+ export declare function defaultWidthFor(viewport: number, percent: number): number;
268
+ /**
269
+ * Structural validation of one persisted state. A malformed or stale shape
270
+ * (older layouts, hand-edited storage) must fall back to the default instead
271
+ * of crashing the panel on every reload; the restored width is also clamped
272
+ * to the current viewport so a stale fullscreen width can never crush the
273
+ * app shell (margin-right larger than the window) or cover the whole screen.
274
+ * @returns a clean state, or undefined to fall back to the default.
275
+ */
276
+ export declare function sanitizeState(parsed: unknown): SidebarState | undefined;
277
+ /** The session-scoped store: one state per conversation, localStorage-backed. */
278
+ export declare class SidebarStore {
279
+ private readonly bySession;
280
+ private snapshot;
281
+ private readonly listeners;
282
+ private persistTimer;
283
+ /** User-facing side card prefs seeding brand-new session states (defaults until the settings RPC resolves). */
284
+ private prefs;
285
+ /**
286
+ * Replace the side card prefs (the settings RPC result / settings page
287
+ * write). Notifies like any store change: the snapshot carries the prefs,
288
+ * so consumers that gate on enable switches (the + menu, derived flows)
289
+ * re-render with the new values immediately.
290
+ */
291
+ setPrefs(prefs: SidebarPrefs): void;
292
+ /** The current side card prefs (seeds new sessions; persisted states win). */
293
+ getPrefs(): SidebarPrefs;
294
+ /** Select a session (or none); loads its persisted state. */
295
+ setSession(sessionId: string | undefined): void;
296
+ subscribe(listener: () => void): () => void;
297
+ getSnapshot(): SidebarSnapshot;
298
+ /** Mutate the current session's state (no-op without a session). */
299
+ update(mutator: (draft: SidebarState) => void): void;
300
+ /**
301
+ * Whether a tab still exists in its session's state. Views use this on
302
+ * unmount to tell "the tab was closed" (release the terminal now) from
303
+ * "the tree re-rendered / the conversation switched" (the tab is still
304
+ * open — keep the terminal alive through the host's reconnect grace).
305
+ * Checks the session's own map entry (the current snapshot may already
306
+ * point at another session when a conversation switch unmounts the old
307
+ * one's tabs).
308
+ */
309
+ tabOpen(sessionId: string, tabId: string): boolean;
310
+ /** Apply a pure reducer (returns the next state). */
311
+ reduce(reducer: (state: SidebarState) => SidebarState): void;
312
+ private schedulePersist;
313
+ private notify;
314
+ }
315
+ /**
316
+ * Create one sidebar store instance. Production code calls this only from
317
+ * the client plugin's `apply` (the instance is handed to components as a
318
+ * prop); tests call it directly. No module-level singleton: the store's
319
+ * lifetime belongs to the plugin activation, exactly like the official
320
+ * `createXXXStore()` factory rule.
321
+ */
322
+ export declare function createSidebarStore(): SidebarStore;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Pure derivation of the compact LIVE line shown on a running subagent card:
3
+ * the last text output and the last tool call of the child's history tail
4
+ * (`subagent.history` events, oldest → newest). Renders nothing itself — the
5
+ * SubagentLiveLine component turns this into the card's status lines.
6
+ * Kept framework-free so the parser is unit-testable in the node environment.
7
+ */
8
+ import type { SidebarHistoryEntry } from '../context-types.ts';
9
+ /**
10
+ * Extract the concatenated plain text of a content-block list (the durable
11
+ * `ContentBlock[]` shape, structurally: blocks with `type: 'text'` carry
12
+ * `text`; anything else — tool_use, image, … — contributes nothing).
13
+ * @param content - the raw `content` field of a message event.
14
+ * @returns the joined text, or undefined when the message carries no text.
15
+ */
16
+ export declare function contentText(content: unknown): string | undefined;
17
+ /** The live status of one subagent card (both fields optional). */
18
+ export interface LastActivity {
19
+ /** The latest assembled assistant text output in the tail. */
20
+ text?: string;
21
+ /** The latest tool call in the tail. */
22
+ tool?: {
23
+ name: string;
24
+ args: string;
25
+ };
26
+ }
27
+ /**
28
+ * Fold a history tail into the last text output + last tool call (each is
29
+ * the LAST occurrence in event order). Lifecycle events and raw
30
+ * `assistant/chunk` rows are ignored — the card shows what the subagent is
31
+ * doing right now, not its plumbing.
32
+ * @param entries - the tail page from `subagent.history` (oldest → newest).
33
+ * @returns the last text and/or tool call; an empty object when the tail has neither.
34
+ */
35
+ export declare function lastActivity(entries: SidebarHistoryEntry[]): LastActivity;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Pure subagent-membership helpers over the sessions list feed (structural
3
+ * mirror world — no runtime imports). Used by the sidebar's auto-activation
4
+ * effect and the Subagent page:
5
+ *
6
+ * - {@link directSubagentCount}: direct durable children of one session,
7
+ * - {@link detectNewDirectSubagent}: the 0 → N transition that means "a new
8
+ * subagent just spawned under the current session" (the auto-open trigger),
9
+ * - {@link countSubagentDescendants}: uninterrupted subagent-origin lineage
10
+ * totals (mirror of the official `indexSubagentDescendants` over the
11
+ * plugin's own summary rows).
12
+ */
13
+ import type { SidebarSessionList, SidebarSubagentCatalog } from '../context-types.ts';
14
+ /** Count the direct subagent children of one session (durable `origin` rows). */
15
+ export declare function directSubagentCount(byId: SidebarSessionList['byId'], sessionId: string): number;
16
+ /**
17
+ * The main agent of the current session's tree: walk the durable parent
18
+ * chain upward until the first non-subagent session. The Subagent page shows
19
+ * THIS root's full topology regardless of how deep the current selection is
20
+ * (a session whose row is still hydrating, or a broken chain, degrades to
21
+ * the session itself).
22
+ */
23
+ export declare function rootAncestor(byId: SidebarSessionList['byId'], sessionId: string | undefined): string | undefined;
24
+ /**
25
+ * Collect every catalog branch (an entry with `hasChildren`) reachable from
26
+ * the root — the set of catalogs the always-expanded topology consumes.
27
+ * Cycles fail soft.
28
+ */
29
+ export declare function collectBranchIds(catalogs: Readonly<Record<string, SidebarSubagentCatalog>>, rootId: string | undefined): string[];
30
+ /**
31
+ * Whether a new direct subagent appeared under `sessionId` between two
32
+ * consecutive list snapshots (the count crossed 0 → >0). Switching to a
33
+ * session that already has subagents yields `false` (its baseline starts at
34
+ * the current count), so the auto-open never fights an existing layout.
35
+ */
36
+ export declare function detectNewDirectSubagent(prev: SidebarSessionList, next: SidebarSessionList, sessionId: string): boolean;
37
+ /** Descendant totals of one session through an uninterrupted subagent-origin chain. */
38
+ export interface SubagentDescendantTotals {
39
+ count: number;
40
+ runningCount: number;
41
+ }
42
+ /**
43
+ * Index every subagent descendant under each ancestor it reaches through an
44
+ * uninterrupted subagent-origin chain (same semantics as the official
45
+ * `indexSubagentDescendants`; cycles fail soft).
46
+ */
47
+ export declare function countSubagentDescendants(byId: SidebarSessionList['byId'], sessionId: string): SubagentDescendantTotals;