@volter/supercode-frontend-vscode 0.1.61 → 0.1.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/view.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { userProvenance } from '../vendor/supercode-interchange/user-provenance.mjs';
1
2
  /**
2
3
  * The bounded projection this presentation package renders. Mirrors
3
4
  * `@volter/supercode-frontend-browser`'s rule: the view is size-bounded
@@ -173,6 +174,26 @@ export function toolTitles(toolName, argumentsText) {
173
174
  }
174
175
  if (name === 'todowrite' || name === 'update_plan')
175
176
  return both('Updating the plan', 'Updated the plan');
177
+ if (name === 'taskcreate') {
178
+ const subject = field('subject');
179
+ if (subject)
180
+ return both(`Adding a task: ${plain(subject)}`, `Added a task: ${plain(subject)}`);
181
+ }
182
+ if (name === 'taskupdate') {
183
+ const id = field('taskId'), status = field('status');
184
+ if (id) {
185
+ const task = `task #${plain(id)}`;
186
+ if (status === 'in_progress')
187
+ return both(`Starting ${task}`, `Started ${task}`);
188
+ if (status === 'completed')
189
+ return both(`Completing ${task}`, `Completed ${task}`);
190
+ if (status === 'deleted')
191
+ return both(`Removing ${task}`, `Removed ${task}`);
192
+ return both(`Updating ${task}`, `Updated ${task}`);
193
+ }
194
+ }
195
+ if (name === TASK_LIST_TOOL)
196
+ return both('Reading the task list', 'Read the task list');
176
197
  // Claude Code names an MCP tool `mcp__<server>__<tool>`; VS Code writes "Ran `tool` – server".
177
198
  const mcp = /^mcp__(.+?)__(.+)$/.exec(toolName);
178
199
  if (mcp)
@@ -180,6 +201,159 @@ export function toolTitles(toolName, argumentsText) {
180
201
  const subject = toolSubject(argumentsText);
181
202
  return subject ? both(`Running ${code(toolName)} · ${plain(subject)}`, `Ran ${code(toolName)} · ${plain(subject)}`) : both(`Running ${code(toolName)}`, `Ran ${code(toolName)}`);
182
203
  }
204
+ /** A hook's row as words, as VS Code titles its own (`chatHookContentPart.ts`): "Blocked by …" or "Warning from …". */
205
+ export function hookSentence(hookEvent, blocked, reason) {
206
+ return `${blocked ? 'Blocked by' : 'Warning from'} the ${hookEvent} hook${reason ? `: ${reason}` : ''}`;
207
+ }
208
+ /** The hook events VS Code's own Chat names (`ChatHookType`), which are Claude Code's. */
209
+ export const HOOK_TYPES = ['SessionStart', 'SessionEnd', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'PreCompact',
210
+ 'SubagentStart', 'SubagentStop', 'Stop', 'ErrorOccurred'];
211
+ /** Claude Code's TaskList tool, lower-cased as this view compares tool names. */
212
+ const TASK_LIST_TOOL = 'tasklist'; // process-read: a Claude Code tool name, not the Windows command
213
+ /** A plan call's items (Claude Code's TodoWrite `todos` of `content`; the `update_plan` tool's `plan` of `step`, as
214
+ * supercode's own and Codex's take it), each its text and status, read from its own arguments; undefined for any
215
+ * other tool, or arguments holding none. Both carry the whole list, so they need no state. */
216
+ export function planItems(toolName, argumentsText) {
217
+ const name = toolName.toLowerCase();
218
+ if (name !== 'todowrite' && name !== 'update_plan')
219
+ return undefined;
220
+ const list = argumentFields(argumentsText)[name === 'todowrite' ? 'todos' : 'plan'];
221
+ if (!Array.isArray(list))
222
+ return undefined;
223
+ const items = list.flatMap((item) => {
224
+ if (!item || typeof item !== 'object' || Array.isArray(item))
225
+ return [];
226
+ const fields = item;
227
+ const title = planTitle(name === 'todowrite' ? fields.content : fields.step);
228
+ return title ? [{ title, status: planStatus(fields.status) }] : [];
229
+ });
230
+ return items.length > 0 ? items : undefined;
231
+ }
232
+ function argumentFields(argumentsText) {
233
+ try {
234
+ const parsed = JSON.parse(argumentsText);
235
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
236
+ return parsed;
237
+ }
238
+ catch { /* not JSON: no fields */ }
239
+ return {};
240
+ }
241
+ const planTitle = (value) => typeof value === 'string' ? clamp(stripControls(value.trim()), VSCODE_CHAT_LIMITS.titleCharacters) : '';
242
+ /** Claude Code's task statuses (`pending`, `in_progress`, `completed`) as VS Code's own todo statuses. */
243
+ const planStatus = (value) => value === 'completed' ? 'completed' : value === 'in_progress' ? 'in-progress' : 'not-started';
244
+ /**
245
+ * The task list Claude Code keeps with its task tools, which it offers in place of TodoWrite (TaskCreate, TaskUpdate,
246
+ * TaskList; Claude Code 2.1.312 enables them by default and TodoWrite only when its tasks are turned off). Each call
247
+ * changes ONE task, while VS Code's own todo list shows the WHOLE list, so this keeps the list for one conversation as
248
+ * the calls go by and gives each call's row the list as that call left it.
249
+ *
250
+ * Read from the calls' own arguments, and from Claude Code's own result text where only the result has it (measured
251
+ * on 2.1.312, its `mapToolResultToToolResultBlockParam`): TaskCreate's `Task #<id> created successfully: <subject>`,
252
+ * TaskUpdate's `Updated task #<id> <fields>`, and TaskList's lines `#<id> [<status>] <subject>…`. A result in other
253
+ * words changes nothing here; the row is then an ordinary one.
254
+ */
255
+ export class PlanTracker {
256
+ #tasks = [];
257
+ #calls = new Map();
258
+ /** A call that started: the list as it leaves it, or undefined when it is not a task call (or changes nothing). */
259
+ started(toolName, argumentsText, toolCallId) {
260
+ const name = toolName.toLowerCase();
261
+ if (!['taskcreate', 'taskupdate', TASK_LIST_TOOL].includes(name))
262
+ return undefined;
263
+ const fields = argumentFields(argumentsText);
264
+ if (name === TASK_LIST_TOOL) {
265
+ this.#calls.set(toolCallId, { name });
266
+ return undefined;
267
+ }
268
+ if (name === 'taskcreate') {
269
+ const title = planTitle(fields.subject);
270
+ if (!title)
271
+ return undefined;
272
+ const created = { title, status: 'not-started' };
273
+ this.#tasks.push(created);
274
+ this.#calls.set(toolCallId, { name, created });
275
+ return this.items();
276
+ }
277
+ const id = typeof fields.taskId === 'string' ? fields.taskId : undefined;
278
+ const index = id === undefined ? -1 : this.#tasks.findIndex(task => task.id === id);
279
+ if (index < 0)
280
+ return undefined;
281
+ const task = this.#tasks[index];
282
+ const deleted = fields.status === 'deleted';
283
+ this.#calls.set(toolCallId, { name, before: { task: { ...task }, at: index, deleted } });
284
+ if (deleted)
285
+ this.#tasks.splice(index, 1);
286
+ else {
287
+ const title = planTitle(fields.subject);
288
+ // (changed in place, so a create still waiting for its id keeps its entry)
289
+ Object.assign(task, title ? { title } : {}, typeof fields.status === 'string' ? { status: planStatus(fields.status) } : {});
290
+ }
291
+ return this.items();
292
+ }
293
+ /** A call that completed: the list as it now stands (a failed call's change undone; TaskList's result is the
294
+ * list), so a finished row never shows an older list than the calls before it left. */
295
+ completed(toolCallId, output, isError) {
296
+ const call = this.#calls.get(toolCallId);
297
+ if (!call)
298
+ return undefined;
299
+ this.#calls.delete(toolCallId);
300
+ if (call.name === 'taskcreate' && call.created) {
301
+ const id = isError ? undefined : /^Task #(\S+) created successfully: /.exec(output)?.[1];
302
+ if (id !== undefined)
303
+ call.created.id = id;
304
+ // (a task Claude Code did not create is not on its list)
305
+ else
306
+ this.#tasks = this.#tasks.filter(task => task !== call.created);
307
+ return this.items();
308
+ }
309
+ if (call.name === 'taskupdate') {
310
+ const before = call.before;
311
+ if ((isError || !/^Updated task #/.test(output)) && before) {
312
+ if (before.deleted)
313
+ this.#tasks.splice(Math.min(before.at, this.#tasks.length), 0, before.task);
314
+ else {
315
+ const task = this.#tasks.find(other => other.id === before.task.id);
316
+ if (task)
317
+ Object.assign(task, { title: before.task.title, status: before.task.status });
318
+ }
319
+ }
320
+ return this.items();
321
+ }
322
+ if (isError)
323
+ return undefined;
324
+ if (output.trim() === 'No tasks found') {
325
+ this.#tasks = [];
326
+ return [];
327
+ }
328
+ const listed = output.split('\n').flatMap(line => {
329
+ const match = /^#(\S+) \[(\w+)\] (.*?)(?: \[blocked by [^\]]*\])?$/.exec(line);
330
+ const title = match ? planTitle(match[3]) : '';
331
+ return match && title ? [{ id: match[1], title, status: planStatus(match[2]) }] : [];
332
+ });
333
+ if (listed.length === 0)
334
+ return undefined;
335
+ this.#tasks = listed;
336
+ return this.items();
337
+ }
338
+ items() {
339
+ return this.#tasks.map(({ title, status }) => ({ title, status }));
340
+ }
341
+ }
342
+ /** A subagent call's description, kind and prompt (Claude Code's Task and Agent tools: `description`,
343
+ * `subagent_type`, `prompt`), read from its own arguments; undefined for any other tool. */
344
+ export function subagentCall(toolName, argumentsText) {
345
+ if (!['task', 'agent'].includes(toolName.toLowerCase()))
346
+ return undefined;
347
+ const fields = argumentFields(argumentsText);
348
+ const field = (name, limit) => {
349
+ const value = fields[name];
350
+ return typeof value === 'string' && value.trim() ? clamp(stripControls(value.trim()), limit) : undefined;
351
+ };
352
+ const description = field('description', VSCODE_CHAT_LIMITS.titleCharacters);
353
+ const agentName = field('subagent_type', VSCODE_CHAT_LIMITS.titleCharacters);
354
+ const prompt = field('prompt', VSCODE_CHAT_LIMITS.partCharacters);
355
+ return { ...(description ? { description } : {}), ...(agentName ? { agentName } : {}), ...(prompt ? { prompt } : {}) };
356
+ }
183
357
  /** The command line of a shell tool call, read from its own arguments; undefined for any other tool. */
184
358
  export function shellCommand(toolName, argumentsText) {
185
359
  if (!['bash', 'shell', 'exec_command', 'local_shell', 'powershell'].includes(toolName.toLowerCase()))
@@ -365,6 +539,52 @@ export function acceptedDecisions(request) {
365
539
  const decisions = declared.filter((entry) => typeof entry === 'string');
366
540
  return decisions.length > 0 ? decisions : undefined;
367
541
  }
542
+ /**
543
+ * An elicitation that asks questions, the shape supercode's `ask_user` asks with and a hosted Claude Code's
544
+ * AskUserQuestion is raised as (`question-tool.mjs` `questionElicitation`): every property of its
545
+ * `requested_schema` is one question, with a `title`, the question alone (`x-question`) and its `x-options`
546
+ * (`[{label, description}]`, with `x-multi-select`; none for free text). Any other form (an MCP server's schema with
547
+ * its own `enum`, `format` or `required`) is undefined here and keeps the `Answer…` door, which honours them.
548
+ */
549
+ export function requestQuestions(request) {
550
+ if (request.kind !== 'elicitation')
551
+ return undefined;
552
+ const payload = request.payload;
553
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
554
+ return undefined;
555
+ const schema = payload.requested_schema;
556
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema))
557
+ return undefined;
558
+ const properties = schema.properties;
559
+ if (!properties || typeof properties !== 'object' || Array.isArray(properties))
560
+ return undefined;
561
+ const fields = [];
562
+ for (const [key, value] of Object.entries(properties)) {
563
+ if (!value || typeof value !== 'object' || Array.isArray(value))
564
+ return undefined;
565
+ const property = value;
566
+ const title = typeof property.title === 'string' ? clamp(stripControls(property.title.trim()), VSCODE_CHAT_LIMITS.titleCharacters) : '';
567
+ if (!title)
568
+ return undefined;
569
+ const listed = property['x-options'];
570
+ if (typeof property['x-question'] !== 'string' || !Array.isArray(listed))
571
+ return undefined;
572
+ const options = listed.flatMap(option => {
573
+ if (!option || typeof option !== 'object' || Array.isArray(option) || typeof option.label !== 'string' || !option.label.trim())
574
+ return [];
575
+ const label = clamp(stripControls(option.label.trim()), VSCODE_CHAT_LIMITS.titleCharacters);
576
+ const description = typeof option.description === 'string' && option.description.trim()
577
+ ? clamp(stripControls(option.description.trim()), VSCODE_CHAT_LIMITS.titleCharacters) : undefined;
578
+ return [{ label, ...(description ? { description } : {}) }];
579
+ });
580
+ // The question alone (`x-question`) where the elicitation states it: its `description` repeats the options for
581
+ // a frontend that cannot show them, and the carousel shows them itself.
582
+ const asked = clamp(stripControls(property['x-question'].trim()), VSCODE_CHAT_LIMITS.partCharacters);
583
+ const message = asked && asked !== title ? asked : undefined;
584
+ fields.push({ key, title, ...(message ? { message } : {}), options, multiSelect: property['x-multi-select'] === true });
585
+ }
586
+ return fields.length > 0 ? fields : undefined;
587
+ }
368
588
  /** True when this request asks for content a button click cannot carry. */
