@keith9681/dsh-sidebar 0.15.2

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 (220) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +0 -0
  3. package/README_EN.md +0 -0
  4. package/cordis.patch.yml +49 -0
  5. package/lib/client-editor.js +34651 -0
  6. package/lib/client-mermaid.js +200629 -0
  7. package/lib/client-registry.js +13388 -0
  8. package/lib/client-terminal.js +8904 -0
  9. package/lib/client.js +13388 -0
  10. package/lib/index.js +3959 -0
  11. package/lib/invariant.js +22 -0
  12. package/lib/types/agent-pty.d.ts +226 -0
  13. package/lib/types/browser-probe.d.ts +15 -0
  14. package/lib/types/bundle-route.d.ts +12 -0
  15. package/lib/types/client/BrowserView.d.ts +22 -0
  16. package/lib/types/client/DiffTab.d.ts +6 -0
  17. package/lib/types/client/DiffView.d.ts +51 -0
  18. package/lib/types/client/EditorHost.d.ts +12 -0
  19. package/lib/types/client/FileTree.d.ts +36 -0
  20. package/lib/types/client/GitView.d.ts +8 -0
  21. package/lib/types/client/OrphanedTab.d.ts +11 -0
  22. package/lib/types/client/PdfView.d.ts +6 -0
  23. package/lib/types/client/RenderBoundary.d.ts +32 -0
  24. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  25. package/lib/types/client/SideCardSection.d.ts +74 -0
  26. package/lib/types/client/SideChatView.d.ts +16 -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 +43 -0
  30. package/lib/types/client/TerminalView.d.ts +24 -0
  31. package/lib/types/client/TextEditor.d.ts +10 -0
  32. package/lib/types/client/TreePanel.d.ts +23 -0
  33. package/lib/types/client/UploadOverlay.d.ts +20 -0
  34. package/lib/types/client/add-plugin-modal.d.ts +15 -0
  35. package/lib/types/client/api.d.ts +268 -0
  36. package/lib/types/client/binary-download.d.ts +12 -0
  37. package/lib/types/client/breakpoints.d.ts +12 -0
  38. package/lib/types/client/browser.d.ts +50 -0
  39. package/lib/types/client/builtins/index.d.ts +18 -0
  40. package/lib/types/client/builtins/tabs.d.ts +11 -0
  41. package/lib/types/client/builtins/viewers.d.ts +3 -0
  42. package/lib/types/client/chunk-loader.d.ts +124 -0
  43. package/lib/types/client/chunks/editor.d.ts +9 -0
  44. package/lib/types/client/chunks/mermaid.d.ts +10 -0
  45. package/lib/types/client/chunks/terminal.d.ts +9 -0
  46. package/lib/types/client/cm-themes.d.ts +24 -0
  47. package/lib/types/client/conversation-draft.d.ts +14 -0
  48. package/lib/types/client/desktop-env.d.ts +35 -0
  49. package/lib/types/client/editable-state.d.ts +27 -0
  50. package/lib/types/client/editor-load.d.ts +66 -0
  51. package/lib/types/client/frame-batcher.d.ts +24 -0
  52. package/lib/types/client/icons.d.ts +57 -0
  53. package/lib/types/client/image-types.d.ts +3 -0
  54. package/lib/types/client/ime-guard.d.ts +36 -0
  55. package/lib/types/client/index.d.ts +19 -0
  56. package/lib/types/client/intercept.d.ts +33 -0
  57. package/lib/types/client/lang.d.ts +12 -0
  58. package/lib/types/client/lazy-chunk.d.ts +25 -0
  59. package/lib/types/client/link-intercept.d.ts +42 -0
  60. package/lib/types/client/locales.d.ts +366 -0
  61. package/lib/types/client/mermaid-blocks.d.ts +41 -0
  62. package/lib/types/client/mermaid-sanitize.d.ts +2 -0
  63. package/lib/types/client/mermaid.d.ts +12 -0
  64. package/lib/types/client/open-when-sized.d.ts +20 -0
  65. package/lib/types/client/open-with-settings.d.ts +4 -0
  66. package/lib/types/client/open-with.d.ts +87 -0
  67. package/lib/types/client/openpath-intercept.d.ts +41 -0
  68. package/lib/types/client/paths.d.ts +35 -0
  69. package/lib/types/client/pdf-types.d.ts +2 -0
  70. package/lib/types/client/plugin-settings.d.ts +8 -0
  71. package/lib/types/client/plugins-shared.d.ts +27 -0
  72. package/lib/types/client/plugins-tabs.d.ts +3 -0
  73. package/lib/types/client/plugins-viewers.d.ts +3 -0
  74. package/lib/types/client/prefs.d.ts +41 -0
  75. package/lib/types/client/produced-files.d.ts +23 -0
  76. package/lib/types/client/selection-payload.d.ts +27 -0
  77. package/lib/types/client/service.d.ts +440 -0
  78. package/lib/types/client/settings-nav-icon.d.ts +19 -0
  79. package/lib/types/client/shell-presets.d.ts +48 -0
  80. package/lib/types/client/sidechat-transcript.d.ts +103 -0
  81. package/lib/types/client/split-pane.d.ts +30 -0
  82. package/lib/types/client/state.d.ts +359 -0
  83. package/lib/types/client/subagent-detect.d.ts +54 -0
  84. package/lib/types/client/subagent-jobs.d.ts +63 -0
  85. package/lib/types/client/tab-content-memo.d.ts +33 -0
  86. package/lib/types/client/terminal-font.d.ts +20 -0
  87. package/lib/types/client/theme.d.ts +45 -0
  88. package/lib/types/client/titlebar-strip.d.ts +23 -0
  89. package/lib/types/client/upload.d.ts +69 -0
  90. package/lib/types/client/wco.d.ts +47 -0
  91. package/lib/types/config.d.ts +64 -0
  92. package/lib/types/context-types.d.ts +603 -0
  93. package/lib/types/fs-operations.d.ts +28 -0
  94. package/lib/types/fs-search.d.ts +24 -0
  95. package/lib/types/fs-tree.d.ts +51 -0
  96. package/lib/types/git.d.ts +82 -0
  97. package/lib/types/html-route.d.ts +59 -0
  98. package/lib/types/index.d.ts +47 -0
  99. package/lib/types/invariant.d.ts +15 -0
  100. package/lib/types/jobs-routes.d.ts +45 -0
  101. package/lib/types/open-external.d.ts +24 -0
  102. package/lib/types/prefs-shared.d.ts +225 -0
  103. package/lib/types/pty-deps.d.ts +78 -0
  104. package/lib/types/pty-manager.d.ts +168 -0
  105. package/lib/types/sidechat-core.d.ts +172 -0
  106. package/lib/types/sidechat-routes.d.ts +30 -0
  107. package/lib/types/subagent-activity.d.ts +44 -0
  108. package/lib/types/subagent-live-route.d.ts +44 -0
  109. package/lib/types/tools.d.ts +31 -0
  110. package/lib/types/trust-fence.d.ts +24 -0
  111. package/lib/types/wire.d.ts +38 -0
  112. package/package.json +173 -0
  113. package/scripts/install.ps1 +289 -0
  114. package/scripts/install.sh +287 -0
  115. package/src/agent-pty.ts +527 -0
  116. package/src/browser-probe.ts +26 -0
  117. package/src/bundle-route.ts +129 -0
  118. package/src/client/BrowserView.tsx +253 -0
  119. package/src/client/DiffTab.tsx +110 -0
  120. package/src/client/DiffView.tsx +305 -0
  121. package/src/client/EditorHost.tsx +525 -0
  122. package/src/client/FileTree.tsx +653 -0
  123. package/src/client/GitView.tsx +558 -0
  124. package/src/client/OrphanedTab.tsx +27 -0
  125. package/src/client/PdfView.tsx +110 -0
  126. package/src/client/RenderBoundary.tsx +49 -0
  127. package/src/client/SandboxStatusBar.tsx +60 -0
  128. package/src/client/SideCardSection.module.css +960 -0
  129. package/src/client/SideCardSection.tsx +1152 -0
  130. package/src/client/SideChatView.module.css +437 -0
  131. package/src/client/SideChatView.tsx +644 -0
  132. package/src/client/Sidebar.tsx +1148 -0
  133. package/src/client/SubagentView.module.css +547 -0
  134. package/src/client/SubagentView.tsx +883 -0
  135. package/src/client/TabBar.tsx +305 -0
  136. package/src/client/TerminalView.tsx +383 -0
  137. package/src/client/TextEditor.tsx +500 -0
  138. package/src/client/TreePanel.tsx +273 -0
  139. package/src/client/UploadOverlay.tsx +62 -0
  140. package/src/client/add-plugin-modal.tsx +199 -0
  141. package/src/client/api.ts +335 -0
  142. package/src/client/binary-download.tsx +23 -0
  143. package/src/client/breakpoints.ts +51 -0
  144. package/src/client/browser.ts +119 -0
  145. package/src/client/builtins/index.ts +37 -0
  146. package/src/client/builtins/tabs.tsx +331 -0
  147. package/src/client/builtins/viewers.tsx +125 -0
  148. package/src/client/chunk-loader.ts +351 -0
  149. package/src/client/chunks/editor.tsx +9 -0
  150. package/src/client/chunks/mermaid.tsx +10 -0
  151. package/src/client/chunks/terminal.tsx +9 -0
  152. package/src/client/cm-themes.ts +129 -0
  153. package/src/client/conversation-draft.ts +29 -0
  154. package/src/client/css-modules.d.ts +5 -0
  155. package/src/client/desktop-env.ts +72 -0
  156. package/src/client/editable-state.ts +70 -0
  157. package/src/client/editor-load.ts +92 -0
  158. package/src/client/frame-batcher.ts +56 -0
  159. package/src/client/icons.tsx +161 -0
  160. package/src/client/image-types.ts +8 -0
  161. package/src/client/ime-guard.ts +47 -0
  162. package/src/client/index.tsx +369 -0
  163. package/src/client/intercept.tsx +110 -0
  164. package/src/client/lang.ts +103 -0
  165. package/src/client/layout.css +94 -0
  166. package/src/client/lazy-chunk.tsx +89 -0
  167. package/src/client/link-intercept.ts +73 -0
  168. package/src/client/locales.ts +734 -0
  169. package/src/client/mermaid-blocks.ts +110 -0
  170. package/src/client/mermaid-sanitize.ts +93 -0
  171. package/src/client/mermaid.tsx +401 -0
  172. package/src/client/open-when-sized.ts +43 -0
  173. package/src/client/open-with-settings.tsx +135 -0
  174. package/src/client/open-with.ts +221 -0
  175. package/src/client/openpath-intercept.ts +62 -0
  176. package/src/client/paths.ts +47 -0
  177. package/src/client/pdf-types.ts +4 -0
  178. package/src/client/plugin-settings.ts +43 -0
  179. package/src/client/plugins-shared.ts +29 -0
  180. package/src/client/plugins-tabs.ts +87 -0
  181. package/src/client/plugins-viewers.ts +30 -0
  182. package/src/client/prefs.ts +207 -0
  183. package/src/client/produced-files.ts +91 -0
  184. package/src/client/selection-payload.ts +86 -0
  185. package/src/client/service.ts +838 -0
  186. package/src/client/settings-nav-icon.ts +45 -0
  187. package/src/client/shell-presets.ts +83 -0
  188. package/src/client/sidebar.module.css +2577 -0
  189. package/src/client/sidechat-transcript.ts +321 -0
  190. package/src/client/split-pane.tsx +322 -0
  191. package/src/client/state.ts +1175 -0
  192. package/src/client/subagent-detect.ts +137 -0
  193. package/src/client/subagent-jobs.ts +161 -0
  194. package/src/client/tab-content-memo.ts +46 -0
  195. package/src/client/terminal-font.ts +28 -0
  196. package/src/client/theme.ts +110 -0
  197. package/src/client/titlebar-strip.ts +37 -0
  198. package/src/client/upload.ts +187 -0
  199. package/src/client/wco.ts +120 -0
  200. package/src/config.ts +155 -0
  201. package/src/context-types.ts +610 -0
  202. package/src/fs-operations.ts +99 -0
  203. package/src/fs-search.ts +86 -0
  204. package/src/fs-tree.ts +158 -0
  205. package/src/git.ts +244 -0
  206. package/src/html-route.ts +106 -0
  207. package/src/index.ts +1132 -0
  208. package/src/invariant.ts +32 -0
  209. package/src/jobs-routes.ts +257 -0
  210. package/src/open-external.ts +90 -0
  211. package/src/prefs-shared.ts +268 -0
  212. package/src/pty-deps.ts +240 -0
  213. package/src/pty-manager.ts +376 -0
  214. package/src/sidechat-core.ts +468 -0
  215. package/src/sidechat-routes.ts +344 -0
  216. package/src/subagent-activity.ts +91 -0
  217. package/src/subagent-live-route.ts +93 -0
  218. package/src/tools.ts +479 -0
  219. package/src/trust-fence.ts +77 -0
  220. package/src/wire.ts +100 -0
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Side Chat transcript mapping (browser half): turns a thread child's
3
+ * history rows (`session.history` — the generic RPC, which reads the durable
4
+ * log without activating the child) into compact display rows.
5
+ *
6
+ * A thread child's log starts with the ENTIRE inherited parent log as its
7
+ * fork seed. The mapping therefore cuts everything up to the LAST
8
+ * `session/end-seed` marker and maps context injections (the "Side
9
+ * conversation boundary" prompt, plugin-sourced context) onto a collapsible
10
+ * injection row, so the view shows only the thread's own conversation.
11
+ *
12
+ * Live streaming: `assistant/message` events only land when a step
13
+ * completes, but `assistant/chunk` events stream token-level text and
14
+ * reasoning deltas. The mapping accumulates both per block and supersedes
15
+ * them with the assembled message once it lands (settled rows).
16
+ */
17
+ import type { SidebarHistoryEntry } from '../context-types.ts';
18
+ /** One compact transcript row rendered in the thread view. `seq` is the
19
+ * source event's log sequence — stable row identity for React keys across
20
+ * polls (streaming caches ride the key, so window slides must not re-key
21
+ * rows). */
22
+ export type SidechatTranscriptRow = {
23
+ kind: 'user';
24
+ seq: number;
25
+ text: string;
26
+ }
27
+ /** A context injection (the side boundary prompt + the parked in-progress
28
+ * snapshot, or any plugin-sourced context): rendered as one collapsible
29
+ * row, never as a user bubble. */
30
+ | {
31
+ kind: 'injection';
32
+ seq: number;
33
+ text: string;
34
+ }
35
+ /** `settled` distinguishes an assembled message from a still-streaming
36
+ * chunk accumulation (streaming rows are superseded by the settle). */
37
+ | {
38
+ kind: 'assistant';
39
+ seq: number;
40
+ text: string;
41
+ settled: boolean;
42
+ } | {
43
+ kind: 'reasoning';
44
+ seq: number;
45
+ text: string;
46
+ settled: boolean;
47
+ } | {
48
+ kind: 'tool';
49
+ seq: number;
50
+ name: string;
51
+ failed: boolean;
52
+ /** Raw arguments JSON as the model produced it. */
53
+ args?: string;
54
+ /** Plain text of the paired result. */
55
+ resultText?: string;
56
+ /** True while the call's result has not landed yet. */
57
+ executing?: boolean;
58
+ };
59
+ /** Extract the visible text of a content-block list (`text` blocks verbatim,
60
+ * joined by blank lines); empty reads `…` so rows never render blank. */
61
+ export declare function blockText(content: readonly unknown[]): string;
62
+ /**
63
+ * One-line summary of a tool call's raw arguments JSON for the collapsed
64
+ * row: the first identifying string field when the JSON parses, else the
65
+ * flattened raw text; empty when there is nothing worth showing.
66
+ */
67
+ export declare function toolArgsSummary(args: string | undefined): string;
68
+ /**
69
+ * Collect the thread's OWN events on first attach: walk backward from the
70
+ * log tail (oldest-first accumulation) until the `session/end-seed` marker
71
+ * surfaces, then keep everything after it.
72
+ *
73
+ * Page size matters: cold reads re-expand persisted chunk-rows into one
74
+ * `assistant/chunk` event per delta, so a single streamed answer can be
75
+ * HUNDREDS of events. A small walk window (the old 8×32 = 256 events) let
76
+ * earlier `tool/call` events fall out of the loaded window — the tool rows
77
+ * vanished on re-entry while the settled text survived. The walk therefore
78
+ * pages big; tail polls stay small.
79
+ *
80
+ * Exhaustion (log start reached without a marker — a thread created before
81
+ * seeding existed, or a pathological log) returns `seedBoundary: 0` so the
82
+ * caller stops re-walking and renders the window as-is.
83
+ *
84
+ * @param fetchPage - one history page (newest-first window ending at
85
+ * `beforeSeq`, exclusive; omit for the tail page).
86
+ * @param pageCap - safety bound on backward pages.
87
+ */
88
+ export declare function collectOwnEvents(fetchPage: (beforeSeq?: number) => Promise<readonly SidebarHistoryEntry[]>, pageCap?: number): Promise<{
89
+ seedBoundary: number;
90
+ entries: SidebarHistoryEntry[];
91
+ }>;
92
+ /**
93
+ * Map a thread child's history rows onto compact transcript rows: the
94
+ * inherited fork seed is cut at the last `session/end-seed`, context
95
+ * injections map onto a collapsible injection row, `assistant/chunk`
96
+ * deltas accumulate into streaming rows per (turn, step, block) and are
97
+ * superseded by the assembled `assistant/message`, and tool invocations
98
+ * render one expandable line each (arguments, paired result text, failure
99
+ * marker; a still-executing call is marked until its result lands).
100
+ * @param entries - history rows (event + host-computed view) in seq order.
101
+ * @returns display rows in log order.
102
+ */
103
+ export declare function transcriptRows(entries: readonly SidebarHistoryEntry[]): SidechatTranscriptRow[];
@@ -0,0 +1,30 @@
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
+ getTabBadge?: (tab: SidebarTab) => ReactNode;
30
+ }): import("react").JSX.Element;
@@ -0,0 +1,359 @@
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 (editor / git / terminal
15
+ * / subagent / browser / 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 (git/terminal);
33
+ * `diff` carries the change a diff tab shows; `meta` (v0.12.0+) carries
34
+ * plugin-owned JSON-serializable state, preserved across reloads. */
35
+ export interface SidebarTab {
36
+ id: string;
37
+ type: TabType;
38
+ title: string;
39
+ path?: string;
40
+ diff?: SidebarDiffRef;
41
+ /** Plugin-owned state (v0.12.0+): MUST be JSON-serializable — it is
42
+ * persisted with the layout and restored verbatim on reload. */
43
+ meta?: unknown;
44
+ }
45
+ /** A tab group. */
46
+ export interface SidebarLeaf {
47
+ kind: 'leaf';
48
+ id: string;
49
+ tabs: SidebarTab[];
50
+ active: string | null;
51
+ }
52
+ /** A recursive split between child panes (fractional sizes summing to 1). */
53
+ export interface SidebarSplit {
54
+ kind: 'split';
55
+ id: string;
56
+ dir: 'row' | 'col';
57
+ sizes: number[];
58
+ children: SplitNode[];
59
+ }
60
+ export type SplitNode = SidebarLeaf | SidebarSplit;
61
+ /** The full per-session state. */
62
+ export interface SidebarState {
63
+ panelOpen: boolean;
64
+ width: number;
65
+ /** The pane receiving newly opened tabs (last pane the user touched).
66
+ * Pane ids are globally unique across BOTH trees (shared uid counter), so
67
+ * one field resolves into either tree — see {@link treeOf}. */
68
+ activePane: string | null;
69
+ /** Monotonic terminal tab counter (ids survive reloads). */
70
+ nextTerminal: number;
71
+ /** Monotonic browser tab counter (ids survive reloads; mirrors nextTerminal). */
72
+ nextBrowser: number;
73
+ /** Explorer expansion set (absolute directory paths). */
74
+ expanded: string[];
75
+ /** The right sidebar's split tree (the original workbench). */
76
+ splits: SplitNode;
77
+ /** Whether the bottom panel (a second, independent workbench) is open. */
78
+ bottomOpen: boolean;
79
+ /** The bottom panel's height (clamped to the contract range). */
80
+ bottomHeight: number;
81
+ /**
82
+ * Whether the bottom panel has been expanded at least once in this
83
+ * session — the FIRST expansion tries to auto-open a terminal tab (gated
84
+ * on the bottomPanelAutoTerminal pref); later expansions never do.
85
+ */
86
+ bottomOpenedOnce: boolean;
87
+ /** The bottom panel's own split tree (panes/tabs live only in ONE tree;
88
+ * tabs never cross panels — the two panels only share panel-size drags). */
89
+ bottomSplits: SplitNode;
90
+ }
91
+ export declare const PANEL_MIN = 280;
92
+ export declare const PANEL_MAX = 640;
93
+ export declare const PANEL_DEFAULT = 400;
94
+ export declare const TAB_MAX_WIDTH = 160;
95
+ /** Bottom panel geometry contract (mirrors the width contract; the upper
96
+ * bound is the viewport, enforced by {@link setBottomHeight}). */
97
+ export declare const BOTTOM_MIN = 120;
98
+ export declare const BOTTOM_DEFAULT = 220;
99
+ /** Mint a fresh uid-based tab id. The `'editor:' + path` convention only
100
+ * covers openSidebarFile opens (per-path dedupe); opens that must not
101
+ * dedupe (the tree's "open to the side") mint through here. */
102
+ export declare function mintTabId(): string;
103
+ /** The default tab a fresh session seeds. */
104
+ export type DefaultSeed = 'editor-home' | 'none';
105
+ /** A fresh default state: one seeded tab in one pane, open per the caller's
106
+ * preference. `width` is the caller's preferred panel width (default
107
+ * PANEL_DEFAULT) and `panelOpen` whether the panel starts expanded (default
108
+ * true); the store seeds new sessions from the user's side card prefs.
109
+ * `seed` picks the seeded tab: 'editor-home' places the EMPTY files window
110
+ * (an editor tab with no path whose tree panel starts open,
111
+ * `meta.treeOpen: true`) — in BOTH editorExplorer modes that window is the
112
+ * file explorer page — and 'none' starts with an empty pane (the store
113
+ * passes it when the user disabled the editor tab type in settings). */
114
+ export declare function makeDefaultState(width?: number, panelOpen?: boolean, seed?: DefaultSeed): SidebarState;
115
+ /** Which tree owns a pane/split id: 'bottomSplits' when the id lives in the
116
+ * bottom panel's tree, else 'splits' (the right panel's tree). Ids are
117
+ * globally unique (the shared uid counter), so an id in neither tree falls
118
+ * back to the right tree, where tree operations no-op on a missing node —
119
+ * the pre-bottom-panel behavior. */
120
+ export declare function treeOf(state: SidebarState, id: string): 'splits' | 'bottomSplits';
121
+ /** Walk the tree and apply `visit` to the leaf with the given id. */
122
+ export declare function mapLeaf(node: SplitNode, paneId: string, visit: (leaf: SidebarLeaf) => void): SplitNode;
123
+ /** The first leaf of the tree (fallback pane when activePane is gone). */
124
+ export declare function firstLeaf(node: SplitNode): SidebarLeaf;
125
+ /**
126
+ * Narrow-viewport migration: the bottom panel's tabs are thrown INTO the
127
+ * right sidebar — the "merged display" on mobile is the right panel alone,
128
+ * whose tab strips now carry the bottom tree's tabs (depth-first order,
129
+ * appended to the right tree's FIRST leaf). The bottom tree is emptied (its
130
+ * structure stays — the desktop bottom panel re-renders its welcome cards)
131
+ * and the panel closes. The active pane moves to the right tree's first
132
+ * leaf so every new tab lands in the visible panel.
133
+ *
134
+ * Idempotent: a bottom tree with no tabs and a closed panel returns the
135
+ * same reference. Runs when the viewport enters narrow (see the Sidebar
136
+ * shell); migrating is permanent for the session — the tabs now live in the
137
+ * right tree, exactly like the user "threw them in".
138
+ */
139
+ export declare function migrateBottomTabs(state: SidebarState): SidebarState;
140
+ /** Find the leaf containing a tab id, if any. */
141
+ export declare function leafWithTab(node: SplitNode, tabId: string): SidebarLeaf | undefined;
142
+ /** All leaves of the tree, depth-first. */
143
+ export declare function allLeaves(node: SplitNode): SidebarLeaf[];
144
+ /** Whether a tab exists anywhere in a state (either tree, any pane). */
145
+ export declare function tabOpenIn(state: SidebarState, tabId: string): boolean;
146
+ /** Replace a leaf with a split of it plus a fresh empty leaf. */
147
+ export declare function splitLeafAt(node: SplitNode, paneId: string, dir: 'row' | 'col'): SplitNode;
148
+ /**
149
+ * Split a leaf by inserting a fresh leaf holding `tab` beside it — the
150
+ * VSCode drag-to-edge gesture. `dir` is the split direction ('row' for
151
+ * left/right, 'col' for up/down); `front` places the new leaf first (left/
152
+ * up) or second (right/down).
153
+ * @returns the new tree plus the fresh leaf's id (the drop's active pane).
154
+ */
155
+ export declare function insertLeafAt(node: SplitNode, paneId: string, dir: 'row' | 'col', tab: SidebarTab, front: boolean): {
156
+ node: SplitNode;
157
+ leafId: string;
158
+ };
159
+ /** Where a tab drop lands on a pane: an edge creates a split, center merges. */
160
+ export type DropZone = 'left' | 'right' | 'up' | 'down' | 'center';
161
+ /**
162
+ * The VSCode drag gesture: move a tab out of its pane and either merge it
163
+ * into the target pane (center) or split the target pane with the tab in a
164
+ * fresh leaf (edge). The source pane collapses when it empties.
165
+ *
166
+ * The panes may live in DIFFERENT trees (dragging a tab between the two
167
+ * panels): the tab then leaves its own tree and lands in the other one.
168
+ */
169
+ export declare function moveTabToEdge(state: SidebarState, fromPane: string, tabId: string, toPane: string, zone: DropZone): SidebarState;
170
+ /**
171
+ * Remove a leaf from the tree. A split left with one child promotes that
172
+ * child; removing the last leaf yields an empty leaf.
173
+ */
174
+ export declare function removeLeafAt(node: SplitNode, paneId: string): SplitNode;
175
+ /** Close a tab; an emptied leaf is removed (unless it is the only pane). */
176
+ export declare function closeTab(state: SidebarState, paneId: string, tabId: string): SidebarState;
177
+ /** Activate a tab in its pane (the pane's own tree). */
178
+ export declare function activateTab(state: SidebarState, paneId: string, tabId: string): SidebarState;
179
+ /** Update the display fields of one open tab (title / path / meta) without
180
+ * re-opening it. The browser tab persists its current URL and hostname
181
+ * title through this reducer so a reload restores the visited page. A
182
+ * missing tab id is a no-op. The tab may live in either tree. */
183
+ export declare function patchTab(state: SidebarState, tabId: string, patch: {
184
+ title?: string;
185
+ path?: string;
186
+ meta?: unknown;
187
+ }): SidebarState;
188
+ /**
189
+ * Land a tab in the active pane (or focus its existing instance by id).
190
+ * Dedup strategies (single-instance, per-path, per-change) are owned by the
191
+ * tab descriptor through {@link BetterSidebarService.openTab} / `dedupeKey`;
192
+ * this reducer only handles the id-based safety net (reconcile and
193
+ * openDiffTab already check existence before calling) and the landing
194
+ * itself — the service's dedupe path delegates here after its dedupeKey
195
+ * check misses.
196
+ *
197
+ * The active pane may live in EITHER tree (pane ids are globally unique):
198
+ * a stale id that survives in neither tree falls back to the right tree's
199
+ * first pane instead of swallowing the open.
200
+ */
201
+ export declare function openTabInActivePane(state: SidebarState, tab: SidebarTab): SidebarState;
202
+ /** Move a tab from one pane to another (insert at index; -1 appends).
203
+ * The panes may live in DIFFERENT trees — dragging a tab between the two
204
+ * panels removes it from its own tree and lands it in the other one. */
205
+ export declare function moveTab(state: SidebarState, fromPane: string, tabId: string, toPane: string, index?: number): SidebarState;
206
+ /** Split the active pane (or the pane containing the active tab). */
207
+ export declare function splitPane(state: SidebarState, dir: 'row' | 'col'): SidebarState;
208
+ /**
209
+ * Open a diff tab the VSCode way: an existing instance of the same change is
210
+ * focused wherever it lives; otherwise the tab joins the first pane that
211
+ * already holds diff tabs (diff panes are sticky — repeated clicks stack
212
+ * there); on the FIRST diff of a layout the source pane splits vertically so
213
+ * the diff lands in a fresh pane below it ("默认在下半栏新增一个").
214
+ *
215
+ * This is split-tree placement surgery, not registry dispatch: the diff tab
216
+ * descriptor's `dedupeKey` is `(tab) => tab.id`, and the existing-instance
217
+ * check below is exactly that rule — the two agree by construction (asserted
218
+ * in tests). Diff tabs minted by the Git view carry change-derived ids, so
219
+ * the id check is the per-change dedupe.
220
+ * @returns the new state, with the diff pane active.
221
+ */
222
+ export declare function openDiffTab(state: SidebarState, sourcePaneId: string, tab: SidebarTab): SidebarState;
223
+ /** Toggle the panel open/closed (opening restores the previous layout). */
224
+ export declare function togglePanel(state: SidebarState): SidebarState;
225
+ /** Toggle the bottom panel open/closed (independent of the right panel). */
226
+ export declare function toggleBottomPanel(state: SidebarState): SidebarState;
227
+ /** Set the panel width (clamped to the contract range; the upper bound is
228
+ * the viewport so the fullscreen expansion can fill the window). */
229
+ export declare function setWidth(state: SidebarState, width: number): SidebarState;
230
+ /** Set the bottom panel height (clamped to the contract range). The upper
231
+ * bound leaves the center column (the agent output area) at least PANEL_MIN
232
+ * tall — without the cap the bottom panel could swallow the whole viewport
233
+ * and squeeze the conversation to zero height. */
234
+ export declare function setBottomHeight(state: SidebarState, height: number): SidebarState;
235
+ /** Toggle a directory in the explorer expansion set. */
236
+ export declare function toggleExpanded(state: SidebarState, path: string): SidebarState;
237
+ /** Adjust one split divider: `i` is the left/top child index, delta in fractions. */
238
+ export declare function resizeSplit(node: SplitNode, splitId: string, index: number, delta: number): SplitNode;
239
+ /** State-level {@link resizeSplit} route: the divider may live in either
240
+ * tree (split ids are globally unique). */
241
+ export declare function resizeSplitIn(state: SidebarState, splitId: string, index: number, delta: number): SidebarState;
242
+ /** Prefix marking a tab id as an agent-owned terminal (suffix is the uuid). */
243
+ export declare const AGENT_TAB_PREFIX = "agent:";
244
+ /** Whether a tab id refers to an agent-owned terminal. */
245
+ export declare function isAgentTabId(tabId: string): boolean;
246
+ /** Extract the agent terminal uuid from an `agent:<uuid>` tab id. */
247
+ export declare function agentUuidOf(tabId: string): string;
248
+ /** Build the sidebar tab id for one agent terminal uuid. */
249
+ export declare function agentTabId(uuid: string): string;
250
+ /**
251
+ * Reconcile the sidebar's agent-terminal tabs with the host's live list.
252
+ * The host pushes the current list of agent terminals (created by the model
253
+ * through the `terminal_create` tool) over a dedicated WebSocket; this
254
+ * reducer mirrors that list into tabs: new uuids get a tab, vanished uuids
255
+ * lose theirs. The agent owns the lifetime — the user closing a tab sends a
256
+ * WS close frame that kills the pty, which fires a change, which converges
257
+ * the view. Idempotent: a no-op when the lists already match.
258
+ * @param state - the current per-session sidebar state.
259
+ * @param agentTerminals - the live agent terminal snapshots from the host.
260
+ * @returns the next state (or the same reference if no change was needed).
261
+ */
262
+ export declare function reconcileAgentTerminals(state: SidebarState, agentTerminals: ReadonlyArray<{
263
+ uuid: string;
264
+ title: string;
265
+ }>): SidebarState;
266
+ /** Immutable snapshot handed to React (replaced only on real changes). */
267
+ export interface SidebarSnapshot {
268
+ sessionId: string | undefined;
269
+ state: SidebarState | undefined;
270
+ /**
271
+ * The current side card prefs. Carried IN the snapshot (not a separate
272
+ * subscription) so prefs changes re-render the consumers that gate on
273
+ * them — the + menu hides a tab type the moment its switch flips.
274
+ */
275
+ prefs: SidebarPrefs;
276
+ }
277
+ /** Default panel width for one viewport: the prefs percent of the window,
278
+ * clamped to the panel floor (a tiny percent must stay usable) and to the
279
+ * viewport (a large one must never cover the whole window). */
280
+ export declare function defaultWidthFor(viewport: number, percent: number): number;
281
+ /**
282
+ * Structural validation of one persisted state. A malformed or stale shape
283
+ * (older layouts, hand-edited storage) must fall back to the default instead
284
+ * of crashing the panel on every reload; the restored width is also clamped
285
+ * to the current viewport so a stale fullscreen width can never crush the
286
+ * app shell (margin-right larger than the window) or cover the whole screen.
287
+ * @returns a clean state, or undefined to fall back to the default.
288
+ */
289
+ export declare function sanitizeState(parsed: unknown): SidebarState | undefined;
290
+ /** The session-scoped store: one state per conversation, localStorage-backed. */
291
+ export declare class SidebarStore {
292
+ private readonly bySession;
293
+ private snapshot;
294
+ private readonly listeners;
295
+ /** Per-session persist debounce timers (v0.12.0+: one per session, so a
296
+ * targeted open never cancels another session's pending write). */
297
+ private readonly persistTimers;
298
+ /** User-facing side card prefs seeding brand-new session states (defaults until the settings RPC resolves). */
299
+ private prefs;
300
+ /**
301
+ * External disable (the dsh-web-ui family's aionui-panel provider choice):
302
+ * while true the sidebar must not mount at all. Not part of the snapshot —
303
+ * nothing renders on it; the mount gate and the intercept predicates read
304
+ * it directly.
305
+ */
306
+ private suspended;
307
+ /**
308
+ * Set the external-disable flag (from the settings route) and remember it
309
+ * for the mount gate and the intercept predicates.
310
+ */
311
+ setSuspended(suspended: boolean): void;
312
+ /** Whether the sidebar is externally disabled (aionui-panel chosen). */
313
+ getSuspended(): boolean;
314
+ /**
315
+ * Replace the side card prefs (the settings RPC result / settings page
316
+ * write). Notifies like any store change: the snapshot carries the prefs,
317
+ * so consumers that gate on enable switches (the + menu, derived flows)
318
+ * re-render with the new values immediately.
319
+ */
320
+ setPrefs(prefs: SidebarPrefs): void;
321
+ /** The current side card prefs (seeds new sessions; persisted states win). */
322
+ getPrefs(): SidebarPrefs;
323
+ /** Select a session (or none); loads its persisted state. */
324
+ setSession(sessionId: string | undefined): void;
325
+ subscribe(listener: () => void): () => void;
326
+ getSnapshot(): SidebarSnapshot;
327
+ /** Mutate the current session's state (no-op without a session). */
328
+ update(mutator: (draft: SidebarState) => void): void;
329
+ /**
330
+ * Whether a tab still exists in its session's state. Views use this on
331
+ * unmount to tell "the tab was closed" (release the terminal now) from
332
+ * "the tree re-rendered / the conversation switched" (the tab is still
333
+ * open — keep the terminal alive through the host's reconnect grace).
334
+ * Checks the session's own map entry (the current snapshot may already
335
+ * point at another session when a conversation switch unmounts the old
336
+ * one's tabs).
337
+ */
338
+ tabOpen(sessionId: string, tabId: string): boolean;
339
+ /** Apply a pure reducer (returns the next state). */
340
+ reduce(reducer: (state: SidebarState) => SidebarState): void;
341
+ /**
342
+ * Apply a pure reducer to a TARGET session's state (not the active one),
343
+ * loading it on demand and persisting the result — WITHOUT switching the
344
+ * active snapshot or notifying (the UI must not follow along). Used by the
345
+ * service's targeted `openTab(seed, scope)`: the open lands in the target
346
+ * session's layout and is visible whenever the user switches to it.
347
+ */
348
+ reduceFor(sessionId: string, reducer: (state: SidebarState) => SidebarState): void;
349
+ private schedulePersist;
350
+ private notify;
351
+ }
352
+ /**
353
+ * Create one sidebar store instance. Production code calls this only from
354
+ * the client plugin's `apply` (the instance is handed to components as a
355
+ * prop); tests call it directly. No module-level singleton: the store's
356
+ * lifetime belongs to the plugin activation, exactly like the official
357
+ * `createXXXStore()` factory rule.
358
+ */
359
+ export declare function createSidebarStore(): SidebarStore;
@@ -0,0 +1,54 @@
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, SidebarSessionSummary, SidebarSubagentCatalog } from '../context-types.ts';
14
+ /**
15
+ * Side Chat threads ride the subagent origin (main-list hiding + the RPC
16
+ * ownership fence) but they are NOT subagent topology: they carry the
17
+ * durable 'Side: ' label and live as sidebar tabs. Excluding them here
18
+ * keeps the auto-open trigger and the Subagent page counts clean.
19
+ */
20
+ export declare function isSideThreadSummary(summary: SidebarSessionSummary): boolean;
21
+ /** Count the direct subagent children of one session (durable `origin` rows). */
22
+ export declare function directSubagentCount(byId: SidebarSessionList['byId'], sessionId: string): number;
23
+ /**
24
+ * The main agent of the current session's tree: walk the durable parent
25
+ * chain upward until the first non-subagent session. The Subagent page shows
26
+ * THIS root's full topology regardless of how deep the current selection is
27
+ * (a session whose row is still hydrating, or a broken chain, degrades to
28
+ * the session itself).
29
+ */
30
+ export declare function rootAncestor(byId: SidebarSessionList['byId'], sessionId: string | undefined): string | undefined;
31
+ /**
32
+ * Collect every catalog branch (an entry with `hasChildren`) reachable from
33
+ * the root — the set of catalogs the always-expanded topology consumes.
34
+ * Cycles fail soft.
35
+ */
36
+ export declare function collectBranchIds(catalogs: Readonly<Record<string, SidebarSubagentCatalog>>, rootId: string | undefined): string[];
37
+ /**
38
+ * Whether a new direct subagent appeared under `sessionId` between two
39
+ * consecutive list snapshots (the count crossed 0 → >0). Switching to a
40
+ * session that already has subagents yields `false` (its baseline starts at
41
+ * the current count), so the auto-open never fights an existing layout.
42
+ */
43
+ export declare function detectNewDirectSubagent(prev: SidebarSessionList, next: SidebarSessionList, sessionId: string): boolean;
44
+ /** Descendant totals of one session through an uninterrupted subagent-origin chain. */
45
+ export interface SubagentDescendantTotals {
46
+ count: number;
47
+ runningCount: number;
48
+ }
49
+ /**
50
+ * Index every subagent descendant under each ancestor it reaches through an
51
+ * uninterrupted subagent-origin chain (same semantics as the official
52
+ * `indexSubagentDescendants`; cycles fail soft).
53
+ */
54
+ export declare function countSubagentDescendants(byId: SidebarSessionList['byId'], sessionId: string): SubagentDescendantTotals;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Pure derivations for the Subagent page's background-job section. Kept
3
+ * framework-free so the node test environment can unit-test them: the job
4
+ * rows arrive through the harness `session/jobs` push mirror
5
+ * (`jobsBySession` in the sessions list feed) — nothing here issues
6
+ * requests, and the row ordering / status mapping mirror the official
7
+ * ui-jobs header list.
8
+ */
9
+ import type { SidebarSessionList, SidebarJobStatus, SidebarJobView } from '../context-types.ts';
10
+ import type { CopyKey } from './locales.ts';
11
+ /** One row of the jobs section: the job plus its owning session's title. */
12
+ export interface TreeJob {
13
+ ownerSessionId: string;
14
+ ownerTitle: string;
15
+ job: SidebarJobView;
16
+ }
17
+ /** Whether the registry still holds the job open (its duration ticks). */
18
+ export declare function isJobLive(job: SidebarJobView): boolean;
19
+ /**
20
+ * Every session id of the topology tree rooted at `rootId` (the root plus
21
+ * each session whose uninterrupted subagent-origin chain reaches it — same
22
+ * lineage semantics as {@link countSubagentDescendants}; cycles fail soft).
23
+ * Sessions outside the tree (orphans, other trees) are excluded, so the
24
+ * jobs section never shows foreign work.
25
+ */
26
+ export declare function treeSessionIds(byId: SidebarSessionList['byId'], rootId: string | undefined): Set<string>;
27
+ /**
28
+ * Whether a NEW background job appeared for one session between two
29
+ * consecutive list snapshots (a job id the previous snapshot lacked).
30
+ * Unlike the subagent auto-open (0 → N only), ANY new job id triggers: the
31
+ * agent may start several jobs over a session, and each new one should
32
+ * surface the Jobs page (a fresh page load never triggers — its baseline
33
+ * starts at the current snapshot).
34
+ */
35
+ export declare function detectNewJob(prev: SidebarSessionList, next: SidebarSessionList, sessionId: string): boolean;
36
+ /**
37
+ * Collect the background jobs of the whole current tree, owner-labeled.
38
+ * Sessions without a mirror entry contribute nothing; an absent mirror
39
+ * (runtime older than the jobs feed) yields an empty list.
40
+ */
41
+ export declare function collectTreeJobs(byId: SidebarSessionList['byId'], jobsBySession: Readonly<Record<string, readonly SidebarJobView[]>> | undefined, rootId: string | undefined): TreeJob[];
42
+ /**
43
+ * Live rows first in start order, then settled rows newest-first (mirror of
44
+ * the official ui-jobs ordering); a tie falls back to start order so the
45
+ * sort never depends on the host's map iteration.
46
+ */
47
+ export declare function orderJobs(rows: readonly TreeJob[]): TreeJob[];
48
+ /** The sidebar's StateDot states for the five wire statuses. */
49
+ export type JobDotState = 'ongoing' | 'warning' | 'done' | 'error';
50
+ /**
51
+ * Status marker semantics. `stopping` and `killed` share the attention
52
+ * color: both mean the work ended (or is ending) on request rather than on
53
+ * its own.
54
+ */
55
+ export declare function jobDotState(status: SidebarJobStatus): JobDotState;
56
+ /** Human status word of one wire status (localized through the passed translator). */
57
+ export declare function jobStatusLabel(status: SidebarJobStatus, t: (key: CopyKey, params?: Record<string, string | number>) => string): string;
58
+ /**
59
+ * Elapsed time in at most two adjacent units (mirror of the official
60
+ * ui-jobs duration wording). A background job that outlives an hour is
61
+ * already exceptional, so hours is the widest unit.
62
+ */
63
+ export declare function formatJobDuration(elapsedMs: number, t: (key: CopyKey, params?: Record<string, string | number>) => string): string;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The memo comparator for one tab's content cell (TabContent in Sidebar.tsx).
3
+ *
4
+ * Kept as a pure module so the "what re-renders a tab cell" contract is unit
5
+ * testable: geometry/store re-renders of the Sidebar shell must NOT reconcile
6
+ * every mounted tab (xterm/CodeMirror/FileTree subtrees — issue #315), while
7
+ * locale switches (localeRevision), tab-registry updates (tabsVersion), and
8
+ * a tab MOVING BETWEEN PANES (paneId — onOpenDiff closes over the pane id,
9
+ * and moveTab reuses the SAME tab object, so only the pane id identifies the
10
+ * move) must re-render the cell.
11
+ */
12
+ import type { SidebarTab } from './state.ts';
13
+ /** The fields whose change invalidates a tab cell's memo. Anything a cell's
14
+ * render (or its callbacks) depends on must be listed here. */
15
+ export interface TabContentMemoKey {
16
+ tab: SidebarTab;
17
+ /** The pane this tab currently lives in. onOpenDiff closes over it, and
18
+ * moveTab reuses the same tab object when the tab moves panes — without
19
+ * comparing paneId the cell would keep the source pane's callback. */
20
+ paneId: string;
21
+ sessionId: string;
22
+ cwd: string | undefined;
23
+ visible: boolean;
24
+ expanded: string[];
25
+ localeRevision: string;
26
+ tabsVersion: number;
27
+ }
28
+ /** True when the cell may skip a re-render (all render-affecting fields
29
+ * unchanged). Callback/context identities are deliberately ignored: their
30
+ * captured dependencies are stable or covered by the compared fields
31
+ * (onReferenceFile → sessionId/cwd, onSubagentJump/onToggleDir → stable
32
+ * refs/closures, onOpenDiff → paneId). */
33
+ export declare function tabContentCompare(prev: TabContentMemoKey, next: TabContentMemoKey): boolean;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Terminal font resolution: the user's custom font prefs (SidebarPrefs,
3
+ * configured under the terminal card's secondary settings) turned into the
4
+ * xterm options. Kept as a pure module (no DOM, no xterm) so the fallback
5
+ * chain and clamping are unit-testable without mounting a terminal.
6
+ */
7
+ import { type SidebarPrefs } from '../prefs-shared.ts';
8
+ /** The built-in fallback stack when neither the user nor the theme sets one. */
9
+ export declare const DEFAULT_TERMINAL_FONT_FAMILY = "\"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace";
10
+ /**
11
+ * Resolve the xterm font options for the given prefs.
12
+ * @param prefs - the current side card preferences.
13
+ * @param themeFontFamily - the app's theme code font (`--ds-font-family-code`
14
+ * token value, read live by the caller); undefined when the token is absent.
15
+ * @returns the `fontFamily` / `fontSize` xterm options.
16
+ */
17
+ export declare function resolveTerminalFont(prefs: SidebarPrefs, themeFontFamily: string | undefined): {
18
+ fontFamily: string;
19
+ fontSize: number;
20
+ };