@volter/supercode-frontend-vscode 0.1.62 → 0.1.63

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/README.md CHANGED
@@ -320,11 +320,56 @@ request), and each part declares its own fallback.
320
320
  | Proposal | What it buys | Without it |
321
321
  |---|---|---|
322
322
  | `defaultChatParticipant` | `isDefault` — Volter Harness answers the Chat view with no `@` mention | the ordinary `@supercode` participant; a mention is required |
323
- | `chatParticipantAdditions` | `ChatToolInvocationPart`, `ChatResponseConfirmationPart`, `ChatResponseWarningPart`, `codeblockUri`, `ChatResponseThinkingProgressPart` | progress lines, labeled Markdown results, typed replies, plain fenced diffs, blockquoted reasoning |
323
+ | `chatParticipantAdditions` | `ChatToolInvocationPart` (with its terminal, todo-list and subagent data), `ChatResponseConfirmationPart`, `ChatResponseWarningPart`, `codeblockUri`, `ChatResponseThinkingProgressPart`, `ChatResponseHookPart`, `questionCarousel` | progress lines, labeled Markdown results, typed replies, plain fenced diffs, blockquoted reasoning, quoted hook lines, answer buttons |
324
324
  | `chatSessionsProvider` | the runtime's sessions in the Chat view's session picker, each reopening with its history | the participant still attaches; there is no session list |
325
325
  | `chatParticipantPrivate` | dynamic participant identity with the harness and provider icon | static participant identity |
326
326
  | `chatProvider` | native fixed-model identity and provider icon | host new-chat configuration |
327
327
 
