@volter/supercode-frontend-vscode 0.1.51 → 0.1.53

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'];
@@ -979,7 +979,9 @@ export async function activate(context) {
979
979
  // A new draft's harness, not the previous runtime, owns its offered modes.
980
980
  const harness = resource ? controls?.state.sessions.find(session => session.id === id)?.selection.harness : draftSelection()?.harness;
981
981
  const canApprove = resource ? (active ? canAnswer(active) : ['claude-code', 'codex'].includes(harness ?? ''))
982
- : (harness ? ['claude-code', 'codex'].includes(harness) : newChatDefault() === 'autoApprove' || ['claude-code', 'codex'].includes(controls?.state.selection.harness ?? ''));
982
+ : (harness ? ['claude-code', 'codex'].includes(harness)
983
+ // the default harness decides; only with none known does the new-chat default stand in for it
984
+ : controls?.state.selection.harness ? ['claude-code', 'codex'].includes(controls.state.selection.harness) : newChatDefault() === 'autoApprove');
983
985
  // An unbound input is a new chat, not the active conversation left behind it.
984
986
  const selectedPermission = resource ? currentOptions(resource).permissions : permission ?? newChatDefault();
985
987
  const permissionItems = [{ id: 'ask', name: canApprove ? 'Ask for approval' : 'Harness permissions', description: canApprove ? 'Show each approval requested by the harness.' : 'Approval controls are unavailable for this harness.' },
@@ -1067,8 +1069,9 @@ export async function activate(context) {
1067
1069
  await revealManaged();
1068
1070
  }));
1069
1071
  context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.configure', async (supplied) => {
1072
+ // with a new chat's draft open, a permission chosen is the draft's, never the conversation behind it
1070
1073
  if (supplied?.permissions) {
1071
- const id = controls?.state.activeSession;
1074
+ const id = activeDraft() ? undefined : controls?.state.activeSession;
1072
1075
  if (id)
1073
1076
  choosePermission(id, turnPermission(supplied.permissions));
1074
1077
  else
@@ -1295,6 +1298,11 @@ export async function activate(context) {
1295
1298
  } };
1296
1299
  };
1297
1300
  const runTurnInto = async (connection, prompt, stream, token, approval) => {
1301
+ const throwIfCancelled = () => {
1302
+ if (token.isCancellationRequested)
1303
+ throw new vscode.CancellationError();
1304
+ };
1305
+ throwIfCancelled();
1298
1306
  if (approval() === 'autoApprove' && !canAnswer(connection)) {
1299
1307
  throw new Error('This harness does not support automatic approval through Chat. Select Harness permissions.');
1300
1308
  }
@@ -1335,13 +1343,20 @@ export async function activate(context) {
1335
1343
  const unwatchTasks = connection.session.onChange(sayTasks);
1336
1344
  let completed = false;
1337
1345
  try {
1346
+ // Observe Stop throughout the save, then check the token before input.
1347
+ // A cancellation while this idle runtime saves cannot be handled by an
1348
+ // interrupt alone: there is no running turn to interrupt yet.
1349
+ if (controls && !attachedIds.has(connection.id) && connection.session.descriptor?.session_id) {
1350
+ await controls.remember(connection.id, connection.session.descriptor.session_id);
1351
+ }
1352
+ throwIfCancelled();
1338
1353
  await connection.session.runTurn(prompt, (event) => {
1339
1354
  // A turn the person stopped is marked stopped by VS Code itself; only an interruption from elsewhere is said.
1340
1355
  if (event.kind === 'turn_interrupted' && token.isCancellationRequested)
1341
1356
  return;
1342
1357
  writeProjected(stream, projectEvent(event, projection), granted, state);
1343
1358
  watch.sweep();
1344
- });
1359
+ }, { throwIfCancelled });
1345
1360
  completed = true;
1346
1361
  }
