@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 +79 -1
- package/dist/extension.js +132 -23
- package/dist/view.d.ts +93 -1
- package/dist/view.js +298 -10
- package/package.json +1 -1
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
|
|
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 (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
...
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
:
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
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