328
+ ### Parity with Code-OSS's own chat parts
329
+
330
+ Every chat response part and tool-row kind the pinned Code-OSS (`4a7cd45`, `src/vscode-dts`) offers an extension,
331
+ and what this frontend does with it. **Native**: the harness's event is drawn as VS Code draws its own.
332
+ **Fallback**: drawn, but with a plainer part than VS Code's own (why given). **Not used**: no harness event this
333
+ frontend receives carries what the part shows. **Gap**: the event exists and the native part is not built yet.
334
+ This count is the measure of done; a change that moves a row says so here.
335
+
336
+ | Part / tool data | Status | What draws it, or why not |
337
+ |---|---|---|
338
+ | `ChatResponseMarkdownPart` | native | the agent's text |
339
+ | `ChatResponseProgressPart` | native | status lines; a finished subagent task as one line |
340
+ | `ChatResponseWarningPart` | native | warnings, failed turns, usage limits |
341
+ | `ChatResponseThinkingProgressPart` | native | the agent's reasoning (a subagent's is not shown, as VS Code shows none) |
342
+ | `ChatResponseReferencePart` | native | each file a Codex diff names |
343
+ | `ChatResponseCommandButtonPart` | native | approval answers; Stop for a background task |
344
+ | `ChatToolInvocationPart` | native | every tool call, updated in place with its result |
345
+ | ↳ terminal data | native | shell calls: command and output |
346
+ | ↳ todo-list data | native | Claude Code's TodoWrite and its task tools (TaskCreate, TaskUpdate, TaskList), and supercode's own `update_plan`, shown in VS Code's todo list. Codex app-server's plan (`turn/plan/updated`, not a tool call) is still a status line: fallback |
347
+ | ↳ subagent data, `subAgentInvocationId` | native | Claude Code's Agent/Task call is the subagent row; its own calls are grouped under it (`parent_tool_use_id`) |
348
+ | ↳ simple input/output data | native | any other finished call |
349
+ | ↳ MCP data | fallback | an MCP call's result arrives as text, so it is the simple input/output |
350
+ | ↳ resources data | not used | no tool event lists resources as such |
351
+ | `ChatResponseHookPart` | native | a blocked prompt, a blocked tool call, a hook that failed (Claude Code). The extension API cannot place a hook inside a subagent row |
352
+ | `ChatResponseQuestionCarouselPart` | native | an elicitation of questions (`ask_user`, Claude Code's AskUserQuestion) on a turn this window runs; command buttons on a re-joined response, where the pinned host never delivers the carousel's answer |
353
+ | `ChatResponseConfirmationPart` | native / fallback | approvals, where the host advertises in-turn approval cards; command buttons otherwise (a stock confirmation answers with a new request) |
354
+ | `ChatResponseCodeblockUriPart` | fallback | a Codex diff: `codeblockUri` with a fenced diff, not an edit (see *Why `diff_updated` is not an edit part*) |
355
+ | `ChatResponseTextEditPart`, `ChatResponseNotebookEditPart`, `ChatResponseWorkspaceEditPart`, `ChatResponseMultiDiffPart`, `ChatResponseExternalEditPart` | **gap (known)** | native file edits with Keep/Undo and the multi-file diff: the harness has already written the files, and no event carries the before/after the editor needs |
356
+ | `ChatResponseAnchorPart` | not used | no event names a location inline in text |
357
+ | `ChatResponseFileTreePart` | not used | no event lists a file tree |
358
+ | `ChatResponseMarkdownWithVulnerabilitiesPart` | not used | no harness reports vulnerabilities |
359
+ | `ChatResponseCodeCitationPart` | not used | no harness reports a licensed citation |
360
+ | `ChatResponseMovePart` | not used | no event moves a range |
361
+ | `ChatResponseExtensionsPart` | not used | no harness recommends extensions |
362
+ | `ChatResponsePullRequestPart` | not used | no structured event announces a pull request |
363
+ | `ChatResponseInfoPart` | not used | informational records are warnings or are dropped as telemetry |
364
+ | `ChatResponseAutoModeResolutionPart` | not used | the harness's model is the one chosen; there is no automatic routing to report |
365
+ | `ChatResponseProgressPart2`, `ChatResponseReferencePart2` | not used | no event carries a resolvable task or a reference status |
366
+ | `ChatResponseVoiceProgressPart` (`chatParticipantPrivate`) | not used | voice is not a harness event |
367
+ | `stream.usage` | gap | token counts are dropped from the transcript; VS Code's own context indicator could show them and does not yet |
368
+
369
+ Count, by row: 14 native (the confirmation card only where the host advertises it), 2 fallback (and Codex's plan
370
+ inside the todo-list row), 12 not used, 2 gaps — native edits (the known gap, five parts in one row) and
371
+ `stream.usage`.
372
+
328
373
  The session participant uses the exact session type id (`supercode`). With
329
374
  `chatParticipantPrivate`, the extension registers its dynamic identity directly and keeps
330
375
  `canDelegate` disabled so the workbench does not pre-register a generic agent that masks the
@@ -365,6 +410,9 @@ it needs and the fallback it degrades to. Each arm names the producer it reads.
365
410
  | `diff_updated` | `crates/codex-frontend/src/codex_app_server_v0_144.rs` (stock Codex `turn/diff/updated`) | `codeblockUri` + a fenced `diff` block, plus one stable `reference` per file the diff names → the fenced block alone |
366
411
  | `request` | `sdk/interchange/frontend-request-bridge.mjs` request publication | a titled markdown block plus one **command button per answer** — stable API, no proposal |
367
412
  | `cache_warning` | `AgentEvent::CacheWarning` | `ChatResponseWarningPart` → blockquote |
413
+ | `hook` | `runtime-native-projection.mjs` from Claude Code's failed `hook_response`, its blocked-prompt `informational` record, and a tool result in its own `PreToolUse:<tool> hook error:` words | `ChatResponseHookPart` ("Blocked by" / "Warning from" the hook) → blockquote |
414
+ | `background_tasks` | `runtime-native-projection.mjs` from Claude Code's `background_tasks_changed` | nothing in the transcript: the turn lists them with a Stop each |
415
+ | any event with `parent_id` | `runtime-native-projection.mjs` from Claude Code's `parent_tool_use_id` | a subagent's calls are grouped under its call; its own text, reasoning and input are not shown |
368
416
  | `usage` | `AgentEvent::Usage` | a progress line |
369
417
  | `background_output` | `AgentEvent::BackgroundOutput` | a fenced block, saying so when the runtime stopped retaining |
370
418
  | `scheduled_prompt_started` / `_deferred` / `_completed` | declared by `frontend_descriptor`'s `display.event_kinds` | a progress line |
@@ -402,6 +450,36 @@ resolution sentence lands directly beneath them. An elicitation that carries a `
402
450
  `Answer…` button that asks for the value first; a request still pending while the session is idle
403
451
  can also be answered by typing.
404
452
 
453
+ **A harness's questions are VS Code's question carousel.** Why the carousel once swallowed its answer, read
454
+ from the pinned Code-OSS (`4a7cd45`): `questionCarousel` resolves through the resolver map of the stream it
455
+ was called on, and `$handleQuestionCarouselAnswer` reads only the participant's map
456
+ (`extHostChatAgents2.ts`). A session's *re-joined* response (`activeResponseCallback`) gets a stream with a
457
+ map of its own (`extHostChatSessions.ts`) that no answer ever reaches. So the carousel is used only on a
458
+ turn this window's request handler is running; a re-joined response keeps the buttons above. The design:
459
+
460
+ - *The question reaches Chat as a request.* Supercode's own `ask_user` already asks through an `elicitation`
461
+ whose `requested_schema.properties.qN` carry `title`, `description`, `x-options` (`[{label, description}]`)
462
+ and `x-multi-select` (`sdk/interchange/question-tool.mjs`). Claude Code's AskUserQuestion takes the same
463
+ input (`questions[{question, header, options[{label, description}], multiSelect}]`, at most four of each)
464
+ and, with `--permission-prompt-tool stdio`, asks for it as a `can_use_tool` control request (Claude Code
465
+ enables the tool only when a permission prompt tool answers it — measured on 2.1.312). The runtime's
466
+ projection (`runtime-native-projection.mjs`) raises that request as an `elicitation` built by the same
467
+ `questionElicitation` the `ask_user` tool uses, not as an approval, so Auto approve never answers it and
468
+ every frontend that answers an elicitation answers this one.
469
+ - *Chat shows the carousel.* An elicitation whose every property is such a question (a `title`, the question
470
+ alone as `x-question`, and `x-options`) is one `ChatQuestion` per property: single or multi select with
471
+ free text allowed, or text when it has no options; Skip is allowed. Any other form (an MCP server's own
472
+ schema, with its `enum`, `format` or `required`) keeps the `Answer…` door, which honours them. Without the
473
+ carousel (no `chatParticipantAdditions`, or a re-joined response) it is the `Answer…` button.
474
+ - *The answer reaches the harness in its own format.* The carousel's answers become the elicitation's
475
+ `{action:'accept', content:{qN: label | [labels] | text}}`, or `decline` when skipped or submitted with
476
+ nothing chosen; a stopped turn sends nothing (Stop cancels its requests). A reply typed into Chat instead
477
+ answers every question with its words. For Claude Code the
478
+ hosted runtime (`runtime-hosted.mjs`) turns that into its permission reply
479
+ `{behavior:'allow', updatedInput:{questions, answers:{<question>: <answer>}}}`, multiple answers joined
480
+ with `, ` as AskUserQuestion's own output schema states; a decline is `{behavior:'deny', message}`.
481
+ `ask_user` already reads the same `content`.
482
+
405
483
  **A hosted harness runtime.** When the runtime is one Volter Harness HOSTS — a Claude Code, Codex, pi or
406
484
  opencode session it started through `harness.v1.runtimes.start` — an approval it raises is
407
485
  **answerable from this view** for every harness whose reply this runtime can translate
package/dist/extension.js CHANGED
@@ -11,7 +11,7 @@ import { readCredentialFile } from './credential.js';
11
11
  import { grantedProposals, PROPOSAL_EFFECTS, REQUESTED_PROPOSALS } from './proposals.js';
12
12
  import { observeActiveResponse, sessionStatus } from './native-session.js';
13
13
  import { RequestNotPendingError, VscodeFrontendSession } from './session.js';
14
- import { acceptedDecisions, opaqueText, projectEvent, projectPendingRequests, projectHistory, projectReplayTurns, HISTORY_MARKER_PROMPT, resolveParts, resolutionSentence, typedAnswer, } from './view.js';
14
+ import { acceptedDecisions, opaqueText, projectEvent, projectPendingRequests, projectHistory, projectReplayTurns, HISTORY_MARKER_PROMPT, PlanTracker, resolveParts, resolutionSentence, typedAnswer, } from './view.js';
15
15
  const SESSION_SCHEME = 'supercode';
16
16
  /** The harnesses whose model and reasoning effort a new chat can set (the host passes them to the CLI). */
17
17
  const CONFIGURABLE_HARNESSES = ['claude-code', 'codex'];
@@ -204,6 +204,88 @@ class SupercodeFrontends {
204
204
  }
205
205
  }
206
206
  let nativeInTurnApprovalCards = false;
207
+ /**
208
+ * A harness's questions as VS Code's own carousel, inside the running turn; the person's answers are the
209
+ * elicitation's `{action:'accept', content:{<key>: label | [labels] | text}}`, and Skip is a decline. The answer
210
+ * takes the same door as a button's (`supercode.frontend.respond`).
211
+ */
212
+ function askQuestions(stream, part, pending, cancelled) {
213
+ const questions = part.questions.map(field => new vscode.ChatQuestion(field.key, field.options.length === 0 ? vscode.ChatQuestionType.Text
214
+ : field.multiSelect ? vscode.ChatQuestionType.MultiSelect : vscode.ChatQuestionType.SingleSelect, field.title, {
215
+ ...(field.message ? { message: field.message } : {}),
216
+ ...(field.options.length ? {
217
+ options: field.options.map((option, index) => ({ id: `${field.key}-${index}`, label: option.description ? `${option.label} — ${option.description}` : option.label, value: option.label })),
218
+ allowFreeformInput: true,
219
+ } : {}),
220
+ }));
221
+ const respond = (response) => vscode.commands.executeCommand('supercode.frontend.respond', { connectionId: part.connectionId, requestId: part.requestId, response, label: 'Answer' });
222
+ void Promise.resolve(stream.questionCarousel(questions, true)).then(answers => {
223
+ // (the editor also resolves an unanswered carousel when its response ends: a request answered by another door
224
+ // by then is left alone)
225
+ // (and a stopped turn: Stop already cancels its requests)
226
+ if ((pending && !pending(part.requestId)) || cancelled?.())
227
+ return;
228
+ const content = {};
229
+ for (const field of part.questions) {
230
+ const answer = carouselAnswer(answers?.[field.key]);
231
+ if (answer !== undefined)
232
+ content[field.key] = answer;
233
+ }
234
+ // Skip, and a carousel submitted with nothing chosen, decline: a question is never answered with nothing.
235
+ if (Object.keys(content).length === 0)
236
+ return respond({ kind: 'elicitation', request_id: part.requestId, action: 'decline' });
237
+ return respond({ kind: 'elicitation', request_id: part.requestId, action: 'accept', content });
238
+ }, () => undefined);
239
+ }
240
+ /** One carousel answer as the elicitation's value: the text, the selected label (or the person's own words), or the
241
+ * selected labels with any words of their own after them. */
242
+ function carouselAnswer(value) {
243
+ if (typeof value === 'string')
244
+ return value.trim() || undefined;
245
+ if (!value || typeof value !== 'object')
246
+ return undefined;
247
+ const answer = value;
248
+ const own = typeof answer.freeformValue === 'string' && answer.freeformValue.trim() ? answer.freeformValue.trim() : undefined;
249
+ if (Array.isArray(answer.selectedValues)) {
250
+ const picked = [...answer.selectedValues.filter((item) => typeof item === 'string'), ...(own ? [own] : [])];
251
+ return picked.length ? picked : undefined;
252
+ }
253
+ return typeof answer.selectedValue === 'string' && answer.selectedValue ? answer.selectedValue : own;
254
+ }
255
+ /** VS Code's own hook row: which hook, and what it said — a stop reason where it blocked, a warning otherwise. */
256
+ function hookPart(part) {
257
+ return part.blocked
258
+ ? new vscode.ChatResponseHookPart(part.hookType, part.reason || 'The hook gave no reason.')
259
+ : new vscode.ChatResponseHookPart(part.hookType, undefined, part.reason || 'The hook gave no reason.');
260
+ }
261
+ /**
262
+ * What a tool row shows inside it, the way VS Code's own rows do, live and in history alike: a plan call is the todo
263
+ * list as the call left it; a subagent call is the subagent row, with its answer once it has one (none for a subagent
264
+ * started in the background, whose call returns only a launch notice); a shell call is the terminal block; any other
265
+ * finished call its input and output. A subagent's own calls are grouped under the call they were made inside.
266
+ */
267
+ function setToolDetails(invocation, row) {
268
+ if (row.parentToolCallId)
269
+ invocation.subAgentInvocationId = row.parentToolCallId;
270
+ if (row.plan) {
271
+ invocation.toolSpecificData = { todoList: row.plan.map((item, index) => ({
272
+ id: index + 1,
273
+ title: item.title,
274
+ status: item.status === 'completed' ? vscode.ChatTodoStatus.Completed
275
+ : item.status === 'in-progress' ? vscode.ChatTodoStatus.InProgress : vscode.ChatTodoStatus.NotStarted,
276
+ })) };
277
+ }
278
+ else if (row.subagent) {
279
+ invocation.toolSpecificData = new vscode.ChatSubagentToolInvocationData(row.subagent.description, row.subagent.agentName, row.subagent.prompt, row.isComplete && !row.background ? row.resultText ?? '' : undefined);
280
+ }
281
+ else if (row.commandLine) {
282
+ invocation.toolSpecificData = { commandLine: { original: row.commandLine }, language: 'sh',
283
+ ...(row.isComplete ? { output: { text: row.resultText ?? '' } } : {}) };
284
+ }
285
+ else if (row.isComplete) {
286
+ invocation.toolSpecificData = { input: row.inputText ?? '', output: row.resultText ?? '' };
287
+ }
288
+ }
207
289
  function writeProjected(stream, projected, granted, state) {
208
290
  for (const candidate of projected) {
209
291
  const parts = resolveParts([candidate], granted);
@@ -244,6 +326,15 @@ function writePart(stream, part, state) {
244
326
  case 'thinking':
245
327
  stream.push(new vscode.ChatResponseThinkingProgressPart(part.value));
246
328
  return;
329
+ case 'hook':
330
+ stream.push(hookPart(part));
331
+ return;
332
+ case 'questions':
333
+ if (state.carousel)
334
+ askQuestions(stream, part, state.pending, state.cancelled);
335
+ else
336
+ writePart(stream, part.fallback, state);
337
+ return;
247
338
  case 'reference': {
248
339
  // Only a path the runtime's own payload named, resolved against the
249
340
  // workspace this window already has. Nothing is created or fabricated.
@@ -299,17 +390,22 @@ function writePart(stream, part, state) {
299
390
  invocation.isError = part.isError;
300
391
  if (part.inputText !== undefined)
301
392
  state.toolInputs.set(part.toolCallId, part.inputText);
302
- if (commandLine) {
303
- // VS Code's own terminal block: the command, and its output once it has run.
304
- invocation.toolSpecificData = { commandLine: { original: commandLine }, language: 'sh',
305
- ...(part.isComplete ? { output: { text: part.resultText ?? '' } } : {}) };
306
- }
307
- else if (part.isComplete) {
308
- invocation.toolSpecificData = {
309
- input: state.toolInputs.get(part.toolCallId) ?? '',
310
- output: part.resultText ?? '',
311
- };
393
+ if (part.plan || part.subagent || part.parentToolCallId || part.background) {
394
+ const kept = state.toolKinds.get(part.toolCallId) ?? {};
395
+ state.toolKinds.set(part.toolCallId, {
396
+ ...kept,
397
+ ...(part.plan ? { plan: part.plan } : {}),
398
+ ...(part.subagent ? { subagent: part.subagent } : {}),
399
+ ...(part.parentToolCallId ? { parentToolCallId: part.parentToolCallId } : {}),
400
+ ...(part.background ? { background: true } : {}),
401
+ });
312
402
  }
403
+ setToolDetails(invocation, {
404
+ ...part,
405
+ ...state.toolKinds.get(part.toolCallId),
406
+ commandLine,
407
+ inputText: state.toolInputs.get(part.toolCallId),
408
+ });
313
409
  stream.push(invocation);
314
410
  return;
315
411
  }
@@ -1496,7 +1592,7 @@ export async function activate(context) {
1496
1592
  writeProjected(stream, projectPendingRequests([pending], {
1497
1593
  connectionId: connection.id,
1498
1594
  canRespond: connection.session.descriptor?.actions.respond !== false,
1499
- }), granted, { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map() });
1595
+ }), granted, { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map(), toolKinds: new Map() });
1500
1596
  }
1501
1597
  catch {
1502
1598
  void vscode.window.showWarningMessage(`${connection.label}: ${label} is waiting for approval. Open its chat to answer it.`);
@@ -1538,11 +1634,14 @@ export async function activate(context) {
1538
1634
  if (approval() === 'autoApprove' && !canAnswer(connection)) {
1539
1635
  throw new Error('This harness does not support automatic approval through Chat. Select Harness permissions.');
1540
1636
  }
1541
- const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map() };
1637
+ const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map(), toolKinds: new Map(),
1638
+ carousel: true, pending: (requestId) => connection.session.pendingRequests.has(requestId),
1639
+ cancelled: () => token.isCancellationRequested };
1542
1640
  const projection = {
1543
1641
  connectionId: connection.id,
1544
1642
  pendingRequest: (requestId) => connection.session.pendingRequests.get(requestId) ?? connection.session.resolvedRequests.get(requestId),
1545
1643
  canRespond: connection.session.descriptor?.actions.respond !== false,
1644
+ plan: connection.plan ??= new PlanTracker(),
1546
1645
  };
1547
1646
  const cancellation = token.onCancellationRequested(() => {
1548
1647
  void stopConnection(connection).catch(() => {
@@ -2143,15 +2242,15 @@ function historyParts(parts) {
2143
2242
  invocation.isComplete = part.isComplete;
2144
2243
  if (part.isError !== undefined)
2145
2244
  invocation.isError = part.isError;
2146
- if (part.commandLine) {
2147
- invocation.toolSpecificData = { commandLine: { original: part.commandLine }, language: 'sh', output: { text: part.resultText ?? '' } };
2148
- }
2149
- else if (part.isComplete) {
2150
- invocation.toolSpecificData = { input: part.inputText ?? '', output: part.resultText ?? '' };
2151
- }
2245
+ setToolDetails(invocation, part);
2152
2246
  out.push(invocation);
2153
2247
  continue;
2154
2248
  }
2249
+ if (part.part === 'hook') {
2250
+ flush();
2251
+ out.push(hookPart(part));
2252
+ continue;
2253
+ }
2155
2254
  const value = historyText(part);
2156
2255
  if (value)
2157
2256
  text.push(value);
@@ -2172,6 +2271,9 @@ function historyText(part) {
2172
2271
  return `> ⚠ ${part.value}`;
2173
2272
  case 'thinking':
2174
2273
  return `> ${part.value}`;
2274
+ case 'hook':
2275
+ // Drawn as VS Code's own hook row by `historyParts`.
2276
+ return '';
2175
2277
  case 'codeblock':
2176
2278
  return part.value;
2177
2279
  case 'reference':
@@ -2181,6 +2283,8 @@ function historyText(part) {
2181
2283
  return '';
2182
2284
  case 'answers':
2183
2285
  return `**${part.title}**\n\n${part.message}`;
2286
+ case 'questions':
2287
+ return `**${part.fallback.title}**\n\n${part.fallback.message}`;
2184
2288
  }
2185
2289
  }
2186
2290
  function registerSessions(context, frontends, participant, granted, handler, historyParticipant, controls) {
@@ -2366,6 +2470,8 @@ function registerSessions(context, frontends, participant, granted, handler, his
2366
2470
  pendingToolNames.add(name);
2367
2471
  }
2368
2472
  }
2473
+ // The task list is replayed with the history it was built in, and live turns carry on from it.
2474
+ const plan = new PlanTracker();
2369
2475
  const projected = projectReplayTurns(replay.filter(event => {
2370
2476
  if (event.kind !== 'request')
2371
2477
  return true;
@@ -2375,7 +2481,9 @@ function registerSessions(context, frontends, participant, granted, handler, his
2375
2481
  connectionId: connection.id,
2376
2482
  pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
2377
2483
  canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
2378
- }, projectHistory(canonicalHistory, granted, pendingToolNames));
2484
+ plan,
2485
+ }, projectHistory(canonicalHistory, granted, pendingToolNames, plan));
2486
+ connection.plan = plan;
2379
2487
  const live = connection.session.descriptor?.turn_state === 'busy' || connection.session.pendingRequests.size > 0;
2380
2488
  controls?.rememberIdentity(connection);
2381
2489
  if (!live)
@@ -2401,8 +2509,8 @@ function registerSessions(context, frontends, participant, granted, handler, his
2401
2509
  // responses can be stale; their command buttons are disabled.
2402
2510
  const last = projected.at(-1);
2403
2511
  if (last?.role === 'assistant') {
2404
- activeParts.push(...(projected.pop()?.parts ?? []).map(part => part.part === 'answers'
2405
- ? { part: 'markdown', value: `**${part.title}**\n\n${part.message}` }
2512
+ activeParts.push(...(projected.pop()?.parts ?? []).map(part => part.part === 'answers' || part.part === 'questions'
2513
+ ? { part: 'markdown', value: `**${(part.part === 'questions' ? part.fallback : part).title}**\n\n${(part.part === 'questions' ? part.fallback : part).message}` }
2406
2514
  : part));
2407
2515
  }
2408
2516
  }
@@ -2438,7 +2546,7 @@ function registerSessions(context, frontends, participant, granted, handler, his
2438
2546
  // already waiting and later ones; Ask leaves them for the person, as before.
2439
2547
  const watch = controls && !controls.attached(connection) ? controls.watchApprovals(connection, stream, token) : undefined;
2440
2548
  try {
2441
- const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map(), quiet: watch?.answers };
2549
+ const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map(), toolKinds: new Map(), quiet: watch?.answers };
2442
2550
  for (const part of oneRowPerToolCall(activeParts))
2443
2551
  writePart(stream, part, state);
2444
2552
  writeProjected(stream, projectPendingRequests(pendingAtCursor.filter(request => connection.session.pendingRequests.has(request.id)), {
@@ -2451,6 +2559,7 @@ function registerSessions(context, frontends, participant, granted, handler, his
2451
2559
  writeProjected(stream, projectEvent(event, {
2452
2560
  connectionId: connection.id,
2453
2561
  liveUserMessages: true,
2562
+ plan,
2454
2563
  pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
2455
2564
  canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
2456
2565
  }), granted, state);
package/dist/view.d.ts CHANGED
@@ -50,6 +50,13 @@ export type ChatPart = {
50
50
  uri: string;
51
51
  value: string;
52
52
  language: string;
53
+ }
54
+ /** A harness hook that blocked or warned: which event it ran on, and what it said. */
55
+ | {
56
+ part: 'hook';
57
+ hookType: HookType;
58
+ blocked: boolean;
59
+ reason: string;
53
60
  } | {
54
61
  part: 'tool';
55
62
  toolName: string;
@@ -62,13 +69,45 @@ export type ChatPart = {
62
69
  resultText?: string;
63
70
  /** A shell call's command line: the row is then VS Code's own terminal block, command and output. */
64
71
  commandLine?: string;
72
+ /** The plan as this call left it (`planItems`, `PlanTracker`): the row is then VS Code's own todo list. */
73
+ plan?: PlanItem[];
74
+ /** A subagent call's own fields (`subagentCall`): the row is then VS Code's own subagent row. */
75
+ subagent?: SubagentCall;
76
+ /** The subagent call this call was made inside (Claude Code's `parent_tool_use_id`): VS Code groups it there. */
77
+ parentToolCallId?: string;
78
+ /** The call started its subagent in the background: its result is a launch notice, not the subagent's answer. */
79
+ background?: boolean;
65
80
  } | {
66
81
  part: 'answers';
67
82
  approval: boolean;
68
83
  title: string;
69
84
  message: string;
70
85
  answers: ChatAnswer[];
86
+ }
87
+ /** A harness's questions (`requestQuestions`): VS Code's question carousel on a turn this window runs, its
88
+ * `answers` part anywhere else. */
89
+ | {
90
+ part: 'questions';
91
+ connectionId: string;
92
+ requestId: number;
93
+ questions: QuestionField[];
94
+ fallback: Extract<ChatPart, {
95
+ part: 'answers';
96
+ }>;
71
97
  };
98
+ /** One question of an elicitation that asks questions (`requestQuestions`), as VS Code's carousel asks it. */
99
+ export interface QuestionField {
100
+ /** The elicitation's property, which the answer's `content` is keyed by. */
101
+ key: string;
102
+ title: string;
103
+ message?: string;
104
+ /** Each option's label (also its answer) and what it means. None: a free-text question. */
105
+ options: {
106
+ label: string;
107
+ description?: string;
108
+ }[];
109
+ multiSelect: boolean;
110
+ }
72
111
  /**
73
112
  * One rendered part plus the API proposal it needs and what this view renders
74
113
  * instead when the host did not grant it. README.md's proposal table is this
@@ -105,6 +144,49 @@ export declare function toolTitles(toolName: string, argumentsText: string): {
105
144
  invocation: string;
106
145
  pastTense: string;
107
146
  };
147
+ /** A hook's row as words, as VS Code titles its own (`chatHookContentPart.ts`): "Blocked by …" or "Warning from …". */
148
+ export declare function hookSentence(hookEvent: string, blocked: boolean, reason: string): string;
149
+ /** The hook events VS Code's own Chat names (`ChatHookType`), which are Claude Code's. */
150
+ export declare const HOOK_TYPES: readonly ["SessionStart", "SessionEnd", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PreCompact", "SubagentStart", "SubagentStop", "Stop", "ErrorOccurred"];
151
+ export type HookType = (typeof HOOK_TYPES)[number];
152
+ /** One item of an agent's plan, as VS Code's own todo list holds it. */
153
+ export interface PlanItem {
154
+ title: string;
155
+ status: 'not-started' | 'in-progress' | 'completed';
156
+ }
157
+ /** A plan call's items (Claude Code's TodoWrite `todos` of `content`; the `update_plan` tool's `plan` of `step`, as
158
+ * supercode's own and Codex's take it), each its text and status, read from its own arguments; undefined for any
159
+ * other tool, or arguments holding none. Both carry the whole list, so they need no state. */
160
+ export declare function planItems(toolName: string, argumentsText: string): PlanItem[] | undefined;
161
+ /**
162
+ * The task list Claude Code keeps with its task tools, which it offers in place of TodoWrite (TaskCreate, TaskUpdate,
163
+ * TaskList; Claude Code 2.1.312 enables them by default and TodoWrite only when its tasks are turned off). Each call
164
+ * changes ONE task, while VS Code's own todo list shows the WHOLE list, so this keeps the list for one conversation as
165
+ * the calls go by and gives each call's row the list as that call left it.
166
+ *
167
+ * Read from the calls' own arguments, and from Claude Code's own result text where only the result has it (measured
168
+ * on 2.1.312, its `mapToolResultToToolResultBlockParam`): TaskCreate's `Task #<id> created successfully: <subject>`,
169
+ * TaskUpdate's `Updated task #<id> <fields>`, and TaskList's lines `#<id> [<status>] <subject>…`. A result in other
170
+ * words changes nothing here; the row is then an ordinary one.
171
+ */
172
+ export declare class PlanTracker {
173
+ #private;
174
+ /** A call that started: the list as it leaves it, or undefined when it is not a task call (or changes nothing). */
175
+ started(toolName: string, argumentsText: string, toolCallId: string): PlanItem[] | undefined;
176
+ /** A call that completed: the list as it now stands (a failed call's change undone; TaskList's result is the
177
+ * list), so a finished row never shows an older list than the calls before it left. */
178
+ completed(toolCallId: string, output: string, isError: boolean): PlanItem[] | undefined;
179
+ private items;
180
+ }
181
+ /** What a subagent call says of its subagent, as VS Code's own subagent row shows it. */
182
+ export interface SubagentCall {
183
+ description?: string;
184
+ agentName?: string;
185
+ prompt?: string;
186
+ }
187
+ /** A subagent call's description, kind and prompt (Claude Code's Task and Agent tools: `description`,
188
+ * `subagent_type`, `prompt`), read from its own arguments; undefined for any other tool. */
189
+ export declare function subagentCall(toolName: string, argumentsText: string): SubagentCall | undefined;
108
190
  /** The command line of a shell tool call, read from its own arguments; undefined for any other tool. */
109
191
  export declare function shellCommand(toolName: string, argumentsText: string): string | undefined;
110
192
  /** The information this view shows for a request payload. */
@@ -118,6 +200,14 @@ export declare function requestAnswers(request: FrontendRequest, connectionId: s
118
200
  export declare function typedAnswer(request: FrontendRequest, reply: string): FrontendResponse;
119
201
  /** The decisions a runtime said its harness accepts, when it said so. */
120
202
  export declare function acceptedDecisions(request: FrontendRequest): string[] | undefined;
203
+ /**
204
+ * An elicitation that asks questions, the shape supercode's `ask_user` asks with and a hosted Claude Code's
205
+ * AskUserQuestion is raised as (`question-tool.mjs` `questionElicitation`): every property of its
206
+ * `requested_schema` is one question, with a `title`, the question alone (`x-question`) and its `x-options`
207
+ * (`[{label, description}]`, with `x-multi-select`; none for free text). Any other form (an MCP server's schema with
208
+ * its own `enum`, `format` or `required`) is undefined here and keeps the `Answer…` door, which honours them.
209
+ */
210
+ export declare function requestQuestions(request: FrontendRequest): QuestionField[] | undefined;
121
211
  /** True when this request asks for content a button click cannot carry. */
122
212
  export declare function requestNeedsTypedReply(request: FrontendRequest): boolean;
123
213
  /**
@@ -147,6 +237,8 @@ export interface ProjectionContext {
147
237
  * buttons are absent rather than dead.
148
238
  */
149
239
  canRespond?: boolean;
240
+ /** The conversation's task list as its task tools change it; without one, those calls are ordinary rows. */
241
+ plan?: PlanTracker;
150
242
  }
151
243
  /** Pending requests belong to the live runtime, even when saved transcript
152
244
  * history predates them. Rehydrate their controls before waiting for new events. */
@@ -179,4 +271,4 @@ export declare function projectReplayTurns(events: readonly FrontendEvent[], gra
179
271
  */
180
272
  export declare function projectHistory(history: readonly (ChatMessage & {
181
273
  metadata?: Record<string, string>;
182
- })[], granted: ReadonlySet<string>, pendingToolNames?: ReadonlySet<string>): ProjectedTurn[];
274
+ })[], granted: ReadonlySet<string>, pendingToolNames?: ReadonlySet<string>, plan?: PlanTracker): ProjectedTurn[];
package/dist/view.js CHANGED
@@ -174,6 +174,26 @@ export function toolTitles(toolName, argumentsText) {
174
174
  }