1347
1362
  finally {
@@ -2026,12 +2041,16 @@ function registerSessions(context, frontends, participant, granted, handler, his
2026
2041
  // canonical transcript, replayed as turns.
2027
2042
  const history = [];
2028
2043
  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;
2044
+ // History and replay are one runtime snapshot, joined by its cursor.
2045
+ // A host's separately loaded native transcript has no replay cursor;
2046
+ // selecting it instead hid every turn started since the runtime was
2047
+ // created. Keep it only as a fallback when no runtime user history is
2048
+ // available, rather than trying to align equal-looking prompt text.
2049
+ const runtimeHistory = connection.session.history;
2050
+ const replay = connection.session.replay.filter(event => event.sequence > connection.session.historyCursor);
2051
+ const hasRuntimeHistory = runtimeHistory.length > 0 || replay.some(event => event.kind === 'user_message');
2052
+ const usingSavedHistory = !hasRuntimeHistory && Boolean(connection.persistedHistory?.length);
2053
+ const canonicalHistory = usingSavedHistory ? connection.persistedHistory : runtimeHistory;
2035
2054
  const pendingToolNames = new Set();
2036
2055
  if (connection.session.descriptor?.turn_state === 'busy') {
2037
2056
  for (const request of connection.session.pendingRequests.values()) {
@@ -2043,14 +2062,44 @@ function registerSessions(context, frontends, participant, granted, handler, his
2043
2062
  pendingToolNames.add(name);
2044
2063
  }
2045
2064
  }
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)
2065
+ const projected = projectReplayTurns(replay.filter(event => {
2066
+ if (event.kind !== 'request')
2067
+ return true;
2068
+ const payload = event.payload;
2069
+ return !payload?.request || !connection.session.pendingRequests.has(payload.request.id);
2070
+ }), granted, {
2071
+ connectionId: connection.id,
2072
+ pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
2073
+ canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
2074
+ }, projectHistory(canonicalHistory, granted, pendingToolNames));
2075
+ const live = connection.session.descriptor?.turn_state === 'busy' || connection.session.pendingRequests.size > 0;
2076
+ const activeParts = [];
2077
+ if (live) {
2078
+ // Standalone requests have no user turn. Give their live response
2079
+ // an explicit runtime marker instead of reopening an old answer.
2080
+ let lastBoundary = -1;
2081
+ let lastUser = -1;
2082
+ for (const event of replay) {
2083
+ if (['turn_succeeded', 'turn_failed', 'turn_interrupted'].includes(event.kind))
2084
+ lastBoundary = event.sequence;
2085
+ if (event.kind === 'user_message')
2086
+ lastUser = event.sequence;
2087
+ }
2088
+ if (!projected.some(turn => turn.role === 'user')
2089
+ || (connection.session.descriptor?.turn_state !== 'busy' && lastUser <= lastBoundary)) {
2090
+ projected.push({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
2091
+ }
2092
+ // Leave the final request without a historical response, so the
2093
+ // callback creates its response from live progress. Rehydrated
2094
+ // responses can be stale; their command buttons are disabled.
2095
+ const last = projected.at(-1);
2096
+ if (last?.role === 'assistant') {
2097
+ activeParts.push(...(projected.pop()?.parts ?? []).map(part => part.part === 'answers'
2098
+ ? { part: 'markdown', value: `**${part.title}**\n\n${part.message}` }
2099
+ : part));
2100
+ }
2101
+ }
2102
+ if (usingSavedHistory && connection.historyTruncated)
2054
2103
  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
2104
  for (const turn of projected) {
2056
2105
  if (turn.role === 'user') {
@@ -2059,16 +2108,16 @@ function registerSessions(context, frontends, participant, granted, handler, his
2059
2108
  }
2060
2109
  history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {}, authorId));
2061
2110
  }
2062
- const busy = connection.session.descriptor?.turn_state === 'busy';
2063
2111
  const renderedCursor = connection.session.cursor;
2064
- if (!busy)
2112
+ const pendingAtCursor = [...connection.session.pendingRequests.values()];
2113
+ if (!live)
2065
2114
  connection.viewReady = attached;
2066
2115
  return {
2067
2116
  title: connection.label,
2068
2117
  options: controls?.attached(connection) ? undefined : controls?.currentOptions(resource),
2069
2118
  history,
2070
2119
  requestHandler: boundHandler,
2071
- activeResponseCallback: busy
2120
+ activeResponseCallback: live
2072
2121
  ? async (stream, token) => {
2073
2122
  // A turn re-joined after a reload has lost the handler that ran it, so its approvals are
2074
2123
  // answered here under the conversation's mode (t_39dac582): Auto approve answers the cards
@@ -2076,22 +2125,24 @@ function registerSessions(context, frontends, participant, granted, handler, his
2076
2125
  const watch = controls && !controls.attached(connection) ? controls.watchApprovals(connection, stream, token) : undefined;
2077
2126
  try {
2078
2127
  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(), {
2128
+ for (const part of oneRowPerToolCall(activeParts))
2129
+ writePart(stream, part, state);
2130
+ writeProjected(stream, projectPendingRequests(pendingAtCursor.filter(request => connection.session.pendingRequests.has(request.id)), {
2080
2131
  connectionId: connection.id,
2081
- canRespond: connection.session.descriptor?.actions.respond !== false,
2132
+ canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
2082
2133
  }), granted, state);
2083
2134
  watch?.sweep();
2084
2135
  await observeActiveResponse(connection.session, token, (event) => {
2085
2136
  writeProjected(stream, projectEvent(event, {
2086
2137
  connectionId: connection.id,
2087
2138
  pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
2088
- canRespond: connection.session.descriptor?.actions.respond !== false,
2139
+ canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
2089
2140
  }), granted, state);
2090
2141
  watch?.sweep();
2091
2142
  }, () => { connection.viewReady = true; }, renderedCursor);
2092
2143
  }
2093
2144
  finally {
2094
- connection.viewReady = connection.session.descriptor?.turn_state !== 'busy';
2145
+ connection.viewReady = connection.session.descriptor?.turn_state !== 'busy' && !connection.session.pendingRequests.size;
2095
2146
  watch?.dispose();
2096
2147
  }
2097
2148
  }
@@ -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.53",
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",