369
589
  export function requestNeedsTypedReply(request) {
370
590
  if (request.kind === 'approval')
@@ -439,18 +659,21 @@ export function projectPendingRequests(requests, context) {
439
659
  export function projectEvent(event, context) {
440
660
  const payload = payloadOf(event);
441
661
  switch (event.kind) {
442
- // crates/harness/src/server.rs `submit_claimed` — the runtime's echo of the
443
- // initial prompt, before `turn_started`. VS Code renders that request turn.
444
- // A rejoined turn has no submitting handler; later runtime input is visible
662
+ // runtime-hosted.mjs `#submitClaimed` — the runtime's echo of the initial
663
+ // prompt, before `turn_started`. VS Code renders that request turn. A
664
+ // rejoined turn has no submitting handler; later runtime input is visible
445
665
  // within its live response and remains a user turn in canonical history.
666
+ // The harness's own user-role records (runtime-native-projection.mjs
667
+ // `claudeBlocks`) arrive stamped `source: 'harness'`: a notice or nothing.
446
668
  case 'user_message': {
669
+ // (a subagent's own input, `parent_id`: what it was asked is on its row)
670
+ if (text(payload.parent_id))
671
+ return [];
672
+ const notice = notPersonNotice(payload);
673
+ if (notice !== undefined)
674
+ return notice ? [stable({ part: 'markdown', value: `\n\n${notice}\n\n` })] : [];
447
675
  if (context.liveUserMessages) {
448
676
  const body = text(payload.text);
449
- const automatic = automaticPrompt(body);
450
- if (automatic)
451
- return automatic.notice
452
- ? [stable({ part: 'markdown', value: `\n\n${automatic.notice}\n\n` })]
453
- : [];
454
677
  const prefix = '\n\n**Additional input**\n\n';
455
678
  const suffix = '\n\n';
456
679
  const value = clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters - prefix.length - suffix.length);
@@ -469,6 +692,10 @@ export function projectEvent(event, context) {
469
692
  return [];
470
693
  // crates/runtime/src/event.rs `AgentEvent::TextDelta`.
471
694
  case 'text_delta':
695
+ // A subagent's own words (`parent_id`): VS Code's own subagent row shows its calls and its answer, not its
696
+ // running text, which would otherwise read as the agent's reply.
697
+ if (text(payload.parent_id))
698
+ return [];
472
699
  return [stable({ part: 'markdown', value: text(payload.text) })];
473
700
  // crates/runtime/src/event.rs `AgentEvent::ToolCallStarted`.
474
701
  case 'tool_call_started': {
@@ -478,6 +705,9 @@ export function projectEvent(event, context) {
478
705
  const inputText = clamp(stripControls(parseArguments(rawArguments)), VSCODE_CHAT_LIMITS.partCharacters);
479
706
  const titles = toolTitles(toolName, rawArguments);
480
707
  const commandLine = shellCommand(toolName, rawArguments);
708
+ const plan = planItems(toolName, rawArguments) ?? context.plan?.started(toolName, rawArguments, toolCallId);
709
+ const subagent = subagentCall(toolName, rawArguments);
710
+ const parentToolCallId = text(payload.parent_id);
481
711
  return [{
482
712
  emit: {
483
713
  part: 'tool',
@@ -488,6 +718,9 @@ export function projectEvent(event, context) {
488
718
  inputText,
489
719
  isComplete: false,
490
720
  ...(commandLine ? { commandLine } : {}),
721
+ ...(plan ? { plan } : {}),
722
+ ...(subagent ? { subagent } : {}),
723
+ ...(parentToolCallId ? { parentToolCallId } : {}),
491
724
  },
492
725
  needs: 'chatParticipantAdditions',
493
726
  fallback: [{ part: 'progress', value: titles.invocation }],
@@ -505,6 +738,9 @@ export function projectEvent(event, context) {
505
738
  // The titles stay the ones the call started with (the writer keeps them per call); a completion that names its
506
739
  // tool without having started on this stream is titled by that name.
507
740
  const titles = toolName ? toolTitles(toolName, text(payload.arguments)) : undefined;
741
+ const parentToolCallId = text(payload.parent_id);
742
+ const plan = context.plan?.completed(toolCallId, text(payload.output), isError);
743
+ const background = payload.background === true;
508
744
  return [{
509
745
  emit: {
510
746
  part: 'tool',
@@ -515,6 +751,9 @@ export function projectEvent(event, context) {
515
751
  isError,
516
752
  isComplete: true,
517
753
  resultText: output,
754
+ ...(plan ? { plan } : {}),
755
+ ...(parentToolCallId ? { parentToolCallId } : {}),
756
+ ...(background ? { background } : {}),
518
757
  },
519
758
  needs: 'chatParticipantAdditions',
520
759
  fallback: [{
@@ -563,9 +802,11 @@ export function projectEvent(event, context) {
563
802
  const summary = requestSummary(request);
564
803
  const answers = requestAnswers(request, context.connectionId);
565
804
  const subject = requestSubject(request);
805
+ const asked = requestQuestions(request);
566
806
  const title = request.kind === 'approval'
567
807
  ? subject ? `Allow ${stripControls(subject)}?` : 'Approval required'
568
- : subject ? `Approve ${subject}` : `Assistant request #${request.id}`;
808
+ : asked ? asked.length === 1 ? asked[0].title : `${asked.length} questions`
809
+ : subject ? `Approve ${subject}` : `Assistant request #${request.id}`;
569
810
  // The host capability decides whether these original answers can use
570
811
  // a native in-turn confirmation card. Stock confirmation clicks start a
571
812
  // new chat request, so stable command buttons remain the fallback.
@@ -575,13 +816,22 @@ export function projectEvent(event, context) {
575
816
  value: `\n\n**${title}**\n\n${summary}\n\n_This runtime answers its own requests; Volter Harness is observing._\n\n`,
576
817
  })];
577
818
  }
578
- return [stable({
579
- part: 'answers',
580
- approval: request.kind === 'approval',
581
- title: clamp(title, VSCODE_CHAT_LIMITS.titleCharacters),
582
- message: summary,
583
- answers,
584
- })];
819
+ const buttons = {
820
+ part: 'answers',
821
+ approval: request.kind === 'approval',
822
+ title: clamp(title, VSCODE_CHAT_LIMITS.titleCharacters),
823
+ message: summary,
824
+ answers,
825
+ };
826
+ // A harness's questions: VS Code's own carousel, answered inside the running turn.
827
+ if (asked) {
828
+ return [{
829
+ emit: { part: 'questions', connectionId: context.connectionId, requestId: request.id, questions: asked, fallback: buttons },
830
+ needs: 'chatParticipantAdditions',
831
+ fallback: [buttons],
832
+ }];
833
+ }
834
+ return [stable(buttons)];
585
835
  }
586
836
  // crates/harness/src/server.rs `FrontendRequestBroker::respond`.
587
837
  case 'request_resolved': {
@@ -603,10 +853,29 @@ export function projectEvent(event, context) {
603
853
  fallback: [{ part: 'markdown', value: `> ⚠ ${message}` }],
604
854
  }];
605
855
  }
856
+ // A harness hook that blocked or warned (`runtime-native-projection.mjs`, from Claude Code's own hook records).
857
+ // VS Code's own hook row, for the hook events it names; its words otherwise. In the pinned host `push` of this part
858
+ // checks no proposal and `hookProgress` checks `chatParticipantAdditions`; `chatHooks` gates nothing this frontend
859
+ // uses. It is keyed to `chatParticipantAdditions` with the other proposed parts, so a host without it gets words.
860
+ case 'hook': {
861
+ const hookType = HOOK_TYPES.find(type => type === text(payload.hook_event));
862
+ const reason = clamp(stripControls(text(payload.reason)), VSCODE_CHAT_LIMITS.partCharacters);
863
+ const blocked = payload.blocked === true;
864
+ const said = hookSentence(text(payload.hook_event, 'harness'), blocked, reason);
865
+ return [{
866
+ emit: hookType ? { part: 'hook', hookType, blocked, reason } : { part: 'warning', value: said },
867
+ needs: 'chatParticipantAdditions',
868
+ fallback: [{ part: 'markdown', value: `> ⚠ ${said}` }],
869
+ }];
870
+ }
606
871
  // crates/runtime/src/event.rs `AgentEvent::Usage`.
607
872
  // Token counts are not conversation: VS Code's own chat keeps them out of the transcript.
608
873
  case 'usage':
609
874
  return [];
875
+ // The harness's background tasks are the session's state (`session.ts` lists them while a turn runs, each with
876
+ // its Stop), not conversation.
877
+ case 'background_tasks':
878
+ return [];
610
879
  // crates/runtime/src/event.rs `AgentEvent::BackgroundOutput`.
611
880
  case 'background_output': {
612
881
  const chunk = fence(clamp(stripControls(text(payload.chunk)), VSCODE_CHAT_LIMITS.partCharacters));
@@ -664,6 +933,15 @@ export function projectEvent(event, context) {
664
933
  case 'native_event':
665
934
  return [];
666
935
  case 'status':
936
+ // A subagent's task finishing (`call_id` names the subagent call): its answer belongs to that call's row (or to
937
+ // the agent's reply, for a subagent started in the background), so the notice is its first line, and the task's
938
+ // output file where it names one (a background shell's notice names the same call).
939
+ if (text(payload.call_id)) {
940
+ const lines = stripControls(text(payload.message)).split('\n');
941
+ const output = lines.find(line => line.startsWith('Output: ') && line.length > 8);
942
+ const line = [clamp((lines[0] ?? '').trim(), VSCODE_CHAT_LIMITS.titleCharacters), output].filter(Boolean).join('\n');
943
+ return line ? [stable({ part: 'progress', value: clamp(line, VSCODE_CHAT_LIMITS.partCharacters) })] : [];
944
+ }
667
945
  return [stable({ part: 'progress', value: clamp(stripControls(text(payload.message)), VSCODE_CHAT_LIMITS.partCharacters) })];
668
946
  case 'warning':
669
947
  return [stable({ part: 'warning', value: clamp(stripControls(text(payload.message)), VSCODE_CHAT_LIMITS.partCharacters) })];
@@ -671,6 +949,9 @@ export function projectEvent(event, context) {
671
949
  // sdk/interchange/frontend-transcript.mjs reads a donor that emits them.
672
950
  case 'reasoning':
673
951
  case 'thinking': {
952
+ // (a subagent's own reasoning, `parent_id`, as its running text above)
953
+ if (text(payload.parent_id))
954
+ return [];
674
955
  const body = clamp(stripControls(text(payload.text ?? payload.summary, opaqueText(event.payload))), VSCODE_CHAT_LIMITS.partCharacters);
675
956
  return [{
676
957
  emit: { part: 'thinking', value: body },
@@ -730,47 +1011,65 @@ function messageText(message) {
730
1011
  .join('\n');
731
1012
  }
732
1013
  /**
733
- * A "user" message no person typed: Claude Code's `<task-notification>` (a background task reporting back) or the
734
- * editor's automatic tripwire nudge or Claude's unmarked interruption record. None is shown as the person's words.
735
- * A task notification and interruption are short notices; a nudge is not shown. Undefined for a person's message.
1014
+ * What a `user_message` shows when no person typed it: undefined for the person's words, '' to show nothing, else a
1015
+ * short notice. The harness's own records carry `source: 'harness'` (and a `notice` where one is shown) from the one
1016
+ * provenance rule the runtime applies (`@volter/supercode-interchange/user-provenance`). The editor's automatic
1017
+ * tripwire nudge travels through the person's input door, so the editor's frame is what names it.
736
1018
  */
737
- export function automaticPrompt(body) {
738
- const trimmed = body.trimStart();
739
- if (trimmed.startsWith('<task-notification>')) {
740
- const tag = (name) => clamp(stripControls(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`).exec(trimmed)?.[1]?.trim() ?? ''), 200);
741
- const summary = tag('summary');
742
- const status = tag('status');
743
- return { notice: `_Background task${status ? ` ${status}` : ''}${summary ? `: ${summary}` : ''}_` };
744
- }
745
- if (trimmed.startsWith('[Volter editor tripwire'))
746
- return {};
747
- // Claude persists this reserved notice as an unmarked user-role record.
748
- if (trimmed.trimEnd() === '[Request interrupted by user]')
749
- return { notice: '_Turn interrupted._' };
1019
+ function notPersonNotice(payload) {
1020
+ if (payload.source === 'harness')
1021
+ return noticeMarkdown(text(payload.notice));
1022
+ if (editorNudge(text(payload.text)))
1023
+ return '';
750
1024
  return undefined;
751
1025
  }
1026
+ /** The rule's plain notice as this view's italic line; '' for none. */
1027
+ function noticeMarkdown(notice) {
1028
+ const value = clamp(stripControls(notice ?? ''), VSCODE_CHAT_LIMITS.partCharacters - 2);
1029
+ return value ? `_${value}_` : '';
1030
+ }
1031
+ function editorNudge(body) {
1032
+ return body.trimStart().startsWith('[Volter editor tripwire');
1033
+ }
752
1034
  export function projectReplayTurns(events, granted, context, history = []) {
753
1035
  // Join canonical history with its sequenced tail. Retained request/decision
754
1036
  // events can be re-sequenced past history_cursor with no originating turn.
755
1037
  // Before the replay supplies a prompt, give its parts their own runtime
756
1038
  // history turn rather than attributing old approvals to the latest answer.
757
1039
  const turns = history.map(turn => ({ ...turn, parts: turn.parts?.slice() }));
1040
+ // An approval the replay shows allowed is not drawn again: live, its card is hidden (Auto approve) or answered,
1041
+ // and its allowed resolution says nothing more, so the tool row is the record (`request_resolved` above).
1042
+ const allowed = new Set();
1043
+ for (const event of events) {
1044
+ if (event.kind !== 'request_resolved')
1045
+ continue;
1046
+ const payload = payloadOf(event);
1047
+ const response = payload.response;
1048
+ if (typeof payload.request_id === 'number' && response?.kind === 'approval' && response.decision !== 'deny')
1049
+ allowed.add(payload.request_id);
1050
+ }
758
1051
  let replayTurn = false;
759
1052
  let streamedText;
760
1053
  for (const event of events) {
761
- if (event.kind === 'user_message') {
1054
+ if (event.kind === 'request') {
1055
+ const request = (payloadOf(event).request ?? payloadOf(event));
1056
+ if (typeof request?.id === 'number' && allowed.has(request.id))
1057
+ continue;
1058
+ }
1059
+ // (a subagent's own input is no turn of the conversation; `projectEvent` draws nothing for it)
1060
+ if (event.kind === 'user_message' && !text(payloadOf(event).parent_id)) {
762
1061
  streamedText = undefined;
763
1062
  const payload = payloadOf(event);
764
- const automatic = automaticPrompt(text(payload.text));
765
- if (automatic) {
766
- if (!automatic.notice)
1063
+ const shown = notPersonNotice(payload);
1064
+ if (shown !== undefined) {
1065
+ if (!shown)
767
1066
  continue;
768
1067
  if (!replayTurn) {
769
1068
  turns.push({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
770
1069
  replayTurn = true;
771
1070
  }
772
1071
  const last = turns[turns.length - 1];
773
- const notice = { part: 'markdown', value: `\n\n${automatic.notice}\n\n` };
1072
+ const notice = { part: 'markdown', value: `\n\n${shown}\n\n` };
774
1073
  if (last.role === 'assistant' && last.parts)
775
1074
  last.parts.push(notice);
776
1075
  else
@@ -822,7 +1121,7 @@ export function projectReplayTurns(events, granted, context, history = []) {
822
1121
  * The attach snapshot's canonical history as chat turns. Bounded: when older
823
1122
  * turns are omitted the view says so, the way the browser observer does.
824
1123
  */
825
- export function projectHistory(history, granted, pendingToolNames = new Set()) {
1124
+ export function projectHistory(history, granted, pendingToolNames = new Set(), plan = new PlanTracker()) {
826
1125
  const turns = [];
827
1126
  // Keep results inside the native invocation, as the live stream does. A
828
1127
  // separate Markdown result bypasses the editor's collapsed tool-step group.
@@ -870,17 +1169,16 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
870
1169
  pushAssistant([{ part: 'markdown', value: clamp(stripControls(text), VSCODE_CHAT_LIMITS.partCharacters) }]);
871
1170
  continue;
872
1171
  }
873
- const automatic = automaticPrompt(body);
874
- if (automatic) {
875
- if (automatic.notice)
876
- pushAssistant([{ part: 'markdown', value: `\n\n${automatic.notice}\n\n` }]);
1172
+ // The codec preserves the harness's provenance; the one rule reads it.
1173
+ // Image-coordinate notes, compact summaries, task notifications and the
1174
+ // interruption record are not the person's words.
1175
+ const provenance = userProvenance(message.metadata, body);
1176
+ if (provenance.source === 'harness' || editorNudge(body)) {
1177
+ const notice = noticeMarkdown(provenance.notice);
1178
+ if (notice)
1179
+ pushAssistant([{ part: 'markdown', value: `\n\n${notice}\n\n` }]);
877
1180
  continue;
878
1181
  }
879
- // The codec preserves the harness's provenance. Image-coordinate notes,
880
- // compact summaries and injected context are not the person's words.
881
- if (message.metadata?.isMeta === 'true' || message.metadata?.isCompactSummary === 'true' ||
882
- message.metadata?.promptSource === 'system')
883
- continue;
884
1182
  toolRows.clear();
885
1183
  turns.push({ role: 'user', prompt: clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters) });
886
1184
  continue;
@@ -906,6 +1204,8 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
906
1204
  for (const call of message.tool_calls ?? []) {
907
1205
  const titles = toolTitles(call.function.name, call.function.arguments);
908
1206
  const commandLine = shellCommand(call.function.name, call.function.arguments);
1207
+ const items = planItems(call.function.name, call.function.arguments) ?? plan.started(call.function.name, call.function.arguments, call.id);
1208
+ const subagent = subagentCall(call.function.name, call.function.arguments);
909
1209
  const row = {
910
1210
  part: 'tool',
911
1211
  toolName: call.function.name,
@@ -913,6 +1213,8 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
913
1213
  invocationMessage: titles.invocation,
914
1214
  pastTenseMessage: titles.pastTense,
915
1215
  ...(commandLine ? { commandLine } : {}),
1216
+ ...(items ? { plan: items } : {}),
1217
+ ...(subagent ? { subagent } : {}),
916
1218
  inputText: clamp(stripControls(parseArguments(call.function.arguments)), VSCODE_CHAT_LIMITS.partCharacters),
917
1219
  isComplete: true,
918
1220
  };
@@ -950,6 +1252,9 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
950
1252
  // establish failure; do not guess from arbitrary result prose.
951
1253
  if (body === '[no tool result recorded — turn interrupted]' || (Array.isArray(message.content) && message.content.some(part => part && typeof part === 'object' && !Array.isArray(part) && part.is_error === true)))
952
1254
  row.isError = true;
1255
+ const items = plan.completed(message.tool_call_id, body, row.isError === true);
1256
+ if (items)
1257
+ row.plan = items;
953
1258
  toolRows.delete(message.tool_call_id);
954
1259
  }
955
1260
  else {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@volter/supercode-frontend-vscode",
3
3
  "displayName": "Volter Harness",
4
- "version": "0.1.61",
4
+ "version": "0.1.63",
5
5
  "type": "module",
6
6
  "description": "VS Code chat-view frontend for an existing Volter Harness SDK runtime",
7
7
  "repository": {
@@ -22,6 +22,7 @@
22
22
  "LICENSE-APACHE",
23
23
  "dist",
24
24
  "vendor/supercode-frontend",
25
+ "vendor/supercode-interchange",
25
26
  "scripts/pack-extension.mjs",
26
27
  "README.md",
27
28
  "NOTICE.md",
@@ -0,0 +1,19 @@
1
+ // Generated from sdk/interchange/user-provenance.mjs by scripts/user-provenance-copies.mjs (the pre-commit hook
2
+ // runs it): edit that rule, not this file.
3
+ export type UserSource = "person" | "harness";
4
+ export interface UserProvenance {
5
+ source: UserSource;
6
+ /** The short line shown in place of a harness record, where one is shown. */
7
+ notice?: string;
8
+ }
9
+ export const CLAUDE_INTERRUPTION_TEXTS: readonly string[];
10
+ export function isClaudeInterruption(text: unknown): boolean;
11
+ export function userProvenance(
12
+ metadata: Readonly<Record<string, string>> | null | undefined,
13
+ text?: string,
14
+ ): UserProvenance;
15
+ export function isHarnessAuthoredUser(
16
+ metadata: Readonly<Record<string, string>> | null | undefined,
17
+ text?: string,
18
+ ): boolean;
19
+ export function claudeRecordProvenance(record: unknown): Record<string, string>;