@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.
- package/LICENSE +21 -0
- package/README.md +0 -0
- package/README_EN.md +0 -0
- package/cordis.patch.yml +49 -0
- package/lib/client-editor.js +34651 -0
- package/lib/client-mermaid.js +200629 -0
- package/lib/client-registry.js +13388 -0
- package/lib/client-terminal.js +8904 -0
- package/lib/client.js +13388 -0
- package/lib/index.js +3959 -0
- package/lib/invariant.js +22 -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 +22 -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 +12 -0
- package/lib/types/client/FileTree.d.ts +36 -0
- package/lib/types/client/GitView.d.ts +8 -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 +43 -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 +23 -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 +268 -0
- package/lib/types/client/binary-download.d.ts +12 -0
- package/lib/types/client/breakpoints.d.ts +12 -0
- package/lib/types/client/browser.d.ts +50 -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 +124 -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/editable-state.d.ts +27 -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 +57 -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 +19 -0
- package/lib/types/client/intercept.d.ts +33 -0
- package/lib/types/client/lang.d.ts +12 -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.d.ts +366 -0
- package/lib/types/client/mermaid-blocks.d.ts +41 -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 +41 -0
- package/lib/types/client/paths.d.ts +35 -0
- package/lib/types/client/pdf-types.d.ts +2 -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 +23 -0
- package/lib/types/client/selection-payload.d.ts +27 -0
- package/lib/types/client/service.d.ts +440 -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 +30 -0
- package/lib/types/client/state.d.ts +359 -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 +33 -0
- package/lib/types/client/terminal-font.d.ts +20 -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 +603 -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 +82 -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/prefs-shared.d.ts +225 -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 +31 -0
- package/lib/types/trust-fence.d.ts +24 -0
- package/lib/types/wire.d.ts +38 -0
- package/package.json +173 -0
- package/scripts/install.ps1 +289 -0
- package/scripts/install.sh +287 -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 +253 -0
- package/src/client/DiffTab.tsx +110 -0
- package/src/client/DiffView.tsx +305 -0
- package/src/client/EditorHost.tsx +525 -0
- package/src/client/FileTree.tsx +653 -0
- package/src/client/GitView.tsx +558 -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 +1152 -0
- package/src/client/SideChatView.module.css +437 -0
- package/src/client/SideChatView.tsx +644 -0
- package/src/client/Sidebar.tsx +1148 -0
- package/src/client/SubagentView.module.css +547 -0
- package/src/client/SubagentView.tsx +883 -0
- package/src/client/TabBar.tsx +305 -0
- package/src/client/TerminalView.tsx +383 -0
- package/src/client/TextEditor.tsx +500 -0
- package/src/client/TreePanel.tsx +273 -0
- package/src/client/UploadOverlay.tsx +62 -0
- package/src/client/add-plugin-modal.tsx +199 -0
- package/src/client/api.ts +335 -0
- package/src/client/binary-download.tsx +23 -0
- package/src/client/breakpoints.ts +51 -0
- package/src/client/browser.ts +119 -0
- package/src/client/builtins/index.ts +37 -0
- package/src/client/builtins/tabs.tsx +331 -0
- package/src/client/builtins/viewers.tsx +125 -0
- package/src/client/chunk-loader.ts +351 -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/editable-state.ts +70 -0
- package/src/client/editor-load.ts +92 -0
- package/src/client/frame-batcher.ts +56 -0
- package/src/client/icons.tsx +161 -0
- package/src/client/image-types.ts +8 -0
- package/src/client/ime-guard.ts +47 -0
- package/src/client/index.tsx +369 -0
- package/src/client/intercept.tsx +110 -0
- package/src/client/lang.ts +103 -0
- package/src/client/layout.css +94 -0
- package/src/client/lazy-chunk.tsx +89 -0
- package/src/client/link-intercept.ts +73 -0
- package/src/client/locales.ts +734 -0
- package/src/client/mermaid-blocks.ts +110 -0
- package/src/client/mermaid-sanitize.ts +93 -0
- package/src/client/mermaid.tsx +401 -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 +62 -0
- package/src/client/paths.ts +47 -0
- package/src/client/pdf-types.ts +4 -0
- package/src/client/plugin-settings.ts +43 -0
- package/src/client/plugins-shared.ts +29 -0
- package/src/client/plugins-tabs.ts +87 -0
- package/src/client/plugins-viewers.ts +30 -0
- package/src/client/prefs.ts +207 -0
- package/src/client/produced-files.ts +91 -0
- package/src/client/selection-payload.ts +86 -0
- package/src/client/service.ts +838 -0
- package/src/client/settings-nav-icon.ts +45 -0
- package/src/client/shell-presets.ts +83 -0
- package/src/client/sidebar.module.css +2577 -0
- package/src/client/sidechat-transcript.ts +321 -0
- package/src/client/split-pane.tsx +322 -0
- package/src/client/state.ts +1175 -0
- package/src/client/subagent-detect.ts +137 -0
- package/src/client/subagent-jobs.ts +161 -0
- package/src/client/tab-content-memo.ts +46 -0
- package/src/client/terminal-font.ts +28 -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 +155 -0
- package/src/context-types.ts +610 -0
- package/src/fs-operations.ts +99 -0
- package/src/fs-search.ts +86 -0
- package/src/fs-tree.ts +158 -0
- package/src/git.ts +244 -0
- package/src/html-route.ts +106 -0
- package/src/index.ts +1132 -0
- package/src/invariant.ts +32 -0
- package/src/jobs-routes.ts +257 -0
- package/src/open-external.ts +90 -0
- package/src/prefs-shared.ts +268 -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 +479 -0
- package/src/trust-fence.ts +77 -0
- package/src/wire.ts +100 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side Chat routes of the /sidebar JSON API ('sidechat.start' /
|
|
3
|
+
* 'sidechat.prompt' / 'sidechat.cancel' / 'sidechat.dispose').
|
|
4
|
+
*
|
|
5
|
+
* A side thread is a child session the plugin creates ITSELF with a custom
|
|
6
|
+
* seed — the parent's full event log up to the click moment, honestly closed
|
|
7
|
+
* at an in-progress turn (see sidechat-core.ts). The child is marked
|
|
8
|
+
* `origin: 'subagent'` so the main session list hides it, and EVERY
|
|
9
|
+
* operation goes through these routes because the generic session RPCs are
|
|
10
|
+
* fenced away from subagent-origin identities (the api-remotes
|
|
11
|
+
* agent-lookup ownership fence). No DSH source is touched:
|
|
12
|
+
*
|
|
13
|
+
* - creation uses the public AgentRegistry.create seam (the same one
|
|
14
|
+
* api-proxy's session.fork and the subagent fork provider use), with the
|
|
15
|
+
* parent's preset composition and provider/model selection so the child's
|
|
16
|
+
* first request shares the parent's token prefix (provider-side prefix
|
|
17
|
+
* cache reuse);
|
|
18
|
+
* - the first prompt (boundary + question) and every follow-up are admitted
|
|
19
|
+
* with the stock `agent.followup`;
|
|
20
|
+
* - a cold thread (DSH restart, or a closed thread) is resumed with
|
|
21
|
+
* AgentRegistry.resume, composing the preset the child recorded.
|
|
22
|
+
*/
|
|
23
|
+
import { randomUUID } from 'node:crypto'
|
|
24
|
+
import { createUserMessage, type ContentBlock, type UserMessage } from '@deepseek-ai/dsh-llm'
|
|
25
|
+
import type { Agent, AgentSetup, CreateAgentOptions, ResumeAgentOptions } from '@deepseek-ai/dsh-agent'
|
|
26
|
+
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
|
|
27
|
+
import type { Context as CordisContext } from '@deepseek-ai/cordis'
|
|
28
|
+
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
|
29
|
+
import type {
|
|
30
|
+
Context,
|
|
31
|
+
SidebarAgentPresetsService,
|
|
32
|
+
SidebarSessionPersistenceService,
|
|
33
|
+
SidebarSessionTitleService,
|
|
34
|
+
} from './context-types.ts'
|
|
35
|
+
import {
|
|
36
|
+
boundaryDelivered,
|
|
37
|
+
buildSidechatInheritance,
|
|
38
|
+
resolvePresetId,
|
|
39
|
+
SIDE_BOUNDARY_PROMPT,
|
|
40
|
+
SIDE_INJECTION_PLUGIN,
|
|
41
|
+
SIDE_NEW_THREAD_TITLE,
|
|
42
|
+
sideLabel,
|
|
43
|
+
type SeedEvent,
|
|
44
|
+
type SidechatLogEvent,
|
|
45
|
+
type SidechatThreadInfo,
|
|
46
|
+
} from './sidechat-core.ts'
|
|
47
|
+
import { requireString, SidebarError } from './wire.ts'
|
|
48
|
+
|
|
49
|
+
/** The five Side Chat routes of the sidebar API (wire method names). */
|
|
50
|
+
export interface SidechatRoutes {
|
|
51
|
+
/** Create a side thread child seeded with the parent's log up to now.
|
|
52
|
+
* `question` is optional: empty creates an EMPTY thread (Codex-style
|
|
53
|
+
* immediate create); the first `sidechat.prompt` then carries the
|
|
54
|
+
* boundary + snapshot and earns the thread its real label. */
|
|
55
|
+
'sidechat.start'(payload: unknown): Promise<{ childId: string }>
|
|
56
|
+
/** Deliver one follow-up message to a thread (live, or cold-resumed). */
|
|
57
|
+
'sidechat.prompt'(payload: unknown): Promise<{ accepted: true }>
|
|
58
|
+
/** Abort the thread's running turn (queued work is preserved). */
|
|
59
|
+
'sidechat.cancel'(payload: unknown): Promise<{ accepted: true }>
|
|
60
|
+
/** Release the thread's live agent (session and history stay persisted). */
|
|
61
|
+
'sidechat.dispose'(payload: unknown): Promise<{ accepted: true }>
|
|
62
|
+
/** Live state + agent identity for the thread header. */
|
|
63
|
+
'sidechat.info'(payload: unknown): Promise<SidechatThreadInfo>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Timeout guarding the create call (the registry detaches it before the
|
|
67
|
+
* handle becomes visible, so the child is never cancelled by it). */
|
|
68
|
+
const CREATE_TIMEOUT_MS = 15_000
|
|
69
|
+
|
|
70
|
+
/** Per-activation disposers of created thread agents (the dispose route
|
|
71
|
+
* releases them; the session and its history always stay persisted). */
|
|
72
|
+
const threadDisposers = new Map<string, () => Promise<void>>()
|
|
73
|
+
|
|
74
|
+
/** The in-progress-turn snapshot captured at creation of an EMPTY thread,
|
|
75
|
+
* waiting to ride the first prompt (lost on a host restart — the boundary
|
|
76
|
+
* prompt is then delivered alone, a logged degradation). */
|
|
77
|
+
const pendingSnapshots = new Map<string, string>()
|
|
78
|
+
|
|
79
|
+
/** Resolve the parent's preset and build the child's composition setup
|
|
80
|
+
* (mirror of api-proxy's composeAgent minus the model-selection install —
|
|
81
|
+
* the child carries the parent's provider/model in agentOptions). */
|
|
82
|
+
async function composeChildSetup(
|
|
83
|
+
ctx: Context,
|
|
84
|
+
presetId: string | undefined,
|
|
85
|
+
): Promise<{ agentPreset?: string; setup: AgentSetup }> {
|
|
86
|
+
const presets = ctx.get('agentPresets') as SidebarAgentPresetsService | undefined
|
|
87
|
+
if (presets === undefined) {
|
|
88
|
+
return { setup: () => Promise.resolve() }
|
|
89
|
+
}
|
|
90
|
+
const resolved = await presets.resolve(presetId)
|
|
91
|
+
return {
|
|
92
|
+
agentPreset: resolved.id,
|
|
93
|
+
setup: async (agentCtx: CordisContext) => { await presets.mount(agentCtx, resolved.id) },
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Build the cold-resume setup from the thread's PERSISTED record (the
|
|
98
|
+
* recorded preset wins, newest selection event first). */
|
|
99
|
+
async function composePersistedSetup(
|
|
100
|
+
ctx: Context,
|
|
101
|
+
childId: string,
|
|
102
|
+
): Promise<AgentSetup> {
|
|
103
|
+
const persistence = ctx.get('sessionPersistence') as SidebarSessionPersistenceService | undefined
|
|
104
|
+
if (persistence === undefined) {
|
|
105
|
+
return () => Promise.resolve()
|
|
106
|
+
}
|
|
107
|
+
const inspected = await persistence.inspect(childId)
|
|
108
|
+
const presetId = resolvePresetId(inspected.meta, inspected.events)
|
|
109
|
+
const presets = ctx.get('agentPresets') as SidebarAgentPresetsService | undefined
|
|
110
|
+
if (presets === undefined || presetId === undefined) {
|
|
111
|
+
return () => Promise.resolve()
|
|
112
|
+
}
|
|
113
|
+
const resolved = await presets.resolve(presetId)
|
|
114
|
+
return async (agentCtx: CordisContext) => { await presets.mount(agentCtx, resolved.id) }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** One text-block prompt (the thread boundary + question, or a follow-up). */
|
|
118
|
+
function textPrompt(text: string): ContentBlock[] {
|
|
119
|
+
return [{ type: 'text', text }]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Admit one user message to a live agent through the stock followup path. */
|
|
123
|
+
function admitFollowup(agent: Agent, blocks: ContentBlock[]): void {
|
|
124
|
+
const message: UserMessage = createUserMessage({ content: blocks, source: { kind: 'user' } })
|
|
125
|
+
agent.followup(message)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Deliver the thread's FIRST contact as TWO log-separated messages: the
|
|
130
|
+
* boundary prompt (+ the parked in-progress snapshot) rides `agent.inject`
|
|
131
|
+
* — queued model-facing context that does NOT wake the driver and is
|
|
132
|
+
* claimed FIRST at the opening step (Inbox.claim drains next-step before
|
|
133
|
+
* next-turn) — and the user's question is the follow-up that wakes it. The
|
|
134
|
+
* log therefore records two user/message events (injection, then question)
|
|
135
|
+
* instead of one wrapped blob: the transcript shows the question as a user
|
|
136
|
+
* bubble and collapses the injection as a context row. The injection source
|
|
137
|
+
* is stamped `kind: 'plugin'` so recognition is structural; its text still
|
|
138
|
+
* opens with SIDE_BOUNDARY_PREFIX, keeping boundaryDelivered intact.
|
|
139
|
+
*/
|
|
140
|
+
function admitFirstContact(agent: Agent, injectionText: string, question: string): void {
|
|
141
|
+
agent.inject(createUserMessage({
|
|
142
|
+
content: textPrompt(injectionText),
|
|
143
|
+
source: { kind: 'plugin', plugin: SIDE_INJECTION_PLUGIN },
|
|
144
|
+
}))
|
|
145
|
+
admitFollowup(agent, textPrompt(question))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The live thread agent, or undefined (cold — the caller resumes). */
|
|
149
|
+
function liveThreadAgent(ctx: Context, childId: string): Agent | undefined {
|
|
150
|
+
const agents = ctx.get('agents') as { get(id: string): Agent | undefined } | undefined
|
|
151
|
+
return agents?.get(childId)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Build the Side Chat routes (all optional services degrade to a wire
|
|
155
|
+
* error the tab surfaces inline). The record keys are the FULL wire method
|
|
156
|
+
* names the /sidebar/api dispatcher looks up (`api[method]`). */
|
|
157
|
+
export function buildSidechatApi(ctx: Context): SidechatRoutes {
|
|
158
|
+
return {
|
|
159
|
+
'sidechat.start': async (payload: unknown) => {
|
|
160
|
+
const sessionId = requireString(payload, 'sessionId')
|
|
161
|
+
const rawQuestion = (payload as { question?: unknown }).question
|
|
162
|
+
const question = typeof rawQuestion === 'string' ? rawQuestion.trim() : ''
|
|
163
|
+
const parent = liveThreadAgent(ctx, sessionId)
|
|
164
|
+
if (parent === undefined) {
|
|
165
|
+
throw new SidebarError('sidechat-error', `parent session "${sessionId}" is not running`, 409)
|
|
166
|
+
}
|
|
167
|
+
const parentSession = parent.session
|
|
168
|
+
const inheritance = buildSidechatInheritance(
|
|
169
|
+
parentSession.events as unknown as readonly SidechatLogEvent[],
|
|
170
|
+
)
|
|
171
|
+
const { agentPreset, setup } = await composeChildSetup(
|
|
172
|
+
ctx,
|
|
173
|
+
resolvePresetId(parentSession.header, parentSession.events),
|
|
174
|
+
)
|
|
175
|
+
const childId = `session-${randomUUID()}` as SessionId
|
|
176
|
+
const label = question === '' ? SIDE_NEW_THREAD_TITLE : sideLabel(question)
|
|
177
|
+
// Honest catalog citizenship: the durable descriptor keeps the thread
|
|
178
|
+
// a HEALTHY row in the host's subagents.list — a cold child without
|
|
179
|
+
// one is deterministically rendered as a 'corrupt' diagnostic. The
|
|
180
|
+
// SubagentView filters the 'Side: ' label out, so the topology UI
|
|
181
|
+
// stays noise-free; the row only serves enumeration correctness.
|
|
182
|
+
const descriptor = snapshotSubagentDescriptor({
|
|
183
|
+
mode: 'continuable',
|
|
184
|
+
provider: 'sidechat',
|
|
185
|
+
label,
|
|
186
|
+
...(parent.options.provider === undefined ? {} : { agentProvider: parent.options.provider }),
|
|
187
|
+
...(parent.options.model === undefined ? {} : { agentModel: parent.options.model }),
|
|
188
|
+
})
|
|
189
|
+
const descriptorEvent: SeedEvent = {
|
|
190
|
+
type: 'subagent/descriptor',
|
|
191
|
+
seq: inheritance.seed.length,
|
|
192
|
+
time: Date.now(),
|
|
193
|
+
data: descriptor as unknown as Record<string, unknown>,
|
|
194
|
+
}
|
|
195
|
+
const seed = [...inheritance.seed, descriptorEvent]
|
|
196
|
+
const options: CreateAgentOptions = {
|
|
197
|
+
sessionId: childId,
|
|
198
|
+
meta: {
|
|
199
|
+
...(parentSession.header.cwd === undefined ? {} : { cwd: parentSession.header.cwd }),
|
|
200
|
+
parentSession: parentSession.id,
|
|
201
|
+
seedLength: seed.length,
|
|
202
|
+
origin: 'subagent',
|
|
203
|
+
delegationDepth: (parentSession.header.delegationDepth ?? 0) + 1,
|
|
204
|
+
...(agentPreset === undefined ? {} : { agentPreset }),
|
|
205
|
+
},
|
|
206
|
+
seed: seed as unknown as readonly SessionEvent[],
|
|
207
|
+
agentOptions: { ...parent.options },
|
|
208
|
+
setup,
|
|
209
|
+
signal: AbortSignal.timeout(CREATE_TIMEOUT_MS),
|
|
210
|
+
}
|
|
211
|
+
const agents = ctx.get('agents') as { create(options: CreateAgentOptions): Promise<{ agent: Agent; dispose(): Promise<void> }> } | undefined
|
|
212
|
+
if (agents?.create === undefined) {
|
|
213
|
+
throw new SidebarError('sidechat-error', 'the agents service is unavailable', 503)
|
|
214
|
+
}
|
|
215
|
+
let handle: { agent: Agent; dispose(): Promise<void> }
|
|
216
|
+
try {
|
|
217
|
+
handle = await agents.create(options)
|
|
218
|
+
} catch (error) {
|
|
219
|
+
throw new SidebarError('sidechat-error', `thread creation failed: ${error instanceof Error ? error.message : String(error)}`, 500)
|
|
220
|
+
}
|
|
221
|
+
threadDisposers.set(childId, () => handle.dispose())
|
|
222
|
+
// Pin the thread label so the client can identify its threads by
|
|
223
|
+
// title prefix (the rename is a live-session op, no RPC fence).
|
|
224
|
+
const titles = ctx.get('sessionTitle') as SidebarSessionTitleService | undefined
|
|
225
|
+
const pinTitle = (label: string): void => {
|
|
226
|
+
if (titles === undefined) return
|
|
227
|
+
try {
|
|
228
|
+
titles.rename(handle.agent.session, label)
|
|
229
|
+
} catch {
|
|
230
|
+
// Keep the auto-generated title; the thread stays usable.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (question === '') {
|
|
234
|
+
// Codex-style immediate create: no prompt yet — the composer owns
|
|
235
|
+
// the first message; the snapshot waits for it.
|
|
236
|
+
if (inheritance.snapshot !== null) pendingSnapshots.set(childId, inheritance.snapshot)
|
|
237
|
+
pinTitle(SIDE_NEW_THREAD_TITLE)
|
|
238
|
+
} else {
|
|
239
|
+
const promptParts = [SIDE_BOUNDARY_PROMPT]
|
|
240
|
+
if (inheritance.snapshot !== null) promptParts.push(inheritance.snapshot)
|
|
241
|
+
admitFirstContact(handle.agent, promptParts.join('\n\n'), question)
|
|
242
|
+
pinTitle(sideLabel(question))
|
|
243
|
+
}
|
|
244
|
+
return { childId }
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
'sidechat.prompt': async (payload: unknown) => {
|
|
248
|
+
const childId = requireString(payload, 'childId')
|
|
249
|
+
const text = requireString(payload, 'text').trim()
|
|
250
|
+
if (text === '') {
|
|
251
|
+
throw new SidebarError('bad-request', 'text is required')
|
|
252
|
+
}
|
|
253
|
+
let agent = liveThreadAgent(ctx, childId)
|
|
254
|
+
if (agent === undefined) {
|
|
255
|
+
// Cold thread: resume the persisted session under its recorded
|
|
256
|
+
// composition, then deliver the follow-up.
|
|
257
|
+
const agents = ctx.get('agents') as { resume(options: ResumeAgentOptions): Promise<{ agent: Agent; dispose(): Promise<void> }> } | undefined
|
|
258
|
+
if (agents?.resume === undefined) {
|
|
259
|
+
throw new SidebarError('sidechat-error', 'the agents service is unavailable', 503)
|
|
260
|
+
}
|
|
261
|
+
const setup = await composePersistedSetup(ctx, childId)
|
|
262
|
+
try {
|
|
263
|
+
const handle = await agents.resume({ resumeSessionId: childId as SessionId, setup })
|
|
264
|
+
threadDisposers.set(childId, () => handle.dispose())
|
|
265
|
+
agent = handle.agent
|
|
266
|
+
} catch (error) {
|
|
267
|
+
throw new SidebarError('sidechat-error', `thread resume failed: ${error instanceof Error ? error.message : String(error)}`, 500)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (boundaryDelivered(agent.session.events as unknown as readonly SidechatLogEvent[])) {
|
|
271
|
+
admitFollowup(agent, textPrompt(text))
|
|
272
|
+
} else {
|
|
273
|
+
// First message of an immediately-created thread: it carries the
|
|
274
|
+
// boundary (+ the snapshot parked at creation, if still around)
|
|
275
|
+
// and earns the thread its real label.
|
|
276
|
+
const parts = [SIDE_BOUNDARY_PROMPT]
|
|
277
|
+
const snapshot = pendingSnapshots.get(childId)
|
|
278
|
+
pendingSnapshots.delete(childId)
|
|
279
|
+
if (snapshot !== undefined) parts.push(snapshot)
|
|
280
|
+
admitFirstContact(agent, parts.join('\n\n'), text)
|
|
281
|
+
const titles = ctx.get('sessionTitle') as SidebarSessionTitleService | undefined
|
|
282
|
+
if (titles !== undefined) {
|
|
283
|
+
try {
|
|
284
|
+
titles.rename(agent.session, sideLabel(text))
|
|
285
|
+
} catch {
|
|
286
|
+
// Keep the placeholder title; the thread stays usable.
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return { accepted: true as const }
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
'sidechat.cancel': async (payload: unknown) => {
|
|
294
|
+
const childId = requireString(payload, 'childId')
|
|
295
|
+
const agent = liveThreadAgent(ctx, childId)
|
|
296
|
+
if (agent !== undefined) {
|
|
297
|
+
agent.cancel({ kind: 'user' }, { keepInbox: true })
|
|
298
|
+
}
|
|
299
|
+
return { accepted: true as const }
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
'sidechat.dispose': async (payload: unknown) => {
|
|
303
|
+
const childId = requireString(payload, 'childId')
|
|
304
|
+
pendingSnapshots.delete(childId)
|
|
305
|
+
const dispose = threadDisposers.get(childId)
|
|
306
|
+
if (dispose !== undefined) {
|
|
307
|
+
threadDisposers.delete(childId)
|
|
308
|
+
try {
|
|
309
|
+
await dispose()
|
|
310
|
+
} catch {
|
|
311
|
+
// The agent may already be gone (restart); the session persists.
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { accepted: true as const }
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
'sidechat.info': async (payload: unknown) => {
|
|
318
|
+
const childId = requireString(payload, 'childId')
|
|
319
|
+
const agent = liveThreadAgent(ctx, childId)
|
|
320
|
+
if (agent !== undefined) {
|
|
321
|
+
const preset = agent.session.header.agentPreset
|
|
322
|
+
return {
|
|
323
|
+
live: true,
|
|
324
|
+
status: agent.status,
|
|
325
|
+
...(agent.options.provider === undefined ? {} : { provider: agent.options.provider }),
|
|
326
|
+
...(agent.options.model === undefined ? {} : { model: agent.options.model }),
|
|
327
|
+
...(preset === undefined ? {} : { preset }),
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Cold thread: only the persisted preset is worth reading back.
|
|
331
|
+
const persistence = ctx.get('sessionPersistence') as SidebarSessionPersistenceService | undefined
|
|
332
|
+
if (persistence !== undefined) {
|
|
333
|
+
try {
|
|
334
|
+
const inspected = await persistence.inspect(childId)
|
|
335
|
+
const preset = resolvePresetId(inspected.meta, inspected.events)
|
|
336
|
+
return { live: false, ...(preset === undefined ? {} : { preset }) }
|
|
337
|
+
} catch {
|
|
338
|
+
// Unknown/gone session: report a bare cold info.
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return { live: false }
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure derivation of the compact LIVE line shown on a running subagent card:
|
|
3
|
+
* the last text output and the last tool call of the child's session events.
|
|
4
|
+
* The host batch route feeds raw session events into this parser; the client
|
|
5
|
+
* only receives the already-folded `LastActivity` map. Renders nothing
|
|
6
|
+
* itself — the SubagentView component turns this into the card's status
|
|
7
|
+
* lines. Kept framework-free so the parser is unit-testable in the node
|
|
8
|
+
* environment.
|
|
9
|
+
*/
|
|
10
|
+
import type { SidebarSessionEvent } from './context-types.ts'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract the concatenated plain text of a content-block list (the durable
|
|
14
|
+
* `ContentBlock[]` shape, structurally: blocks with `type: 'text'` carry
|
|
15
|
+
* `text`; anything else — tool_use, image, … — contributes nothing).
|
|
16
|
+
* @param content - the raw `content` field of a message event.
|
|
17
|
+
* @returns the joined text, or undefined when the message carries no text.
|
|
18
|
+
*/
|
|
19
|
+
export function contentText(content: unknown): string | undefined {
|
|
20
|
+
if (!Array.isArray(content)) return undefined
|
|
21
|
+
const parts: string[] = []
|
|
22
|
+
for (const block of content) {
|
|
23
|
+
if (block === null || typeof block !== 'object') continue
|
|
24
|
+
const candidate = block as { type?: unknown; text?: unknown }
|
|
25
|
+
if (candidate.type === 'text' && typeof candidate.text === 'string') {
|
|
26
|
+
parts.push(candidate.text)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return parts.length > 0 ? parts.join('\n') : undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The live status of one subagent card (both fields optional). */
|
|
33
|
+
export interface LastActivity {
|
|
34
|
+
/** The latest assembled assistant text output in the tail. */
|
|
35
|
+
text?: string
|
|
36
|
+
/** The latest tool call in the tail. */
|
|
37
|
+
tool?: { name: string; args: string }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Fold a session event log into the last text output + last tool call (each
|
|
42
|
+
* is the LAST occurrence in event order). Lifecycle events and raw
|
|
43
|
+
* `assistant/chunk` rows are ignored — the card shows what the subagent is
|
|
44
|
+
* doing right now, not its plumbing. The scan runs BACKWARD from the newest
|
|
45
|
+
* event and stops once both fields are found, so a long history costs only
|
|
46
|
+
* the recent tail in the common case.
|
|
47
|
+
* @param events - the session's append-only event log (oldest → newest).
|
|
48
|
+
* @param maxMessages - optional message-boundary window: only the tail's
|
|
49
|
+
* last `maxMessages` surface messages (`user/message`, `assistant/message`)
|
|
50
|
+
* and the events between them are considered, mirroring the old
|
|
51
|
+
* `subagents.history({ maxMessages })` window. Stale activity older than
|
|
52
|
+
* the window is never surfaced, and a long log is never scanned in full.
|
|
53
|
+
* @returns the last text and/or tool call; an empty object when the log has neither.
|
|
54
|
+
*/
|
|
55
|
+
export function lastActivity(
|
|
56
|
+
events: readonly SidebarSessionEvent[],
|
|
57
|
+
maxMessages = Infinity,
|
|
58
|
+
): LastActivity {
|
|
59
|
+
let text: string | undefined
|
|
60
|
+
let tool: { name: string; args: string } | undefined
|
|
61
|
+
let messagesSeen = 0
|
|
62
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
63
|
+
if (text !== undefined && tool !== undefined) break
|
|
64
|
+
const event = events[index]
|
|
65
|
+
if (event === undefined) continue
|
|
66
|
+
const { type, data } = event
|
|
67
|
+
if (type === 'user/message' || type === 'assistant/message') {
|
|
68
|
+
messagesSeen += 1
|
|
69
|
+
if (messagesSeen > maxMessages) break
|
|
70
|
+
} else if (messagesSeen >= maxMessages) {
|
|
71
|
+
// The window already holds its `maxMessages` messages: anything older
|
|
72
|
+
// than the oldest in-window message sits outside the recent window.
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
if (text === undefined && type === 'assistant/message') {
|
|
76
|
+
const message = data.message as { content?: unknown } | undefined
|
|
77
|
+
const extracted = contentText(message?.content)
|
|
78
|
+
if (extracted !== undefined) text = extracted
|
|
79
|
+
} else if (tool === undefined && type === 'tool/call') {
|
|
80
|
+
tool = {
|
|
81
|
+
name: typeof data.name === 'string' ? data.name : 'tool',
|
|
82
|
+
args: typeof data.arguments === 'string' ? data.arguments : '',
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (text === undefined && tool === undefined) return {}
|
|
87
|
+
return {
|
|
88
|
+
...(text === undefined ? {} : { text }),
|
|
89
|
+
...(tool === undefined ? {} : { tool }),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live-preview route of the Subagent page ('subagents.live'): one
|
|
3
|
+
* request per refresh instead of N per-child `subagents.history` calls.
|
|
4
|
+
*
|
|
5
|
+
* The route takes the already-resolved topology root (`rootSessionId`),
|
|
6
|
+
* enumerates the whole descendant tree ONCE through the host subagent
|
|
7
|
+
* runtime (`ctx.get('subagents')` / `listDescendants`), keeps only rows the
|
|
8
|
+
* catalog reports running (`activity: 'running'` — the same gate the client
|
|
9
|
+
* renders cards on), and folds the newest text/tool activity from each
|
|
10
|
+
* child's attached session event log. It never touches DSH source and never
|
|
11
|
+
* reads the model's `job_output` cursor.
|
|
12
|
+
*
|
|
13
|
+
* Degradation contract:
|
|
14
|
+
* - `ctx.get('subagents')` missing or `listDescendants` failure → 503 (the
|
|
15
|
+
* Subagent page has no topology to show in such deployments anyway).
|
|
16
|
+
* - One child's events missing/corrupt → that child is skipped, the rest of
|
|
17
|
+
* the batch still returns.
|
|
18
|
+
*/
|
|
19
|
+
import type { Context, SidebarSubagentsService } from './context-types.ts'
|
|
20
|
+
import { SIDE_LABEL_PREFIX } from './sidechat-core.ts'
|
|
21
|
+
import { lastActivity, type LastActivity } from './subagent-activity.ts'
|
|
22
|
+
import { requireString, SidebarError } from './wire.ts'
|
|
23
|
+
|
|
24
|
+
/** The live-preview routes of the /sidebar JSON API. */
|
|
25
|
+
export interface SidebarSubagentLiveRoutes {
|
|
26
|
+
/**
|
|
27
|
+
* Fold one tree's running subagent histories into a compact live map.
|
|
28
|
+
* @param payload - `{ rootSessionId }`.
|
|
29
|
+
* @returns `{ live: Record<childSessionId, LastActivity> }`; children with
|
|
30
|
+
* no text/tool yet are omitted.
|
|
31
|
+
*/
|
|
32
|
+
live(payload: unknown): Promise<{ live: Record<string, LastActivity> }>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The recent-message window of the live preview: only the last 12 surface
|
|
37
|
+
* messages of a child's log are folded, matching the old per-card
|
|
38
|
+
* `subagents.history({ maxMessages: 12 })` window. Keeps stale tool calls
|
|
39
|
+
* out of the preview and bounds the backward scan per child.
|
|
40
|
+
*/
|
|
41
|
+
export const LIVE_WINDOW_MESSAGES = 12
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build the live-preview routes bound to the plugin context.
|
|
45
|
+
* @param ctx - host plugin context.
|
|
46
|
+
*/
|
|
47
|
+
export function buildSubagentLiveApi(ctx: Context): SidebarSubagentLiveRoutes {
|
|
48
|
+
return {
|
|
49
|
+
async live(payload) {
|
|
50
|
+
const rootSessionId = requireString(payload, 'rootSessionId')
|
|
51
|
+
const subagents = ctx.get('subagents') as SidebarSubagentsService | undefined
|
|
52
|
+
if (subagents === undefined || typeof subagents.listDescendants !== 'function') {
|
|
53
|
+
throw new SidebarError(
|
|
54
|
+
'subagents-unavailable',
|
|
55
|
+
'the subagent service is not mounted in this deployment',
|
|
56
|
+
503,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
let descendants
|
|
60
|
+
try {
|
|
61
|
+
descendants = await subagents.listDescendants(rootSessionId)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new SidebarError(
|
|
64
|
+
'subagents-unavailable',
|
|
65
|
+
`subagent catalog read failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
66
|
+
503,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const live: Record<string, LastActivity> = {}
|
|
71
|
+
for (const entry of descendants) {
|
|
72
|
+
// Same gate the client renders cards on: only catalog-running
|
|
73
|
+
// children get live lines (spec: "仅对 running 且非 Side Chat").
|
|
74
|
+
if (entry.kind !== 'child' || entry.activity !== 'running') continue
|
|
75
|
+
// Side Chat threads ride the subagent origin but are sidebar tabs,
|
|
76
|
+
// never topology — keep them out of the live map too.
|
|
77
|
+
if (entry.label?.startsWith(SIDE_LABEL_PREFIX) ?? false) continue
|
|
78
|
+
try {
|
|
79
|
+
const activity = lastActivity(
|
|
80
|
+
ctx.sessions.get(entry.id)?.events ?? [],
|
|
81
|
+
LIVE_WINDOW_MESSAGES,
|
|
82
|
+
)
|
|
83
|
+
if (activity.text !== undefined || activity.tool !== undefined) {
|
|
84
|
+
live[entry.id] = activity
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// One child's event log is not readable: skip only that child.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { live }
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|