@volter/supercode-frontend-vscode 0.1.61 → 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
@@ -357,7 +402,7 @@ it needs and the fallback it degrades to. Each arm names the producer it reads.
357
402
  | Event kind | Producer | Rendered as |
358
403
  |---|---|---|
359
404
  | `text_delta` | `crates/runtime/src/event.rs` `AgentEvent::TextDelta` | `response.markdown` (streamed) |
360
- | `user_message` | `sdk/interchange/agent-runtime-host.mjs` accepted-turn projection | the request turn in history; dropped live (VS Code already shows it) |
405
+ | `user_message` | `sdk/interchange/agent-runtime-host.mjs` accepted-turn projection; `runtime-native-projection.mjs` `claudeBlocks` for Claude's own user-role records (`source: harness`) | the request turn in history; dropped live (VS Code already shows it); a harness record shows its `notice` or nothing |
361
406
  | `turn_started` / `turn_completed` / `turn_succeeded` | `submit_claimed`; `AgentEvent::TurnCompleted` | lifecycle only — the handler resolves, nothing renders |
362
407
  | `turn_interrupted` | `submit_claimed` | an italic notice |
363
408
  | `turn_failed` | `submit_claimed` | `ChatResponseWarningPart` → blockquote |
@@ -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
@@ -438,10 +516,17 @@ needs it; a reader does not, and a collapsed block is still a reader's problem.
438
516
  one to a person, so `role: 'system'` is dropped outright and the transcript starts where the
439
517
  conversation does.
440
518
 
441
- Saved user-role records marked `isMeta`, `isCompactSummary` or
442
- `promptSource: system` are also context. They create no person-message bubble;
443
- ordinary quoted text without those flags stays a person message. Claude's
444
- unmarked, exact `[Request interrupted by user]` record becomes a short turn
519
+ Whose words a user-role record carries is one rule,
520
+ `@volter/supercode-interchange/user-provenance`, vendored here unchanged. Live
521
+ and replayed `user_message` events carry its answer as `source`
522
+ (`person` | `harness`) with a `notice` where one is shown; saved history is
523
+ read through the same rule. Records the harness wrote (`isMeta`,
524
+ `isSynthetic` — the stream's image-coordinate note — `isCompactSummary`,
525
+ `promptSource: system`, sidechain and tool-result records, file attachments)
526
+ create no person-message bubble, live or restored; ordinary quoted text
527
+ without those flags stays a person message. A task notification becomes a
528
+ short background-task notice, and Claude's unmarked, exact interruption
529
+ records (`[Request interrupted by user]`, `… for tool use]`) a short turn
445
530
  interruption notice. The native transcript remains authoritative and unchanged.
446
531
  [Source evidence and remaining qualification](../../docs/evidence/chat-native-notices-20261013.md).
447
532
 
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. */
@@ -172,14 +264,6 @@ export interface ProjectedTurn {
172
264
  /** Rendered parts for an assistant turn. */
173
265
  parts?: ChatPart[];
174
266
  }
175
- /**
176
- * A "user" message no person typed: Claude Code's `<task-notification>` (a background task reporting back) or the
177
- * editor's automatic tripwire nudge or Claude's unmarked interruption record. None is shown as the person's words.
178
- * A task notification and interruption are short notices; a nudge is not shown. Undefined for a person's message.
179
- */
180
- export declare function automaticPrompt(body: string): {
181
- notice?: string;
182
- } | undefined;
183
267
  export declare function projectReplayTurns(events: readonly FrontendEvent[], granted: ReadonlySet<string>, context: ProjectionContext, history?: readonly ProjectedTurn[]): ProjectedTurn[];
184
268
  /**
185
269
  * The attach snapshot's canonical history as chat turns. Bounded: when older
@@ -187,4 +271,4 @@ export declare function projectReplayTurns(events: readonly FrontendEvent[], gra
187
271
  */
188
272
  export declare function projectHistory(history: readonly (ChatMessage & {
189
273
  metadata?: Record<string, string>;
190
- })[], granted: ReadonlySet<string>, pendingToolNames?: ReadonlySet<string>): ProjectedTurn[];
274
+ })[], granted: ReadonlySet<string>, pendingToolNames?: ReadonlySet<string>, plan?: PlanTracker): ProjectedTurn[];