@volter/supercode-frontend-vscode 0.1.51 → 0.1.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -393,6 +393,22 @@ are read only for backwards compatibility.
393
393
  Restored response cancellation interrupts the runtime and waits for acknowledgement.
394
394
  Session-list updates consistently prioritize pending input over busy status.
395
395
 
396
+ Reload restoration joins the runtime's canonical history to events after its
397
+ `history_cursor`. A separately loaded native transcript is a fallback when the
398
+ runtime has no user history; it never replaces the live event tail. The final
399
+ running response is replayed through `activeResponseCallback`, with each pending
400
+ request drawn once from the snapshot and later events followed by sequence.
401
+ An idle runtime with a pending request also keeps this callback open until the
402
+ request is resolved. Standalone requests use the explicit runtime-history marker
403
+ instead of reopening an earlier answer. Managed conversations save their native
404
+ identity before sending input, as well as when a turn ends.
405
+ Stop is observed during the save and checked before input, including a lease
406
+ renewal retry. Retained request/decision records with no originating replay
407
+ prompt appear in a separate runtime-history turn.
408
+ The [reload investigation](../../docs/evidence/chat-pending-approval-reload-2026-10-08.md)
409
+ separates the retained first-user failure from this source change; a live
410
+ candidate reload and approval click remain unmeasured.
411
+
396
412
  The pinned Code-OSS host needs a resource-scoping repair in its input-state change
397
413
  routing: stock code broadcasts changes to all input states of a provider. The
398
414
  Volter overlay guards this fix with a native regression test. The former legacy
package/dist/extension.js CHANGED
@@ -10,7 +10,7 @@ import { readCredentialFile } from './credential.js';
10
10
  import { grantedProposals, PROPOSAL_EFFECTS, REQUESTED_PROPOSALS } from './proposals.js';
11
11
  import { observeActiveResponse, sessionStatus } from './native-session.js';
12
12
  import { RequestNotPendingError, VscodeFrontendSession } from './session.js';
13
- import { acceptedDecisions, opaqueText, projectEvent, projectPendingRequests, projectHistory, projectReplayTurns, resolveParts, resolutionSentence, typedAnswer, } from './view.js';
13
+ import { acceptedDecisions, opaqueText, projectEvent, projectPendingRequests, projectHistory, projectReplayTurns, HISTORY_MARKER_PROMPT, resolveParts, resolutionSentence, typedAnswer, } from './view.js';
14
14
  const SESSION_SCHEME = 'supercode';
15
15
  /** The harnesses whose model and reasoning effort a new chat can set (the host passes them to the CLI). */
16
16
  const CONFIGURABLE_HARNESSES = ['claude-code', 'codex'];
@@ -1295,6 +1295,11 @@ export async function activate(context) {
1295
1295
  } };
1296
1296
  };
1297
1297
  const runTurnInto = async (connection, prompt, stream, token, approval) => {
1298
+ const throwIfCancelled = () => {
1299
+ if (token.isCancellationRequested)
1300
+ throw new vscode.CancellationError();
1301
+ };
1302
+ throwIfCancelled();
1298
1303
  if (approval() === 'autoApprove' && !canAnswer(connection)) {
1299
1304
  throw new Error('This harness does not support automatic approval through Chat. Select Harness permissions.');
1300
1305
  }
@@ -1335,13 +1340,20 @@ export async function activate(context) {
1335
1340
  const unwatchTasks = connection.session.onChange(sayTasks);
1336
1341
  let completed = false;
1337
1342
  try {
1343
+ // Observe Stop throughout the save, then check the token before input.
1344
+ // A cancellation while this idle runtime saves cannot be handled by an
1345
+ // interrupt alone: there is no running turn to interrupt yet.
1346
+ if (controls && !attachedIds.has(connection.id) && connection.session.descriptor?.session_id) {
1347
+ await controls.remember(connection.id, connection.session.descriptor.session_id);
1348
+ }
1349
+ throwIfCancelled();
1338
1350
  await connection.session.runTurn(prompt, (event) => {
1339
1351
  // A turn the person stopped is marked stopped by VS Code itself; only an interruption from elsewhere is said.
1340
1352
  if (event.kind === 'turn_interrupted' && token.isCancellationRequested)
1341
1353
  return;
1342
1354
  writeProjected(stream, projectEvent(event, projection), granted, state);
1343
1355
  watch.sweep();
1344
- });
1356
+ }, { throwIfCancelled });
1345
1357
  completed = true;
1346
1358
  }
