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.
- package/LICENSE +21 -0
- package/README.md +524 -0
- package/cordis.patch.yml +49 -0
- package/lib/client-editor.js +48229 -0
- package/lib/client-mermaid.js +200745 -0
- package/lib/client-registry.js +21867 -0
- package/lib/client-terminal.js +9361 -0
- package/lib/client.js +21867 -0
- package/lib/index.js +4556 -0
- package/lib/invariant.js +22 -0
- package/lib/types/agent-opens.d.ts +65 -0
- package/lib/types/agent-pty.d.ts +226 -0
- package/lib/types/browser-probe.d.ts +15 -0
- package/lib/types/bundle-route.d.ts +12 -0
- package/lib/types/client/BrowserView.d.ts +39 -0
- package/lib/types/client/DiffTab.d.ts +6 -0
- package/lib/types/client/DiffView.d.ts +51 -0
- package/lib/types/client/EditorHost.d.ts +13 -0
- package/lib/types/client/FileTree.d.ts +38 -0
- package/lib/types/client/FreeWindow.d.ts +29 -0
- package/lib/types/client/GitView.d.ts +10 -0
- package/lib/types/client/MarkdownHtml.d.ts +32 -0
- package/lib/types/client/OrphanedTab.d.ts +11 -0
- package/lib/types/client/PdfView.d.ts +6 -0
- package/lib/types/client/RenderBoundary.d.ts +32 -0
- package/lib/types/client/SandboxStatusBar.d.ts +10 -0
- package/lib/types/client/SideCardSection.d.ts +74 -0
- package/lib/types/client/SideChatView.d.ts +16 -0
- package/lib/types/client/Sidebar.d.ts +6 -0
- package/lib/types/client/SubagentView.d.ts +16 -0
- package/lib/types/client/TabBar.d.ts +54 -0
- package/lib/types/client/TerminalView.d.ts +24 -0
- package/lib/types/client/TextEditor.d.ts +10 -0
- package/lib/types/client/TreePanel.d.ts +24 -0
- package/lib/types/client/UploadOverlay.d.ts +20 -0
- package/lib/types/client/add-plugin-modal.d.ts +15 -0
- package/lib/types/client/api.d.ts +275 -0
- package/lib/types/client/binary-download.d.ts +12 -0
- package/lib/types/client/breakpoints.d.ts +17 -0
- package/lib/types/client/browser.d.ts +60 -0
- package/lib/types/client/builtins/index.d.ts +18 -0
- package/lib/types/client/builtins/tabs.d.ts +11 -0
- package/lib/types/client/builtins/viewers.d.ts +3 -0
- package/lib/types/client/chunk-loader.d.ts +131 -0
- package/lib/types/client/chunks/editor.d.ts +9 -0
- package/lib/types/client/chunks/mermaid.d.ts +10 -0
- package/lib/types/client/chunks/terminal.d.ts +9 -0
- package/lib/types/client/cm-themes.d.ts +24 -0
- package/lib/types/client/conversation-draft.d.ts +14 -0
- package/lib/types/client/desktop-env.d.ts +35 -0
- package/lib/types/client/editor-load.d.ts +66 -0
- package/lib/types/client/frame-batcher.d.ts +24 -0
- package/lib/types/client/icons.d.ts +59 -0
- package/lib/types/client/image-types.d.ts +3 -0
- package/lib/types/client/ime-guard.d.ts +36 -0
- package/lib/types/client/index.d.ts +25 -0
- package/lib/types/client/intercept.d.ts +46 -0
- package/lib/types/client/lang.d.ts +14 -0
- package/lib/types/client/layout-push.d.ts +15 -0
- package/lib/types/client/lazy-chunk.d.ts +25 -0
- package/lib/types/client/link-intercept.d.ts +42 -0
- package/lib/types/client/locales-ar.d.ts +18 -0
- package/lib/types/client/locales-de.d.ts +4 -0
- package/lib/types/client/locales-fr.d.ts +10 -0
- package/lib/types/client/locales-hi.d.ts +17 -0
- package/lib/types/client/locales-id.d.ts +15 -0
- package/lib/types/client/locales-it.d.ts +8 -0
- package/lib/types/client/locales-ja.d.ts +17 -0
- package/lib/types/client/locales-ko.d.ts +9 -0
- package/lib/types/client/locales-nl.d.ts +16 -0
- package/lib/types/client/locales-pl.d.ts +19 -0
- package/lib/types/client/locales-pt.d.ts +1 -0
- package/lib/types/client/locales-ru.d.ts +13 -0
- package/lib/types/client/locales-sv.d.ts +1 -0
- package/lib/types/client/locales-th.d.ts +17 -0
- package/lib/types/client/locales-tr.d.ts +17 -0
- package/lib/types/client/locales-vi.d.ts +17 -0
- package/lib/types/client/locales-zh-HK.d.ts +32 -0
- package/lib/types/client/locales-zh-MO.d.ts +32 -0
- package/lib/types/client/locales-zh-TW.d.ts +32 -0
- package/lib/types/client/locales.d.ts +452 -0
- package/lib/types/client/markdown-html.d.ts +96 -0
- package/lib/types/client/markdown-images.d.ts +45 -0
- package/lib/types/client/markdown-labels.d.ts +38 -0
- package/lib/types/client/md-toc.d.ts +4 -0
- package/lib/types/client/mermaid-blocks.d.ts +47 -0
- package/lib/types/client/mermaid-sanitize.d.ts +2 -0
- package/lib/types/client/mermaid.d.ts +12 -0
- package/lib/types/client/open-when-sized.d.ts +20 -0
- package/lib/types/client/open-with-settings.d.ts +4 -0
- package/lib/types/client/open-with.d.ts +87 -0
- package/lib/types/client/openpath-intercept.d.ts +85 -0
- package/lib/types/client/paths.d.ts +43 -0
- package/lib/types/client/pdf-types.d.ts +2 -0
- package/lib/types/client/pinned.d.ts +86 -0
- package/lib/types/client/plugin-settings.d.ts +8 -0
- package/lib/types/client/plugins-shared.d.ts +27 -0
- package/lib/types/client/plugins-tabs.d.ts +3 -0
- package/lib/types/client/plugins-viewers.d.ts +3 -0
- package/lib/types/client/prefs.d.ts +41 -0
- package/lib/types/client/produced-files.d.ts +29 -0
- package/lib/types/client/selection-payload.d.ts +27 -0
- package/lib/types/client/service.d.ts +439 -0
- package/lib/types/client/settings-nav-icon.d.ts +19 -0
- package/lib/types/client/shell-presets.d.ts +48 -0
- package/lib/types/client/sidechat-transcript.d.ts +103 -0
- package/lib/types/client/split-pane.d.ts +36 -0
- package/lib/types/client/state.d.ts +432 -0
- package/lib/types/client/subagent-detect.d.ts +54 -0
- package/lib/types/client/subagent-jobs.d.ts +63 -0
- package/lib/types/client/tab-content-memo.d.ts +40 -0
- package/lib/types/client/terminal-font.d.ts +78 -0
- package/lib/types/client/terminal-links.d.ts +119 -0
- package/lib/types/client/theme.d.ts +45 -0
- package/lib/types/client/titlebar-strip.d.ts +23 -0
- package/lib/types/client/upload.d.ts +69 -0
- package/lib/types/client/wco.d.ts +47 -0
- package/lib/types/config.d.ts +64 -0
- package/lib/types/context-types.d.ts +605 -0
- package/lib/types/fs-operations.d.ts +28 -0
- package/lib/types/fs-search.d.ts +24 -0
- package/lib/types/fs-tree.d.ts +51 -0
- package/lib/types/git.d.ts +124 -0
- package/lib/types/html-route.d.ts +59 -0
- package/lib/types/index.d.ts +47 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/jobs-routes.d.ts +45 -0
- package/lib/types/open-external.d.ts +24 -0
- package/lib/types/path-security.d.ts +20 -0
- package/lib/types/prefs-shared.d.ts +236 -0
- package/lib/types/pty-deps.d.ts +78 -0
- package/lib/types/pty-manager.d.ts +168 -0
- package/lib/types/sidechat-core.d.ts +172 -0
- package/lib/types/sidechat-routes.d.ts +30 -0
- package/lib/types/subagent-activity.d.ts +44 -0
- package/lib/types/subagent-live-route.d.ts +44 -0
- package/lib/types/tools.d.ts +34 -0
- package/lib/types/trust-fence.d.ts +24 -0
- package/lib/types/wire.d.ts +38 -0
- package/package.json +172 -0
- package/scripts/install.ps1 +306 -0
- package/scripts/install.sh +287 -0
- package/src/agent-opens.ts +265 -0
- package/src/agent-pty.ts +527 -0
- package/src/browser-probe.ts +26 -0
- package/src/bundle-route.ts +129 -0
- package/src/client/BrowserView.tsx +289 -0
- package/src/client/DiffTab.tsx +114 -0
- package/src/client/DiffView.tsx +305 -0
- package/src/client/EditorHost.tsx +558 -0
- package/src/client/FileTree.tsx +669 -0
- package/src/client/FreeWindow.tsx +337 -0
- package/src/client/GitView.tsx +717 -0
- package/src/client/MarkdownHtml.tsx +296 -0
- package/src/client/OrphanedTab.tsx +27 -0
- package/src/client/PdfView.tsx +110 -0
- package/src/client/RenderBoundary.tsx +49 -0
- package/src/client/SandboxStatusBar.tsx +60 -0
- package/src/client/SideCardSection.module.css +960 -0
- package/src/client/SideCardSection.tsx +1163 -0
- package/src/client/SideChatView.module.css +437 -0
- package/src/client/SideChatView.tsx +645 -0
- package/src/client/Sidebar.tsx +1370 -0
- package/src/client/SubagentView.module.css +547 -0
- package/src/client/SubagentView.tsx +883 -0
- package/src/client/TabBar.tsx +365 -0
- package/src/client/TerminalView.tsx +424 -0
- package/src/client/TextEditor.tsx +468 -0
- package/src/client/TreePanel.tsx +275 -0
- package/src/client/UploadOverlay.tsx +62 -0
- package/src/client/add-plugin-modal.tsx +199 -0
- package/src/client/api.ts +357 -0
- package/src/client/binary-download.tsx +23 -0
- package/src/client/breakpoints.ts +61 -0
- package/src/client/browser.ts +167 -0
- package/src/client/builtins/index.ts +37 -0
- package/src/client/builtins/tabs.tsx +340 -0
- package/src/client/builtins/viewers.tsx +125 -0
- package/src/client/chunk-loader.ts +358 -0
- package/src/client/chunks/editor.tsx +9 -0
- package/src/client/chunks/mermaid.tsx +10 -0
- package/src/client/chunks/terminal.tsx +9 -0
- package/src/client/cm-themes.ts +129 -0
- package/src/client/conversation-draft.ts +29 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/desktop-env.ts +72 -0
- package/src/client/editor-load.ts +92 -0
- package/src/client/frame-batcher.ts +56 -0
- package/src/client/icons.tsx +152 -0
- package/src/client/image-types.ts +8 -0
- package/src/client/ime-guard.ts +47 -0
- package/src/client/index.tsx +427 -0
- package/src/client/intercept.tsx +188 -0
- package/src/client/lang.ts +225 -0
- package/src/client/layout-push.ts +24 -0
- package/src/client/layout.css +90 -0
- package/src/client/lazy-chunk.tsx +89 -0
- package/src/client/link-intercept.ts +73 -0
- package/src/client/locales-ar.ts +368 -0
- package/src/client/locales-de.ts +353 -0
- package/src/client/locales-fr.ts +360 -0
- package/src/client/locales-hi.ts +367 -0
- package/src/client/locales-id.ts +365 -0
- package/src/client/locales-it.ts +358 -0
- package/src/client/locales-ja.ts +367 -0
- package/src/client/locales-ko.ts +359 -0
- package/src/client/locales-nl.ts +365 -0
- package/src/client/locales-pl.ts +369 -0
- package/src/client/locales-pt.ts +350 -0
- package/src/client/locales-ru.ts +365 -0
- package/src/client/locales-sv.ts +350 -0
- package/src/client/locales-th.ts +367 -0
- package/src/client/locales-tr.ts +367 -0
- package/src/client/locales-vi.ts +367 -0
- package/src/client/locales-zh-HK.ts +382 -0
- package/src/client/locales-zh-MO.ts +382 -0
- package/src/client/locales-zh-TW.ts +382 -0
- package/src/client/locales.ts +894 -0
- package/src/client/markdown-html.ts +331 -0
- package/src/client/markdown-images.ts +137 -0
- package/src/client/markdown-labels.tsx +52 -0
- package/src/client/md-toc.tsx +127 -0
- package/src/client/mermaid-blocks.ts +110 -0
- package/src/client/mermaid-sanitize.ts +93 -0
- package/src/client/mermaid.tsx +406 -0
- package/src/client/open-when-sized.ts +43 -0
- package/src/client/open-with-settings.tsx +135 -0
- package/src/client/open-with.ts +221 -0
- package/src/client/openpath-intercept.ts +143 -0
- package/src/client/paths.ts +63 -0
- package/src/client/pdf-types.ts +4 -0
- package/src/client/pinned.ts +183 -0
- package/src/client/plugin-settings.ts +43 -0
- package/src/client/plugins-shared.ts +29 -0
- package/src/client/plugins-tabs.ts +96 -0
- package/src/client/plugins-viewers.ts +30 -0
- package/src/client/prefs.ts +210 -0
- package/src/client/produced-files.ts +120 -0
- package/src/client/selection-payload.ts +86 -0
- package/src/client/service.ts +859 -0
- package/src/client/settings-nav-icon.ts +45 -0
- package/src/client/shell-presets.ts +83 -0
- package/src/client/sidebar.module.css +2788 -0
- package/src/client/sidechat-transcript.ts +321 -0
- package/src/client/split-pane.tsx +331 -0
- package/src/client/state.ts +1497 -0
- package/src/client/subagent-detect.ts +137 -0
- package/src/client/subagent-jobs.ts +161 -0
- package/src/client/tab-content-memo.ts +55 -0
- package/src/client/terminal-font.ts +224 -0
- package/src/client/terminal-links.ts +194 -0
- package/src/client/theme.ts +110 -0
- package/src/client/titlebar-strip.ts +37 -0
- package/src/client/upload.ts +187 -0
- package/src/client/wco.ts +120 -0
- package/src/config.ts +154 -0
- package/src/context-types.ts +611 -0
- package/src/fs-operations.ts +95 -0
- package/src/fs-search.ts +113 -0
- package/src/fs-tree.ts +158 -0
- package/src/git.ts +446 -0
- package/src/html-route.ts +106 -0
- package/src/index.ts +1291 -0
- package/src/invariant.ts +32 -0
- package/src/jobs-routes.ts +257 -0
- package/src/open-external.ts +90 -0
- package/src/path-security.ts +75 -0
- package/src/prefs-shared.ts +280 -0
- package/src/pty-deps.ts +240 -0
- package/src/pty-manager.ts +376 -0
- package/src/sidechat-core.ts +468 -0
- package/src/sidechat-routes.ts +344 -0
- package/src/subagent-activity.ts +91 -0
- package/src/subagent-live-route.ts +93 -0
- package/src/tools.ts +482 -0
- package/src/trust-fence.ts +84 -0
- package/src/wire.ts +100 -0
package/src/invariant.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `dsh-coding-sidebar`.
|
|
3
|
+
* @module dsh-coding-sidebar/invariant
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* jscpd:ignore-start */
|
|
7
|
+
import type { Context } from './context-types.ts'
|
|
8
|
+
|
|
9
|
+
const PACKAGE_NAME = 'dsh-coding-sidebar'
|
|
10
|
+
|
|
11
|
+
/** Cordis companion plugin name. */
|
|
12
|
+
export const name = 'dsh-coding-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-coding-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,75 @@
|
|
|
1
|
+
/** Filesystem path guards shared by sidebar APIs that access a session workspace. */
|
|
2
|
+
import { realpath } from 'node:fs/promises'
|
|
3
|
+
import { basename, dirname, join } from 'node:path'
|
|
4
|
+
import { isWithin, requireAbsolute } from './fs-tree.ts'
|
|
5
|
+
import { SidebarError } from './wire.ts'
|
|
6
|
+
|
|
7
|
+
/** Resolve a path and convert filesystem resolution failures to an API error. */
|
|
8
|
+
async function resolveRealPath(path: string, label: string): Promise<string> {
|
|
9
|
+
try {
|
|
10
|
+
return await realpath(path)
|
|
11
|
+
} catch (error) {
|
|
12
|
+
throw new SidebarError('fs-error', `cannot resolve ${label} "${path}": ${error instanceof Error ? error.message : String(error)}`, 400)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Reject a resolved path whose real filesystem target escapes the workspace. */
|
|
17
|
+
function assertWithinWorkspace(workspace: string, target: string): void {
|
|
18
|
+
if (!isWithin(workspace, target)) {
|
|
19
|
+
throw new SidebarError('forbidden', `path "${target}" is outside workspace`, 403)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve an existing workspace path through symlinks and enforce containment.
|
|
25
|
+
*
|
|
26
|
+
* @param cwd - Session workspace directory.
|
|
27
|
+
* @param target - Client-supplied absolute path.
|
|
28
|
+
* @returns The canonical absolute path used for the filesystem operation.
|
|
29
|
+
*/
|
|
30
|
+
export async function ensureWorkspacePath(cwd: string, target: string): Promise<string> {
|
|
31
|
+
const absolute = requireAbsolute(target)
|
|
32
|
+
const [realCwd, realTarget] = await Promise.all([
|
|
33
|
+
resolveRealPath(cwd, 'workspace'),
|
|
34
|
+
resolveRealPath(absolute, 'target'),
|
|
35
|
+
])
|
|
36
|
+
assertWithinWorkspace(realCwd, realTarget)
|
|
37
|
+
return realTarget
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validate a write destination, including destinations that do not exist yet.
|
|
42
|
+
* Existing targets are resolved to catch symlinks; missing targets are checked
|
|
43
|
+
* against the nearest existing ancestor before the caller creates or renames.
|
|
44
|
+
* The returned path is rebuilt from that canonical ancestor, so an existing
|
|
45
|
+
* symlink is never left in the path passed to the write operation.
|
|
46
|
+
*
|
|
47
|
+
* @param cwd - Session workspace directory.
|
|
48
|
+
* @param target - Client-supplied absolute destination path.
|
|
49
|
+
* @returns A canonical path for an existing target or its nearest existing ancestor.
|
|
50
|
+
*/
|
|
51
|
+
export async function ensureWorkspaceWritePath(cwd: string, target: string): Promise<string> {
|
|
52
|
+
const absolute = requireAbsolute(target)
|
|
53
|
+
const realCwd = await resolveRealPath(cwd, 'workspace')
|
|
54
|
+
let existingPath = absolute
|
|
55
|
+
const missingSegments: string[] = []
|
|
56
|
+
|
|
57
|
+
for (;;) {
|
|
58
|
+
try {
|
|
59
|
+
const realTarget = await realpath(existingPath)
|
|
60
|
+
assertWithinWorkspace(realCwd, realTarget)
|
|
61
|
+
return missingSegments.reduce((path, segment) => join(path, segment), realTarget)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
64
|
+
if (error instanceof SidebarError) throw error
|
|
65
|
+
throw new SidebarError('fs-error', `cannot resolve target "${existingPath}": ${error instanceof Error ? error.message : String(error)}`, 400)
|
|
66
|
+
}
|
|
67
|
+
const parent = dirname(existingPath)
|
|
68
|
+
if (parent === existingPath) {
|
|
69
|
+
throw new SidebarError('fs-error', `cannot resolve target "${absolute}"`, 400)
|
|
70
|
+
}
|
|
71
|
+
missingSegments.unshift(basename(existingPath))
|
|
72
|
+
existingPath = parent
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|