@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,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
+ }