175
175
  if (name === 'todowrite' || name === 'update_plan')
176
176
  return both('Updating the plan', 'Updated the plan');
177
+ if (name === 'taskcreate') {
178
+ const subject = field('subject');
179
+ if (subject)
180
+ return both(`Adding a task: ${plain(subject)}`, `Added a task: ${plain(subject)}`);
181
+ }
182
+ if (name === 'taskupdate') {
183
+ const id = field('taskId'), status = field('status');
184
+ if (id) {
185
+ const task = `task #${plain(id)}`;
186
+ if (status === 'in_progress')
187
+ return both(`Starting ${task}`, `Started ${task}`);
188
+ if (status === 'completed')
189
+ return both(`Completing ${task}`, `Completed ${task}`);
190
+ if (status === 'deleted')
191
+ return both(`Removing ${task}`, `Removed ${task}`);
192
+ return both(`Updating ${task}`, `Updated ${task}`);
193
+ }
194
+ }
195
+ if (name === TASK_LIST_TOOL)
196
+ return both('Reading the task list', 'Read the task list');
177
197
  // Claude Code names an MCP tool `mcp__<server>__<tool>`; VS Code writes "Ran `tool` – server".
178
198
  const mcp = /^mcp__(.+?)__(.+)$/.exec(toolName);