1347
1359
  finally {
@@ -2026,12 +2038,16 @@ function registerSessions(context, frontends, participant, granted, handler, his
2026
2038
  // canonical transcript, replayed as turns.
2027
2039
  const history = [];
2028
2040
  const authorId = historyParticipant(connection);
2029
- // A hosted harness runtime has no canonical ChatMessage history —
2030
- // `HostedHarnessRuntime::attach` returns an empty one and puts the
2031
- // conversation in `replay`. Read whichever the runtime actually
2032
- // filled, or a resumed session opens blank beside the person's own
2033
- // conversation.
2034
- const canonicalHistory = connection.persistedHistory?.length ? connection.persistedHistory : connection.session.history;
2041
+ // History and replay are one runtime snapshot, joined by its cursor.
2042
+ // A host's separately loaded native transcript has no replay cursor;
2043
+ // selecting it instead hid every turn started since the runtime was
2044
+ // created. Keep it only as a fallback when no runtime user history is
2045
+ // available, rather than trying to align equal-looking prompt text.
2046
+ const runtimeHistory = connection.session.history;
2047
+ const replay = connection.session.replay.filter(event => event.sequence > connection.session.historyCursor);
2048
+ const hasRuntimeHistory = runtimeHistory.length > 0 || replay.some(event => event.kind === 'user_message');
2049
+ const usingSavedHistory = !hasRuntimeHistory && Boolean(connection.persistedHistory?.length);
2050
+ const canonicalHistory = usingSavedHistory ? connection.persistedHistory : runtimeHistory;
2035
2051
  const pendingToolNames = new Set();
2036
2052
  if (connection.session.descriptor?.turn_state === 'busy') {
2037
2053
  for (const request of connection.session.pendingRequests.values()) {
@@ -2043,14 +2059,44 @@ function registerSessions(context, frontends, participant, granted, handler, his
2043
2059
  pendingToolNames.add(name);
2044
2060
  }
2045
2061
  }
2046
- const projected = canonicalHistory.length > 0
2047
- ? projectHistory(canonicalHistory, granted, pendingToolNames)
2048
- : projectReplayTurns(connection.session.replay, granted, {
2049
- connectionId: connection.id,
2050
- pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
2051
- canRespond: connection.session.descriptor?.actions.respond !== false,
2052
- });
2053
- if (connection.historyTruncated)
2062
+ const projected = projectReplayTurns(replay.filter(event => {
2063
+ if (event.kind !== 'request')
2064
+ return true;
2065
+ const payload = event.payload;
2066
+ return !payload?.request || !connection.session.pendingRequests.has(payload.request.id);
2067
+ }), granted, {
2068
+ connectionId: connection.id,
2069
+ pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
2070
+ canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
2071
+ }, projectHistory(canonicalHistory, granted, pendingToolNames));
2072
+ const live = connection.session.descriptor?.turn_state === 'busy' || connection.session.pendingRequests.size > 0;
2073
+ const activeParts = [];
2074
+ if (live) {
2075
+ // Standalone requests have no user turn. Give their live response
2076
+ // an explicit runtime marker instead of reopening an old answer.
2077
+ let lastBoundary = -1;
2078
+ let lastUser = -1;
2079
+ for (const event of replay) {
2080
+ if (['turn_succeeded', 'turn_failed', 'turn_interrupted'].includes(event.kind))
2081
+ lastBoundary = event.sequence;
2082
+ if (event.kind === 'user_message')
2083
+ lastUser = event.sequence;
2084
+ }
2085
+ if (!projected.some(turn => turn.role === 'user')
2086
+ || (connection.session.descriptor?.turn_state !== 'busy' && lastUser <= lastBoundary)) {
2087
+ projected.push({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
2088
+ }
2089
+ // Leave the final request without a historical response, so the
2090
+ // callback creates its response from live progress. Rehydrated
2091
+ // responses can be stale; their command buttons are disabled.
2092
+ const last = projected.at(-1);
2093
+ if (last?.role === 'assistant') {
2094
+ activeParts.push(...(projected.pop()?.parts ?? []).map(part => part.part === 'answers'
2095
+ ? { part: 'markdown', value: `**${part.title}**\n\n${part.message}` }
2096
+ : part));
2097
+ }
2098
+ }
2099
+ if (usingSavedHistory && connection.historyTruncated)
2054
2100
  history.push(new turns.ChatRequestTurn('[Earlier conversation history]', undefined, [], authorId, []), new turns.ChatResponseTurn([new vscode.ChatResponseMarkdownPart('_Showing the latest 120 saved messages. Earlier context remains in the harness session._')], {}, authorId));
2055
2101
  for (const turn of projected) {
2056
2102
  if (turn.role === 'user') {
@@ -2059,16 +2105,16 @@ function registerSessions(context, frontends, participant, granted, handler, his
2059
2105
  }
2060
2106
  history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {}, authorId));
2061
2107
  }
2062
- const busy = connection.session.descriptor?.turn_state === 'busy';
2063
2108
  const renderedCursor = connection.session.cursor;
2064
- if (!busy)
2109
+ const pendingAtCursor = [...connection.session.pendingRequests.values()];
2110
+ if (!live)
2065
2111
  connection.viewReady = attached;
2066
2112
  return {
2067
2113
  title: connection.label,
2068
2114
  options: controls?.attached(connection) ? undefined : controls?.currentOptions(resource),
2069
2115
  history,
2070
2116
  requestHandler: boundHandler,
2071
- activeResponseCallback: busy
2117
+ activeResponseCallback: live
2072
2118
  ? async (stream, token) => {
2073
2119
  // A turn re-joined after a reload has lost the handler that ran it, so its approvals are
2074
2120
  // answered here under the conversation's mode (t_39dac582): Auto approve answers the cards
@@ -2076,22 +2122,24 @@ function registerSessions(context, frontends, participant, granted, handler, his
2076
2122
  const watch = controls && !controls.attached(connection) ? controls.watchApprovals(connection, stream, token) : undefined;
2077
2123
  try {
2078
2124
  const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map(), quiet: watch?.answers };
2079
- writeProjected(stream, projectPendingRequests(connection.session.pendingRequests.values(), {
2125
+ for (const part of oneRowPerToolCall(activeParts))
2126
+ writePart(stream, part, state);
2127
+ writeProjected(stream, projectPendingRequests(pendingAtCursor.filter(request => connection.session.pendingRequests.has(request.id)), {
2080
2128
  connectionId: connection.id,
2081
- canRespond: connection.session.descriptor?.actions.respond !== false,
2129
+ canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
2082
2130
  }), granted, state);
2083
2131
  watch?.sweep();
2084
2132
  await observeActiveResponse(connection.session, token, (event) => {
2085
2133
  writeProjected(stream, projectEvent(event, {
2086
2134
  connectionId: connection.id,
2087
2135
  pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
2088
- canRespond: connection.session.descriptor?.actions.respond !== false,
2136
+ canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
2089
2137
  }), granted, state);
2090
2138
  watch?.sweep();
2091
2139
  }, () => { connection.viewReady = true; }, renderedCursor);
2092
2140
  }
2093
2141
  finally {
2094
- connection.viewReady = connection.session.descriptor?.turn_state !== 'busy';
2142
+ connection.viewReady = connection.session.descriptor?.turn_state !== 'busy' && !connection.session.pendingRequests.size;
2095
2143
  watch?.dispose();
2096
2144
  }
2097
2145
  }
@@ -3,4 +3,4 @@ import type { FrontendEvent } from '../vendor/supercode-frontend/generated.mjs';
3
3
  import { type VscodeFrontendSession } from './session.js';
4
4
  export declare function sessionStatus(session: Pick<VscodeFrontendSession, 'descriptor' | 'pendingRequests'>): 'NeedsInput' | 'InProgress' | 'Completed';
5
5
  /** Follow a restored turn, including the interval between snapshot and callback. */
6
- export declare function observeActiveResponse(session: Pick<VscodeFrontendSession, 'descriptor' | 'onEvent' | 'interrupt' | 'actions' | 'replay'>, token: CancellationToken, write: (event: FrontendEvent) => void, onSubscribed?: () => void, afterSequence?: number): Promise<void>;
6
+ export declare function observeActiveResponse(session: Pick<VscodeFrontendSession, 'descriptor' | 'pendingRequests' | 'onEvent' | 'interrupt' | 'actions' | 'replay'>, token: CancellationToken, write: (event: FrontendEvent) => void, onSubscribed?: () => void, afterSequence?: number): Promise<void>;
@@ -34,7 +34,12 @@ export async function observeActiveResponse(session, token, write, onSubscribed,
34
34
  reject(error);
35
35
  return;
36
36
  }
37
- if (!cancelling && TURN_TERMINAL_KINDS.includes(event.kind))
37
+ if (event.kind === 'runtime_disconnected') {
38
+ resolve();
39
+ return;
40
+ }
41
+ const ended = TURN_TERMINAL_KINDS.includes(event.kind) || event.kind === 'request_resolved';
42
+ if (!cancelling && ended && session.descriptor?.turn_state !== 'busy' && !session.pendingRequests.size)
38
43
  resolve();
39
44
  };
40
45
  unsubscribe = session.onEvent(consume);
@@ -47,7 +52,7 @@ export async function observeActiveResponse(session, token, write, onSubscribed,
47
52
  cancellation = token.onCancellationRequested(cancel);
48
53
  if (token.isCancellationRequested)
49
54
  cancel();
50
- else if (session.descriptor?.turn_state !== 'busy')
55
+ else if (session.descriptor?.turn_state !== 'busy' && !session.pendingRequests.size)
51
56
  resolve();
52
57
  });
53
58
  }
package/dist/session.d.ts CHANGED
@@ -147,7 +147,7 @@ export declare class VscodeFrontendSession {
147
147
  actions(): VscodeFrontendActions;
148
148
  /** Run one controller action, re-taking an expired lease exactly once. */
149
149
  private withControl;
150
- submit(prompt: string): Promise<void>;
150
+ submit(prompt: string, throwIfCancelled?: () => void): Promise<void>;
151
151
  steer(prompt: string): Promise<void>;
152
152
  interrupt(): Promise<void>;
153
153
  /** Stop one of the harness's own background tasks, by the id it listed it with. */
@@ -164,6 +164,7 @@ export declare class VscodeFrontendSession {
164
164
  */
165
165
  runTurn(prompt: string, onEvent: (event: FrontendEvent) => void, options?: {
166
166
  onCancel?: (cancel: () => void) => void;
167
+ throwIfCancelled?: () => void;
167
168
  }): Promise<FrontendEvent | null>;
168
169
  private require;
169
170
  private notify;
package/dist/session.js CHANGED
@@ -356,11 +356,16 @@ export class VscodeFrontendSession {
356
356
  return action();
357
357
  }
358
358
  }
359
- async submit(prompt) {
359
+ async submit(prompt, throwIfCancelled) {
360
+ throwIfCancelled?.();
360
361
  this.require('submit');
361
362
  if (prompt.length === 0)
362
363
  throw new TypeError('prompt must not be empty');
363
- await this.withControl(() => this.client.sendInput(prompt));
364
+ await this.withControl(() => {
365
+ // The same guard covers a send retried after an awaited lease renewal.
366
+ throwIfCancelled?.();
367
+ return this.client.sendInput(prompt);
368
+ });
364
369
  }
365
370
  async steer(prompt) {
366
371
  this.require('steer');
@@ -474,7 +479,7 @@ export class VscodeFrontendSession {
474
479
  void this.interrupt().catch((error) => fail?.(error));
475
480
  });
476
481
  try {
477
- await this.submit(prompt);
482
+ await this.submit(prompt, options.throwIfCancelled);
478
483
  return await finished;
479
484
  }
480
485
  finally {
package/dist/view.d.ts CHANGED
@@ -170,14 +170,6 @@ export interface ProjectedTurn {
170
170
  /** Rendered parts for an assistant turn. */
171
171
  parts?: ChatPart[];
172
172
  }
173
- /**
174
- * A HOSTED harness runtime's transcript as chat turns.
175
- *
176
- * `HostedHarnessRuntime::attach` returns `history: Vec::new()` and puts the
177
- * whole conversation in `replay` — the canonical ChatMessage history belongs to
178
- * supercode's own agent loop, and a hosted harness has none. So a resumed
179
- * Claude Code session's turns are here or nowhere.
180
- */
181
173
  /**
182
174
  * A "user" message no person typed: Claude Code's `<task-notification>` (a background task reporting back) or the
183
175
  * editor's automatic tripwire nudge. Neither is shown as the person's words. A task notification is said as one
@@ -186,7 +178,7 @@ export interface ProjectedTurn {
186
178
  export declare function automaticPrompt(body: string): {
187
179
  notice?: string;
188
180
  } | undefined;
189
- export declare function projectReplayTurns(events: readonly FrontendEvent[], granted: ReadonlySet<string>, context: ProjectionContext): ProjectedTurn[];
181
+ export declare function projectReplayTurns(events: readonly FrontendEvent[], granted: ReadonlySet<string>, context: ProjectionContext, history?: readonly ProjectedTurn[]): ProjectedTurn[];
190
182
  /**
191
183
  * The attach snapshot's canonical history as chat turns. Bounded: when older
192
184
  * turns are omitted the view says so, the way the browser observer does.
package/dist/view.js CHANGED
@@ -650,14 +650,6 @@ function messageText(message) {
650
650
  .filter(Boolean)
651
651
  .join('\n');
652
652
  }
653
- /**
654
- * A HOSTED harness runtime's transcript as chat turns.
655
- *
656
- * `HostedHarnessRuntime::attach` returns `history: Vec::new()` and puts the
657
- * whole conversation in `replay` — the canonical ChatMessage history belongs to
658
- * supercode's own agent loop, and a hosted harness has none. So a resumed
659
- * Claude Code session's turns are here or nowhere.
660
- */
661
653
  /**
662
654
  * A "user" message no person typed: Claude Code's `<task-notification>` (a background task reporting back) or the
663
655
  * editor's automatic tripwire nudge. Neither is shown as the person's words. A task notification is said as one
@@ -675,8 +667,13 @@ export function automaticPrompt(body) {
675
667
  return {};
676
668
  return undefined;
677
669
  }
678
- export function projectReplayTurns(events, granted, context) {
679
- const turns = [];
670
+ export function projectReplayTurns(events, granted, context, history = []) {
671
+ // Join canonical history with its sequenced tail. Retained request/decision
672
+ // events can be re-sequenced past history_cursor with no originating turn.
673
+ // Before the replay supplies a prompt, give its parts their own runtime
674
+ // history turn rather than attributing old approvals to the latest answer.
675
+ const turns = history.map(turn => ({ ...turn, parts: turn.parts?.slice() }));
676
+ let replayTurn = false;
680
677
  let streamedText;
681
678
  for (const event of events) {
682
679
  if (event.kind === 'user_message') {
@@ -686,8 +683,10 @@ export function projectReplayTurns(events, granted, context) {
686
683
  if (automatic) {
687
684
  if (!automatic.notice)
688
685
  continue;
689
- if (turns.length === 0)
686
+ if (!replayTurn) {
690
687
  turns.push({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
688
+ replayTurn = true;
689
+ }
691
690
  const last = turns[turns.length - 1];
692
691
  const notice = { part: 'markdown', value: `\n\n${automatic.notice}\n\n` };
693
692
  if (last.role === 'assistant' && last.parts)
@@ -697,6 +696,7 @@ export function projectReplayTurns(events, granted, context) {
697
696
  continue;
698
697
  }
699
698
  turns.push({ role: 'user', prompt: clamp(stripControls(text(payload.text)), VSCODE_CHAT_LIMITS.partCharacters) });
699
+ replayTurn = true;
700
700
  continue;
701
701
  }
702
702
  const parts = resolveParts(projectEvent(event, context), granted);
@@ -712,8 +712,10 @@ export function projectReplayTurns(events, granted, context) {
712
712
  else {
713
713
  streamedText = undefined;
714
714
  }
715
- if (turns.length === 0)
715
+ if (!replayTurn) {
716
716
  turns.push({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
717
+ replayTurn = true;
718
+ }
717
719
  const last = turns[turns.length - 1];
718
720
  if (last.role === 'assistant' && last.parts)
719
721
  last.parts.push(...parts);
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.51",
4
+ "version": "0.1.52",
5
5
  "type": "module",
6
6
  "description": "VS Code chat-view frontend for an existing Volter Harness SDK runtime",
7
7
  "publisher": "volter-ai-dev",