@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,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,46 @@
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
+ localeRevision: string
27
+ tabsVersion: number
28
+ }
29
+
30
+ /** True when the cell may skip a re-render (all render-affecting fields
31
+ * unchanged). Callback/context identities are deliberately ignored: their
32
+ * captured dependencies are stable or covered by the compared fields
33
+ * (onReferenceFile → sessionId/cwd, onSubagentJump/onToggleDir → stable
34
+ * refs/closures, onOpenDiff → paneId). */
35
+ export function tabContentCompare(prev: TabContentMemoKey, next: TabContentMemoKey): boolean {
36
+ return (
37
+ prev.tab === next.tab &&
38
+ prev.paneId === next.paneId &&
39
+ prev.sessionId === next.sessionId &&
40
+ prev.cwd === next.cwd &&
41
+ prev.visible === next.visible &&
42
+ prev.expanded === next.expanded &&
43
+ prev.localeRevision === next.localeRevision &&
44
+ prev.tabsVersion === next.tabsVersion
45
+ )
46
+ }
@@ -0,0 +1,28 @@
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
+ * Resolve the xterm font options for the given prefs.
14
+ * @param prefs - the current side card preferences.
15
+ * @param themeFontFamily - the app's theme code font (`--ds-font-family-code`
16
+ * token value, read live by the caller); undefined when the token is absent.
17
+ * @returns the `fontFamily` / `fontSize` xterm options.
18
+ */
19
+ export function resolveTerminalFont(
20
+ prefs: SidebarPrefs,
21
+ themeFontFamily: string | undefined,
22
+ ): { fontFamily: string; fontSize: number } {
23
+ const custom = prefs.terminalFontFamily.trim()
24
+ return {
25
+ fontFamily: custom !== '' ? custom : (themeFontFamily || DEFAULT_TERMINAL_FONT_FAMILY),
26
+ fontSize: clampTerminalFontSize(prefs.terminalFontSize),
27
+ }
28
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Live theme access for surfaces that cannot consume the token colors
3
+ * directly — xterm's palette and CodeMirror's theme extensions need concrete
4
+ * values, but the app's scheme flips at runtime (ui-layout's ThemePresenter
5
+ * projects prefers-color-scheme and the user's choice onto
6
+ * body[data-ds-dark-theme] and html { color-scheme }). This module reads the
7
+ * resolved scheme and token values, and notifies subscribers on flips, so
8
+ * the terminal and the editor re-theme in place instead of freezing in the
9
+ * scheme they happened to be created under.
10
+ */
11
+
12
+ /** Whether the app shell resolved to the dark scheme.
13
+ *
14
+ * The presenter sets `html { color-scheme }` together with the body palette
15
+ * attribute, so a set color-scheme means the decision is authoritative (an
16
+ * absent attribute is then LIGHT even when the OS prefers dark — the user
17
+ * chose light). Before the presenter has run, fall back to the OS media
18
+ * query as the best guess.
19
+ */
20
+ export function isDarkScheme(): boolean {
21
+ if (typeof document === 'undefined') return true
22
+ const decided = document.documentElement.style.colorScheme !== ''
23
+ if (decided) return document.body.hasAttribute('data-ds-dark-theme')
24
+ return typeof matchMedia !== 'undefined' && matchMedia('(prefers-color-scheme: dark)').matches
25
+ }
26
+
27
+ /** One token's computed value on <body> ('' while the theme has not applied). */
28
+ export function tokenValue(name: string): string {
29
+ if (typeof document === 'undefined') return ''
30
+ return getComputedStyle(document.body).getPropertyValue(name).trim()
31
+ }
32
+
33
+ /** Minimal alpha for a token color to count as effectively opaque. Skin
34
+ * systems turn `--dsw-alias-bg-base` translucent for glass panels (the
35
+ * dsh-web-ui skins use rgba 0.16–0.7; `transparent` is 0); below this
36
+ * floor a text surface (terminal, editor) would render over the skin's
37
+ * backdrop art, so callers fall back to an opaque color. Values at or
38
+ * above the floor (e.g. a skin's scoped 0.96 porcelain) pass through —
39
+ * the skin still controls the surface. */
40
+ const OPAQUE_ALPHA_MIN = 0.9
41
+
42
+ /** The alpha channel of a computed CSS color, or null when the format is
43
+ * not parseable (named colors, `color()`… — treated as opaque). Handles
44
+ * the shapes getComputedStyle actually returns: the rgb()/rgba() and
45
+ * hsl()/hsla() function forms (comma or space syntax, with or without the
46
+ * `/ alpha` slot) and the #rgb/#rgba/#rrggbb/#rrggbbaa hex family. */
47
+ export function colorAlpha(color: string): number | null {
48
+ const s = color.trim()
49
+ // #rgb / #rgba / #rrggbb / #rrggbbaa
50
+ const hex = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(s)
51
+ if (hex !== null) {
52
+ const digits = hex[1]!
53
+ if (digits.length === 3 || digits.length === 4) {
54
+ const a = digits.length === 4 ? digits[3]! : 'f'
55
+ return parseInt(a + a, 16) / 255
56
+ }
57
+ const alphaHex = digits.length === 8 ? digits.slice(6) : 'ff'
58
+ return parseInt(alphaHex, 16) / 255
59
+ }
60
+ // rgb(r g b) / rgb(r g b / a) / rgba(r, g, b, a) — the trailing slot is alpha.
61
+ const fn = /^(rgba?|hsla?)\(([^)]+)\)$/i.exec(s)
62
+ if (fn !== null) {
63
+ const parts = fn[2]!.split(/[,\s/]+/).filter(Boolean)
64
+ const alphaPart = parts[3]
65
+ if (alphaPart === undefined) return 1
66
+ const alpha = Number.parseFloat(alphaPart)
67
+ return Number.isFinite(alpha) ? alpha : 1
68
+ }
69
+ return null
70
+ }
71
+
72
+ /**
73
+ * A token value that actually PAINTS something — the guard for text
74
+ * surfaces (issue #90). Skin systems routinely set global tokens to
75
+ * `transparent` (glass skins) or translucent glass values (`rgba(…,0.16–0.7)`,
76
+ * e.g. the dsh-web-ui skins) — both are truthy strings, so callers using
77
+ * `|| fallback` never fire and the terminal/editor goes see-through over
78
+ * the skin's backdrop. This returns '' for visually inert values (unset
79
+ * keywords, transparent, and any color below the opacity floor) so the
80
+ * caller's fallback chain engages; effectively opaque values pass through.
81
+ */
82
+ export function effectiveTokenValue(name: string): string {
83
+ const raw = tokenValue(name)
84
+ switch (raw) {
85
+ case '':
86
+ case 'transparent':
87
+ case 'initial':
88
+ case 'inherit':
89
+ case 'unset':
90
+ return ''
91
+ default: {
92
+ const alpha = colorAlpha(raw)
93
+ if (alpha !== null && alpha < OPAQUE_ALPHA_MIN) return ''
94
+ return raw
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Subscribe to color-scheme flips (the presenter toggles the body
101
+ * attribute). The callback fires after the attribute changed; re-read the
102
+ * scheme inside it.
103
+ * @returns the disposer.
104
+ */
105
+ export function subscribeColorScheme(callback: () => void): () => void {
106
+ if (typeof document === 'undefined') return () => {}
107
+ const observer = new MutationObserver(() => { callback() })
108
+ observer.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })
109
+ return () => { observer.disconnect() }
110
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The title-bar strip resolution chain — the ONE place that decides how
3
+ * many pixels the sidebar yields at the top. Standard signals first, then
4
+ * the user's chosen scheme; never a per-shell branch:
5
+ *
6
+ * 0. `web` scheme — EXPLICIT "DSH official web": never adapt, not even
7
+ * standard WCO geometry (the user declares the plain web UI).
8
+ * 1. Window Controls Overlay real geometry (standard API, authoritative
9
+ * when present — even 0, e.g. the overlay is hidden while maximized).
10
+ * 2. The `dsh-desktop-titlebar-inset` URL contract parameter (a shell
11
+ * declares the exact pixels it reserves).
12
+ * 3. The active shell preset's strip (scheme `preset` — opt-in data).
13
+ * 4. The legacy manual `titleBarStripPx` (scheme `custom`).
14
+ * 5. 0 — plain-browser semantics, nothing modified.
15
+ *
16
+ * The result drives `--dsh-title-bar-strip` + `body[data-dsh-title-bar-compat]`
17
+ * exactly like the legacy boolean did; only the VALUE source changed.
18
+ */
19
+ import type { DesktopEnv } from './desktop-env.ts'
20
+ import type { TitleBarScheme } from '../prefs-shared.ts'
21
+ import type { WcoSnapshot } from './wco.ts'
22
+ import { presetStripFor, type ShellPreset } from './shell-presets.ts'
23
+
24
+ export function computeTitleBarStrip(
25
+ env: DesktopEnv,
26
+ wco: WcoSnapshot,
27
+ scheme: TitleBarScheme,
28
+ preset: ShellPreset | undefined,
29
+ customStripPx: number,
30
+ ): number {
31
+ if (scheme === 'web') return 0
32
+ if (wco.present) return wco.height
33
+ if (env.titlebarInset > 0) return env.titlebarInset
34
+ if (scheme === 'preset') return presetStripFor(preset, env) ?? 0
35
+ if (scheme === 'custom') return customStripPx
36
+ return 0
37
+ }