dsh-coding-sidebar 1.0.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 (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +524 -0
  3. package/cordis.patch.yml +49 -0
  4. package/lib/client-editor.js +48229 -0
  5. package/lib/client-mermaid.js +200745 -0
  6. package/lib/client-registry.js +21867 -0
  7. package/lib/client-terminal.js +9361 -0
  8. package/lib/client.js +21867 -0
  9. package/lib/index.js +4556 -0
  10. package/lib/invariant.js +22 -0
  11. package/lib/types/agent-opens.d.ts +65 -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 +39 -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 +13 -0
  19. package/lib/types/client/FileTree.d.ts +38 -0
  20. package/lib/types/client/FreeWindow.d.ts +29 -0
  21. package/lib/types/client/GitView.d.ts +10 -0
  22. package/lib/types/client/MarkdownHtml.d.ts +32 -0
  23. package/lib/types/client/OrphanedTab.d.ts +11 -0
  24. package/lib/types/client/PdfView.d.ts +6 -0
  25. package/lib/types/client/RenderBoundary.d.ts +32 -0
  26. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  27. package/lib/types/client/SideCardSection.d.ts +74 -0
  28. package/lib/types/client/SideChatView.d.ts +16 -0
  29. package/lib/types/client/Sidebar.d.ts +6 -0
  30. package/lib/types/client/SubagentView.d.ts +16 -0
  31. package/lib/types/client/TabBar.d.ts +54 -0
  32. package/lib/types/client/TerminalView.d.ts +24 -0
  33. package/lib/types/client/TextEditor.d.ts +10 -0
  34. package/lib/types/client/TreePanel.d.ts +24 -0
  35. package/lib/types/client/UploadOverlay.d.ts +20 -0
  36. package/lib/types/client/add-plugin-modal.d.ts +15 -0
  37. package/lib/types/client/api.d.ts +275 -0
  38. package/lib/types/client/binary-download.d.ts +12 -0
  39. package/lib/types/client/breakpoints.d.ts +17 -0
  40. package/lib/types/client/browser.d.ts +60 -0
  41. package/lib/types/client/builtins/index.d.ts +18 -0
  42. package/lib/types/client/builtins/tabs.d.ts +11 -0
  43. package/lib/types/client/builtins/viewers.d.ts +3 -0
  44. package/lib/types/client/chunk-loader.d.ts +131 -0
  45. package/lib/types/client/chunks/editor.d.ts +9 -0
  46. package/lib/types/client/chunks/mermaid.d.ts +10 -0
  47. package/lib/types/client/chunks/terminal.d.ts +9 -0
  48. package/lib/types/client/cm-themes.d.ts +24 -0
  49. package/lib/types/client/conversation-draft.d.ts +14 -0
  50. package/lib/types/client/desktop-env.d.ts +35 -0
  51. package/lib/types/client/editor-load.d.ts +66 -0
  52. package/lib/types/client/frame-batcher.d.ts +24 -0
  53. package/lib/types/client/icons.d.ts +59 -0
  54. package/lib/types/client/image-types.d.ts +3 -0
  55. package/lib/types/client/ime-guard.d.ts +36 -0
  56. package/lib/types/client/index.d.ts +25 -0
  57. package/lib/types/client/intercept.d.ts +46 -0
  58. package/lib/types/client/lang.d.ts +14 -0
  59. package/lib/types/client/layout-push.d.ts +15 -0
  60. package/lib/types/client/lazy-chunk.d.ts +25 -0
  61. package/lib/types/client/link-intercept.d.ts +42 -0
  62. package/lib/types/client/locales-ar.d.ts +18 -0
  63. package/lib/types/client/locales-de.d.ts +4 -0
  64. package/lib/types/client/locales-fr.d.ts +10 -0
  65. package/lib/types/client/locales-hi.d.ts +17 -0
  66. package/lib/types/client/locales-id.d.ts +15 -0
  67. package/lib/types/client/locales-it.d.ts +8 -0
  68. package/lib/types/client/locales-ja.d.ts +17 -0
  69. package/lib/types/client/locales-ko.d.ts +9 -0
  70. package/lib/types/client/locales-nl.d.ts +16 -0
  71. package/lib/types/client/locales-pl.d.ts +19 -0
  72. package/lib/types/client/locales-pt.d.ts +1 -0
  73. package/lib/types/client/locales-ru.d.ts +13 -0
  74. package/lib/types/client/locales-sv.d.ts +1 -0
  75. package/lib/types/client/locales-th.d.ts +17 -0
  76. package/lib/types/client/locales-tr.d.ts +17 -0
  77. package/lib/types/client/locales-vi.d.ts +17 -0
  78. package/lib/types/client/locales-zh-HK.d.ts +32 -0
  79. package/lib/types/client/locales-zh-MO.d.ts +32 -0
  80. package/lib/types/client/locales-zh-TW.d.ts +32 -0
  81. package/lib/types/client/locales.d.ts +452 -0
  82. package/lib/types/client/markdown-html.d.ts +96 -0
  83. package/lib/types/client/markdown-images.d.ts +45 -0
  84. package/lib/types/client/markdown-labels.d.ts +38 -0
  85. package/lib/types/client/md-toc.d.ts +4 -0
  86. package/lib/types/client/mermaid-blocks.d.ts +47 -0
  87. package/lib/types/client/mermaid-sanitize.d.ts +2 -0
  88. package/lib/types/client/mermaid.d.ts +12 -0
  89. package/lib/types/client/open-when-sized.d.ts +20 -0
  90. package/lib/types/client/open-with-settings.d.ts +4 -0
  91. package/lib/types/client/open-with.d.ts +87 -0
  92. package/lib/types/client/openpath-intercept.d.ts +85 -0
  93. package/lib/types/client/paths.d.ts +43 -0
  94. package/lib/types/client/pdf-types.d.ts +2 -0
  95. package/lib/types/client/pinned.d.ts +86 -0
  96. package/lib/types/client/plugin-settings.d.ts +8 -0
  97. package/lib/types/client/plugins-shared.d.ts +27 -0
  98. package/lib/types/client/plugins-tabs.d.ts +3 -0
  99. package/lib/types/client/plugins-viewers.d.ts +3 -0
  100. package/lib/types/client/prefs.d.ts +41 -0
  101. package/lib/types/client/produced-files.d.ts +29 -0
  102. package/lib/types/client/selection-payload.d.ts +27 -0
  103. package/lib/types/client/service.d.ts +439 -0
  104. package/lib/types/client/settings-nav-icon.d.ts +19 -0
  105. package/lib/types/client/shell-presets.d.ts +48 -0
  106. package/lib/types/client/sidechat-transcript.d.ts +103 -0
  107. package/lib/types/client/split-pane.d.ts +36 -0
  108. package/lib/types/client/state.d.ts +432 -0
  109. package/lib/types/client/subagent-detect.d.ts +54 -0
  110. package/lib/types/client/subagent-jobs.d.ts +63 -0
  111. package/lib/types/client/tab-content-memo.d.ts +40 -0
  112. package/lib/types/client/terminal-font.d.ts +78 -0
  113. package/lib/types/client/terminal-links.d.ts +119 -0
  114. package/lib/types/client/theme.d.ts +45 -0
  115. package/lib/types/client/titlebar-strip.d.ts +23 -0
  116. package/lib/types/client/upload.d.ts +69 -0
  117. package/lib/types/client/wco.d.ts +47 -0
  118. package/lib/types/config.d.ts +64 -0
  119. package/lib/types/context-types.d.ts +605 -0
  120. package/lib/types/fs-operations.d.ts +28 -0
  121. package/lib/types/fs-search.d.ts +24 -0
  122. package/lib/types/fs-tree.d.ts +51 -0
  123. package/lib/types/git.d.ts +124 -0
  124. package/lib/types/html-route.d.ts +59 -0
  125. package/lib/types/index.d.ts +47 -0
  126. package/lib/types/invariant.d.ts +15 -0
  127. package/lib/types/jobs-routes.d.ts +45 -0
  128. package/lib/types/open-external.d.ts +24 -0
  129. package/lib/types/path-security.d.ts +20 -0
  130. package/lib/types/prefs-shared.d.ts +236 -0
  131. package/lib/types/pty-deps.d.ts +78 -0
  132. package/lib/types/pty-manager.d.ts +168 -0
  133. package/lib/types/sidechat-core.d.ts +172 -0
  134. package/lib/types/sidechat-routes.d.ts +30 -0
  135. package/lib/types/subagent-activity.d.ts +44 -0
  136. package/lib/types/subagent-live-route.d.ts +44 -0
  137. package/lib/types/tools.d.ts +34 -0
  138. package/lib/types/trust-fence.d.ts +24 -0
  139. package/lib/types/wire.d.ts +38 -0
  140. package/package.json +172 -0
  141. package/scripts/install.ps1 +306 -0
  142. package/scripts/install.sh +287 -0
  143. package/src/agent-opens.ts +265 -0
  144. package/src/agent-pty.ts +527 -0
  145. package/src/browser-probe.ts +26 -0
  146. package/src/bundle-route.ts +129 -0
  147. package/src/client/BrowserView.tsx +289 -0
  148. package/src/client/DiffTab.tsx +114 -0
  149. package/src/client/DiffView.tsx +305 -0
  150. package/src/client/EditorHost.tsx +558 -0
  151. package/src/client/FileTree.tsx +669 -0
  152. package/src/client/FreeWindow.tsx +337 -0
  153. package/src/client/GitView.tsx +717 -0
  154. package/src/client/MarkdownHtml.tsx +296 -0
  155. package/src/client/OrphanedTab.tsx +27 -0
  156. package/src/client/PdfView.tsx +110 -0
  157. package/src/client/RenderBoundary.tsx +49 -0
  158. package/src/client/SandboxStatusBar.tsx +60 -0
  159. package/src/client/SideCardSection.module.css +960 -0
  160. package/src/client/SideCardSection.tsx +1163 -0
  161. package/src/client/SideChatView.module.css +437 -0
  162. package/src/client/SideChatView.tsx +645 -0
  163. package/src/client/Sidebar.tsx +1370 -0
  164. package/src/client/SubagentView.module.css +547 -0
  165. package/src/client/SubagentView.tsx +883 -0
  166. package/src/client/TabBar.tsx +365 -0
  167. package/src/client/TerminalView.tsx +424 -0
  168. package/src/client/TextEditor.tsx +468 -0
  169. package/src/client/TreePanel.tsx +275 -0
  170. package/src/client/UploadOverlay.tsx +62 -0
  171. package/src/client/add-plugin-modal.tsx +199 -0
  172. package/src/client/api.ts +357 -0
  173. package/src/client/binary-download.tsx +23 -0
  174. package/src/client/breakpoints.ts +61 -0
  175. package/src/client/browser.ts +167 -0
  176. package/src/client/builtins/index.ts +37 -0
  177. package/src/client/builtins/tabs.tsx +340 -0
  178. package/src/client/builtins/viewers.tsx +125 -0
  179. package/src/client/chunk-loader.ts +358 -0
  180. package/src/client/chunks/editor.tsx +9 -0
  181. package/src/client/chunks/mermaid.tsx +10 -0
  182. package/src/client/chunks/terminal.tsx +9 -0
  183. package/src/client/cm-themes.ts +129 -0
  184. package/src/client/conversation-draft.ts +29 -0
  185. package/src/client/css-modules.d.ts +5 -0
  186. package/src/client/desktop-env.ts +72 -0
  187. package/src/client/editor-load.ts +92 -0
  188. package/src/client/frame-batcher.ts +56 -0
  189. package/src/client/icons.tsx +152 -0
  190. package/src/client/image-types.ts +8 -0
  191. package/src/client/ime-guard.ts +47 -0
  192. package/src/client/index.tsx +427 -0
  193. package/src/client/intercept.tsx +188 -0
  194. package/src/client/lang.ts +225 -0
  195. package/src/client/layout-push.ts +24 -0
  196. package/src/client/layout.css +90 -0
  197. package/src/client/lazy-chunk.tsx +89 -0
  198. package/src/client/link-intercept.ts +73 -0
  199. package/src/client/locales-ar.ts +368 -0
  200. package/src/client/locales-de.ts +353 -0
  201. package/src/client/locales-fr.ts +360 -0
  202. package/src/client/locales-hi.ts +367 -0
  203. package/src/client/locales-id.ts +365 -0
  204. package/src/client/locales-it.ts +358 -0
  205. package/src/client/locales-ja.ts +367 -0
  206. package/src/client/locales-ko.ts +359 -0
  207. package/src/client/locales-nl.ts +365 -0
  208. package/src/client/locales-pl.ts +369 -0
  209. package/src/client/locales-pt.ts +350 -0
  210. package/src/client/locales-ru.ts +365 -0
  211. package/src/client/locales-sv.ts +350 -0
  212. package/src/client/locales-th.ts +367 -0
  213. package/src/client/locales-tr.ts +367 -0
  214. package/src/client/locales-vi.ts +367 -0
  215. package/src/client/locales-zh-HK.ts +382 -0
  216. package/src/client/locales-zh-MO.ts +382 -0
  217. package/src/client/locales-zh-TW.ts +382 -0
  218. package/src/client/locales.ts +894 -0
  219. package/src/client/markdown-html.ts +331 -0
  220. package/src/client/markdown-images.ts +137 -0
  221. package/src/client/markdown-labels.tsx +52 -0
  222. package/src/client/md-toc.tsx +127 -0
  223. package/src/client/mermaid-blocks.ts +110 -0
  224. package/src/client/mermaid-sanitize.ts +93 -0
  225. package/src/client/mermaid.tsx +406 -0
  226. package/src/client/open-when-sized.ts +43 -0
  227. package/src/client/open-with-settings.tsx +135 -0
  228. package/src/client/open-with.ts +221 -0
  229. package/src/client/openpath-intercept.ts +143 -0
  230. package/src/client/paths.ts +63 -0
  231. package/src/client/pdf-types.ts +4 -0
  232. package/src/client/pinned.ts +183 -0
  233. package/src/client/plugin-settings.ts +43 -0
  234. package/src/client/plugins-shared.ts +29 -0
  235. package/src/client/plugins-tabs.ts +96 -0
  236. package/src/client/plugins-viewers.ts +30 -0
  237. package/src/client/prefs.ts +210 -0
  238. package/src/client/produced-files.ts +120 -0
  239. package/src/client/selection-payload.ts +86 -0
  240. package/src/client/service.ts +859 -0
  241. package/src/client/settings-nav-icon.ts +45 -0
  242. package/src/client/shell-presets.ts +83 -0
  243. package/src/client/sidebar.module.css +2788 -0
  244. package/src/client/sidechat-transcript.ts +321 -0
  245. package/src/client/split-pane.tsx +331 -0
  246. package/src/client/state.ts +1497 -0
  247. package/src/client/subagent-detect.ts +137 -0
  248. package/src/client/subagent-jobs.ts +161 -0
  249. package/src/client/tab-content-memo.ts +55 -0
  250. package/src/client/terminal-font.ts +224 -0
  251. package/src/client/terminal-links.ts +194 -0
  252. package/src/client/theme.ts +110 -0
  253. package/src/client/titlebar-strip.ts +37 -0
  254. package/src/client/upload.ts +187 -0
  255. package/src/client/wco.ts +120 -0
  256. package/src/config.ts +154 -0
  257. package/src/context-types.ts +611 -0
  258. package/src/fs-operations.ts +95 -0
  259. package/src/fs-search.ts +113 -0
  260. package/src/fs-tree.ts +158 -0
  261. package/src/git.ts +446 -0
  262. package/src/html-route.ts +106 -0
  263. package/src/index.ts +1291 -0
  264. package/src/invariant.ts +32 -0
  265. package/src/jobs-routes.ts +257 -0
  266. package/src/open-external.ts +90 -0
  267. package/src/path-security.ts +75 -0
  268. package/src/prefs-shared.ts +280 -0
  269. package/src/pty-deps.ts +240 -0
  270. package/src/pty-manager.ts +376 -0
  271. package/src/sidechat-core.ts +468 -0
  272. package/src/sidechat-routes.ts +344 -0
  273. package/src/subagent-activity.ts +91 -0
  274. package/src/subagent-live-route.ts +93 -0
  275. package/src/tools.ts +482 -0
  276. package/src/trust-fence.ts +84 -0
  277. package/src/wire.ts +100 -0
@@ -0,0 +1,137 @@
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 {
14
+ SidebarSessionList,
15
+ SidebarSessionSummary,
16
+ SidebarSubagentCatalog,
17
+ } from '../context-types.ts'
18
+ import { SIDE_LABEL_PREFIX } from '../sidechat-core.ts'
19
+
20
+ /**
21
+ * Side Chat threads ride the subagent origin (main-list hiding + the RPC
22
+ * ownership fence) but they are NOT subagent topology: they carry the
23
+ * durable 'Side: ' label and live as sidebar tabs. Excluding them here
24
+ * keeps the auto-open trigger and the Subagent page counts clean.
25
+ */
26
+ export function isSideThreadSummary(summary: SidebarSessionSummary): boolean {
27
+ return summary.origin === 'subagent' && summary.displayTitle.startsWith(SIDE_LABEL_PREFIX)
28
+ }
29
+
30
+ /** Count the direct subagent children of one session (durable `origin` rows). */
31
+ export function directSubagentCount(
32
+ byId: SidebarSessionList['byId'],
33
+ sessionId: string,
34
+ ): number {
35
+ let count = 0
36
+ for (const summary of Object.values(byId)) {
37
+ if (summary.origin === 'subagent' && summary.parentId === sessionId
38
+ && !isSideThreadSummary(summary)) count += 1
39
+ }
40
+ return count
41
+ }
42
+
43
+ /**
44
+ * The main agent of the current session's tree: walk the durable parent
45
+ * chain upward until the first non-subagent session. The Subagent page shows
46
+ * THIS root's full topology regardless of how deep the current selection is
47
+ * (a session whose row is still hydrating, or a broken chain, degrades to
48
+ * the session itself).
49
+ */
50
+ export function rootAncestor(
51
+ byId: SidebarSessionList['byId'],
52
+ sessionId: string | undefined,
53
+ ): string | undefined {
54
+ if (sessionId === undefined) return undefined
55
+ const seen = new Set<string>()
56
+ let current: SidebarSessionSummary | undefined = byId[sessionId]
57
+ while (current !== undefined && current.origin === 'subagent'
58
+ && current.parentId !== undefined && !seen.has(current.id)) {
59
+ seen.add(current.id)
60
+ current = byId[current.parentId]
61
+ }
62
+ return current?.id ?? sessionId
63
+ }
64
+
65
+ /**
66
+ * Collect every catalog branch (an entry with `hasChildren`) reachable from
67
+ * the root — the set of catalogs the always-expanded topology consumes.
68
+ * Cycles fail soft.
69
+ */
70
+ export function collectBranchIds(
71
+ catalogs: Readonly<Record<string, SidebarSubagentCatalog>>,
72
+ rootId: string | undefined,
73
+ ): string[] {
74
+ const out: string[] = []
75
+ const seen = new Set<string>()
76
+ const visit = (parentId: string): void => {
77
+ if (seen.has(parentId)) return
78
+ seen.add(parentId)
79
+ for (const entry of catalogs[parentId]?.entries ?? []) {
80
+ if (entry.kind === 'child' && entry.hasChildren) {
81
+ out.push(entry.id)
82
+ visit(entry.id)
83
+ }
84
+ }
85
+ }
86
+ if (rootId !== undefined) visit(rootId)
87
+ return out
88
+ }
89
+
90
+ /**
91
+ * Whether a new direct subagent appeared under `sessionId` between two
92
+ * consecutive list snapshots (the count crossed 0 → >0). Switching to a
93
+ * session that already has subagents yields `false` (its baseline starts at
94
+ * the current count), so the auto-open never fights an existing layout.
95
+ */
96
+ export function detectNewDirectSubagent(
97
+ prev: SidebarSessionList,
98
+ next: SidebarSessionList,
99
+ sessionId: string,
100
+ ): boolean {
101
+ return directSubagentCount(prev.byId, sessionId) === 0
102
+ && directSubagentCount(next.byId, sessionId) > 0
103
+ }
104
+
105
+ /** Descendant totals of one session through an uninterrupted subagent-origin chain. */
106
+ export interface SubagentDescendantTotals {
107
+ count: number
108
+ runningCount: number
109
+ }
110
+
111
+ /**
112
+ * Index every subagent descendant under each ancestor it reaches through an
113
+ * uninterrupted subagent-origin chain (same semantics as the official
114
+ * `indexSubagentDescendants`; cycles fail soft).
115
+ */
116
+ export function countSubagentDescendants(
117
+ byId: SidebarSessionList['byId'],
118
+ sessionId: string,
119
+ ): SubagentDescendantTotals {
120
+ const totals: SubagentDescendantTotals = { count: 0, runningCount: 0 }
121
+ for (const descendant of Object.values(byId)) {
122
+ if (descendant.origin !== 'subagent' || isSideThreadSummary(descendant)) continue
123
+ const seen = new Set<string>()
124
+ let current: SidebarSessionSummary | undefined = descendant
125
+ while (current?.origin === 'subagent' && current.parentId !== undefined
126
+ && !seen.has(current.id)) {
127
+ seen.add(current.id)
128
+ if (current.parentId === sessionId) {
129
+ totals.count += 1
130
+ if (descendant.running === true) totals.runningCount += 1
131
+ break
132
+ }
133
+ current = byId[current.parentId]
134
+ }
135
+ }
136
+ return totals
137
+ }
@@ -0,0 +1,161 @@
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 {
10
+ SidebarSessionList,
11
+ SidebarSessionSummary,
12
+ SidebarJobStatus,
13
+ SidebarJobView,
14
+ } from '../context-types.ts'
15
+ import type { CopyKey } from './locales.ts'
16
+
17
+ /** One row of the jobs section: the job plus its owning session's title. */
18
+ export interface TreeJob {
19
+ ownerSessionId: string
20
+ ownerTitle: string
21
+ job: SidebarJobView
22
+ }
23
+
24
+ /** Whether the registry still holds the job open (its duration ticks). */
25
+ export function isJobLive(job: SidebarJobView): boolean {
26
+ return job.status === 'running' || job.status === 'stopping'
27
+ }
28
+
29
+ /**
30
+ * Every session id of the topology tree rooted at `rootId` (the root plus
31
+ * each session whose uninterrupted subagent-origin chain reaches it — same
32
+ * lineage semantics as {@link countSubagentDescendants}; cycles fail soft).
33
+ * Sessions outside the tree (orphans, other trees) are excluded, so the
34
+ * jobs section never shows foreign work.
35
+ */
36
+ export function treeSessionIds(
37
+ byId: SidebarSessionList['byId'],
38
+ rootId: string | undefined,
39
+ ): Set<string> {
40
+ const ids = new Set<string>()
41
+ if (rootId === undefined) return ids
42
+ for (const summary of Object.values(byId)) {
43
+ const seen = new Set<string>()
44
+ let current: SidebarSessionSummary | undefined = summary
45
+ let reachesRoot = false
46
+ while (current !== undefined && !seen.has(current.id)) {
47
+ seen.add(current.id)
48
+ if (current.id === rootId) {
49
+ reachesRoot = true
50
+ break
51
+ }
52
+ if (current.origin !== 'subagent' || current.parentId === undefined) break
53
+ current = byId[current.parentId]
54
+ }
55
+ if (reachesRoot) ids.add(summary.id)
56
+ }
57
+ return ids
58
+ }
59
+
60
+ /**
61
+ * Whether a NEW background job appeared for one session between two
62
+ * consecutive list snapshots (a job id the previous snapshot lacked).
63
+ * Unlike the subagent auto-open (0 → N only), ANY new job id triggers: the
64
+ * agent may start several jobs over a session, and each new one should
65
+ * surface the Jobs page (a fresh page load never triggers — its baseline
66
+ * starts at the current snapshot).
67
+ */
68
+ export function detectNewJob(
69
+ prev: SidebarSessionList,
70
+ next: SidebarSessionList,
71
+ sessionId: string,
72
+ ): boolean {
73
+ const prevIds = new Set((prev.jobsBySession?.[sessionId] ?? []).map(job => job.id))
74
+ return (next.jobsBySession?.[sessionId] ?? []).some(job => !prevIds.has(job.id))
75
+ }
76
+
77
+ /**
78
+ * Collect the background jobs of the whole current tree, owner-labeled.
79
+ * Sessions without a mirror entry contribute nothing; an absent mirror
80
+ * (runtime older than the jobs feed) yields an empty list.
81
+ */
82
+ export function collectTreeJobs(
83
+ byId: SidebarSessionList['byId'],
84
+ jobsBySession: Readonly<Record<string, readonly SidebarJobView[]>> | undefined,
85
+ rootId: string | undefined,
86
+ ): TreeJob[] {
87
+ const rows: TreeJob[] = []
88
+ if (jobsBySession === undefined) return rows
89
+ for (const sessionId of treeSessionIds(byId, rootId)) {
90
+ const jobs = jobsBySession[sessionId]
91
+ if (jobs === undefined || jobs.length === 0) continue
92
+ const ownerTitle = byId[sessionId]?.displayTitle ?? sessionId
93
+ for (const job of jobs) rows.push({ ownerSessionId: sessionId, ownerTitle, job })
94
+ }
95
+ return rows
96
+ }
97
+
98
+ /**
99
+ * Live rows first in start order, then settled rows newest-first (mirror of
100
+ * the official ui-jobs ordering); a tie falls back to start order so the
101
+ * sort never depends on the host's map iteration.
102
+ */
103
+ export function orderJobs(rows: readonly TreeJob[]): TreeJob[] {
104
+ return [...rows].sort((left, right) => {
105
+ const liveLeft = isJobLive(left.job)
106
+ if (liveLeft !== isJobLive(right.job)) return liveLeft ? -1 : 1
107
+ if (liveLeft) return left.job.startedAt - right.job.startedAt
108
+ const finished = (right.job.finishedAt ?? right.job.startedAt) - (left.job.finishedAt ?? left.job.startedAt)
109
+ return finished !== 0 ? finished : left.job.startedAt - right.job.startedAt
110
+ })
111
+ }
112
+
113
+ /** The sidebar's StateDot states for the five wire statuses. */
114
+ export type JobDotState = 'ongoing' | 'warning' | 'done' | 'error'
115
+
116
+ /**
117
+ * Status marker semantics. `stopping` and `killed` share the attention
118
+ * color: both mean the work ended (or is ending) on request rather than on
119
+ * its own.
120
+ */
121
+ export function jobDotState(status: SidebarJobStatus): JobDotState {
122
+ switch (status) {
123
+ case 'running': return 'ongoing'
124
+ case 'stopping': return 'warning'
125
+ case 'completed': return 'done'
126
+ case 'killed': return 'warning'
127
+ case 'failed': return 'error'
128
+ }
129
+ }
130
+
131
+ /** Human status word of one wire status (localized through the passed translator). */
132
+ export function jobStatusLabel(
133
+ status: SidebarJobStatus,
134
+ t: (key: CopyKey, params?: Record<string, string | number>) => string,
135
+ ): string {
136
+ switch (status) {
137
+ case 'running': return t('jobStatusRunning')
138
+ case 'stopping': return t('jobStatusStopping')
139
+ case 'completed': return t('jobStatusCompleted')
140
+ case 'killed': return t('jobStatusKilled')
141
+ case 'failed': return t('jobStatusFailed')
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Elapsed time in at most two adjacent units (mirror of the official
147
+ * ui-jobs duration wording). A background job that outlives an hour is
148
+ * already exceptional, so hours is the widest unit.
149
+ */
150
+ export function formatJobDuration(
151
+ elapsedMs: number,
152
+ t: (key: CopyKey, params?: Record<string, string | number>) => string,
153
+ ): string {
154
+ const total = Math.max(0, Math.floor(elapsedMs / 1_000))
155
+ const seconds = total % 60
156
+ const minutes = Math.floor(total / 60) % 60
157
+ const hours = Math.floor(total / 3_600)
158
+ if (hours > 0) return t('jobDurationHours', { hours, minutes })
159
+ if (minutes > 0) return t('jobDurationMinutes', { minutes, seconds })
160
+ return t('jobDurationSeconds', { seconds })
161
+ }
@@ -0,0 +1,55 @@
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
+
14
+ /** The fields whose change invalidates a tab cell's memo. Anything a cell's
15
+ * render (or its callbacks) depends on must be listed here. */
16
+ export interface TabContentMemoKey {
17
+ tab: SidebarTab
18
+ /** The pane this tab currently lives in. onOpenDiff closes over it, and
19
+ * moveTab reuses the same tab object when the tab moves panes — without
20
+ * comparing paneId the cell would keep the source pane's callback. */
21
+ paneId: string
22
+ sessionId: string
23
+ cwd: string | undefined
24
+ visible: boolean
25
+ expanded: string[]
26
+ /** Files highlighted in the file tree (the "Show in folder" reveal). */
27
+ revealed: string[]
28
+ localeRevision: string
29
+ tabsVersion: number
30
+ /** When the tab is a pinned virtual tab (injected from another session),
31
+ * this overrides `tab.id` passed to the tab descriptor's component so
32
+ * TerminalView connects to the home session's PTY by the original id.
33
+ * Undefined for regular tabs (no override). */
34
+ effectiveTabId: string | undefined
35
+ }
36
+
37
+ /** True when the cell may skip a re-render (all render-affecting fields
38
+ * unchanged). Callback/context identities are deliberately ignored: their
39
+ * captured dependencies are stable or covered by the compared fields
40
+ * (onReferenceFile → sessionId/cwd, onSubagentJump/onToggleDir → stable
41
+ * refs/closures, onOpenDiff → paneId). */
42
+ export function tabContentCompare(prev: TabContentMemoKey, next: TabContentMemoKey): boolean {
43
+ return (
44
+ prev.tab === next.tab &&
45
+ prev.paneId === next.paneId &&
46
+ prev.sessionId === next.sessionId &&
47
+ prev.cwd === next.cwd &&
48
+ prev.visible === next.visible &&
49
+ prev.expanded === next.expanded &&
50
+ prev.revealed === next.revealed &&
51
+ prev.localeRevision === next.localeRevision &&
52
+ prev.tabsVersion === next.tabsVersion &&
53
+ prev.effectiveTabId === next.effectiveTabId
54
+ )
55
+ }
@@ -0,0 +1,224 @@
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 { clampTerminalFontSize, type SidebarPrefs } from '../prefs-shared.ts'
8
+
9
+ /** The built-in fallback stack when neither the user nor the theme sets one. */
10
+ export const DEFAULT_TERMINAL_FONT_FAMILY = '"SF Mono", Menlo, Consolas, "Liberation Mono", monospace'
11
+
12
+ /**
13
+ * Icon fonts appended to whichever base stack wins, so shell prompts that
14
+ * draw glyphs from the Nerd Font Private Use Areas resolve to a real glyph
15
+ * instead of the missing-glyph box (aka tofu).
16
+ *
17
+ * Why this is needed even though the OS "should" fall back automatically:
18
+ * prompt frameworks (starship, powerlevel10k, oh-my-posh) take their icons
19
+ * from the PUA. Chromium's implicit system fallback reliably covers the
20
+ * *BMP* PUA (U+E000–U+F8FF — e.g. the U+E0B0 powerline separator) but NOT
21
+ * the *supplementary-plane* PUA-B (U+F0000+) where Nerd Fonts v3 relocated
22
+ * the Material Design icon set. Naming the families explicitly makes the
23
+ * browser consult them per character, which covers both planes.
24
+ *
25
+ * Deliberately NOT listed: color-emoji families. Chromium routes genuine
26
+ * emoji code points through a dedicated emoji fallback path (which is why
27
+ * emoji already render), so naming them buys nothing here — while placing a
28
+ * color font ahead of the generic family risks capturing BMP symbols the
29
+ * prompt expects in monospace (U+26A0 ⚠, U+2714 ✔ …) and rendering them as
30
+ * wide color glyphs that break the cell grid.
31
+ *
32
+ * Ordering rationale: the symbols-only patches ship glyphs without Latin,
33
+ * so they can never hijack ASCII metrics — the safest first hop. The
34
+ * fully-patched distributions follow for users who installed one of those
35
+ * instead. Both the `… Mono` and proportional family names are listed
36
+ * because the Nerd Fonts installers register them as distinct families.
37
+ *
38
+ * These are strictly *appended*, never prepended: xterm derives its cell
39
+ * metrics from the first entry, so the base font must stay in front or the
40
+ * whole grid would be re-measured against an icon font.
41
+ */
42
+ export const ICON_FONT_FALLBACKS: readonly string[] = [
43
+ // Symbols-only patches (glyph coverage without Latin) — ideal fallbacks.
44
+ '"Symbols Nerd Font Mono"',
45
+ '"Symbols Nerd Font"',
46
+ // Common fully-patched Nerd Font distributions.
47
+ '"Hack Nerd Font Mono"',
48
+ '"Hack Nerd Font"',
49
+ '"JetBrainsMono Nerd Font Mono"',
50
+ '"JetBrainsMono Nerd Font"',
51
+ '"FiraCode Nerd Font Mono"',
52
+ '"FiraCode Nerd Font"',
53
+ '"CaskaydiaCove Nerd Font Mono"',
54
+ '"CaskaydiaCove Nerd Font"',
55
+ '"SauceCodePro Nerd Font Mono"',
56
+ '"UbuntuMono Nerd Font Mono"',
57
+ '"Iosevka Nerd Font Mono"',
58
+ '"MesloLGS Nerd Font Mono"',
59
+ '"MesloLGS NF"',
60
+ ]
61
+
62
+ /** The symbols-only patch count at the head of {@link ICON_FONT_FALLBACKS}
63
+ * (these carry no Latin glyphs, so they can never hijack ASCII metrics). */
64
+ const SYMBOLS_ONLY_COUNT = 2
65
+
66
+ /**
67
+ * CSS generic font families. A generic is a catch-all that always resolves,
68
+ * so icon fonts must be spliced in *before* the first one to stay reachable.
69
+ */
70
+ const GENERIC_FAMILIES = new Set([
71
+ 'monospace', 'serif', 'sans-serif', 'cursive', 'fantasy',
72
+ 'system-ui', 'ui-monospace', 'ui-serif', 'ui-sans-serif', 'ui-rounded',
73
+ 'math', 'emoji', 'fangsong',
74
+ ])
75
+
76
+ /**
77
+ * CSS-wide keywords. These are only valid as an *entire* declaration value,
78
+ * so a stack cannot be appended to one.
79
+ */
80
+ const CSS_WIDE_KEYWORDS = new Set(['inherit', 'initial', 'unset', 'revert', 'revert-layer'])
81
+
82
+ /** Normalize one family name for comparison: unquote, collapse runs of
83
+ * whitespace, casefold. */
84
+ function normalizeFamily(family: string): string {
85
+ return family
86
+ .trim()
87
+ .replace(/^["']|["']$/g, '')
88
+ .replace(/\s+/g, ' ')
89
+ .trim()
90
+ .toLowerCase()
91
+ }
92
+
93
+ /**
94
+ * Split a CSS font-family stack on its top-level commas.
95
+ *
96
+ * Naive `split(',')` would corrupt quoted family names containing a comma
97
+ * and function values such as `var(--x, monospace)`, so quotes (with
98
+ * backslash escapes) and parentheses are tracked.
99
+ *
100
+ * @param stack - a CSS font-family stack.
101
+ * @returns the trimmed, non-empty family entries in source order.
102
+ */
103
+ function splitFamilies(stack: string): string[] {
104
+ const entries: string[] = []
105
+ let buffer = ''
106
+ let quote: string | null = null
107
+ let depth = 0
108
+ for (let i = 0; i < stack.length; i += 1) {
109
+ const char = stack[i] as string
110
+ if (quote !== null) {
111
+ // Escapes are copied verbatim so `\"` does not close the quote.
112
+ if (char === '\\' && i + 1 < stack.length) {
113
+ buffer += char + (stack[i + 1] as string)
114
+ i += 1
115
+ continue
116
+ }
117
+ buffer += char
118
+ if (char === quote) quote = null
119
+ continue
120
+ }
121
+ if (char === '"' || char === "'") {
122
+ quote = char
123
+ buffer += char
124
+ continue
125
+ }
126
+ if (char === '(') depth += 1
127
+ else if (char === ')') depth = Math.max(0, depth - 1)
128
+ else if (char === ',' && depth === 0) {
129
+ entries.push(buffer)
130
+ buffer = ''
131
+ continue
132
+ }
133
+ buffer += char
134
+ }
135
+ entries.push(buffer)
136
+ return entries.map(entry => entry.trim()).filter(entry => entry !== '')
137
+ }
138
+
139
+ /**
140
+ * Append {@link ICON_FONT_FALLBACKS} to a CSS font-family stack, keeping
141
+ * the caller's own entries and order intact.
142
+ *
143
+ * - Families already named in `stack` are not duplicated (quote-, case- and
144
+ * whitespace-insensitive), so a user who already lists their Nerd Font
145
+ * keeps their exact priority.
146
+ * - The icon fonts are spliced in ahead of the *first* generic family
147
+ * (`monospace` etc.), because a generic always resolves: anything after it
148
+ * would never be consulted. A stack that OPENS with a generic is the one
149
+ * exception: only the symbols-only patches (no Latin) may precede it — a
150
+ * fully-patched Nerd Font there would become xterm's measuring base font
151
+ * and override the user/theme family precedence, so it is placed after the
152
+ * generic instead.
153
+ * - Idempotent — re-applying to an already-topped-up stack is a no-op, which
154
+ * matters because `TerminalView` diffs the resolved value against the live
155
+ * `term.options.fontFamily` before reflowing.
156
+ *
157
+ * @param stack - a CSS font-family stack (base font first).
158
+ * @returns the stack with icon fallbacks merged in.
159
+ */
160
+ export function withIconFontFallbacks(stack: string): string {
161
+ const entries = splitFamilies(stack)
162
+ if (entries.length === 0) return ICON_FONT_FALLBACKS.join(', ')
163
+
164
+ const present = new Set(entries.map(normalizeFamily))
165
+ const notPresent = (family: string): boolean => !present.has(normalizeFamily(family))
166
+ const symbolsOnly = ICON_FONT_FALLBACKS.slice(0, SYMBOLS_ONLY_COUNT).filter(notPresent)
167
+ const patched = ICON_FONT_FALLBACKS.slice(SYMBOLS_ONLY_COUNT).filter(notPresent)
168
+ if (symbolsOnly.length === 0 && patched.length === 0) return entries.join(', ')
169
+
170
+ const firstGeneric = entries.findIndex(entry => GENERIC_FAMILIES.has(normalizeFamily(entry)))
171
+ const cut = firstGeneric === -1 ? entries.length : firstGeneric
172
+ // A fully-patched distribution ships Latin glyphs: placed ahead of a
173
+ // LEADING generic (cut === 0) it would become xterm's measuring/base font
174
+ // and override the user/theme family precedence. Only the symbols-only
175
+ // patches — which carry no Latin — may precede it; the patched fonts go
176
+ // after the generic (still reachable per character, never the base).
177
+ if (cut === 0) {
178
+ return [...symbolsOnly, entries[0]!, ...patched, ...entries.slice(1)].join(', ')
179
+ }
180
+ return [...entries.slice(0, cut), ...symbolsOnly, ...patched, ...entries.slice(cut)].join(', ')
181
+ }
182
+
183
+ /**
184
+ * Reduce one link of the base-family chain to a usable stack, or `''` when
185
+ * it cannot carry appended fallbacks so the next link should win.
186
+ *
187
+ * Rejecting CSS-wide keywords matters because the theme font arrives as a
188
+ * raw token value (`tokenValue('--ds-font-family-code')`); skins do set
189
+ * tokens to `initial`/`inherit`/`unset` (see `effectiveTokenValue` in
190
+ * `theme.ts`, which guards the color tokens for the same reason). Appending
191
+ * to such a value yields an invalid `font-family`, which the CSSOM discards
192
+ * silently — the terminal would lose the theme font *and* the icon fonts.
193
+ */
194
+ function usableBase(value: string | undefined): string {
195
+ const trimmed = (value ?? '').trim()
196
+ if (trimmed === '') return ''
197
+ if (CSS_WIDE_KEYWORDS.has(trimmed.toLowerCase())) return ''
198
+ return trimmed
199
+ }
200
+
201
+ /**
202
+ * Resolve the xterm font options for the given prefs.
203
+ *
204
+ * The base family keeps its existing precedence — user pref > theme code
205
+ * font > built-in stack — and then {@link withIconFontFallbacks} tops it up
206
+ * so prompt icons resolve regardless of which base won.
207
+ *
208
+ * @param prefs - the current side card preferences.
209
+ * @param themeFontFamily - the app's theme code font (`--ds-font-family-code`
210
+ * token value, read live by the caller); undefined when the token is absent.
211
+ * @returns the `fontFamily` / `fontSize` xterm options.
212
+ */
213
+ export function resolveTerminalFont(
214
+ prefs: SidebarPrefs,
215
+ themeFontFamily: string | undefined,
216
+ ): { fontFamily: string; fontSize: number } {
217
+ const base = usableBase(prefs.terminalFontFamily)
218
+ || usableBase(themeFontFamily)
219
+ || DEFAULT_TERMINAL_FONT_FAMILY
220
+ return {
221
+ fontFamily: withIconFontFallbacks(base),
222
+ fontSize: clampTerminalFontSize(prefs.terminalFontSize),
223
+ }
224
+ }