179
199
  if (mcp)
@@ -181,6 +201,159 @@ export function toolTitles(toolName, argumentsText) {
181
201
  const subject = toolSubject(argumentsText);
182
202
  return subject ? both(`Running ${code(toolName)} · ${plain(subject)}`, `Ran ${code(toolName)} · ${plain(subject)}`) : both(`Running ${code(toolName)}`, `Ran ${code(toolName)}`);
183
203
  }
204
+ /** A hook's row as words, as VS Code titles its own (`chatHookContentPart.ts`): "Blocked by …" or "Warning from …". */
205
+ export function hookSentence(hookEvent, blocked, reason) {
206
+ return `${blocked ? 'Blocked by' : 'Warning from'} the ${hookEvent} hook${reason ? `: ${reason}` : ''}`;
207
+ }
208
+ /** The hook events VS Code's own Chat names (`ChatHookType`), which are Claude Code's. */
209
+ export const HOOK_TYPES = ['SessionStart', 'SessionEnd', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'PreCompact',
210
+ 'SubagentStart', 'SubagentStop', 'Stop', 'ErrorOccurred'];
211
+ /** Claude Code's TaskList tool, lower-cased as this view compares tool names. */
212
+ const TASK_LIST_TOOL = 'tasklist'; // process-read: a Claude Code tool name, not the Windows command
213
+ /** A plan call's items (Claude Code's TodoWrite `todos` of `content`; the `update_plan` tool's `plan` of `step`, as
214
+ * supercode's own and Codex's take it), each its text and status, read from its own arguments; undefined for any
215
+ * other tool, or arguments holding none. Both carry the whole list, so they need no state. */
216
+ export function planItems(toolName, argumentsText) {
217
+ const name = toolName.toLowerCase();
218
+ if (name !== 'todowrite' && name !== 'update_plan')
219
+ return undefined;
220
+ const list = argumentFields(argumentsText)[name === 'todowrite' ? 'todos' : 'plan'];
221
+ if (!Array.isArray(list))
222
+ return undefined;
223
+ const items = list.flatMap((item) => {
224
+ if (!item || typeof item !== 'object' || Array.isArray(item))
225
+ return [];
226
+ const fields = item;
227
+ const title = planTitle(name === 'todowrite' ? fields.content : fields.step);
228
+ return title ? [{ title, status: planStatus(fields.status) }] : [];
229
+ });
230
+ return items.length > 0 ? items : undefined;
231
+ }
232
+ function argumentFields(argumentsText) {
233
+ try {
234
+ const parsed = JSON.parse(argumentsText);
235
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
236
+ return parsed;
237
+ }
238
+ catch { /* not JSON: no fields */ }
239
+ return {};
240
+ }
241
+ const planTitle = (value) => typeof value === 'string' ? clamp(stripControls(value.trim()), VSCODE_CHAT_LIMITS.titleCharacters) : '';
242
+ /** Claude Code's task statuses (`pending`, `in_progress`, `completed`) as VS Code's own todo statuses. */
243
+ const planStatus = (value) => value === 'completed' ? 'completed' : value === 'in_progress' ? 'in-progress' : 'not-started';
244
+ /**
245
+ * The task list Claude Code keeps with its task tools, which it offers in place of TodoWrite (TaskCreate, TaskUpdate,
246
+ * TaskList; Claude Code 2.1.312 enables them by default and TodoWrite only when its tasks are turned off). Each call
247
+ * changes ONE task, while VS Code's own todo list shows the WHOLE list, so this keeps the list for one conversation as
248
+ * the calls go by and gives each call's row the list as that call left it.
249
+ *
250
+ * Read from the calls' own arguments, and from Claude Code's own result text where only the result has it (measured
251
+ * on 2.1.312, its `mapToolResultToToolResultBlockParam`): TaskCreate's `Task #<id> created successfully: <subject>`,
252
+ * TaskUpdate's `Updated task #<id> <fields>`, and TaskList's lines `#<id> [<status>] <subject>…`. A result in other
253
+ * words changes nothing here; the row is then an ordinary one.
254
+ */
255
+ export class PlanTracker {
256
+ #tasks = [];
257
+ #calls = new Map();
258
+ /** A call that started: the list as it leaves it, or undefined when it is not a task call (or changes nothing). */
259
+ started(toolName, argumentsText, toolCallId) {
260
+ const name = toolName.toLowerCase();
261
+ if (!['taskcreate', 'taskupdate', TASK_LIST_TOOL].includes(name))
262
+ return undefined;
263
+ const fields = argumentFields(argumentsText);
264
+ if (name === TASK_LIST_TOOL) {
265
+ this.#calls.set(toolCallId, { name });
266
+ return undefined;
267
+ }
268
+ if (name === 'taskcreate') {
269
+ const title = planTitle(fields.subject);
270
+ if (!title)
271
+ return undefined;
272
+ const created = { title, status: 'not-started' };
273
+ this.#tasks.push(created);
274
+ this.#calls.set(toolCallId, { name, created });
275
+ return this.items();
276
+ }
277
+ const id = typeof fields.taskId === 'string' ? fields.taskId : undefined;
278
+ const index = id === undefined ? -1 : this.#tasks.findIndex(task => task.id === id);
279
+ if (index < 0)
280
+ return undefined;
281
+ const task = this.#tasks[index];
282
+ const deleted = fields.status === 'deleted';
283
+ this.#calls.set(toolCallId, { name, before: { task: { ...task }, at: index, deleted } });
284
+ if (deleted)
285
+ this.#tasks.splice(index, 1);
286
+ else {
287
+ const title = planTitle(fields.subject);
288
+ // (changed in place, so a create still waiting for its id keeps its entry)
289
+ Object.assign(task, title ? { title } : {}, typeof fields.status === 'string' ? { status: planStatus(fields.status) } : {});
290
+ }
291
+ return this.items();
292
+ }
293
+ /** A call that completed: the list as it now stands (a failed call's change undone; TaskList's result is the
294
+ * list), so a finished row never shows an older list than the calls before it left. */
295
+ completed(toolCallId, output, isError) {
296
+ const call = this.#calls.get(toolCallId);
297
+ if (!call)
298
+ return undefined;
299
+ this.#calls.delete(toolCallId);
300
+ if (call.name === 'taskcreate' && call.created) {
301
+ const id = isError ? undefined : /^Task #(\S+) created successfully: /.exec(output)?.[1];
302
+ if (id !== undefined)
303
+ call.created.id = id;
304
+ // (a task Claude Code did not create is not on its list)
305
+ else
306
+ this.#tasks = this.#tasks.filter(task => task !== call.created);
307
+ return this.items();
308
+ }
309
+ if (call.name === 'taskupdate') {
310
+ const before = call.before;
311
+ if ((isError || !/^Updated task #/.test(output)) && before) {
312
+ if (before.deleted)
313
+ this.#tasks.splice(Math.min(before.at, this.#tasks.length), 0, before.task);
314
+ else {
315
+ const task = this.#tasks.find(other => other.id === before.task.id);
316
+ if (task)
317
+ Object.assign(task, { title: before.task.title, status: before.task.status });
318
+ }
319
+ }
320
+ return this.items();
321
+ }
322
+ if (isError)
323
+ return undefined;
324
+ if (output.trim() === 'No tasks found') {
325
+ this.#tasks = [];
326
+ return [];
327
+ }
328
+ const listed = output.split('\n').flatMap(line => {
329
+ const match = /^#(\S+) \[(\w+)\] (.*?)(?: \[blocked by [^\]]*\])?$/.exec(line);
330
+ const title = match ? planTitle(match[3]) : '';
331
+ return match && title ? [{ id: match[1], title, status: planStatus(match[2]) }] : [];
332
+ });
333
+ if (listed.length === 0)
334
+ return undefined;
335
+ this.#tasks = listed;
336
+ return this.items();
337
+ }
338
+ items() {
339
+ return this.#tasks.map(({ title, status }) => ({ title, status }));
340
+ }
341
+ }
342
+ /** A subagent call's description, kind and prompt (Claude Code's Task and Agent tools: `description`,
343
+ * `subagent_type`, `prompt`), read from its own arguments; undefined for any other tool. */
344
+ export function subagentCall(toolName, argumentsText) {
345
+ if (!['task', 'agent'].includes(toolName.toLowerCase()))
346
+ return undefined;
347
+ const fields = argumentFields(argumentsText);
348
+ const field = (name, limit) => {
349
+ const value = fields[name];
350
+ return typeof value === 'string' && value.trim() ? clamp(stripControls(value.trim()), limit) : undefined;
351
+ };
352
+ const description = field('description', VSCODE_CHAT_LIMITS.titleCharacters);
353
+ const agentName = field('subagent_type', VSCODE_CHAT_LIMITS.titleCharacters);
354
+ const prompt = field('prompt', VSCODE_CHAT_LIMITS.partCharacters);
355
+ return { ...(description ? { description } : {}), ...(agentName ? { agentName } : {}), ...(prompt ? { prompt } : {}) };
356
+ }
184
357
  /** The command line of a shell tool call, read from its own arguments; undefined for any other tool. */
185
358
  export function shellCommand(toolName, argumentsText) {
186
359
  if (!['bash', 'shell', 'exec_command', 'local_shell', 'powershell'].includes(toolName.toLowerCase()))
@@ -366,6 +539,52 @@ export function acceptedDecisions(request) {
366
539
  const decisions = declared.filter((entry) => typeof entry === 'string');
367
540
  return decisions.length > 0 ? decisions : undefined;
368
541
  }
542
+ /**
543
+ * An elicitation that asks questions, the shape supercode's `ask_user` asks with and a hosted Claude Code's
544
+ * AskUserQuestion is raised as (`question-tool.mjs` `questionElicitation`): every property of its
545
+ * `requested_schema` is one question, with a `title`, the question alone (`x-question`) and its `x-options`
546
+ * (`[{label, description}]`, with `x-multi-select`; none for free text). Any other form (an MCP server's schema with
547
+ * its own `enum`, `format` or `required`) is undefined here and keeps the `Answer…` door, which honours them.
548
+ */
549
+ export function requestQuestions(request) {
550
+ if (request.kind !== 'elicitation')
551
+ return undefined;
552
+ const payload = request.payload;
553
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
554
+ return undefined;
555
+ const schema = payload.requested_schema;
556
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema))
557
+ return undefined;
558
+ const properties = schema.properties;
559
+ if (!properties || typeof properties !== 'object' || Array.isArray(properties))
560
+ return undefined;
561
+ const fields = [];
562
+ for (const [key, value] of Object.entries(properties)) {
563
+ if (!value || typeof value !== 'object' || Array.isArray(value))
564
+ return undefined;
565
+ const property = value;
566
+ const title = typeof property.title === 'string' ? clamp(stripControls(property.title.trim()), VSCODE_CHAT_LIMITS.titleCharacters) : '';
567
+ if (!title)
568
+ return undefined;
569
+ const listed = property['x-options'];
570
+ if (typeof property['x-question'] !== 'string' || !Array.isArray(listed))
571
+ return undefined;
572
+ const options = listed.flatMap(option => {
573
+ if (!option || typeof option !== 'object' || Array.isArray(option) || typeof option.label !== 'string' || !option.label.trim())
574
+ return [];
575
+ const label = clamp(stripControls(option.label.trim()), VSCODE_CHAT_LIMITS.titleCharacters);
576
+ const description = typeof option.description === 'string' && option.description.trim()
577
+ ? clamp(stripControls(option.description.trim()), VSCODE_CHAT_LIMITS.titleCharacters) : undefined;
578
+ return [{ label, ...(description ? { description } : {}) }];
579
+ });
580
+ // The question alone (`x-question`) where the elicitation states it: its `description` repeats the options for
581
+ // a frontend that cannot show them, and the carousel shows them itself.
582
+ const asked = clamp(stripControls(property['x-question'].trim()), VSCODE_CHAT_LIMITS.partCharacters);
583
+ const message = asked && asked !== title ? asked : undefined;
584
+ fields.push({ key, title, ...(message ? { message } : {}), options, multiSelect: property['x-multi-select'] === true });
585
+ }
586
+ return fields.length > 0 ? fields : undefined;
587
+ }
369
588
  /** True when this request asks for content a button click cannot carry. */
