@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/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
- /** The command line of a shell tool call, read from its own arguments; undefined for any other tool. */
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
- if (!['bash', 'shell', 'exec_command', 'local_shell', 'powershell'].includes(toolName.toLowerCase()))
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({ part: 'markdown', value: `\n\n${notice}\n\n` })] : [];
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
- : subject ? `Approve ${subject}` : `Assistant request #${request.id}`;
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
- part: 'markdown',
576
- value: `\n\n**${title}**\n\n${summary}\n\n_This runtime answers its own requests; Volter Harness is observing._\n\n`,
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
- part: 'answers',
581
- approval: request.kind === 'approval',
582
- title: clamp(title, VSCODE_CHAT_LIMITS.titleCharacters),
583
- message: summary,
584
- answers,
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
- // An allowed approval says nothing more, as in VS Code's own chat: its tool row carries on and is the record.
592
- // A denial, and an answer to any other request, is said once.
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
- // crates/runtime/src/event.rs `AgentEvent::BackgroundOutput`.
612
- case 'background_output': {
613
- const chunk = fence(clamp(stripControls(text(payload.chunk)), VSCODE_CHAT_LIMITS.partCharacters));
614
- const truncated = payload.truncated === true ? '\n\n_the runtime stopped retaining this job’s output_' : '';
615
- return [stable({
616
- part: 'markdown',
617
- value: `\n\n**background job ${text(payload.job_id)}**\n\n${chunk}${truncated}\n\n`,
618
- })];
619
- }
620
- // crates/harness/src/server.rs `submit_claimed`.
621
- // The live writer drops it when the person stopped the turn (VS Code marks a stopped response itself).
622
- case 'turn_interrupted':
623
- return [stable({ part: 'warning', value: 'The turn was interrupted.' })];
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 message = typeof payload.message === 'string' ? payload.message : opaqueText(event.payload);
638
- const value = `${event.kind.replaceAll('_', ' ')}: ${clamp(stripControls(message), VSCODE_CHAT_LIMITS.partCharacters)}`;
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
- // (crates/harness/src/server.rs `frontend_descriptor`); read here the way
647
- // sdk/interchange/frontend-transcript.mjs reads them.
648
- case 'scheduled_prompt_started':
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 [stable({
652
- part: 'progress',
653
- value: `${event.kind.replaceAll('_', ' ')} · ${opaqueText(event.payload, 256)}`,
654
- })];
965
+ return [];
655
966
  case 'scheduler_error': {
656
- const value = `scheduler error: ${opaqueText(event.payload, 256)}`;
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
- return [stable({ part: 'warning', value: clamp(stripControls(text(payload.message)), VSCODE_CHAT_LIMITS.partCharacters) })];
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
- // The contract says every kind but `request` is opaque JSON by design, so a
683
- // kind this version does not know is rendered bounded, never dropped.
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
- // The surrounding blank lines are load-bearing: a fenced block that the
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
- * of its own. It names itself; nothing here is presented as a person's words.
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 = '⟨supercode runtime history⟩';
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 noticeMarkdown(text(payload.notice));
1066
+ return noticeText(text(payload.notice));
742
1067
  if (editorNudge(text(payload.text)))
743
1068
  return '';
744
1069
  return undefined;
745
1070
  }
746
- /** The rule's plain notice as this view's italic line; '' for none. */
747
- function noticeMarkdown(notice) {
748
- const value = clamp(stripControls(notice ?? ''), VSCODE_CHAT_LIMITS.partCharacters - 2);
749
- return value ? `_${value}_` : '';
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
- if (event.kind === 'user_message') {
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 = { part: 'markdown', value: `\n\n${shown}\n\n` };
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
- const parts = resolveParts(projectEvent(event, context), granted);
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
- if (turns.length > VSCODE_CHAT_LIMITS.historyTurns) {
826
- const omitted = turns.length - VSCODE_CHAT_LIMITS.historyTurns;
827
- const kept = turns.slice(-VSCODE_CHAT_LIMITS.historyTurns);
828
- kept.unshift({
829
- role: 'assistant',
830
- parts: [{
831
- part: 'markdown',
832
- value: `_${omitted} earlier turns are omitted by this view’s bound; the runtime’s own history is unchanged and authoritative._`,
833
- }],
834
- });
835
- return kept;
836
- }
837
- return turns;
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 = noticeMarkdown(provenance.notice);
1263
+ const notice = noticeText(provenance.notice);
897
1264
  if (notice)
898
- pushAssistant([{ part: 'markdown', value: `\n\n${notice}\n\n` }]);
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
- if (pending) {
955
- // The pinned editor serializes history tool parts as complete even if
956
- // isComplete=false. Keep this explicit marker for matched AND orphan
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
- if (turns.length <= VSCODE_CHAT_LIMITS.historyTurns)
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
  }