@volter/supercode-frontend-vscode 0.1.62 → 0.1.64
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 +119 -22
- package/dist/extension.js +256 -111
- package/dist/host-controls.d.ts +0 -1
- package/dist/proposals.js +2 -2
- package/dist/session.d.ts +2 -21
- package/dist/session.js +3 -35
- package/dist/view.d.ts +135 -6
- package/dist/view.js +470 -110
- package/package.json +1 -1
package/dist/view.js
CHANGED
|
@@ -137,6 +137,11 @@ export function toolTitles(toolName, argumentsText) {
|
|
|
137
137
|
const command = field('command', 'cmd');
|
|
138
138
|
return command ? both(`Running ${code(command)}`, `Ran ${code(command)}`) : both(`Running a command`, `Ran a command`);
|
|
139
139
|
}
|
|
140
|
+
// Code-OSS's own Claude host's words for the shell's output and stop rows (`claudeToolDisplay.ts`).
|
|
141
|
+
if (name === 'bashoutput')
|
|
142
|
+
return both('Reading shell output', 'Read shell output');
|
|
143
|
+
if (name === 'killbash')
|
|
144
|
+
return both('Kill shell command', 'Kill shell command');
|
|
140
145
|
if (name === 'read' && file) {
|
|
141
146
|
const offset = typeof fields.offset === 'number' ? fields.offset : undefined;
|
|
142
147
|
const limit = typeof fields.limit === 'number' ? fields.limit : undefined;
|
|
@@ -174,6 +179,26 @@ export function toolTitles(toolName, argumentsText) {
|
|
|
174
179
|
}
|
|
175
180
|
if (name === 'todowrite' || name === 'update_plan')
|
|
176
181
|
return both('Updating the plan', 'Updated the plan');
|
|
182
|
+
if (name === 'taskcreate') {
|
|
183
|
+
const subject = field('subject');
|
|
184
|
+
if (subject)
|
|
185
|
+
return both(`Adding a task: ${plain(subject)}`, `Added a task: ${plain(subject)}`);
|
|
186
|
+
}
|
|
187
|
+
if (name === 'taskupdate') {
|
|
188
|
+
const id = field('taskId'), status = field('status');
|
|
189
|
+
if (id) {
|
|
190
|
+
const task = `task #${plain(id)}`;
|
|
191
|
+
if (status === 'in_progress')
|
|
192
|
+
return both(`Starting ${task}`, `Started ${task}`);
|
|
193
|
+
if (status === 'completed')
|
|
194
|
+
return both(`Completing ${task}`, `Completed ${task}`);
|
|
195
|
+
if (status === 'deleted')
|
|
196
|
+
return both(`Removing ${task}`, `Removed ${task}`);
|
|
197
|
+
return both(`Updating ${task}`, `Updated ${task}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (name === TASK_LIST_TOOL)
|
|
201
|
+
return both('Reading the task list', 'Read the task list');
|
|
177
202
|
// Claude Code names an MCP tool `mcp__<server>__<tool>`; VS Code writes "Ran `tool` – server".
|
|
178
203
|
const mcp = /^mcp__(.+?)__(.+)$/.exec(toolName);
|
|
179
204
|
if (mcp)
|
|
@@ -181,9 +206,171 @@ export function toolTitles(toolName, argumentsText) {
|
|
|
181
206
|
const subject = toolSubject(argumentsText);
|
|
182
207
|
return subject ? both(`Running ${code(toolName)} · ${plain(subject)}`, `Ran ${code(toolName)} · ${plain(subject)}`) : both(`Running ${code(toolName)}`, `Ran ${code(toolName)}`);
|
|
183
208
|
}
|
|
184
|
-
/**
|
|
209
|
+
/** A hook's row as words, as VS Code titles its own (`chatHookContentPart.ts`): "Blocked by …" or "Warning from …". */
|
|
210
|
+
export function hookSentence(hookEvent, blocked, reason) {
|
|
211
|
+
return `${blocked ? 'Blocked by' : 'Warning from'} the ${hookEvent} hook${reason ? `: ${reason}` : ''}`;
|
|
212
|
+
}
|
|
213
|
+
/** The hook events VS Code's own Chat names (`ChatHookType`), which are Claude Code's. */
|
|
214
|
+
export const HOOK_TYPES = ['SessionStart', 'SessionEnd', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'PreCompact',
|
|
215
|
+
'SubagentStart', 'SubagentStop', 'Stop', 'ErrorOccurred'];
|
|
216
|
+
/** Claude Code's TaskList tool, lower-cased as this view compares tool names. */
|
|
217
|
+
const TASK_LIST_TOOL = 'tasklist'; // process-read: a Claude Code tool name, not the Windows command
|
|
218
|
+
/** A plan call's items (Claude Code's TodoWrite `todos` of `content`; the `update_plan` tool's `plan` of `step`, as
|
|
219
|
+
* supercode's own and Codex's take it), each its text and status, read from its own arguments; undefined for any
|
|
220
|
+
* other tool, or arguments holding none. Both carry the whole list, so they need no state. */
|
|
221
|
+
export function planItems(toolName, argumentsText) {
|
|
222
|
+
const name = toolName.toLowerCase();
|
|
223
|
+
if (name !== 'todowrite' && name !== 'update_plan')
|
|
224
|
+
return undefined;
|
|
225
|
+
const list = argumentFields(argumentsText)[name === 'todowrite' ? 'todos' : 'plan'];
|
|
226
|
+
if (!Array.isArray(list))
|
|
227
|
+
return undefined;
|
|
228
|
+
const items = list.flatMap((item) => {
|
|
229
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
230
|
+
return [];
|
|
231
|
+
const fields = item;
|
|
232
|
+
const title = planTitle(name === 'todowrite' ? fields.content : fields.step);
|
|
233
|
+
return title ? [{ title, status: planStatus(fields.status) }] : [];
|
|
234
|
+
});
|
|
235
|
+
return items.length > 0 ? items : undefined;
|
|
236
|
+
}
|
|
237
|
+
function argumentFields(argumentsText) {
|
|
238
|
+
try {
|
|
239
|
+
const parsed = JSON.parse(argumentsText);
|
|
240
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
241
|
+
return parsed;
|
|
242
|
+
}
|
|
243
|
+
catch { /* not JSON: no fields */ }
|
|
244
|
+
return {};
|
|
245
|
+
}
|
|
246
|
+
const planTitle = (value) => typeof value === 'string' ? clamp(stripControls(value.trim()), VSCODE_CHAT_LIMITS.titleCharacters) : '';
|
|
247
|
+
/** Claude Code's task statuses (`pending`, `in_progress`, `completed`) as VS Code's own todo statuses. */
|
|
248
|
+
const planStatus = (value) => value === 'completed' ? 'completed' : value === 'in_progress' ? 'in-progress' : 'not-started';
|
|
249
|
+
/**
|
|
250
|
+
* The task list Claude Code keeps with its task tools, which it offers in place of TodoWrite (TaskCreate, TaskUpdate,
|
|
251
|
+
* TaskList; Claude Code 2.1.312 enables them by default and TodoWrite only when its tasks are turned off). Each call
|
|
252
|
+
* changes ONE task, while VS Code's own todo list shows the WHOLE list, so this keeps the list for one conversation as
|
|
253
|
+
* the calls go by and gives each call's row the list as that call left it.
|
|
254
|
+
*
|
|
255
|
+
* Read from the calls' own arguments, and from Claude Code's own result text where only the result has it (measured
|
|
256
|
+
* on 2.1.312, its `mapToolResultToToolResultBlockParam`): TaskCreate's `Task #<id> created successfully: <subject>`,
|
|
257
|
+
* TaskUpdate's `Updated task #<id> <fields>`, and TaskList's lines `#<id> [<status>] <subject>…`. A result in other
|
|
258
|
+
* words changes nothing here; the row is then an ordinary one.
|
|
259
|
+
*/
|
|
260
|
+
export class PlanTracker {
|
|
261
|
+
#tasks = [];
|
|
262
|
+
#calls = new Map();
|
|
263
|
+
/** A call that started: the list as it leaves it, or undefined when it is not a task call (or changes nothing). */
|
|
264
|
+
started(toolName, argumentsText, toolCallId) {
|
|
265
|
+
const name = toolName.toLowerCase();
|
|
266
|
+
if (!['taskcreate', 'taskupdate', TASK_LIST_TOOL].includes(name))
|
|
267
|
+
return undefined;
|
|
268
|
+
const fields = argumentFields(argumentsText);
|
|
269
|
+
if (name === TASK_LIST_TOOL) {
|
|
270
|
+
this.#calls.set(toolCallId, { name });
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
if (name === 'taskcreate') {
|
|
274
|
+
const title = planTitle(fields.subject);
|
|
275
|
+
if (!title)
|
|
276
|
+
return undefined;
|
|
277
|
+
const created = { title, status: 'not-started' };
|
|
278
|
+
this.#tasks.push(created);
|
|
279
|
+
this.#calls.set(toolCallId, { name, created });
|
|
280
|
+
return this.items();
|
|
281
|
+
}
|
|
282
|
+
const id = typeof fields.taskId === 'string' ? fields.taskId : undefined;
|
|
283
|
+
const index = id === undefined ? -1 : this.#tasks.findIndex(task => task.id === id);
|
|
284
|
+
if (index < 0)
|
|
285
|
+
return undefined;
|
|
286
|
+
const task = this.#tasks[index];
|
|
287
|
+
const deleted = fields.status === 'deleted';
|
|
288
|
+
this.#calls.set(toolCallId, { name, before: { task: { ...task }, at: index, deleted } });
|
|
289
|
+
if (deleted)
|
|
290
|
+
this.#tasks.splice(index, 1);
|
|
291
|
+
else {
|
|
292
|
+
const title = planTitle(fields.subject);
|
|
293
|
+
// (changed in place, so a create still waiting for its id keeps its entry)
|
|
294
|
+
Object.assign(task, title ? { title } : {}, typeof fields.status === 'string' ? { status: planStatus(fields.status) } : {});
|
|
295
|
+
}
|
|
296
|
+
return this.items();
|
|
297
|
+
}
|
|
298
|
+
/** A call that completed: the list as it now stands (a failed call's change undone; TaskList's result is the
|
|
299
|
+
* list), so a finished row never shows an older list than the calls before it left. */
|
|
300
|
+
completed(toolCallId, output, isError) {
|
|
301
|
+
const call = this.#calls.get(toolCallId);
|
|
302
|
+
if (!call)
|
|
303
|
+
return undefined;
|
|
304
|
+
this.#calls.delete(toolCallId);
|
|
305
|
+
if (call.name === 'taskcreate' && call.created) {
|
|
306
|
+
const id = isError ? undefined : /^Task #(\S+) created successfully: /.exec(output)?.[1];
|
|
307
|
+
if (id !== undefined)
|
|
308
|
+
call.created.id = id;
|
|
309
|
+
// (a task Claude Code did not create is not on its list)
|
|
310
|
+
else
|
|
311
|
+
this.#tasks = this.#tasks.filter(task => task !== call.created);
|
|
312
|
+
return this.items();
|
|
313
|
+
}
|
|
314
|
+
if (call.name === 'taskupdate') {
|
|
315
|
+
const before = call.before;
|
|
316
|
+
if ((isError || !/^Updated task #/.test(output)) && before) {
|
|
317
|
+
if (before.deleted)
|
|
318
|
+
this.#tasks.splice(Math.min(before.at, this.#tasks.length), 0, before.task);
|
|
319
|
+
else {
|
|
320
|
+
const task = this.#tasks.find(other => other.id === before.task.id);
|
|
321
|
+
if (task)
|
|
322
|
+
Object.assign(task, { title: before.task.title, status: before.task.status });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return this.items();
|
|
326
|
+
}
|
|
327
|
+
if (isError)
|
|
328
|
+
return undefined;
|
|
329
|
+
if (output.trim() === 'No tasks found') {
|
|
330
|
+
this.#tasks = [];
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
const listed = output.split('\n').flatMap(line => {
|
|
334
|
+
const match = /^#(\S+) \[(\w+)\] (.*?)(?: \[blocked by [^\]]*\])?$/.exec(line);
|
|
335
|
+
const title = match ? planTitle(match[3]) : '';
|
|
336
|
+
return match && title ? [{ id: match[1], title, status: planStatus(match[2]) }] : [];
|
|
337
|
+
});
|
|
338
|
+
if (listed.length === 0)
|
|
339
|
+
return undefined;
|
|
340
|
+
this.#tasks = listed;
|
|
341
|
+
return this.items();
|
|
342
|
+
}
|
|
343
|
+
items() {
|
|
344
|
+
return this.#tasks.map(({ title, status }) => ({ title, status }));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/** A subagent call's description, kind and prompt (Claude Code's Task and Agent tools: `description`,
|
|
348
|
+
* `subagent_type`, `prompt`), read from its own arguments; undefined for any other tool. */
|
|
349
|
+
export function subagentCall(toolName, argumentsText) {
|
|
350
|
+
if (!['task', 'agent'].includes(toolName.toLowerCase()))
|
|
351
|
+
return undefined;
|
|
352
|
+
const fields = argumentFields(argumentsText);
|
|
353
|
+
const field = (name, limit) => {
|
|
354
|
+
const value = fields[name];
|
|
355
|
+
return typeof value === 'string' && value.trim() ? clamp(stripControls(value.trim()), limit) : undefined;
|
|
356
|
+
};
|
|
357
|
+
const description = field('description', VSCODE_CHAT_LIMITS.titleCharacters);
|
|
358
|
+
const agentName = field('subagent_type', VSCODE_CHAT_LIMITS.titleCharacters);
|
|
359
|
+
const prompt = field('prompt', VSCODE_CHAT_LIMITS.partCharacters);
|
|
360
|
+
return { ...(description ? { description } : {}), ...(agentName ? { agentName } : {}), ...(prompt ? { prompt } : {}) };
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* The command line of a shell tool call, read from its own arguments; undefined for any other tool. Claude Code's
|
|
364
|
+
* BashOutput and KillBash are terminal rows too, as Code-OSS's own Claude host draws them (`claudeToolDisplay.ts`
|
|
365
|
+
* `TOOL_ROWS`): their command line is their `command`, else their input (`getClaudeToolInputString`).
|
|
366
|
+
*/
|
|
185
367
|
export function shellCommand(toolName, argumentsText) {
|
|
186
|
-
|
|
368
|
+
const name = toolName.toLowerCase();
|
|
369
|
+
if (name === 'bashoutput' || name === 'killbash') {
|
|
370
|
+
const command = argumentFields(argumentsText).command;
|
|
371
|
+
return stripControls(typeof command === 'string' && command.trim() ? command : parseArguments(argumentsText)) || undefined;
|
|
372
|
+
}
|
|
373
|
+
if (!['bash', 'shell', 'exec_command', 'local_shell', 'powershell'].includes(name))
|
|
187
374
|
return undefined;
|
|
188
375
|
try {
|
|
189
376
|
const fields = JSON.parse(argumentsText);
|
|
@@ -366,6 +553,52 @@ export function acceptedDecisions(request) {
|
|
|
366
553
|
const decisions = declared.filter((entry) => typeof entry === 'string');
|
|
367
554
|
return decisions.length > 0 ? decisions : undefined;
|
|
368
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* An elicitation that asks questions, the shape supercode's `ask_user` asks with and a hosted Claude Code's
|
|
558
|
+
* AskUserQuestion is raised as (`question-tool.mjs` `questionElicitation`): every property of its
|
|
559
|
+
* `requested_schema` is one question, with a `title`, the question alone (`x-question`) and its `x-options`
|
|
560
|
+
* (`[{label, description}]`, with `x-multi-select`; none for free text). Any other form (an MCP server's schema with
|
|
561
|
+
* its own `enum`, `format` or `required`) is undefined here and keeps the `Answer…` door, which honours them.
|
|
562
|
+
*/
|
|
563
|
+
export function requestQuestions(request) {
|
|
564
|
+
if (request.kind !== 'elicitation')
|
|
565
|
+
return undefined;
|
|
566
|
+
const payload = request.payload;
|
|
567
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
568
|
+
return undefined;
|
|
569
|
+
const schema = payload.requested_schema;
|
|
570
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema))
|
|
571
|
+
return undefined;
|
|
572
|
+
const properties = schema.properties;
|
|
573
|
+
if (!properties || typeof properties !== 'object' || Array.isArray(properties))
|
|
574
|
+
return undefined;
|
|
575
|
+
const fields = [];
|
|
576
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
577
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
578
|
+
return undefined;
|
|
579
|
+
const property = value;
|
|
580
|
+
const title = typeof property.title === 'string' ? clamp(stripControls(property.title.trim()), VSCODE_CHAT_LIMITS.titleCharacters) : '';
|
|
581
|
+
if (!title)
|
|
582
|
+
return undefined;
|
|
583
|
+
const listed = property['x-options'];
|
|
584
|
+
if (typeof property['x-question'] !== 'string' || !Array.isArray(listed))
|
|
585
|
+
return undefined;
|
|
586
|
+
const options = listed.flatMap(option => {
|
|
587
|
+
if (!option || typeof option !== 'object' || Array.isArray(option) || typeof option.label !== 'string' || !option.label.trim())
|
|
588
|
+
return [];
|
|
589
|
+
const label = clamp(stripControls(option.label.trim()), VSCODE_CHAT_LIMITS.titleCharacters);
|
|
590
|
+
const description = typeof option.description === 'string' && option.description.trim()
|
|
591
|
+
? clamp(stripControls(option.description.trim()), VSCODE_CHAT_LIMITS.titleCharacters) : undefined;
|
|
592
|
+
return [{ label, ...(description ? { description } : {}) }];
|
|
593
|
+
});
|
|
594
|
+
// The question alone (`x-question`) where the elicitation states it: its `description` repeats the options for
|
|
595
|
+
// a frontend that cannot show them, and the carousel shows them itself.
|
|
596
|
+
const asked = clamp(stripControls(property['x-question'].trim()), VSCODE_CHAT_LIMITS.partCharacters);
|
|
597
|
+
const message = asked && asked !== title ? asked : undefined;
|
|
598
|
+
fields.push({ key, title, ...(message ? { message } : {}), options, multiSelect: property['x-multi-select'] === true });
|
|
599
|
+
}
|
|
600
|
+
return fields.length > 0 ? fields : undefined;
|
|
601
|
+
}
|
|
369
602
|
/** True when this request asks for content a button click cannot carry. */
|
|
370
603
|
export function requestNeedsTypedReply(request) {
|
|
371
604
|
if (request.kind === 'approval')
|
|
@@ -425,6 +658,24 @@ export function requestSubject(request) {
|
|
|
425
658
|
return clamp(stripControls(subject), 120);
|
|
426
659
|
return undefined;
|
|
427
660
|
}
|
|
661
|
+
/** The words an error event states (`message`, else `error` or `reason`), bounded; '' when it states none. */
|
|
662
|
+
function errorMessage(payload) {
|
|
663
|
+
const said = [payload.message, payload.error, payload.reason].find((value) => typeof value === 'string' && value.trim() !== '');
|
|
664
|
+
return said ? clamp(stripControls(said.trim()), VSCODE_CHAT_LIMITS.partCharacters) : '';
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* How a turn that did not succeed ends the response its request handler runs, as VS Code's own agent host ends one
|
|
668
|
+
* (`agentHostSessionHandler.ts` `_getTurnErrorDetails`): a failed turn is the response's error, an interrupted one its
|
|
669
|
+
* warning-level, expected error. Undefined for any other event.
|
|
670
|
+
*/
|
|
671
|
+
export function turnOutcome(event) {
|
|
672
|
+
const payload = payloadOf(event);
|
|
673
|
+
if (event.kind === 'turn_interrupted')
|
|
674
|
+
return { message: errorMessage(payload) || 'Turn interrupted.', interrupted: true };
|
|
675
|
+
if (event.kind === 'turn_failed')
|
|
676
|
+
return { message: errorMessage(payload) || 'Turn failed.', interrupted: false };
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
428
679
|
/** Pending requests belong to the live runtime, even when saved transcript
|
|
429
680
|
* history predates them. Rehydrate their controls before waiting for new events. */
|
|
430
681
|
export function projectPendingRequests(requests, context) {
|
|
@@ -447,9 +698,14 @@ export function projectEvent(event, context) {
|
|
|
447
698
|
// The harness's own user-role records (runtime-native-projection.mjs
|
|
448
699
|
// `claudeBlocks`) arrive stamped `source: 'harness'`: a notice or nothing.
|
|
449
700
|
case 'user_message': {
|
|
701
|
+
// (a subagent's own input, `parent_id`: what it was asked is on its row)
|
|
702
|
+
if (text(payload.parent_id))
|
|
703
|
+
return [];
|
|
704
|
+
// A harness record (a background task's notification, an interruption) is Code-OSS's system notification row,
|
|
705
|
+
// which the extension API does not carry: here it is its own words as a line that stays (`noticeLine`).
|
|
450
706
|
const notice = notPersonNotice(payload);
|
|
451
707
|
if (notice !== undefined)
|
|
452
|
-
return notice ? [stable(
|
|
708
|
+
return notice && firstNotice(context, notifiedCall(payload)) ? [stable(noticeLine(notice))] : [];
|
|
453
709
|
if (context.liveUserMessages) {
|
|
454
710
|
const body = text(payload.text);
|
|
455
711
|
const prefix = '\n\n**Additional input**\n\n';
|
|
@@ -470,6 +726,10 @@ export function projectEvent(event, context) {
|
|
|
470
726
|
return [];
|
|
471
727
|
// crates/runtime/src/event.rs `AgentEvent::TextDelta`.
|
|
472
728
|
case 'text_delta':
|
|
729
|
+
// A subagent's own words (`parent_id`): VS Code's own subagent row shows its calls and its answer, not its
|
|
730
|
+
// running text, which would otherwise read as the agent's reply.
|
|
731
|
+
if (text(payload.parent_id))
|
|
732
|
+
return [];
|
|
473
733
|
return [stable({ part: 'markdown', value: text(payload.text) })];
|
|
474
734
|
// crates/runtime/src/event.rs `AgentEvent::ToolCallStarted`.
|
|
475
735
|
case 'tool_call_started': {
|
|
@@ -479,6 +739,9 @@ export function projectEvent(event, context) {
|
|
|
479
739
|
const inputText = clamp(stripControls(parseArguments(rawArguments)), VSCODE_CHAT_LIMITS.partCharacters);
|
|
480
740
|
const titles = toolTitles(toolName, rawArguments);
|
|
481
741
|
const commandLine = shellCommand(toolName, rawArguments);
|
|
742
|
+
const plan = planItems(toolName, rawArguments) ?? context.plan?.started(toolName, rawArguments, toolCallId);
|
|
743
|
+
const subagent = subagentCall(toolName, rawArguments);
|
|
744
|
+
const parentToolCallId = text(payload.parent_id);
|
|
482
745
|
return [{
|
|
483
746
|
emit: {
|
|
484
747
|
part: 'tool',
|
|
@@ -489,6 +752,9 @@ export function projectEvent(event, context) {
|
|
|
489
752
|
inputText,
|
|
490
753
|
isComplete: false,
|
|
491
754
|
...(commandLine ? { commandLine } : {}),
|
|
755
|
+
...(plan ? { plan } : {}),
|
|
756
|
+
...(subagent ? { subagent } : {}),
|
|
757
|
+
...(parentToolCallId ? { parentToolCallId } : {}),
|
|
492
758
|
},
|
|
493
759
|
needs: 'chatParticipantAdditions',
|
|
494
760
|
fallback: [{ part: 'progress', value: titles.invocation }],
|
|
@@ -506,6 +772,9 @@ export function projectEvent(event, context) {
|
|
|
506
772
|
// The titles stay the ones the call started with (the writer keeps them per call); a completion that names its
|
|
507
773
|
// tool without having started on this stream is titled by that name.
|
|
508
774
|
const titles = toolName ? toolTitles(toolName, text(payload.arguments)) : undefined;
|
|
775
|
+
const parentToolCallId = text(payload.parent_id);
|
|
776
|
+
const plan = context.plan?.completed(toolCallId, text(payload.output), isError);
|
|
777
|
+
const background = payload.background === true;
|
|
509
778
|
return [{
|
|
510
779
|
emit: {
|
|
511
780
|
part: 'tool',
|
|
@@ -516,6 +785,9 @@ export function projectEvent(event, context) {
|
|
|
516
785
|
isError,
|
|
517
786
|
isComplete: true,
|
|
518
787
|
resultText: output,
|
|
788
|
+
...(plan ? { plan } : {}),
|
|
789
|
+
...(parentToolCallId ? { parentToolCallId } : {}),
|
|
790
|
+
...(background ? { background } : {}),
|
|
519
791
|
},
|
|
520
792
|
needs: 'chatParticipantAdditions',
|
|
521
793
|
fallback: [{
|
|
@@ -555,45 +827,56 @@ export function projectEvent(event, context) {
|
|
|
555
827
|
// crates/harness/src/server.rs `FrontendRequestBroker::publish`.
|
|
556
828
|
case 'request': {
|
|
557
829
|
const request = (payload.request ?? payload);
|
|
830
|
+
// (a request with no id cannot be answered, so it is a malformed record, not conversation)
|
|
558
831
|
if (!request || typeof request.id !== 'number') {
|
|
559
|
-
return [stable({
|
|
560
|
-
part: 'markdown',
|
|
561
|
-
value: `**supercode** · malformed request event\n\n${fence(opaqueText(event.payload), 'json')}`,
|
|
562
|
-
})];
|
|
832
|
+
return [stable({ part: 'unshown', value: `malformed request event #${event.sequence}: ${opaqueText(event.payload)}` })];
|
|
563
833
|
}
|
|
564
834
|
const summary = requestSummary(request);
|
|
565
835
|
const answers = requestAnswers(request, context.connectionId);
|
|
566
836
|
const subject = requestSubject(request);
|
|
837
|
+
const asked = requestQuestions(request);
|
|
567
838
|
const title = request.kind === 'approval'
|
|
568
839
|
? subject ? `Allow ${stripControls(subject)}?` : 'Approval required'
|
|
569
|
-
:
|
|
840
|
+
: asked ? asked.length === 1 ? asked[0].title : `${asked.length} questions`
|
|
841
|
+
: subject ? `Approve ${subject}` : `Assistant request #${request.id}`;
|
|
570
842
|
// The host capability decides whether these original answers can use
|
|
571
843
|
// a native in-turn confirmation card. Stock confirmation clicks start a
|
|
572
844
|
// new chat request, so stable command buttons remain the fallback.
|
|
845
|
+
const buttons = {
|
|
846
|
+
part: 'answers',
|
|
847
|
+
connectionId: context.connectionId,
|
|
848
|
+
requestId: request.id,
|
|
849
|
+
approval: request.kind === 'approval',
|
|
850
|
+
title: clamp(title, VSCODE_CHAT_LIMITS.titleCharacters),
|
|
851
|
+
message: summary,
|
|
852
|
+
answers,
|
|
853
|
+
};
|
|
854
|
+
// A request this window cannot answer (another client, the runtime's owner, answers it) is still drawn: its
|
|
855
|
+
// title and summary, without controls. Code-OSS has no read-only confirmation; it draws another client's call
|
|
856
|
+
// as its row, "Running {0} on another client…", with a cancel action (`stateToProgressAdapter.ts`
|
|
857
|
+
// `toolCallStateToInvocation`), which the extension API cannot draw.
|
|
573
858
|
if (context.canRespond === false || (request.payload && typeof request.payload === 'object' && !Array.isArray(request.payload) && request.payload.observe_only === true)) {
|
|
574
|
-
return [stable({
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
859
|
+
return [stable({ ...buttons, answers: [] })];
|
|
860
|
+
}
|
|
861
|
+
// A harness's questions: VS Code's own carousel, answered inside the running turn.
|
|
862
|
+
if (asked) {
|
|
863
|
+
return [{
|
|
864
|
+
emit: { part: 'questions', connectionId: context.connectionId, requestId: request.id, questions: asked, fallback: buttons },
|
|
865
|
+
needs: 'chatParticipantAdditions',
|
|
866
|
+
fallback: [buttons],
|
|
867
|
+
}];
|
|
578
868
|
}
|
|
579
|
-
return [stable(
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
})];
|
|
586
|
-
}
|
|
587
|
-
// crates/harness/src/server.rs `FrontendRequestBroker::respond`.
|
|
869
|
+
return [stable(buttons)];
|
|
870
|
+
}
|
|
871
|
+
// crates/harness/src/server.rs `FrontendRequestBroker::respond`. A card answered through itself shows what was
|
|
872
|
+
// chosen (`chatConfirmationContentPart`, `chatQuestionCarouselPart`). One answered any other way (another window,
|
|
873
|
+
// Auto approve, a typed reply) cannot be retired through the extension API, so its answer is said beneath it, once,
|
|
874
|
+
// as one plain line; the writer decides which (`resolution`).
|
|
588
875
|
case 'request_resolved': {
|
|
589
876
|
const requestId = typeof payload.request_id === 'number' ? payload.request_id : Number.NaN;
|
|
590
877
|
const response = payload.response;
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (response?.kind === 'approval' && response.decision !== 'deny')
|
|
594
|
-
return [];
|
|
595
|
-
const sentence = resolutionSentence(response, context.pendingRequest?.(requestId), requestId);
|
|
596
|
-
return [stable({ part: 'markdown', value: `\n\n_${sentence}_\n\n` })];
|
|
878
|
+
const value = plain(resolutionSentence(response, context.pendingRequest?.(requestId), requestId));
|
|
879
|
+
return [stable({ part: 'resolution', connectionId: context.connectionId, requestId, value })];
|
|
597
880
|
}
|
|
598
881
|
// crates/runtime/src/event.rs `AgentEvent::CacheWarning`.
|
|
599
882
|
case 'cache_warning': {
|
|
@@ -604,23 +887,44 @@ export function projectEvent(event, context) {
|
|
|
604
887
|
fallback: [{ part: 'markdown', value: `> ⚠ ${message}` }],
|
|
605
888
|
}];
|
|
606
889
|
}
|
|
890
|
+
// A harness hook that blocked or warned (`runtime-native-projection.mjs`, from Claude Code's own hook records).
|
|
891
|
+
// VS Code's own hook row, for the hook events it names; its words otherwise. In the pinned host `push` of this part
|
|
892
|
+
// checks no proposal and `hookProgress` checks `chatParticipantAdditions`; `chatHooks` gates nothing this frontend
|
|
893
|
+
// uses. It is keyed to `chatParticipantAdditions` with the other proposed parts, so a host without it gets words.
|
|
894
|
+
case 'hook': {
|
|
895
|
+
const hookType = HOOK_TYPES.find(type => type === text(payload.hook_event));
|
|
896
|
+
const reason = clamp(stripControls(text(payload.reason)), VSCODE_CHAT_LIMITS.partCharacters);
|
|
897
|
+
const blocked = payload.blocked === true;
|
|
898
|
+
const said = hookSentence(text(payload.hook_event, 'harness'), blocked, reason);
|
|
899
|
+
return [{
|
|
900
|
+
emit: hookType ? { part: 'hook', hookType, blocked, reason } : { part: 'warning', value: said },
|
|
901
|
+
needs: 'chatParticipantAdditions',
|
|
902
|
+
fallback: [{ part: 'markdown', value: `> ⚠ ${said}` }],
|
|
903
|
+
}];
|
|
904
|
+
}
|
|
607
905
|
// crates/runtime/src/event.rs `AgentEvent::Usage`.
|
|
608
906
|
// Token counts are not conversation: VS Code's own chat keeps them out of the transcript.
|
|
609
907
|
case 'usage':
|
|
610
908
|
return [];
|
|
611
|
-
//
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
return [
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
//
|
|
622
|
-
|
|
623
|
-
|
|
909
|
+
// A harness's listing of its background tasks is not conversation: Code-OSS's own Claude host draws a background
|
|
910
|
+
// command as its call's terminal row, and BashOutput and KillBash as rows of their own (`claudeToolDisplay.ts`).
|
|
911
|
+
// Current Claude Code stops a task with TaskStop, an ordinary row there and here.
|
|
912
|
+
case 'background_tasks':
|
|
913
|
+
return [];
|
|
914
|
+
// sdk/interchange/agent-background.mjs `status`: the output a `background_status` call drained, which that call's
|
|
915
|
+
// own result also carries (`output`, `output_truncated`). It is that row's output, as a VS Code background
|
|
916
|
+
// terminal's output is its own row's, and nothing more in the transcript.
|
|
917
|
+
case 'background_output':
|
|
918
|
+
return [];
|
|
919
|
+
// crates/harness/src/server.rs `submit_claimed`. VS Code's own agent host draws an interrupted turn as its
|
|
920
|
+
// response's warning-level error (`agentHostSessionHandler.ts` `_getTurnErrorDetails`, `executionInterrupted`):
|
|
921
|
+
// the handler's turn returns it as its result (`turnOutcome`), and a re-joined response or history, which have
|
|
922
|
+
// no result to carry it, shows its warning part. The live writer drops it when the person stopped the turn
|
|
923
|
+
// (VS Code marks a stopped response itself).
|
|
924
|
+
case 'turn_interrupted': {
|
|
925
|
+
const value = turnOutcome(event).message;
|
|
926
|
+
return [{ emit: { part: 'warning', value }, needs: 'chatParticipantAdditions', fallback: [{ part: 'markdown', value }] }];
|
|
927
|
+
}
|
|
624
928
|
// crates/harness/src/server.rs `submit_claimed` emits `turn_failed`. The
|
|
625
929
|
// other four are the error kinds the donor-neutral reader in
|
|
626
930
|
// sdk/interchange/frontend-transcript.mjs treats identically:
|
|
@@ -629,31 +933,40 @@ export function projectEvent(event, context) {
|
|
|
629
933
|
// `runtime_disconnected`, which a frontend synthesizes for itself
|
|
630
934
|
// (sdk/interchange/frontend-app.mjs). `stream_error` has no producer in this
|
|
631
935
|
// repository and is read only because that reader reads it.
|
|
936
|
+
//
|
|
937
|
+
// A failed turn is VS Code's own request error (`ChatResult.errorDetails`) on the turn the handler runs
|
|
938
|
+
// (`turnOutcome`); a re-joined response and history carry no result, so there it is the warning part. The
|
|
939
|
+
// error's own words, as VS Code's agent host shows a turn's error, never the event kind's name.
|
|
632
940
|
case 'turn_failed':
|
|
633
941
|
case 'stream_error':
|
|
634
942
|
case 'transport_error':
|
|
635
943
|
case 'watch_error':
|
|
636
944
|
case 'runtime_disconnected': {
|
|
637
|
-
const
|
|
638
|
-
|
|
945
|
+
const value = turnOutcome(event)?.message ?? errorMessage(payload);
|
|
946
|
+
if (!value)
|
|
947
|
+
return [];
|
|
639
948
|
return [{
|
|
640
949
|
emit: { part: 'warning', value },
|
|
641
950
|
needs: 'chatParticipantAdditions',
|
|
642
951
|
fallback: [{ part: 'markdown', value: `> ⚠ ${value}` }],
|
|
643
952
|
}];
|
|
644
953
|
}
|
|
645
|
-
// Declared by the runtime descriptor's `display.event_kinds`
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
|
|
954
|
+
// Declared by the runtime descriptor's `display.event_kinds` (`agent-runtime-host.mjs` `displayKinds`); nothing
|
|
955
|
+
// in this repository produces them, so their payload is read for the words it states and never shown as JSON.
|
|
956
|
+
// A scheduled prompt is a turn the runtime starts itself: its start is a labelled line naming the prompt, its
|
|
957
|
+
// deferral and completion are lifecycle like `turn_started` / `turn_completed`, and an error is a warning.
|
|
958
|
+
case 'scheduled_prompt_started': {
|
|
959
|
+
const value = clamp(stripControls(text(payload.prompt ?? payload.name ?? payload.message)), VSCODE_CHAT_LIMITS.titleCharacters);
|
|
960
|
+
// (labelled, so the runtime's prompt is not read as the agent's words)
|
|
961
|
+
return value ? [stable(noticeLine(`Scheduled prompt: ${value}`))] : [];
|
|
962
|
+
}
|
|
649
963
|
case 'scheduled_prompt_deferred':
|
|
650
964
|
case 'scheduled_prompt_completed':
|
|
651
|
-
return [
|
|
652
|
-
part: 'progress',
|
|
653
|
-
value: `${event.kind.replaceAll('_', ' ')} · ${opaqueText(event.payload, 256)}`,
|
|
654
|
-
})];
|
|
965
|
+
return [];
|
|
655
966
|
case 'scheduler_error': {
|
|
656
|
-
const value =
|
|
967
|
+
const value = errorMessage(payload);
|
|
968
|
+
if (!value)
|
|
969
|
+
return [];
|
|
657
970
|
return [{
|
|
658
971
|
emit: { part: 'warning', value },
|
|
659
972
|
needs: 'chatParticipantAdditions',
|
|
@@ -665,13 +978,30 @@ export function projectEvent(event, context) {
|
|
|
665
978
|
case 'native_event':
|
|
666
979
|
return [];
|
|
667
980
|
case 'status':
|
|
981
|
+
// A background task's notification (`call_id` names the call that started it): a harness record, drawn as the
|
|
982
|
+
// `user_message` notices are (`noticeLine`), since a progress part would be hidden by whatever follows. Its
|
|
983
|
+
// answer belongs to that call's row (or to the agent's reply, for a subagent started in the background), so the
|
|
984
|
+
// notice is its first line, and the task's output file where it names one.
|
|
985
|
+
if (text(payload.call_id)) {
|
|
986
|
+
if (!firstNotice(context, text(payload.call_id)))
|
|
987
|
+
return [];
|
|
988
|
+
const lines = stripControls(text(payload.message)).split('\n');
|
|
989
|
+
const output = lines.find(line => line.startsWith('Output: ') && line.length > 8);
|
|
990
|
+
const line = [clamp((lines[0] ?? '').trim(), VSCODE_CHAT_LIMITS.titleCharacters), output].filter(Boolean).join('\n\n');
|
|
991
|
+
return line ? [stable(noticeLine(clamp(line, VSCODE_CHAT_LIMITS.partCharacters)))] : [];
|
|
992
|
+
}
|
|
668
993
|
return [stable({ part: 'progress', value: clamp(stripControls(text(payload.message)), VSCODE_CHAT_LIMITS.partCharacters) })];
|
|
669
|
-
case 'warning':
|
|
670
|
-
|
|
994
|
+
case 'warning': {
|
|
995
|
+
const value = clamp(stripControls(text(payload.message)), VSCODE_CHAT_LIMITS.partCharacters);
|
|
996
|
+
return [{ emit: { part: 'warning', value }, needs: 'chatParticipantAdditions', fallback: [{ part: 'markdown', value: `> ⚠ ${value}` }] }];
|
|
997
|
+
}
|
|
671
998
|
// Not in this runtime's declared set; read the way
|
|
672
999
|
// sdk/interchange/frontend-transcript.mjs reads a donor that emits them.
|
|
673
1000
|
case 'reasoning':
|
|
674
1001
|
case 'thinking': {
|
|
1002
|
+
// (a subagent's own reasoning, `parent_id`, as its running text above)
|
|
1003
|
+
if (text(payload.parent_id))
|
|
1004
|
+
return [];
|
|
675
1005
|
const body = clamp(stripControls(text(payload.text ?? payload.summary, opaqueText(event.payload))), VSCODE_CHAT_LIMITS.partCharacters);
|
|
676
1006
|
return [{
|
|
677
1007
|
emit: { part: 'thinking', value: body },
|
|
@@ -679,17 +1009,11 @@ export function projectEvent(event, context) {
|
|
|
679
1009
|
fallback: [{ part: 'markdown', value: `\n\n> ${body.replaceAll('\n', '\n> ')}\n\n` }],
|
|
680
1010
|
}];
|
|
681
1011
|
}
|
|
682
|
-
//
|
|
683
|
-
//
|
|
1012
|
+
// A kind this version does not know is a protocol record, not conversation, like `native_event` above: VS Code's
|
|
1013
|
+
// own chat shows none (its agent host drops a record it does not map). It goes to the output channel; the
|
|
1014
|
+
// runtime's history keeps it.
|
|
684
1015
|
default:
|
|
685
|
-
|
|
686
|
-
// next streamed delta lands against on the same line never closes, and
|
|
687
|
-
// it swallows everything after it — observed doing exactly that to an
|
|
688
|
-
// approval request.
|
|
689
|
-
return [stable({
|
|
690
|
-
part: 'markdown',
|
|
691
|
-
value: `\n\n_supercode event \`${clamp(event.kind, VSCODE_CHAT_LIMITS.titleCharacters)}\` is not displayable by this frontend version_\n\n${fence(opaqueText(event.payload), 'json')}\n\n`,
|
|
692
|
-
})];
|
|
1016
|
+
return [stable({ part: 'unshown', value: `event kind ${clamp(event.kind, VSCODE_CHAT_LIMITS.titleCharacters)} #${event.sequence} is not displayed: ${opaqueText(event.payload)}` })];
|
|
693
1017
|
}
|
|
694
1018
|
}
|
|
695
1019
|
/** Replace every part whose proposal the host did not grant with its fallback. */
|
|
@@ -704,10 +1028,11 @@ export function resolveParts(parts, granted) {
|
|
|
704
1028
|
return resolved;
|
|
705
1029
|
}
|
|
706
1030
|
/**
|
|
707
|
-
* The prompt of the marker turn that carries runtime history with no user turn
|
|
708
|
-
*
|
|
1031
|
+
* The prompt of the marker turn that carries runtime history with no user turn of its own: VS Code's own words for a
|
|
1032
|
+
* turn whose request is not in the transcript (`claudeReplayMapper.ts` `missingPromptPlaceholder`), so nothing is
|
|
1033
|
+
* presented as a person's words.
|
|
709
1034
|
*/
|
|
710
|
-
export const HISTORY_MARKER_PROMPT = '
|
|
1035
|
+
export const HISTORY_MARKER_PROMPT = 'Message content could not be retrieved';
|
|
711
1036
|
function messageText(message) {
|
|
712
1037
|
const content = message.content;
|
|
713
1038
|
if (typeof content === 'string')
|
|
@@ -732,21 +1057,40 @@ function messageText(message) {
|
|
|
732
1057
|
}
|
|
733
1058
|
/**
|
|
734
1059
|
* What a `user_message` shows when no person typed it: undefined for the person's words, '' to show nothing, else a
|
|
735
|
-
* short notice. The harness's own records carry `source: 'harness'` (and a `notice` where one is shown) from the one
|
|
1060
|
+
* short notice, plain text drawn as a status line. The harness's own records carry `source: 'harness'` (and a `notice` where one is shown) from the one
|
|
736
1061
|
* provenance rule the runtime applies (`@volter/supercode-interchange/user-provenance`). The editor's automatic
|
|
737
1062
|
* tripwire nudge travels through the person's input door, so the editor's frame is what names it.
|
|
738
1063
|
*/
|
|
739
1064
|
function notPersonNotice(payload) {
|
|
740
1065
|
if (payload.source === 'harness')
|
|
741
|
-
return
|
|
1066
|
+
return noticeText(text(payload.notice));
|
|
742
1067
|
if (editorNudge(text(payload.text)))
|
|
743
1068
|
return '';
|
|
744
1069
|
return undefined;
|
|
745
1070
|
}
|
|
746
|
-
/**
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1071
|
+
/**
|
|
1072
|
+
* A notice as a line of the transcript: its own words, plain. Not a progress part: Code-OSS hides a progress message
|
|
1073
|
+
* as soon as any other part follows it (`chatProgressContentPart.ts`), so the notice would never be seen.
|
|
1074
|
+
*/
|
|
1075
|
+
function noticeLine(notice) {
|
|
1076
|
+
return { part: 'markdown', value: `\n\n${plain(notice)}\n\n` };
|
|
1077
|
+
}
|
|
1078
|
+
/** The call a background task's notification record names (`<tool-use-id>`), for `firstNotice`. */
|
|
1079
|
+
function notifiedCall(payload) {
|
|
1080
|
+
return /<tool-use-id>([^<]+)<\/tool-use-id>/.exec(text(payload.text))?.[1]?.trim() ?? '';
|
|
1081
|
+
}
|
|
1082
|
+
/** Whether this is the first notification of `call`'s task in this stream (always, for one that names no call). */
|
|
1083
|
+
function firstNotice(context, call) {
|
|
1084
|
+
if (!call || !context.notices)
|
|
1085
|
+
return true;
|
|
1086
|
+
if (context.notices.has(call))
|
|
1087
|
+
return false;
|
|
1088
|
+
context.notices.add(call);
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
/** The rule's plain notice, bounded; '' for none. */
|
|
1092
|
+
function noticeText(notice) {
|
|
1093
|
+
return clamp(stripControls(notice ?? ''), VSCODE_CHAT_LIMITS.titleCharacters);
|
|
750
1094
|
}
|
|
751
1095
|
function editorNudge(body) {
|
|
752
1096
|
return body.trimStart().startsWith('[Volter editor tripwire');
|
|
@@ -757,6 +1101,18 @@ export function projectReplayTurns(events, granted, context, history = []) {
|
|
|
757
1101
|
// Before the replay supplies a prompt, give its parts their own runtime
|
|
758
1102
|
// history turn rather than attributing old approvals to the latest answer.
|
|
759
1103
|
const turns = history.map(turn => ({ ...turn, parts: turn.parts?.slice() }));
|
|
1104
|
+
// A reopened chat names what each answer answered from the replay's own requests first: the session's maps hold
|
|
1105
|
+
// only what is pending now and the latest few resolved.
|
|
1106
|
+
const requests = new Map();
|
|
1107
|
+
for (const event of events) {
|
|
1108
|
+
if (event.kind !== 'request')
|
|
1109
|
+
continue;
|
|
1110
|
+
const request = (payloadOf(event).request ?? payloadOf(event));
|
|
1111
|
+
if (typeof request?.id === 'number')
|
|
1112
|
+
requests.set(request.id, request);
|
|
1113
|
+
}
|
|
1114
|
+
const known = context.pendingRequest;
|
|
1115
|
+
context = { ...context, notices: context.notices ?? new Set(), pendingRequest: (id) => requests.get(id) ?? known?.(id) };
|
|
760
1116
|
// An approval the replay shows allowed is not drawn again: live, its card is hidden (Auto approve) or answered,
|
|
761
1117
|
// and its allowed resolution says nothing more, so the tool row is the record (`request_resolved` above).
|
|
762
1118
|
const allowed = new Set();
|
|
@@ -776,19 +1132,23 @@ export function projectReplayTurns(events, granted, context, history = []) {
|
|
|
776
1132
|
if (typeof request?.id === 'number' && allowed.has(request.id))
|
|
777
1133
|
continue;
|
|
778
1134
|
}
|
|
779
|
-
|
|
1135
|
+
// (nor is its answer, which would answer a card that is not there)
|
|
1136
|
+
if (event.kind === 'request_resolved' && allowed.has(payloadOf(event).request_id))
|
|
1137
|
+
continue;
|
|
1138
|
+
// (a subagent's own input is no turn of the conversation; `projectEvent` draws nothing for it)
|
|
1139
|
+
if (event.kind === 'user_message' && !text(payloadOf(event).parent_id)) {
|
|
780
1140
|
streamedText = undefined;
|
|
781
1141
|
const payload = payloadOf(event);
|
|
782
1142
|
const shown = notPersonNotice(payload);
|
|
783
1143
|
if (shown !== undefined) {
|
|
784
|
-
if (!shown)
|
|
1144
|
+
if (!shown || !firstNotice(context, notifiedCall(payload)))
|
|
785
1145
|
continue;
|
|
786
1146
|
if (!replayTurn) {
|
|
787
1147
|
turns.push({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
|
|
788
1148
|
replayTurn = true;
|
|
789
1149
|
}
|
|
790
1150
|
const last = turns[turns.length - 1];
|
|
791
|
-
const notice =
|
|
1151
|
+
const notice = noticeLine(shown);
|
|
792
1152
|
if (last.role === 'assistant' && last.parts)
|
|
793
1153
|
last.parts.push(notice);
|
|
794
1154
|
else
|
|
@@ -799,7 +1159,13 @@ export function projectReplayTurns(events, granted, context, history = []) {
|
|
|
799
1159
|
replayTurn = true;
|
|
800
1160
|
continue;
|
|
801
1161
|
}
|
|
802
|
-
|
|
1162
|
+
// (a record that is not conversation is no part of a turn: it goes to `unshown`, wherever it falls)
|
|
1163
|
+
const parts = resolveParts(projectEvent(event, context), granted).filter(part => {
|
|
1164
|
+
if (part.part !== 'unshown')
|
|
1165
|
+
return true;
|
|
1166
|
+
context.unshown?.(part.value);
|
|
1167
|
+
return false;
|
|
1168
|
+
});
|
|
803
1169
|
if (parts.length === 0)
|
|
804
1170
|
continue;
|
|
805
1171
|
if (event.kind === 'text_delta' && parts[0]?.part === 'markdown') {
|
|
@@ -822,25 +1188,26 @@ export function projectReplayTurns(events, granted, context, history = []) {
|
|
|
822
1188
|
else
|
|
823
1189
|
turns.push({ role: 'assistant', parts });
|
|
824
1190
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1191
|
+
return boundTurns(turns);
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* The latest `historyTurns` turns. The earlier ones are left out without a word, as VS Code's own agent host shows a
|
|
1195
|
+
* transcript its harness truncated; a response the cut leaves without its request gets the marker request, as that
|
|
1196
|
+
* host gives one its missing-prompt placeholder (`claudeReplayMapper.ts` `missingPromptPlaceholder`).
|
|
1197
|
+
*/
|
|
1198
|
+
function boundTurns(turns) {
|
|
1199
|
+
if (turns.length <= VSCODE_CHAT_LIMITS.historyTurns)
|
|
1200
|
+
return turns;
|
|
1201
|
+
const kept = turns.slice(-VSCODE_CHAT_LIMITS.historyTurns);
|
|
1202
|
+
if (kept[0]?.role === 'assistant')
|
|
1203
|
+
kept.unshift({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
|
|
1204
|
+
return kept;
|
|
838
1205
|
}
|
|
839
1206
|
/**
|
|
840
1207
|
* The attach snapshot's canonical history as chat turns. Bounded: when older
|
|
841
1208
|
* turns are omitted the view says so, the way the browser observer does.
|
|
842
1209
|
*/
|
|
843
|
-
export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
1210
|
+
export function projectHistory(history, granted, pendingToolNames = new Set(), plan = new PlanTracker()) {
|
|
844
1211
|
const turns = [];
|
|
845
1212
|
// Keep results inside the native invocation, as the live stream does. A
|
|
846
1213
|
// separate Markdown result bypasses the editor's collapsed tool-step group.
|
|
@@ -893,9 +1260,9 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
893
1260
|
// interruption record are not the person's words.
|
|
894
1261
|
const provenance = userProvenance(message.metadata, body);
|
|
895
1262
|
if (provenance.source === 'harness' || editorNudge(body)) {
|
|
896
|
-
const notice =
|
|
1263
|
+
const notice = noticeText(provenance.notice);
|
|
897
1264
|
if (notice)
|
|
898
|
-
pushAssistant([
|
|
1265
|
+
pushAssistant([noticeLine(notice)]);
|
|
899
1266
|
continue;
|
|
900
1267
|
}
|
|
901
1268
|
toolRows.clear();
|
|
@@ -923,6 +1290,8 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
923
1290
|
for (const call of message.tool_calls ?? []) {
|
|
924
1291
|
const titles = toolTitles(call.function.name, call.function.arguments);
|
|
925
1292
|
const commandLine = shellCommand(call.function.name, call.function.arguments);
|
|
1293
|
+
const items = planItems(call.function.name, call.function.arguments) ?? plan.started(call.function.name, call.function.arguments, call.id);
|
|
1294
|
+
const subagent = subagentCall(call.function.name, call.function.arguments);
|
|
926
1295
|
const row = {
|
|
927
1296
|
part: 'tool',
|
|
928
1297
|
toolName: call.function.name,
|
|
@@ -930,6 +1299,8 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
930
1299
|
invocationMessage: titles.invocation,
|
|
931
1300
|
pastTenseMessage: titles.pastTense,
|
|
932
1301
|
...(commandLine ? { commandLine } : {}),
|
|
1302
|
+
...(items ? { plan: items } : {}),
|
|
1303
|
+
...(subagent ? { subagent } : {}),
|
|
933
1304
|
inputText: clamp(stripControls(parseArguments(call.function.arguments)), VSCODE_CHAT_LIMITS.partCharacters),
|
|
934
1305
|
isComplete: true,
|
|
935
1306
|
};
|
|
@@ -951,13 +1322,10 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
951
1322
|
// interruption. Only the final matching tool is affected, never old history.
|
|
952
1323
|
const pending = message === history.at(-1) && pendingToolNames.has(message.name ?? '')
|
|
953
1324
|
&& body === '[no tool result recorded — turn interrupted]';
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
// results; the active response separately restores the approval controls.
|
|
958
|
-
pushAssistant([{ part: 'markdown', value: '**Awaiting approval** — no tool result yet.' }]);
|
|
1325
|
+
// Its call's row stays without a result, and the active response restores the approval it waits on, which is
|
|
1326
|
+
// where VS Code shows a call waiting for confirmation: on its own card, not in words in the transcript.
|
|
1327
|
+
if (pending)
|
|
959
1328
|
continue;
|
|
960
|
-
}
|
|
961
1329
|
if (granted.has('chatParticipantAdditions')) {
|
|
962
1330
|
const row = message.tool_call_id ? toolRows.get(message.tool_call_id) : undefined;
|
|
963
1331
|
const output = clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters);
|
|
@@ -967,6 +1335,9 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
967
1335
|
// establish failure; do not guess from arbitrary result prose.
|
|
968
1336
|
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
1337
|
row.isError = true;
|
|
1338
|
+
const items = plan.completed(message.tool_call_id, body, row.isError === true);
|
|
1339
|
+
if (items)
|
|
1340
|
+
row.plan = items;
|
|
970
1341
|
toolRows.delete(message.tool_call_id);
|
|
971
1342
|
}
|
|
972
1343
|
else {
|
|
@@ -990,16 +1361,5 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
990
1361
|
value: `**Tool result${message.name ? ` · ${message.name}` : ''}**\n\n${fence(clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters))}`,
|
|
991
1362
|
}]);
|
|
992
1363
|
}
|
|
993
|
-
|
|
994
|
-
return turns;
|
|
995
|
-
const omitted = turns.length - VSCODE_CHAT_LIMITS.historyTurns;
|
|
996
|
-
const kept = turns.slice(-VSCODE_CHAT_LIMITS.historyTurns);
|
|
997
|
-
kept.unshift({
|
|
998
|
-
role: 'assistant',
|
|
999
|
-
parts: [{
|
|
1000
|
-
part: 'markdown',
|
|
1001
|
-
value: `_${omitted} earlier turns are omitted by this view’s bound; the runtime’s own history is unchanged and authoritative._`,
|
|
1002
|
-
}],
|
|
1003
|
-
});
|
|
1004
|
-
return kept;
|
|
1364
|
+
return boundTurns(turns);
|
|
1005
1365
|
}
|