@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,32 @@
1
+ /**
2
+ * Package-owned invariant companion for `dsh-better-sidebar`.
3
+ * @module dsh-better-sidebar/invariant
4
+ */
5
+
6
+ /* jscpd:ignore-start */
7
+ import type { Context } from './context-types.ts'
8
+
9
+ const PACKAGE_NAME = 'dsh-better-sidebar'
10
+
11
+ /** Cordis companion plugin name. */
12
+ export const name = 'dsh-better-sidebar-invariant'
13
+ /** Service required before the companion can reserve package ownership. */
14
+ export const inject = ['invariants']
15
+
16
+ /**
17
+ * No runtime invariant: the sidebar owns no service state or event protocol
18
+ * of its own — every route is mounted under the host's webServer fence, the
19
+ * pty lifecycle is exercised by the smoke spec, and the panel's store is a
20
+ * plain snapshot registry asserted by the behavior specs. The route fence,
21
+ * pty quota, and store semantics are each observed through their seams.
22
+ */
23
+ const install: () => void = () => {}
24
+
25
+ /**
26
+ * Register this package's invariant companion.
27
+ * @param ctx - Cordis context carrying the invariant service.
28
+ * @returns the installed registration's disposer after setup succeeds.
29
+ */
30
+ export const apply = (ctx: Context): Promise<() => void> =>
31
+ Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
32
+ /* jscpd:ignore-end */
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Background-job routes of the /sidebar JSON API ('jobs.output' /
3
+ * 'jobs.kill'). The job LIST needs no route: it arrives through the
4
+ * harness's `session/jobs` push mirror (`jobsBySession` in the sessions
5
+ * list feed). The routes:
6
+ *
7
+ * - 'jobs.output' — REPLAYS the output the MODEL has read so far for one
8
+ * job. The source is the owner session's own event log: `tool/call` rows
9
+ * of `job_output` name the job via `arguments.job_id`, and the paired
10
+ * `tool/result` rows carry the finalized content the model received.
11
+ * Because the session store's in-memory log can lag the live append feed
12
+ * after a host restart (the store session stays frozen at its
13
+ * rehydration boundary), the plugin ALSO mirrors job_output events from
14
+ * the live `session/event` feed and merges both sources (deduped by seq).
15
+ * This touches NO DSH source: the model's `job_output` cursor is never
16
+ * consumed, and the pane stays empty until the agent reads the job.
17
+ * - 'jobs.kill' — the registry's stock `kill` (a pristine DSH API),
18
+ * fenced by the owning session via the live agent caller. Absent registry
19
+ * → 503, mirroring the settings routes' optional-service downgrade.
20
+ */
21
+ import type { Context, SidebarSessionEvent } from './context-types.ts'
22
+ import { requireString, SidebarError } from './wire.ts'
23
+
24
+ /** The two background-job routes of the sidebar API. */
25
+ export interface SidebarJobsRoutes {
26
+ /** The output the model has read so far for one job (event replay, capped). */
27
+ output(payload: unknown): { text: string; truncated: boolean; read: boolean }
28
+ /** Request cancellation of one job (live jobs flip to stopping). */
29
+ kill(payload: unknown): { ok: true; outcome: 'requested' | 'already-finished' }
30
+ }
31
+
32
+ /** The 'tool/result' message envelope inside a session event's data. */
33
+ interface ToolResultMessageLike {
34
+ source?: { kind?: unknown; callId?: unknown }
35
+ content?: unknown
36
+ }
37
+
38
+ /** One 'tool-result' content block (the inner blocks carry the text). */
39
+ interface ToolResultBlockLike {
40
+ type?: unknown
41
+ content?: unknown
42
+ isError?: unknown
43
+ }
44
+
45
+ /**
46
+ * Extract the plain text of a finalized tool result: the text blocks inside
47
+ * the 'tool-result' block, joined with newlines. Error results and
48
+ * non-text blocks contribute nothing.
49
+ */
50
+ function resultText(message: ToolResultMessageLike): string | undefined {
51
+ if (!Array.isArray(message.content)) return undefined
52
+ const parts: string[] = []
53
+ for (const block of message.content) {
54
+ if (block === null || typeof block !== 'object') continue
55
+ const candidate = block as ToolResultBlockLike
56
+ if (candidate.type !== 'tool-result') continue
57
+ const inner = candidate.content
58
+ if (!Array.isArray(inner)) continue
59
+ for (const item of inner) {
60
+ if (item === null || typeof item !== 'object') continue
61
+ const textItem = item as { type?: unknown; text?: unknown }
62
+ if (textItem.type === 'text' && typeof textItem.text === 'string') {
63
+ parts.push(textItem.text)
64
+ }
65
+ }
66
+ }
67
+ return parts.length > 0 ? parts.join('\n') : undefined
68
+ }
69
+
70
+ /** Whether a tool/result is an error result (the inner block's isError flag). */
71
+ function resultIsError(message: ToolResultMessageLike): boolean {
72
+ if (!Array.isArray(message.content)) return false
73
+ return message.content.some((block) => {
74
+ if (block === null || typeof block !== 'object') return false
75
+ return (block as ToolResultBlockLike).type === 'tool-result'
76
+ && (block as ToolResultBlockLike).isError === true
77
+ })
78
+ }
79
+
80
+ /** Whether a job_output result carries no new output — the controller's
81
+ * model-facing "(no new output)" body, noise for the human pane. */
82
+ function isNoNewOutput(text: string): boolean {
83
+ return text.startsWith('(no new output)')
84
+ }
85
+
86
+ /** One compact job_output trace (a tool/call or its paired tool/result). */
87
+ interface JobOutputTrace {
88
+ seq: number
89
+ kind: 'call' | 'result'
90
+ /** The tool call identity pairing the two rows. */
91
+ callId: string
92
+ /** tool/call: the job id parsed from the model arguments. */
93
+ jobId?: string
94
+ /** tool/result: the finalized text the model received. */
95
+ text?: string
96
+ /** tool/result: whether the result was an error (read counts, text skipped). */
97
+ isError?: boolean
98
+ }
99
+
100
+ /** Extract the job_output trace of one raw session event (undefined = unrelated). */
101
+ function traceOf(event: SidebarSessionEvent): JobOutputTrace | undefined {
102
+ if (event.type === 'tool/call') {
103
+ const data = event.data as { name?: unknown; callId?: unknown; arguments?: unknown }
104
+ if (data.name !== 'job_output' || typeof data.callId !== 'string') return undefined
105
+ let jobId: string | undefined
106
+ try {
107
+ const args = JSON.parse(typeof data.arguments === 'string' ? data.arguments : '') as { job_id?: unknown }
108
+ if (typeof args.job_id === 'string') jobId = args.job_id
109
+ } catch {
110
+ // Malformed model arguments: not a job_output pair.
111
+ }
112
+ if (jobId === undefined) return undefined
113
+ return { seq: event.seq, kind: 'call', callId: data.callId, jobId }
114
+ }
115
+ if (event.type === 'tool/result') {
116
+ const message = (event.data as { message?: unknown }).message as ToolResultMessageLike | undefined
117
+ if (message === undefined) return undefined
118
+ const callId = message.source?.callId
119
+ if (typeof callId !== 'string') return undefined
120
+ return {
121
+ seq: event.seq,
122
+ kind: 'result',
123
+ callId,
124
+ text: resultText(message),
125
+ isError: resultIsError(message),
126
+ }
127
+ }
128
+ return undefined
129
+ }
130
+
131
+ /** Per-session cap of mirrored live traces (a bounded, lossy ring). */
132
+ const MIRROR_MAX_ENTRIES = 200
133
+
134
+ /**
135
+ * The live job_output mirror: subscribes to the session append feed and
136
+ * caches the job_output traces the session store's own log can lag behind
137
+ * (after a host restart the store session stays frozen at its rehydration
138
+ * boundary, so `session.events` misses everything appended since — the very
139
+ * reads the pane exists to show). Zero DSH writes: the api-proxy pushes the
140
+ * same feed to browsers.
141
+ */
142
+ function createJobOutputMirror(ctx: Context): { entries(sessionId: string): readonly JobOutputTrace[] } {
143
+ const perSession = new Map<string, JobOutputTrace[]>()
144
+ // tool/call identities per session, so unrelated tool/result rows are
145
+ // never cached (only job_output results pair with a cached call).
146
+ const callIds = new Map<string, Set<string>>()
147
+ if (typeof ctx.on !== 'function') {
148
+ // Test doubles without the event API degrade to seed-only replay.
149
+ return { entries: () => [] }
150
+ }
151
+ const dispose = ctx.on('session/event', (session, event) => {
152
+ const sessionId = (session as { id?: unknown } | null)?.id
153
+ if (typeof sessionId !== 'string') return
154
+ if (event.type === 'tool/call') {
155
+ const trace = traceOf(event)
156
+ if (trace?.kind !== 'call') return
157
+ let ids = callIds.get(sessionId)
158
+ if (ids === undefined) callIds.set(sessionId, ids = new Set())
159
+ ids.add(trace.callId)
160
+ push(sessionId, trace)
161
+ } else if (event.type === 'tool/result') {
162
+ const trace = traceOf(event)
163
+ if (trace?.kind !== 'result') return
164
+ if (!callIds.get(sessionId)?.has(trace.callId)) return
165
+ push(sessionId, trace)
166
+ }
167
+ })
168
+ ctx.effect(() => dispose, 'dsh-better-sidebar: job-output event mirror')
169
+
170
+ const push = (sessionId: string, trace: JobOutputTrace): void => {
171
+ let list = perSession.get(sessionId)
172
+ if (list === undefined) perSession.set(sessionId, list = [])
173
+ list.push(trace)
174
+ if (list.length > MIRROR_MAX_ENTRIES) {
175
+ const removed = list.splice(0, list.length - MIRROR_MAX_ENTRIES)
176
+ const ids = callIds.get(sessionId)
177
+ if (ids !== undefined) {
178
+ for (const entry of removed) {
179
+ if (entry.kind === 'call') ids.delete(entry.callId)
180
+ }
181
+ if (ids.size === 0) callIds.delete(sessionId)
182
+ }
183
+ }
184
+ }
185
+
186
+ return { entries: (sessionId) => perSession.get(sessionId) ?? [] }
187
+ }
188
+
189
+ /**
190
+ * Build the jobs routes bound to the plugin context. `output` merges the
191
+ * owner session's own event log with the live job_output mirror; `kill`
192
+ * reads the jobs/agents services lazily and degrades to a 503 when the
193
+ * deployment lacks the registry.
194
+ * @param ctx - host plugin context.
195
+ * @param outputLimit - response cap for one output replay in bytes; longer
196
+ * texts are sliced and flagged `truncated` (mirrors the fs.read cap).
197
+ */
198
+ export function buildJobsApi(ctx: Context, outputLimit: number): SidebarJobsRoutes {
199
+ const jobs = ctx.get('jobs')
200
+ const agents = ctx.get('agents')
201
+ const mirror = createJobOutputMirror(ctx)
202
+ /** The live caller whose session id the registry fence compares against. */
203
+ const callerOf = (sessionId: string) => agents?.get(sessionId)
204
+ /** Registry refusals become a 404 job-error; unknown and foreign ids are indistinguishable. */
205
+ const registryError = (error: unknown): SidebarError =>
206
+ new SidebarError('job-error', error instanceof Error ? error.message : String(error), 404)
207
+ return {
208
+ output(payload) {
209
+ const sessionId = requireString(payload, 'sessionId')
210
+ const id = requireString(payload, 'id')
211
+ // Merge the store's event log (durable seed + whatever it received)
212
+ // with the live mirror, deduped by seq — a trace never double-counts.
213
+ const bySeq = new Map<number, JobOutputTrace>()
214
+ for (const event of ctx.sessions.get(sessionId)?.events ?? []) {
215
+ const trace = traceOf(event)
216
+ if (trace !== undefined) bySeq.set(trace.seq, trace)
217
+ }
218
+ for (const trace of mirror.entries(sessionId)) bySeq.set(trace.seq, trace)
219
+ // Pair calls with results in seq order: the model's reads, oldest first.
220
+ const jobOf = new Map<string, string>()
221
+ const parts: string[] = []
222
+ let read = false
223
+ for (const trace of [...bySeq.values()].sort((left, right) => left.seq - right.seq)) {
224
+ if (trace.kind === 'call') {
225
+ if (trace.jobId !== undefined) jobOf.set(trace.callId, trace.jobId)
226
+ } else if (jobOf.get(trace.callId) === id) {
227
+ read = true
228
+ if (trace.isError !== true && trace.text !== undefined && !isNoNewOutput(trace.text)) {
229
+ parts.push(trace.text)
230
+ }
231
+ }
232
+ }
233
+ const text = parts.join('\n')
234
+ return {
235
+ text: text.length > outputLimit ? text.slice(0, outputLimit) : text,
236
+ truncated: text.length > outputLimit,
237
+ read,
238
+ }
239
+ },
240
+ kill(payload) {
241
+ if (jobs === undefined) {
242
+ throw new SidebarError('job-error', 'the background-job registry is not mounted in this deployment', 503)
243
+ }
244
+ const sessionId = requireString(payload, 'sessionId')
245
+ const id = requireString(payload, 'id')
246
+ const record = payload as { reason?: unknown } | null
247
+ const reason = typeof record?.reason === 'string' && record.reason !== ''
248
+ ? record.reason
249
+ : 'user requested via sidebar'
250
+ try {
251
+ return { ok: true, outcome: jobs.kill(id, callerOf(sessionId), reason) }
252
+ } catch (error) {
253
+ throw registryError(error)
254
+ }
255
+ },
256
+ }
257
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * External open actions for the file tree's "open with" menu: hand a path to
3
+ * the OS file manager (reveal/select) or launch a URL scheme's registered
4
+ * handler (vscode://, cursor://, zed://, custom schemes).
5
+ *
6
+ * The client runs in a browser / DSH Desktop renderer where a raw `vscode://`
7
+ * navigation is unreliable, so both actions fan out through this host route
8
+ * and spawn the platform opener with an argv array (no shell interpolation).
9
+ * The command builders are pure — the platform is injectable — so every
10
+ * per-platform branch is unit-testable without spawning anything.
11
+ */
12
+ import { spawn } from 'node:child_process'
13
+ import { parentOf, requireAbsolute } from './fs-tree.ts'
14
+ import { SidebarError } from './wire.ts'
15
+
16
+ /** The two external open actions the route accepts. */
17
+ export type OpenExternalAction = 'reveal' | 'url'
18
+
19
+ /** One platform opener invocation (argv array — never a shell string). */
20
+ export interface ExternalCommand {
21
+ command: string
22
+ args: string[]
23
+ }
24
+
25
+ /** Reveal/select a path in the OS file manager. On Linux there is no common
26
+ * select protocol — the containing directory is opened instead (KISS). */
27
+ export function revealCommand(path: string, platform: NodeJS.Platform = process.platform): ExternalCommand {
28
+ switch (platform) {
29
+ case 'darwin':
30
+ return { command: 'open', args: ['-R', path] }
31
+ // `explorer /select,<path>` — the classic "show in folder" incantation.
32
+ // Passed as separate argv entries (no shell); verify on Windows, with
33
+ // `cmd /c start "" explorer.exe "/select,<path>"` as the fallback.
34
+ case 'win32':
35
+ return { command: 'explorer.exe', args: ['/select,', path] }
36
+ default: {
37
+ const parent = parentOf(path)
38
+ return { command: 'xdg-open', args: [parent ?? path] }
39
+ }
40
+ }
41
+ }
42
+
43
+ /** Hand a custom-scheme URL to the OS protocol handler. */
44
+ export function urlCommand(url: string, platform: NodeJS.Platform = process.platform): ExternalCommand {
45
+ switch (platform) {
46
+ case 'darwin':
47
+ return { command: 'open', args: [url] }
48
+ // url.dll,FileProtocolHandler launches the registered protocol handler;
49
+ // `cmd /c start "" <url>` is the fallback if rundll32 misbehaves.
50
+ case 'win32':
51
+ return { command: 'rundll32.exe', args: ['url.dll,FileProtocolHandler', url] }
52
+ default:
53
+ return { command: 'xdg-open', args: [url] }
54
+ }
55
+ }
56
+
57
+ /** Validate a URL-scheme open target: a parseable custom-scheme URL (never
58
+ * http/https — those would only dump the URL into a browser tab). */
59
+ export function validateExternalUrl(raw: string): string {
60
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) {
61
+ throw new SidebarError('bad-request', 'url must be a custom-scheme URL')
62
+ }
63
+ let url: URL
64
+ try {
65
+ url = new URL(raw)
66
+ } catch {
67
+ throw new SidebarError('bad-request', 'invalid url')
68
+ }
69
+ if (url.protocol === 'http:' || url.protocol === 'https:') {
70
+ throw new SidebarError('bad-request', 'only custom-scheme urls can be opened externally')
71
+ }
72
+ return raw
73
+ }
74
+
75
+ /**
76
+ * Launch one external open action and return immediately (detached, no
77
+ * stdio). Spawn failures are reported through the child's 'error' event —
78
+ * by then the route already returned, so the event is swallowed (the OS
79
+ * dialog about a missing handler is the user-visible outcome either way).
80
+ */
81
+ export function launchExternal(action: OpenExternalAction, value: string): { started: true } {
82
+ const platform = process.platform
83
+ const spec = action === 'reveal'
84
+ ? revealCommand(requireAbsolute(value), platform)
85
+ : urlCommand(validateExternalUrl(value), platform)
86
+ const child = spawn(spec.command, spec.args, { detached: true, stdio: 'ignore' })
87
+ child.on('error', () => { /* opener missing/denied: handled by the OS */ })
88
+ child.unref()
89
+ return { started: true }
90
+ }
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Shared "Side card" preference vocabulary (types + constants), consumed by
3
+ * BOTH halves: the host registers the schemastery schema over these values
4
+ * (config.ts) and the client reads/writes them through the settings RPC
5
+ * (client/prefs.ts, client/SideCardSection.tsx). Kept free of schemastery so
6
+ * the browser bundle never pulls the schema runtime in.
7
+ */
8
+
9
+ /** The user-settings namespace holding the side card preferences. */
10
+ export const SIDEBAR_PREFS_NS = 'dsh-better-sidebar'
11
+
12
+ /** User-facing side card preferences (new-conversation defaults). */
13
+ export interface SidebarPrefs {
14
+ /** Whether a brand-new conversation opens the side card by default. */
15
+ openByDefault: boolean
16
+ /** Default panel width as a percent of the window width (20–60). */
17
+ defaultWidthPercent: number
18
+ /**
19
+ * Whether the sidebar auto-activates (opens the panel) and expands the
20
+ * Subagent page when the current conversation spawns a new subagent.
21
+ */
22
+ autoOpenSubagent: boolean
23
+ /**
24
+ * Whether the sidebar auto-activates (opens the panel) and expands the
25
+ * Jobs page when a NEW background job appears for the current
26
+ * conversation (any new job id, not just the first one).
27
+ */
28
+ autoOpenJobs: boolean
29
+ /**
30
+ * Whether the model-facing agent terminal tools (terminal_create / list /
31
+ * send / read / wait_for / resize / signal / close) are injected into the
32
+ * model's toolset. Off by default: the feature stays dormant until the
33
+ * user explicitly enables it in the side card settings.
34
+ */
35
+ agentTerminalTools: boolean
36
+ /**
37
+ * Custom terminal font-family stack (a CSS font-family value, e.g.
38
+ * `'JetBrains Mono', monospace`). Empty string follows the app's theme
39
+ * monospace font (`--ds-font-family-code`). Applied live to every
40
+ * terminal tab; configured under the terminal card's secondary settings.
41
+ */
42
+ terminalFontFamily: string
43
+ /**
44
+ * Custom terminal font size in px (9–32). Applied live to every terminal
45
+ * tab; configured under the terminal card's secondary settings.
46
+ */
47
+ terminalFontSize: number
48
+ /**
49
+ * Whether expanding the bottom panel for the FIRST time in a session tries
50
+ * to open a fresh terminal tab there (the terminal quota/type still gates
51
+ * the attempt). On by default; the switch lives under the terminal tab's
52
+ * row in the Side card settings.
53
+ */
54
+ bottomPanelAutoTerminal: boolean
55
+ /**
56
+ * Whether chat-side file opens (tool-row path links, the produced-files
57
+ * row, prose file mentions — every path that funnels through the client
58
+ * runtime's `ctx.workspaces.openPath`) open in the sidebar editor instead
59
+ * of the Host OS's default application. On by default; the editor tab's
60
+ * own enable switch gates it too (both must be on for the takeover).
61
+ */
62
+ interceptOpenPath: boolean
63
+ /**
64
+ * Whether the editor tab runs in merged mode: a path input replaces the
65
+ * plain header and a toggleable file-tree panel (with a global name
66
+ * search) docks at the tab's right edge. On by default; also makes brand
67
+ * new sessions seed an empty editor tab (tree panel open) instead of the
68
+ * explorer tab. The switch lives under the editor card's gear in the
69
+ * Side card settings; off restores the pre-merge editor exactly.
70
+ */
71
+ editorExplorer: boolean
72
+ /**
73
+ * The shell the UI and agent terminals spawn (absolute path or bare
74
+ * executable name). Empty (default) keeps the legacy resolution order:
75
+ * `cordis.patch.yml` `config.shell`, then `$SHELL` / login shell /
76
+ * `powershell.exe` on Windows. Set it from the terminal card's gear in
77
+ * the Side card settings (or the yaml) to pin a specific shell — takes
78
+ * effect for terminals opened afterwards.
79
+ */
80
+ terminalShell: string
81
+ /**
82
+ * Explicit arguments for `terminalShell`, space-separated (empty keeps
83
+ * the platform defaults; when set, they fully replace them — same
84
+ * contract as the yaml `shellArgs`).
85
+ */
86
+ terminalShellArgs: string
87
+ /**
88
+ * Title-bar / shell compatibility scheme (the "位置兼容模式" setting):
89
+ * - `auto` (default): CONSERVATIVE — only the standard Window Controls
90
+ * Overlay API (present in frameless Chromium shells that draw the
91
+ * native caption buttons over web content) contributes real geometry;
92
+ * without it nothing is modified, so plain-browser (web) behavior is
93
+ * untouched.
94
+ * - `web`: EXPLICIT "DSH official web" — never adapt, not even WCO
95
+ * geometry (the user declares they run the plain web UI).
96
+ * - `preset`: apply the built-in shell preset named by
97
+ * `titleBarPresetId` (data-driven, opt-in — see shell-presets.ts).
98
+ * - `custom`: apply the free-form `customCss` (and the legacy
99
+ * `titleBarStripPx` strip).
100
+ */
101
+ titleBarScheme: TitleBarScheme
102
+ /**
103
+ * The built-in shell preset id applied while `titleBarScheme` is
104
+ * `preset` ('' = no preset — nothing extra is applied).
105
+ */
106
+ titleBarPresetId: string
107
+ /**
108
+ * Free-form CSS injected into the page (last in the cascade, so it can
109
+ * override the plugin's styles; use `!important` to override JS-written
110
+ * inline CSS variables). Applied while `titleBarScheme` is `custom`.
111
+ */
112
+ customCss: string
113
+ /**
114
+ * LEGACY (kept for read-migration and downgrade mirroring only): position
115
+ * compatibility mode flag. The UI writes `titleBarScheme` instead; a
116
+ * stored `true` without a scheme migrates to the `custom` scheme (with
117
+ * `titleBarStripPx` preserved).
118
+ */
119
+ titleBarCompat: boolean
120
+ /**
121
+ * LEGACY (kept for read-migration and downgrade mirroring only): the
122
+ * reserved top strip height in px used by the `custom` scheme (0–120,
123
+ * default 40). Drives the `--dsh-title-bar-strip` CSS variable: the
124
+ * toggle cluster drops `strip + 3px` and the right panel's content
125
+ * starts `strip` px below its top edge.
126
+ */
127
+ titleBarStripPx: number
128
+ /**
129
+ * Whether the HTML previewer drops its sandboxed iframe. Sandbox ON (the
130
+ * default) renders previewed HTML in an opaque-origin iframe that cannot
131
+ * touch the GUI; turning it OFF runs the previewed page with the GUI's
132
+ * own origin — full read/write access to session files and internal
133
+ * APIs. Only for trusted local content; the setting copy warns.
134
+ */
135
+ htmlViewerNoSandbox: boolean
136
+ /**
137
+ * Whether a newly opened HTML preview starts UNSANDBOXED (the per-surface
138
+ * temporary unlock pre-applied). Off by default: previews open sandboxed
139
+ * and the status row offers the one-tap unlock; when on, previews open
140
+ * in the red unsandboxed state and the status row offers a one-tap
141
+ * restore for the current file.
142
+ */
143
+ htmlViewerDefaultUnsafe: boolean
144
+ /**
145
+ * Whether the browser tab drops its sandboxed iframe. Sandbox ON (the
146
+ * default) keeps browsed sites in an opaque origin with no GUI access;
147
+ * turning it OFF runs any visited site with the GUI's own origin — it
148
+ * can read session data and act as the logged-in GUI. Only for trusted
149
+ * sites; the setting copy warns.
150
+ */
151
+ browserNoSandbox: boolean
152
+ /**
153
+ * MASTER switch: whether clicking an EXTERNAL link in the GUI (chat
154
+ * messages, tool rows, prose mentions) is taken over into the sidebar at
155
+ * all. On by default; the per-protocol granularity lives in
156
+ * `browserInterceptHttp` / `browserInterceptHttps` (the protocol flag
157
+ * must also be on), and the target tab's own enable switch gates it too.
158
+ * Ctrl/Cmd+click always bypasses the takeover. Kept as the master so old
159
+ * documents keep their meaning with no migration (an explicit `false`
160
+ * stays "never take over").
161
+ */
162
+ browserInterceptLinks: boolean
163
+ /**
164
+ * Whether clicking an http EXTERNAL link in the GUI opens the sidebar
165
+ * (the built-in browser tab, or a plugin tab that declares `urlTarget`)
166
+ * instead of a new browser tab. On by default; gated on the
167
+ * `browserInterceptLinks` master and the target tab's own enable switch.
168
+ */
169
+ browserInterceptHttp: boolean
170
+ /**
171
+ * Whether clicking an https EXTERNAL link in the GUI opens the sidebar
172
+ * instead of a new browser tab. OFF by default — most https sites (e.g.
173
+ * GitHub) refuse iframe embedding, so the system browser is the smoother
174
+ * default; gated on the `browserInterceptLinks` master and the target
175
+ * tab's own enable switch.
176
+ */
177
+ browserInterceptHttps: boolean
178
+ /**
179
+ * Per-tab enable switches, keyed by tab descriptor id (`'explorer'`,
180
+ * `'my-plugin:db'`). An ABSENT key means enabled — only an explicit
181
+ * `false` disables a tab type (hidden from the + menu, `openTab` refuses,
182
+ * and derived flows like subagent auto-open / agent-terminal tabs stop).
183
+ * Already-open tabs of a disabled type keep rendering (closing one
184
+ * prevents reopening), matching the "existing conversations keep their
185
+ * own layouts" rule.
186
+ */
187
+ tabsEnabled: Record<string, boolean>
188
+ /**
189
+ * Per-viewer enable switches, keyed by file viewer descriptor id
190
+ * (`'image'`, `'my-plugin:csv'`). An ABSENT key means enabled; a disabled
191
+ * viewer is skipped by `matchFileViewer` so files fall through to the
192
+ * next matching viewer (or the download button when none match).
193
+ */
194
+ viewersEnabled: Record<string, boolean>
195
+ /**
196
+ * Plugin-owned settings blobs (v0.12.0+), keyed by descriptor id: each
197
+ * registered tab/viewer that declares `settings.pluginToggles` (or writes
198
+ * through `settings.render`'s `updatePluginSetting`) persists its values
199
+ * here — an open map, so third-party keys need no host PrefsSchema field.
200
+ * Values are JSON-serializable (the row controls produce strings /
201
+ * numbers / booleans; custom panels are responsible for their own).
202
+ */
203
+ pluginSettings: Record<string, Record<string, unknown>>
204
+ }
205
+
206
+ /** Range contract of {@link SidebarPrefs.defaultWidthPercent}. */
207
+ export const WIDTH_PERCENT_MIN = 20
208
+ export const WIDTH_PERCENT_MAX = 60
209
+ export const WIDTH_PERCENT_DEFAULT = 35
210
+
211
+ /** Range contract of {@link SidebarPrefs.terminalFontSize}. */
212
+ export const TERMINAL_FONT_SIZE_MIN = 9
213
+ export const TERMINAL_FONT_SIZE_MAX = 32
214
+ export const TERMINAL_FONT_SIZE_DEFAULT = 13
215
+
216
+ /** Range contract of {@link SidebarPrefs.titleBarStripPx}. */
217
+ export const TITLE_BAR_STRIP_MIN = 0
218
+ export const TITLE_BAR_STRIP_MAX = 120
219
+ export const TITLE_BAR_STRIP_DEFAULT = 40
220
+
221
+ /** The title-bar / shell compatibility schemes (see {@link SidebarPrefs.titleBarScheme}). */
222
+ export const TITLE_BAR_SCHEMES = ['auto', 'web', 'preset', 'custom'] as const
223
+ export type TitleBarScheme = typeof TITLE_BAR_SCHEMES[number]
224
+
225
+ /** Fallback prefs used whenever the settings document is unreachable or malformed. */
226
+ export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = {
227
+ openByDefault: false,
228
+ defaultWidthPercent: WIDTH_PERCENT_DEFAULT,
229
+ autoOpenSubagent: true,
230
+ autoOpenJobs: true,
231
+ agentTerminalTools: false,
232
+ bottomPanelAutoTerminal: false,
233
+ terminalFontFamily: '',
234
+ terminalFontSize: TERMINAL_FONT_SIZE_DEFAULT,
235
+ interceptOpenPath: true,
236
+ editorExplorer: false,
237
+ terminalShell: '',
238
+ terminalShellArgs: '',
239
+ titleBarScheme: 'auto',
240
+ titleBarPresetId: '',
241
+ customCss: '',
242
+ titleBarCompat: false,
243
+ titleBarStripPx: TITLE_BAR_STRIP_DEFAULT,
244
+ htmlViewerNoSandbox: false,
245
+ htmlViewerDefaultUnsafe: false,
246
+ browserNoSandbox: false,
247
+ browserInterceptLinks: true,
248
+ browserInterceptHttp: true,
249
+ browserInterceptHttps: false,
250
+ tabsEnabled: {},
251
+ viewersEnabled: {},
252
+ pluginSettings: {},
253
+ }
254
+
255
+ /** Clamp one width percent into the contract range (shared by schema and client reads). */
256
+ export function clampWidthPercent(value: number): number {
257
+ return Math.min(WIDTH_PERCENT_MAX, Math.max(WIDTH_PERCENT_MIN, Math.round(value)))
258
+ }
259
+
260
+ /** Clamp one terminal font size into the contract range (shared by schema and client reads). */
261
+ export function clampTerminalFontSize(value: number): number {
262
+ return Math.min(TERMINAL_FONT_SIZE_MAX, Math.max(TERMINAL_FONT_SIZE_MIN, Math.round(value)))
263
+ }
264
+
265
+ /** Clamp one title-bar strip height into the contract range (shared by schema and client reads). */
266
+ export function clampTitleBarStrip(value: number): number {
267
+ return Math.min(TITLE_BAR_STRIP_MAX, Math.max(TITLE_BAR_STRIP_MIN, Math.round(value)))
268
+ }