370
589
  export function requestNeedsTypedReply(request) {
371
590
  if (request.kind === 'approval')
@@ -447,6 +666,9 @@ export function projectEvent(event, context) {
447
666
  // The harness's own user-role records (runtime-native-projection.mjs
448
667
  // `claudeBlocks`) arrive stamped `source: 'harness'`: a notice or nothing.
449
668
  case 'user_message': {
669
+ // (a subagent's own input, `parent_id`: what it was asked is on its row)
670
+ if (text(payload.parent_id))
671
+ return [];
450
672
  const notice = notPersonNotice(payload);
451
673
  if (notice !== undefined)
452
674
  return notice ? [stable({ part: 'markdown', value: `\n\n${notice}\n\n` })] : [];
@@ -470,6 +692,10 @@ export function projectEvent(event, context) {
470
692
  return [];
471
693
  // crates/runtime/src/event.rs `AgentEvent::TextDelta`.
472
694
  case 'text_delta':
695
+ // A subagent's own words (`parent_id`): VS Code's own subagent row shows its calls and its answer, not its
696
+ // running text, which would otherwise read as the agent's reply.
697
+ if (text(payload.parent_id))
698
+ return [];
473
699
  return [stable({ part: 'markdown', value: text(payload.text) })];
474
700
  // crates/runtime/src/event.rs `AgentEvent::ToolCallStarted`.
475
701
  case 'tool_call_started': {
@@ -479,6 +705,9 @@ export function projectEvent(event, context) {
479
705
  const inputText = clamp(stripControls(parseArguments(rawArguments)), VSCODE_CHAT_LIMITS.partCharacters);
480
706
  const titles = toolTitles(toolName, rawArguments);
481
707
  const commandLine = shellCommand(toolName, rawArguments);
708
+ const plan = planItems(toolName, rawArguments) ?? context.plan?.started(toolName, rawArguments, toolCallId);
709
+ const subagent = subagentCall(toolName, rawArguments);
710
+ const parentToolCallId = text(payload.parent_id);
482
711
  return [{
483
712
  emit: {
484
713
  part: 'tool',
@@ -489,6 +718,9 @@ export function projectEvent(event, context) {
489
718
  inputText,
490
719
  isComplete: false,
491
720
  ...(commandLine ? { commandLine } : {}),
721
+ ...(plan ? { plan } : {}),
722
+ ...(subagent ? { subagent } : {}),
723
+ ...(parentToolCallId ? { parentToolCallId } : {}),
492
724
  },
493
725
  needs: 'chatParticipantAdditions',
494
726
  fallback: [{ part: 'progress', value: titles.invocation }],
@@ -506,6 +738,9 @@ export function projectEvent(event, context) {
506
738
  // The titles stay the ones the call started with (the writer keeps them per call); a completion that names its
507
739
  // tool without having started on this stream is titled by that name.
508
740
  const titles = toolName ? toolTitles(toolName, text(payload.arguments)) : undefined;
741
+ const parentToolCallId = text(payload.parent_id);
742
+ const plan = context.plan?.completed(toolCallId, text(payload.output), isError);
743
+ const background = payload.background === true;
509
744
  return [{
510
745
  emit: {
511
746
  part: 'tool',
@@ -516,6 +751,9 @@ export function projectEvent(event, context) {
516
751
  isError,
517
752
  isComplete: true,
518
753
  resultText: output,
754
+ ...(plan ? { plan } : {}),
755
+ ...(parentToolCallId ? { parentToolCallId } : {}),
756
+ ...(background ? { background } : {}),
519
757
  },
520
758
  needs: 'chatParticipantAdditions',
521
759
  fallback: [{
@@ -564,9 +802,11 @@ export function projectEvent(event, context) {
564
802
  const summary = requestSummary(request);
565
803
  const answers = requestAnswers(request, context.connectionId);
566
804
  const subject = requestSubject(request);
805
+ const asked = requestQuestions(request);
567
806
  const title = request.kind === 'approval'
568
807
  ? subject ? `Allow ${stripControls(subject)}?` : 'Approval required'
569
- : subject ? `Approve ${subject}` : `Assistant request #${request.id}`;
808
+ : asked ? asked.length === 1 ? asked[0].title : `${asked.length} questions`
809
+ : subject ? `Approve ${subject}` : `Assistant request #${request.id}`;
570
810
  // The host capability decides whether these original answers can use
571
811
  // a native in-turn confirmation card. Stock confirmation clicks start a
572
812
  // new chat request, so stable command buttons remain the fallback.
@@ -576,13 +816,22 @@ export function projectEvent(event, context) {
576
816
  value: `\n\n**${title}**\n\n${summary}\n\n_This runtime answers its own requests; Volter Harness is observing._\n\n`,
577
817
  })];
578
818
  }
579
- return [stable({
580
- part: 'answers',
581
- approval: request.kind === 'approval',
582
- title: clamp(title, VSCODE_CHAT_LIMITS.titleCharacters),
583
- message: summary,
584
- answers,
585
- })];
819
+ const buttons = {
820
+ part: 'answers',
821
+ approval: request.kind === 'approval',
822
+ title: clamp(title, VSCODE_CHAT_LIMITS.titleCharacters),
823
+ message: summary,
824
+ answers,
825
+ };
826
+ // A harness's questions: VS Code's own carousel, answered inside the running turn.
827
+ if (asked) {
828
+ return [{
829
+ emit: { part: 'questions', connectionId: context.connectionId, requestId: request.id, questions: asked, fallback: buttons },
830
+ needs: 'chatParticipantAdditions',
831
+ fallback: [buttons],
832
+ }];
833
+ }
834
+ return [stable(buttons)];
586
835
  }
587
836
  // crates/harness/src/server.rs `FrontendRequestBroker::respond`.
588
837
  case 'request_resolved': {
@@ -604,10 +853,29 @@ export function projectEvent(event, context) {
604
853
  fallback: [{ part: 'markdown', value: `> ⚠ ${message}` }],
605
854
  }];
606
855
  }
856
+ // A harness hook that blocked or warned (`runtime-native-projection.mjs`, from Claude Code's own hook records).
857
+ // VS Code's own hook row, for the hook events it names; its words otherwise. In the pinned host `push` of this part
858
+ // checks no proposal and `hookProgress` checks `chatParticipantAdditions`; `chatHooks` gates nothing this frontend
859
+ // uses. It is keyed to `chatParticipantAdditions` with the other proposed parts, so a host without it gets words.
860
+ case 'hook': {
861
+ const hookType = HOOK_TYPES.find(type => type === text(payload.hook_event));
862
+ const reason = clamp(stripControls(text(payload.reason)), VSCODE_CHAT_LIMITS.partCharacters);
863
+ const blocked = payload.blocked === true;
864
+ const said = hookSentence(text(payload.hook_event, 'harness'), blocked, reason);
865
+ return [{
866
+ emit: hookType ? { part: 'hook', hookType, blocked, reason } : { part: 'warning', value: said },
867
+ needs: 'chatParticipantAdditions',
868
+ fallback: [{ part: 'markdown', value: `> ⚠ ${said}` }],
869
+ }];
870
+ }
607
871
  // crates/runtime/src/event.rs `AgentEvent::Usage`.
608
872
  // Token counts are not conversation: VS Code's own chat keeps them out of the transcript.
609
873
  case 'usage':
610
874
  return [];
875
+ // The harness's background tasks are the session's state (`session.ts` lists them while a turn runs, each with
876
+ // its Stop), not conversation.
877
+ case 'background_tasks':
878
+ return [];
611
879
  // crates/runtime/src/event.rs `AgentEvent::BackgroundOutput`.
612
880
  case 'background_output': {
613
881
  const chunk = fence(clamp(stripControls(text(payload.chunk)), VSCODE_CHAT_LIMITS.partCharacters));
@@ -665,6 +933,15 @@ export function projectEvent(event, context) {
665
933
  case 'native_event':
666
934
  return [];
667
935
  case 'status':
936
+ // A subagent's task finishing (`call_id` names the subagent call): its answer belongs to that call's row (or to
937
+ // the agent's reply, for a subagent started in the background), so the notice is its first line, and the task's
938
+ // output file where it names one (a background shell's notice names the same call).
939
+ if (text(payload.call_id)) {
940
+ const lines = stripControls(text(payload.message)).split('\n');
941
+ const output = lines.find(line => line.startsWith('Output: ') && line.length > 8);
942
+ const line = [clamp((lines[0] ?? '').trim(), VSCODE_CHAT_LIMITS.titleCharacters), output].filter(Boolean).join('\n');
943
+ return line ? [stable({ part: 'progress', value: clamp(line, VSCODE_CHAT_LIMITS.partCharacters) })] : [];
944
+ }
668
945
  return [stable({ part: 'progress', value: clamp(stripControls(text(payload.message)), VSCODE_CHAT_LIMITS.partCharacters) })];
669
946
  case 'warning':
670
947
  return [stable({ part: 'warning', value: clamp(stripControls(text(payload.message)), VSCODE_CHAT_LIMITS.partCharacters) })];
@@ -672,6 +949,9 @@ export function projectEvent(event, context) {
672
949
  // sdk/interchange/frontend-transcript.mjs reads a donor that emits them.
673
950
  case 'reasoning':
674
951
  case 'thinking': {
952
+ // (a subagent's own reasoning, `parent_id`, as its running text above)
953
+ if (text(payload.parent_id))
954
+ return [];
675
955
  const body = clamp(stripControls(text(payload.text ?? payload.summary, opaqueText(event.payload))), VSCODE_CHAT_LIMITS.partCharacters);
676
956
  return [{
677
957
  emit: { part: 'thinking', value: body },
@@ -776,7 +1056,8 @@ export function projectReplayTurns(events, granted, context, history = []) {
776
1056
  if (typeof request?.id === 'number' && allowed.has(request.id))
777
1057
  continue;
778
1058
  }
779
- if (event.kind === 'user_message') {
1059
+ // (a subagent's own input is no turn of the conversation; `projectEvent` draws nothing for it)
1060
+ if (event.kind === 'user_message' && !text(payloadOf(event).parent_id)) {
780
1061
  streamedText = undefined;
781
1062
  const payload = payloadOf(event);
782
1063
  const shown = notPersonNotice(payload);
@@ -840,7 +1121,7 @@ export function projectReplayTurns(events, granted, context, history = []) {
840
1121
  * The attach snapshot's canonical history as chat turns. Bounded: when older
841
1122
  * turns are omitted the view says so, the way the browser observer does.
842
1123
  */
843
- export function projectHistory(history, granted, pendingToolNames = new Set()) {
1124
+ export function projectHistory(history, granted, pendingToolNames = new Set(), plan = new PlanTracker()) {
844
1125
  const turns = [];
845
1126
  // Keep results inside the native invocation, as the live stream does. A
846
1127
  // separate Markdown result bypasses the editor's collapsed tool-step group.
@@ -923,6 +1204,8 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
923
1204
  for (const call of message.tool_calls ?? []) {
924
1205
  const titles = toolTitles(call.function.name, call.function.arguments);
925
1206
  const commandLine = shellCommand(call.function.name, call.function.arguments);
1207
+ const items = planItems(call.function.name, call.function.arguments) ?? plan.started(call.function.name, call.function.arguments, call.id);
1208
+ const subagent = subagentCall(call.function.name, call.function.arguments);
926
1209
  const row = {
927
1210
  part: 'tool',
928
1211
  toolName: call.function.name,
@@ -930,6 +1213,8 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
930
1213
  invocationMessage: titles.invocation,
931
1214
  pastTenseMessage: titles.pastTense,
932
1215
  ...(commandLine ? { commandLine } : {}),
1216
+ ...(items ? { plan: items } : {}),
1217
+ ...(subagent ? { subagent } : {}),
933
1218
  inputText: clamp(stripControls(parseArguments(call.function.arguments)), VSCODE_CHAT_LIMITS.partCharacters),
934
1219
  isComplete: true,
935
1220
  };
@@ -967,6 +1252,9 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
967
1252
  // establish failure; do not guess from arbitrary result prose.
968
1253
  if (body === '[no tool result recorded — turn interrupted]' || (Array.isArray(message.content) && message.content.some(part => part && typeof part === 'object' && !Array.isArray(part) && part.is_error === true)))
969
1254
  row.isError = true;
1255
+ const items = plan.completed(message.tool_call_id, body, row.isError === true);
1256
+ if (items)
1257
+ row.plan = items;
970
1258
  toolRows.delete(message.tool_call_id);
971
1259
  }
972
1260
  else {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@volter/supercode-frontend-vscode",
3
3
  "displayName": "Volter Harness",
4
- "version": "0.1.62",
4
+ "version": "0.1.63",
5
5
  "type": "module",
6
6
  "description": "VS Code chat-view frontend for an existing Volter Harness SDK runtime",
7
7
  "repository": {