@volter/supercode-frontend-vscode 0.1.63 → 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.d.ts CHANGED
@@ -79,11 +79,28 @@ export type ChatPart = {
79
79
  background?: boolean;
80
80
  } | {
81
81
  part: 'answers';
82
+ connectionId: string;
83
+ requestId: number;
82
84
  approval: boolean;
83
85
  title: string;
84
86
  message: string;
87
+ /** None: a request this window cannot answer, drawn read-only. */
85
88
  answers: ChatAnswer[];
86
89
  }
90
+ /** How a request drawn in this stream was answered, said only when the answer did not come through that card in
91
+ * this window (`writePart`), so a card answered elsewhere is not left looking open. Markdown-safe. */
92
+ | {
93
+ part: 'resolution';
94
+ connectionId: string;
95
+ requestId: number;
96
+ value: string;
97
+ }
98
+ /** A record that is not conversation (an event kind this version does not know, a malformed request): written to
99
+ * the output channel, never to the transcript. */
100
+ | {
101
+ part: 'unshown';
102
+ value: string;
103
+ }
87
104
  /** A harness's questions (`requestQuestions`): VS Code's question carousel on a turn this window runs, its
88
105
  * `answers` part anywhere else. */
89
106
  | {
@@ -187,7 +204,11 @@ export interface SubagentCall {
187
204
  /** A subagent call's description, kind and prompt (Claude Code's Task and Agent tools: `description`,
188
205
  * `subagent_type`, `prompt`), read from its own arguments; undefined for any other tool. */
189
206
  export declare function subagentCall(toolName: string, argumentsText: string): SubagentCall | undefined;
190
- /** The command line of a shell tool call, read from its own arguments; undefined for any other tool. */
207
+ /**
208
+ * The command line of a shell tool call, read from its own arguments; undefined for any other tool. Claude Code's
209
+ * BashOutput and KillBash are terminal rows too, as Code-OSS's own Claude host draws them (`claudeToolDisplay.ts`
210
+ * `TOOL_ROWS`): their command line is their `command`, else their input (`getClaudeToolInputString`).
211
+ */
191
212
  export declare function shellCommand(toolName: string, argumentsText: string): string | undefined;
192
213
  /** The information this view shows for a request payload. */
193
214
  export declare function requestSummary(request: FrontendRequest): string;
@@ -218,6 +239,15 @@ export declare function requestNeedsTypedReply(request: FrontendRequest): boolea
218
239
  export declare function resolutionSentence(response: FrontendResponse | undefined, request: FrontendRequest | undefined, requestId: number): string;
219
240
  /** `write_file on hello.txt`, read from the request's own payload. */
220
241
  export declare function requestSubject(request: FrontendRequest): string | undefined;
242
+ /**
243
+ * How a turn that did not succeed ends the response its request handler runs, as VS Code's own agent host ends one
244
+ * (`agentHostSessionHandler.ts` `_getTurnErrorDetails`): a failed turn is the response's error, an interrupted one its
245
+ * warning-level, expected error. Undefined for any other event.
246
+ */
247
+ export declare function turnOutcome(event: FrontendEvent): {
248
+ message: string;
249
+ interrupted: boolean;
250
+ } | undefined;
221
251
  export interface ProjectionContext {
222
252
  /** Identifies which attached runtime a confirmation click must answer. */
223
253
  connectionId: string;
@@ -225,8 +255,14 @@ export interface ProjectionContext {
225
255
  echoOwnPrompts?: boolean;
226
256
  /** Show input arriving in an observed turn, without inventing a native request turn. */
227
257
  liveUserMessages?: boolean;
228
- /** The request a `request_resolved` answers, so its sentence can name it. */
258
+ /** The request a `request_resolved` answers, so its line can name it. */
229
259
  pendingRequest?: (requestId: number) => FrontendRequest | undefined;
260
+ /** The calls whose background task's notification this stream has drawn (`firstNotice`), so a task finishing is one
261
+ * line: Claude Code reports it both as `system/task_notification` (a `status`) and as the user-role record that
262
+ * wakes the agent (a `user_message` notice). */
263
+ notices?: Set<string>;
264
+ /** Where a record that is not conversation goes when there is no writer to log it (history). */
265
+ unshown?: (value: string) => void;
230
266
  /**
231
267
  * Whether the runtime's own descriptor advertises `respond`. A HOSTED
232
268
  * harness runtime sets `actions.respond: false` — the portable contract
@@ -252,10 +288,11 @@ export declare function projectEvent(event: FrontendEvent, context: ProjectionCo
252
288
  /** Replace every part whose proposal the host did not grant with its fallback. */
253
289
  export declare function resolveParts(parts: readonly ProjectedPart[], granted: ReadonlySet<string>): ChatPart[];
254
290
  /**
255
- * The prompt of the marker turn that carries runtime history with no user turn
256
- * of its own. It names itself; nothing here is presented as a person's words.
291
+ * The prompt of the marker turn that carries runtime history with no user turn of its own: VS Code's own words for a
292
+ * turn whose request is not in the transcript (`claudeReplayMapper.ts` `missingPromptPlaceholder`), so nothing is
293
+ * presented as a person's words.
257
294
  */
258
- export declare const HISTORY_MARKER_PROMPT = "\u27E8supercode runtime history\u27E9";
295
+ export declare const HISTORY_MARKER_PROMPT = "Message content could not be retrieved";
259
296
  export type TurnRole = 'user' | 'assistant';
260
297
  export interface ProjectedTurn {
261
298
  role: TurnRole;
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;
@@ -354,9 +359,18 @@ export function subagentCall(toolName, argumentsText) {
354
359
  const prompt = field('prompt', VSCODE_CHAT_LIMITS.partCharacters);
355
360
  return { ...(description ? { description } : {}), ...(agentName ? { agentName } : {}), ...(prompt ? { prompt } : {}) };
356
361
  }
357
- /** The command line of a shell tool call, read from its own arguments; undefined for any other tool. */
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
+ */
358
367
  export function shellCommand(toolName, argumentsText) {
359
- 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))
360
374
  return undefined;
361
375
  try {
362
376
  const fields = JSON.parse(argumentsText);
@@ -644,6 +658,24 @@ export function requestSubject(request) {
644
658
  return clamp(stripControls(subject), 120);
645
659
  return undefined;
646
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
+ }
647
679
  /** Pending requests belong to the live runtime, even when saved transcript
648
680
  * history predates them. Rehydrate their controls before waiting for new events. */
649
681
  export function projectPendingRequests(requests, context) {
@@ -669,9 +701,11 @@ export function projectEvent(event, context) {
669
701
  // (a subagent's own input, `parent_id`: what it was asked is on its row)
670
702
  if (text(payload.parent_id))
671
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`).
672
706
  const notice = notPersonNotice(payload);
673
707
  if (notice !== undefined)
674
- return notice ? [stable({ part: 'markdown', value: `\n\n${notice}\n\n` })] : [];
708
+ return notice && firstNotice(context, notifiedCall(payload)) ? [stable(noticeLine(notice))] : [];
675
709
  if (context.liveUserMessages) {
676
710
  const body = text(payload.text);
677
711
  const prefix = '\n\n**Additional input**\n\n';
@@ -793,11 +827,9 @@ export function projectEvent(event, context) {
793
827
  // crates/harness/src/server.rs `FrontendRequestBroker::publish`.
794
828
  case 'request': {
795
829
  const request = (payload.request ?? payload);
830
+ // (a request with no id cannot be answered, so it is a malformed record, not conversation)
796
831
  if (!request || typeof request.id !== 'number') {
797
- return [stable({
798
- part: 'markdown',
799
- value: `**supercode** · malformed request event\n\n${fence(opaqueText(event.payload), 'json')}`,
800
- })];
832
+ return [stable({ part: 'unshown', value: `malformed request event #${event.sequence}: ${opaqueText(event.payload)}` })];
801
833
  }
802
834
  const summary = requestSummary(request);
803
835
  const answers = requestAnswers(request, context.connectionId);
@@ -810,19 +842,22 @@ export function projectEvent(event, context) {
810
842
  // The host capability decides whether these original answers can use
811
843
  // a native in-turn confirmation card. Stock confirmation clicks start a
812
844
  // new chat request, so stable command buttons remain the fallback.
813
- if (context.canRespond === false || (request.payload && typeof request.payload === 'object' && !Array.isArray(request.payload) && request.payload.observe_only === true)) {
814
- return [stable({
815
- part: 'markdown',
816
- value: `\n\n**${title}**\n\n${summary}\n\n_This runtime answers its own requests; Volter Harness is observing._\n\n`,
817
- })];
818
- }
819
845
  const buttons = {
820
846
  part: 'answers',
847
+ connectionId: context.connectionId,
848
+ requestId: request.id,
821
849
  approval: request.kind === 'approval',
822
850
  title: clamp(title, VSCODE_CHAT_LIMITS.titleCharacters),
823
851
  message: summary,
824
852
  answers,
825
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.
858
+ if (context.canRespond === false || (request.payload && typeof request.payload === 'object' && !Array.isArray(request.payload) && request.payload.observe_only === true)) {
859
+ return [stable({ ...buttons, answers: [] })];
860
+ }
826
861
  // A harness's questions: VS Code's own carousel, answered inside the running turn.
827
862
  if (asked) {
828
863
  return [{
@@ -833,16 +868,15 @@ export function projectEvent(event, context) {
833
868
  }
834
869
  return [stable(buttons)];
835
870
  }
836
- // crates/harness/src/server.rs `FrontendRequestBroker::respond`.
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`).
837
875
  case 'request_resolved': {
838
876
  const requestId = typeof payload.request_id === 'number' ? payload.request_id : Number.NaN;
839
877
  const response = payload.response;
840
- // An allowed approval says nothing more, as in VS Code's own chat: its tool row carries on and is the record.
841
- // A denial, and an answer to any other request, is said once.
842
- if (response?.kind === 'approval' && response.decision !== 'deny')
843
- return [];
844
- const sentence = resolutionSentence(response, context.pendingRequest?.(requestId), requestId);
845
- 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 })];
846
880
  }
847
881
  // crates/runtime/src/event.rs `AgentEvent::CacheWarning`.
848
882
  case 'cache_warning': {
@@ -872,23 +906,25 @@ export function projectEvent(event, context) {
872
906
  // Token counts are not conversation: VS Code's own chat keeps them out of the transcript.
873
907
  case 'usage':
874
908
  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.
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.
877
912
  case 'background_tasks':
878
913
  return [];
879
- // crates/runtime/src/event.rs `AgentEvent::BackgroundOutput`.
880
- case 'background_output': {
881
- const chunk = fence(clamp(stripControls(text(payload.chunk)), VSCODE_CHAT_LIMITS.partCharacters));
882
- const truncated = payload.truncated === true ? '\n\n_the runtime stopped retaining this job’s output_' : '';
883
- return [stable({
884
- part: 'markdown',
885
- value: `\n\n**background job ${text(payload.job_id)}**\n\n${chunk}${truncated}\n\n`,
886
- })];
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 }] }];
887
927
  }
888
- // crates/harness/src/server.rs `submit_claimed`.
889
- // The live writer drops it when the person stopped the turn (VS Code marks a stopped response itself).
890
- case 'turn_interrupted':
891
- return [stable({ part: 'warning', value: 'The turn was interrupted.' })];
892
928
  // crates/harness/src/server.rs `submit_claimed` emits `turn_failed`. The
893
929
  // other four are the error kinds the donor-neutral reader in
894
930
  // sdk/interchange/frontend-transcript.mjs treats identically:
@@ -897,31 +933,40 @@ export function projectEvent(event, context) {
897
933
  // `runtime_disconnected`, which a frontend synthesizes for itself
898
934
  // (sdk/interchange/frontend-app.mjs). `stream_error` has no producer in this
899
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.
900
940
  case 'turn_failed':
901
941
  case 'stream_error':
902
942
  case 'transport_error':
903
943
  case 'watch_error':
904
944
  case 'runtime_disconnected': {
905
- const message = typeof payload.message === 'string' ? payload.message : opaqueText(event.payload);
906
- 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 [];
907
948
  return [{
908
949
  emit: { part: 'warning', value },
909
950
  needs: 'chatParticipantAdditions',
910
951
  fallback: [{ part: 'markdown', value: `> ⚠ ${value}` }],
911
952
  }];
912
953
  }
913
- // Declared by the runtime descriptor's `display.event_kinds`
914
- // (crates/harness/src/server.rs `frontend_descriptor`); read here the way
915
- // sdk/interchange/frontend-transcript.mjs reads them.
916
- 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
+ }
917
963
  case 'scheduled_prompt_deferred':
918
964
  case 'scheduled_prompt_completed':
919
- return [stable({
920
- part: 'progress',
921
- value: `${event.kind.replaceAll('_', ' ')} · ${opaqueText(event.payload, 256)}`,
922
- })];
965
+ return [];
923
966
  case 'scheduler_error': {
924
- const value = `scheduler error: ${opaqueText(event.payload, 256)}`;
967
+ const value = errorMessage(payload);
968
+ if (!value)
969
+ return [];
925
970
  return [{
926
971
  emit: { part: 'warning', value },
927
972
  needs: 'chatParticipantAdditions',
@@ -933,18 +978,23 @@ export function projectEvent(event, context) {
933
978
  case 'native_event':
934
979
  return [];
935
980
  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).
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.
939
985
  if (text(payload.call_id)) {
986
+ if (!firstNotice(context, text(payload.call_id)))
987
+ return [];
940
988
  const lines = stripControls(text(payload.message)).split('\n');
941
989
  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) })] : [];
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)))] : [];
944
992
  }
945
993
  return [stable({ part: 'progress', value: clamp(stripControls(text(payload.message)), VSCODE_CHAT_LIMITS.partCharacters) })];
946
- case 'warning':
947
- 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
+ }
948
998
  // Not in this runtime's declared set; read the way
949
999
  // sdk/interchange/frontend-transcript.mjs reads a donor that emits them.
950
1000
  case 'reasoning':
@@ -959,17 +1009,11 @@ export function projectEvent(event, context) {
959
1009
  fallback: [{ part: 'markdown', value: `\n\n> ${body.replaceAll('\n', '\n> ')}\n\n` }],
960
1010
  }];
961
1011
  }
962
- // The contract says every kind but `request` is opaque JSON by design, so a
963
- // 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.
964
1015
  default:
965
- // The surrounding blank lines are load-bearing: a fenced block that the
966
- // next streamed delta lands against on the same line never closes, and
967
- // it swallows everything after it — observed doing exactly that to an
968
- // approval request.
969
- return [stable({
970
- part: 'markdown',
971
- 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`,
972
- })];
1016
+ return [stable({ part: 'unshown', value: `event kind ${clamp(event.kind, VSCODE_CHAT_LIMITS.titleCharacters)} #${event.sequence} is not displayed: ${opaqueText(event.payload)}` })];
973
1017
  }
974
1018
  }
975
1019
  /** Replace every part whose proposal the host did not grant with its fallback. */
@@ -984,10 +1028,11 @@ export function resolveParts(parts, granted) {
984
1028
  return resolved;
985
1029
  }
986
1030
  /**
987
- * The prompt of the marker turn that carries runtime history with no user turn
988
- * 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.
989
1034
  */
990
- export const HISTORY_MARKER_PROMPT = '⟨supercode runtime history⟩';
1035
+ export const HISTORY_MARKER_PROMPT = 'Message content could not be retrieved';
991
1036
  function messageText(message) {
992
1037
  const content = message.content;
993
1038
  if (typeof content === 'string')
@@ -1012,21 +1057,40 @@ function messageText(message) {
1012
1057
  }
1013
1058
  /**
1014
1059
  * 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
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
1016
1061
  * provenance rule the runtime applies (`@volter/supercode-interchange/user-provenance`). The editor's automatic
1017
1062
  * tripwire nudge travels through the person's input door, so the editor's frame is what names it.
1018
1063
  */
1019
1064
  function notPersonNotice(payload) {
1020
1065
  if (payload.source === 'harness')
1021
- return noticeMarkdown(text(payload.notice));
1066
+ return noticeText(text(payload.notice));
1022
1067
  if (editorNudge(text(payload.text)))
1023
1068
  return '';
1024
1069
  return undefined;
1025
1070
  }
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}_` : '';
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);
1030
1094
  }
1031
1095
  function editorNudge(body) {
1032
1096
  return body.trimStart().startsWith('[Volter editor tripwire');
@@ -1037,6 +1101,18 @@ export function projectReplayTurns(events, granted, context, history = []) {
1037
1101
  // Before the replay supplies a prompt, give its parts their own runtime
1038
1102
  // history turn rather than attributing old approvals to the latest answer.
1039
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) };
1040
1116
  // An approval the replay shows allowed is not drawn again: live, its card is hidden (Auto approve) or answered,
1041
1117
  // and its allowed resolution says nothing more, so the tool row is the record (`request_resolved` above).
1042
1118
  const allowed = new Set();
@@ -1056,20 +1132,23 @@ export function projectReplayTurns(events, granted, context, history = []) {
1056
1132
  if (typeof request?.id === 'number' && allowed.has(request.id))
1057
1133
  continue;
1058
1134
  }
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;
1059
1138
  // (a subagent's own input is no turn of the conversation; `projectEvent` draws nothing for it)
1060
1139
  if (event.kind === 'user_message' && !text(payloadOf(event).parent_id)) {
1061
1140
  streamedText = undefined;
1062
1141
  const payload = payloadOf(event);
1063
1142
  const shown = notPersonNotice(payload);
1064
1143
  if (shown !== undefined) {
1065
- if (!shown)
1144
+ if (!shown || !firstNotice(context, notifiedCall(payload)))
1066
1145
  continue;
1067
1146
  if (!replayTurn) {
1068
1147
  turns.push({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
1069
1148
  replayTurn = true;
1070
1149
  }
1071
1150
  const last = turns[turns.length - 1];
1072
- const notice = { part: 'markdown', value: `\n\n${shown}\n\n` };
1151
+ const notice = noticeLine(shown);
1073
1152
  if (last.role === 'assistant' && last.parts)
1074
1153
  last.parts.push(notice);
1075
1154
  else
@@ -1080,7 +1159,13 @@ export function projectReplayTurns(events, granted, context, history = []) {
1080
1159
  replayTurn = true;
1081
1160
  continue;
1082
1161
  }
1083
- 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
+ });
1084
1169
  if (parts.length === 0)
1085
1170
  continue;
1086
1171
  if (event.kind === 'text_delta' && parts[0]?.part === 'markdown') {
@@ -1103,19 +1188,20 @@ export function projectReplayTurns(events, granted, context, history = []) {
1103
1188
  else
1104
1189
  turns.push({ role: 'assistant', parts });
1105
1190
  }
1106
- if (turns.length > VSCODE_CHAT_LIMITS.historyTurns) {
1107
- const omitted = turns.length - VSCODE_CHAT_LIMITS.historyTurns;
1108
- const kept = turns.slice(-VSCODE_CHAT_LIMITS.historyTurns);
1109
- kept.unshift({
1110
- role: 'assistant',
1111
- parts: [{
1112
- part: 'markdown',
1113
- value: `_${omitted} earlier turns are omitted by this view’s bound; the runtime’s own history is unchanged and authoritative._`,
1114
- }],
1115
- });
1116
- return kept;
1117
- }
1118
- 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;
1119
1205
  }
1120
1206
  /**
1121
1207
  * The attach snapshot's canonical history as chat turns. Bounded: when older
@@ -1174,9 +1260,9 @@ export function projectHistory(history, granted, pendingToolNames = new Set(), p
1174
1260
  // interruption record are not the person's words.
1175
1261
  const provenance = userProvenance(message.metadata, body);
1176
1262
  if (provenance.source === 'harness' || editorNudge(body)) {
1177
- const notice = noticeMarkdown(provenance.notice);
1263
+ const notice = noticeText(provenance.notice);
1178
1264
  if (notice)
1179
- pushAssistant([{ part: 'markdown', value: `\n\n${notice}\n\n` }]);
1265
+ pushAssistant([noticeLine(notice)]);
1180
1266
  continue;
1181
1267
  }
1182
1268
  toolRows.clear();
@@ -1236,13 +1322,10 @@ export function projectHistory(history, granted, pendingToolNames = new Set(), p
1236
1322
  // interruption. Only the final matching tool is affected, never old history.
1237
1323
  const pending = message === history.at(-1) && pendingToolNames.has(message.name ?? '')
1238
1324
  && body === '[no tool result recorded — turn interrupted]';
1239
- if (pending) {
1240
- // The pinned editor serializes history tool parts as complete even if
1241
- // isComplete=false. Keep this explicit marker for matched AND orphan
1242
- // results; the active response separately restores the approval controls.
1243
- 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)
1244
1328
  continue;
1245
- }
1246
1329
  if (granted.has('chatParticipantAdditions')) {
1247
1330
  const row = message.tool_call_id ? toolRows.get(message.tool_call_id) : undefined;
1248
1331
  const output = clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters);
@@ -1278,16 +1361,5 @@ export function projectHistory(history, granted, pendingToolNames = new Set(), p
1278
1361
  value: `**Tool result${message.name ? ` · ${message.name}` : ''}**\n\n${fence(clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters))}`,
1279
1362
  }]);
1280
1363
  }
1281
- if (turns.length <= VSCODE_CHAT_LIMITS.historyTurns)
1282
- return turns;
1283
- const omitted = turns.length - VSCODE_CHAT_LIMITS.historyTurns;
1284
- const kept = turns.slice(-VSCODE_CHAT_LIMITS.historyTurns);
1285
- kept.unshift({
1286
- role: 'assistant',
1287
- parts: [{
1288
- part: 'markdown',
1289
- value: `_${omitted} earlier turns are omitted by this view’s bound; the runtime’s own history is unchanged and authoritative._`,
1290
- }],
1291
- });
1292
- return kept;
1364
+ return boundTurns(turns);
1293
1365
  }
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.63",
4
+ "version": "0.1.64",
5
5
  "type": "module",
6
6
  "description": "VS Code chat-view frontend for an existing Volter Harness SDK runtime",
7
7
  "repository": {