@volter/supercode-frontend-vscode 0.1.58 → 0.1.60

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/extension.js CHANGED
@@ -108,8 +108,10 @@ class SupercodeFrontends {
108
108
  const { baseUrl, token } = options;
109
109
  const id = options.connectionId ?? options.clientId ?? `vscode-${randomUUID()}`;
110
110
  const existing = this.#connections.get(id);
111
- if (existing)
112
- return existing;
111
+ if (existing) {
112
+ await existing.controlling;
113
+ return this.#connections.get(id) === existing && existing.session.connection !== 'closed' ? existing : undefined;
114
+ }
113
115
  const permissions = normalizePermissions(options.permissions, ['observe']);
114
116
  const client = createFrontendClient({
115
117
  baseUrl,
@@ -121,9 +123,9 @@ class SupercodeFrontends {
121
123
  // The generated client opens SSE before the snapshot RPC; `attach` is
122
124
  // therefore the whole race-free attachment and `follow` only drains it.
123
125
  await session.attach();
124
- // An interactive attachment holds the runtime's controller lease; without
125
- // it the first submit comes back `lease_expired`.
126
- await session.takeControlAndHold().catch((error) => {
126
+ // Taking control must not hold up observation of an attached snapshot.
127
+ // Actions share this acquisition; connect still awaits it before returning.
128
+ const controlling = session.takeControlAndHold().catch((error) => {
127
129
  log.warn(`Volter Harness could not take control of ${baseUrl}: ${String(error?.message ?? error)}`);
128
130
  });
129
131
  // The runtime's stream can end under a live attachment: an HTTP client's
@@ -135,14 +137,17 @@ class SupercodeFrontends {
135
137
  const stillWanted = () => connection !== undefined && this.#connections.get(id) === connection && session.connection !== 'closed';
136
138
  const following = (async () => {
137
139
  for (let attempt = 0;; attempt += 1) {
140
+ let failure;
138
141
  try {
139
142
  await session.follow();
140
143
  }
141
144
  catch (error) {
145
+ failure = error;
142
146
  log.warn(`Volter Harness event stream ended: ${String(error?.message ?? error)}`);
143
147
  }
144
148
  if (!stillWanted())
145
149
  return;
150
+ session.reportFailure(failure ?? new Error('The conversation event stream ended before this attachment closed. Reconnecting to the runtime; its turn may still be running.'));
146
151
  await new Promise((resolve) => setTimeout(resolve, Math.min(15_000, 500 * 2 ** Math.min(attempt, 5))));
147
152
  if (!stillWanted())
148
153
  return;
@@ -162,6 +167,7 @@ class SupercodeFrontends {
162
167
  baseUrl,
163
168
  session,
164
169
  following,
170
+ controlling,
165
171
  };
166
172
  session.onChange(() => this.#changed.fire());
167
173
  this.#connections.set(id, connection);
@@ -169,6 +175,9 @@ class SupercodeFrontends {
169
175
  this.setUnavailable(undefined);
170
176
  log.info(`Volter Harness attached: ${connection.label} (${baseUrl}) as ${permissions.join(',')}` +
171
177
  ` · ${session.history.length} history messages`);
178
+ await controlling;
179
+ if (this.#connections.get(id) !== connection || session.connection === 'closed')
180
+ return undefined;
172
181
  return connection;
173
182
  }
174
183
  async disconnect(id) {
@@ -383,13 +392,26 @@ export async function activate(context) {
383
392
  // protocol (an ACP agent) keeps asking until a host names it.
384
393
  return ['claude-code', 'codex'].includes(harness) && (!connection || canAnswer(connection)) ? 'autoApprove' : 'ask';
385
394
  };
386
- // What governs a conversation's approvals: the person's choice, else the host's default.
387
- const approvalFor = (id) => permissions.get(id) ?? defaultFor(id);
395
+ // Runtime YOLO is the host's recorded launch policy, never an approval-picker choice.
396
+ const yoloFor = (id) => {
397
+ const session = controls?.state.sessions.find(entry => entry.id === id);
398
+ return !attachedIds.has(id) && session?.runtimePolicy === 'yolo'
399
+ && ['claude-code', 'codex'].includes(session.selection.harness);
400
+ };
401
+ // A saved Ask choice cannot turn prompts back on in a runtime already launched with bypass flags.
402
+ const approvalFor = (id) => {
403
+ if (!yoloFor(id))
404
+ return permissions.get(id) ?? defaultFor(id);
405
+ const connection = frontends.get(id);
406
+ return !connection || canAnswer(connection) ? 'autoApprove' : 'ask';
407
+ };
388
408
  // A picker value becomes the person's choice only when it is one: it differs from the default the picker showed
389
409
  // (`shown`; null when the person already picked it), or replaces an earlier choice. Echoes of the displayed default
390
410
  // (the picker re-applied when a conversation opens) are not saved, and neither is an Auto approve the conversation
391
411
  // cannot answer: the picker offers it only where it can, so there it is a default shown before the harness was known.
392
412
  const choosePermission = (id, value, shown = defaultFor(id)) => {
413
+ if (yoloFor(id))
414
+ return;
393
415
  const connection = frontends.get(id);
394
416
  if (value === 'autoApprove' && connection && !canAnswer(connection))
395
417
  return;
@@ -405,6 +427,11 @@ export async function activate(context) {
405
427
  let focusedResource;
406
428
  let focusGeneration = 0;
407
429
  const activeDraft = () => pendingDraft?.resource && pendingDraft.resource.toString() === focusedResource?.toString() ? pendingDraft : undefined;
430
+ const draftChoiceRefusal = () => {
431
+ const draft = activeDraft();
432
+ return draft && (draft.choosing || !draft.choice || draft.choice.kind === 'cancelled')
433
+ ? 'Choose your coding agent before sending. Your draft is saved; send it after choosing.' : undefined;
434
+ };
408
435
  const draftSelection = () => {
409
436
  const choice = activeDraft()?.choice;
410
437
  return choice?.kind === 'agent' ? choice.selection : undefined;
@@ -624,9 +651,11 @@ export async function activate(context) {
624
651
  provideTokenCount: async () => 0,
625
652
  }));
626
653
  }
654
+ const newChatYolo = () => controls?.state.newChatRuntimePolicy === 'yolo'
655
+ && ['claude-code', 'codex'].includes(draftSelection()?.harness ?? controls.state.selection.harness);
627
656
  const currentOptions = (resource) => {
628
657
  const id = resource ? boundId(resource) : activeDraft() ? undefined : controls?.state.activeSession;
629
- return { permissions: id ? approvalFor(id) : permission ?? newChatDefault() };
658
+ return { permissions: id ? yoloFor(id) ? 'yolo' : approvalFor(id) : newChatYolo() ? 'yolo' : permission ?? newChatDefault() };
630
659
  };
631
660
  const savedConnections = new Map();
632
661
  const saveManagedHistory = async (connection) => {
@@ -658,6 +687,12 @@ export async function activate(context) {
658
687
  return connection;
659
688
  };
660
689
  let setupHandoff;
690
+ let canceledSetupReveal;
691
+ const revealCanceledFor = (basis, connection) => Boolean(canceledSetupReveal
692
+ && canceledSetupReveal.id === basis.id && canceledSetupReveal.baseUrl === basis.baseUrl
693
+ && canceledSetupReveal.generation === basis.generation && canceledSetupReveal.focusResource === basis.focusResource
694
+ && canceledSetupReveal.live === basis.live && canceledSetupReveal.connection.deref() === connection
695
+ && connection.session.connection === 'connected');
661
696
  const setupHandoffComplete = () => {
662
697
  const id = controls?.state.activeSession;
663
698
  const connection = id ? frontends.get(id) : undefined;
@@ -725,6 +760,7 @@ export async function activate(context) {
725
760
  }
726
761
  else {
727
762
  setupHandoff = undefined;
763
+ canceledSetupReveal = undefined;
728
764
  controlsChanged.fire();
729
765
  return { kind: 'setup', generation };
730
766
  }
@@ -745,8 +781,27 @@ export async function activate(context) {
745
781
  if ((controls.state.setup && !controls.state.setup.ready) || controls.state.activeSession !== target.id
746
782
  || controls.state.connection?.SUPERCODE_FRONTEND_URL !== target.baseUrl)
747
783
  return;
748
- if (!setupHandoffComplete())
749
- await revealManaged(target, { resource: target.focusResource });
784
+ if (!setupHandoffComplete()) {
785
+ const connection = frontends.get(target.id);
786
+ const basis = { ...target, live };
787
+ if (!connection || revealCanceledFor(basis, connection))
788
+ return;
789
+ try {
790
+ await revealManaged(target, { resource: target.focusResource });
791
+ canceledSetupReveal = undefined;
792
+ }
793
+ catch (error) {
794
+ // The native focus guard canceled this navigation. Repeating the same request cannot earn new focus;
795
+ // attachment remains real and the reveal remains incomplete. A changed basis or explicit refresh retries.
796
+ if (!(error instanceof vscode.CancellationError)
797
+ && !(error instanceof Error && error.name === 'Canceled' && error.message === 'Canceled'))
798
+ throw error;
799
+ canceledSetupReveal = { ...basis, connection: new WeakRef(connection) };
800
+ log.debug('Automatic Chat reveal canceled for the current conversation and focus.');
801
+ controlsChanged.fire();
802
+ return;
803
+ }
804
+ }
750
805
  if (setupHandoffComplete())
751
806
  welcomeWasVisible = false;
752
807
  }
@@ -795,6 +850,41 @@ export async function activate(context) {
795
850
  throw new vscode.CancellationError();
796
851
  if (action.command !== offered.command)
797
852
  throw new Error('The sign-in command changed. Try again.');
853
+ if (kind === 'install') {
854
+ // A task observes native process exit without closing the terminal that
855
+ // holds its diagnostics. Each click gets its own panel and identity;
856
+ // cancellation terminates only the execution returned for this task.
857
+ const operation = randomUUID();
858
+ const task = new vscode.Task({ type: 'supercode-setup', operation }, vscode.workspace.getWorkspaceFolder(vscode.Uri.file(action.cwd)) ?? vscode.TaskScope.Workspace, `${action.name} · install`, 'supercode', new vscode.ProcessExecution(action.program, action.arguments, { cwd: action.cwd, env: action.env }));
859
+ task.presentationOptions = { reveal: vscode.TaskRevealKind.Always, panel: vscode.TaskPanelKind.New, clear: false, close: false };
860
+ const listeners = [];
861
+ let finished = false;
862
+ let resolveExit;
863
+ const exited = new Promise(resolve => { resolveExit = resolve; });
864
+ const finish = (code) => {
865
+ if (finished)
866
+ return;
867
+ finished = true;
868
+ for (const listener of listeners)
869
+ listener.dispose();
870
+ resolveExit(code);
871
+ };
872
+ const owns = (execution) => execution.task.definition.type === 'supercode-setup'
873
+ && execution.task.definition.operation === operation;
874
+ listeners.push(vscode.tasks.onDidEndTaskProcess(event => { if (owns(event.execution))
875
+ finish(event.exitCode); }), vscode.tasks.onDidEndTask(event => { if (owns(event.execution))
876
+ finish(undefined); }));
877
+ let execution;
878
+ try {
879
+ execution = await vscode.tasks.executeTask(task);
880
+ }
881
+ catch (error) {
882
+ finish(undefined);
883
+ throw error;
884
+ }
885
+ return { exited, cancel: () => { if (!finished)
886
+ execution.terminate(); } };
887
+ }
798
888
  const terminal = vscode.window.createTerminal({ name: `${action.name} · ${kind === 'login' ? 'sign in' : 'install'}`,
799
889
  cwd: action.cwd, shellPath: action.program, shellArgs: action.arguments, env: action.env });
800
890
  const exited = new Promise(resolve => {
@@ -810,7 +900,10 @@ export async function activate(context) {
810
900
  },
811
901
  }) : undefined;
812
902
  const setupState = () => controls ? setupPresentation(controls.state, setupFlow?.operations ?? new Map(), setupFlow?.notice) : null;
813
- context.subscriptions.push({ dispose: () => setupFlow?.dispose() }, vscode.commands.registerCommand('supercode.frontend.refreshSetup', refreshSetup), vscode.commands.registerCommand(SETUP_STATE, setupState), vscode.commands.registerCommand(SETUP_BEGIN, (harness) => {
903
+ context.subscriptions.push({ dispose: () => setupFlow?.dispose() }, vscode.commands.registerCommand('supercode.frontend.refreshSetup', () => {
904
+ canceledSetupReveal = undefined;
905
+ return refreshSetup();
906
+ }), vscode.commands.registerCommand(SETUP_STATE, setupState), vscode.commands.registerCommand(SETUP_BEGIN, (harness) => {
814
907
  if (!setupFlow)
815
908
  throw new Error('This host does not expose sign-in.');
816
909
  return setupFlow.begin(harness);
@@ -1014,7 +1107,7 @@ export async function activate(context) {
1014
1107
  if (draft && draft.choosing === picking)
1015
1108
  draft.choosing = undefined;
1016
1109
  if (draft?.choice?.kind === 'signIn') {
1017
- void refreshSetup().catch(error => log.warn(`Could not refresh sign-in: ${String(error)}`)).finally(pollSetup);
1110
+ void refreshSetup().catch(error => log.warn(`Could not refresh Chat setup: ${String(error)}`)).finally(pollSetup);
1018
1111
  }
1019
1112
  }
1020
1113
  };
@@ -1028,12 +1121,16 @@ export async function activate(context) {
1028
1121
  if (busy)
1029
1122
  throw new Error(busy);
1030
1123
  const draft = activeDraft();
1031
- // the picker ends in a cancellation for a sign-in choice too: the draft's choice says which (sign-in is refused
1032
- // below, in the chat); a draft that went away is the real cancellation
1033
- await draft?.choosing?.catch(() => undefined);
1034
- if (draft && activeDraft() !== draft)
1124
+ // Refuse before native materialization accepts the input. A pending picker
1125
+ // must not hold an invisible send that starts when the choice later ends.
1126
+ const choiceRefusal = draftChoiceRefusal();
1127
+ if (choiceRefusal) {
1128
+ void vscode.window.showInformationMessage(choiceRefusal);
1035
1129
  throw new vscode.CancellationError();
1130
+ }
1036
1131
  await controls.refresh();
1132
+ if (draft && activeDraft() !== draft)
1133
+ throw new vscode.CancellationError();
1037
1134
  controlsChanged.fire();
1038
1135
  const choice = draft?.choice;
1039
1136
  const loginHarness = choice?.kind === 'signIn' ? choice.harness : undefined;
@@ -1051,7 +1148,8 @@ export async function activate(context) {
1051
1148
  // The picker shows the host's default until someone changes it; only a change is the person's choice.
1052
1149
  const selected = inputState.groups.find(group => group.id === 'permissions')?.selected?.id;
1053
1150
  const picked = selected === 'ask' || selected === 'autoApprove' ? selected : permission ?? newChatDefault();
1054
- choosePermission(connection.id, picked, permission !== undefined ? null : newChatDefault());
1151
+ if (!yoloFor(connection.id))
1152
+ choosePermission(connection.id, picked, permission !== undefined ? null : newChatDefault());
1055
1153
  permission = undefined;
1056
1154
  if (pendingDraft === draft) {
1057
1155
  pendingDraft = undefined;
@@ -1074,8 +1172,11 @@ export async function activate(context) {
1074
1172
  // the default harness decides; only with none known does the new-chat default stand in for it
1075
1173
  : controls?.state.selection.harness ? ['claude-code', 'codex'].includes(controls.state.selection.harness) : newChatDefault() === 'autoApprove');
1076
1174
  // An unbound input is a new chat, not the active conversation left behind it.
1077
- const selectedPermission = resource ? currentOptions(resource).permissions : permission ?? newChatDefault();
1078
- 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.' },
1175
+ const isYolo = id ? yoloFor(id) : newChatYolo();
1176
+ const selectedPermission = isYolo ? 'yolo' : resource ? currentOptions(resource).permissions : permission ?? newChatDefault();
1177
+ const permissionItems = isYolo ? [{ id: 'yolo', name: 'YOLO', description: id
1178
+ ? 'This runtime runs without approval prompts. Change the project Chat runtime policy for its next start or resume.'
1179
+ : 'Project policy: start native Claude Code or Codex without approval prompts or the Codex sandbox.' }] : [{ 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.' },
1079
1180
  ...((canApprove || (!active && resource && currentOptions(resource).permissions === 'autoApprove')) ? [{ id: 'autoApprove', name: 'Auto approve', description: 'Approve the requests the harness would ask you about, including requests to run outside its sandbox. Its deny rules still apply.' }] : [])];
1080
1181
  const groups = [{ id: 'permissions', name: 'Permissions', kind: 'permissions', items: permissionItems, selected: permissionItems.find(i => i.id === selectedPermission) ?? permissionItems[0] }];
1081
1182
  const selected = controls?.state.sessions.find(session => session.id === id);
@@ -1094,24 +1195,34 @@ export async function activate(context) {
1094
1195
  },
1095
1196
  setPermission: (resource, value) => {
1096
1197
  const id = boundId(resource);
1198
+ if (value === 'yolo' && !id && newChatYolo())
1199
+ return;
1097
1200
  if (controls && !id)
1098
1201
  throw new Error('This conversation has no session binding.');
1202
+ if (id && yoloFor(id))
1203
+ return;
1099
1204
  if (id)
1100
1205
  choosePermission(id, turnPermission(value));
1101
1206
  else
1102
1207
  permission = turnPermission(value);
1103
1208
  },
1104
- // A send that makes no conversation (another turn runs, or the host or a sign-in refuses it) answers no item: the
1105
- // host then runs the request on the draft itself, and its handler says why in the chat. A rejection here reaches
1106
- // nobody (the host's acceptInput drops it), so every refusal goes that way (#1282 review).
1209
+ // A send that makes no conversation keeps its refusal for the draft handler.
1210
+ // Materialization can stop before that callback, so creation also shows the
1211
+ // failure directly instead of relying on a rejected native callback (#1282).
1107
1212
  create: async (inputState) => {
1213
+ const refusedKey = activeDraft()?.resource.toString() ?? inputState.sessionResource?.toString() ?? '';
1108
1214
  try {
1109
1215
  return await createConversation(inputState);
1110
1216
  }
1111
1217
  catch (error) {
1112
1218
  if (error instanceof vscode.CancellationError)
1113
1219
  throw error;
1114
- refusedSends.set(activeDraft()?.resource.toString() ?? '', String(error?.message ?? error));
1220
+ const reason = String(error?.message ?? error);
1221
+ refusedSends.set(refusedKey, reason);
1222
+ // Materialization can end before the native workbench calls a request
1223
+ // handler. Surface the original launch error on this path as well.
1224
+ log.error(`Chat creation failed: ${reason}`);
1225
+ void vscode.window.showErrorMessage(`Chat could not start: ${reason}. No message was sent.`);
1115
1226
  return undefined;
1116
1227
  }
1117
1228
  },
@@ -1125,6 +1236,10 @@ export async function activate(context) {
1125
1236
  finishIdentitySave: (connection) => finishIdentitySave(connection, true),
1126
1237
  };
1127
1238
  context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.activateSession', async (resource) => { await connectionFor(vscode.Uri.parse(resource)); }));
1239
+ // A paired native host can explain Enter when its model/options precondition
1240
+ // prevents the submit command from reaching session materialization at all.
1241
+ context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.sendRefusal', () => draftChoiceRefusal()
1242
+ ?? (controls?.state.setup?.ready === false ? 'Finish setting up your coding agent before sending. Your draft is saved.' : undefined)));
1128
1243
  // A background task the chat named (sayTasks in runTurnInto), stopped through the harness's own control.
1129
1244
  context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.stopBackgroundTask', async (connectionId, taskId) => {
1130
1245
  const connection = frontends.get(connectionId);
@@ -1165,10 +1280,12 @@ export async function activate(context) {
1165
1280
  // with a new chat's draft open, a permission chosen is the draft's, never the conversation behind it
1166
1281
  if (supplied?.permissions) {
1167
1282
  const id = activeDraft() ? undefined : controls?.state.activeSession;
1168
- if (id)
1169
- choosePermission(id, turnPermission(supplied.permissions));
1170
- else
1171
- permission = turnPermission(supplied.permissions);
1283
+ if (!(id ? yoloFor(id) : supplied.permissions === 'yolo' && newChatYolo())) {
1284
+ if (id)
1285
+ choosePermission(id, turnPermission(supplied.permissions));
1286
+ else
1287
+ permission = turnPermission(supplied.permissions);
1288
+ }
1172
1289
  controlsChanged.fire();
1173
1290
  return currentOptions();
1174
1291
  }
@@ -1192,9 +1309,11 @@ export async function activate(context) {
1192
1309
  ?? controls.state.selection.harness;
1193
1310
  const agents = setupAgents(controls.state);
1194
1311
  const localChoices = agents.map(agent => ({ label: agent.name, id: agent.harness,
1195
- description: agent.harness === 'codex' ? (agent.signedIn ? 'Signed in with ChatGPT' : 'Sign in with ChatGPT…')
1196
- : agent.harness === 'claude-code' ? (agent.signedIn ? 'Signed in with Claude' : 'Sign in with Claude…')
1197
- : controls.state.harnesses.find(h => h.id === agent.harness)?.description,
1312
+ // An agent the host's account signs in (its launch carries that account's route) names that account.
1313
+ description: agent.signedIn && agent.account ? `Signed in with ${agent.account}`
1314
+ : agent.harness === 'codex' ? (agent.signedIn ? 'Signed in with ChatGPT' : 'Sign in with ChatGPT…')
1315
+ : agent.harness === 'claude-code' ? (agent.signedIn ? 'Signed in with Claude' : 'Sign in with Claude…')
1316
+ : controls.state.harnesses.find(h => h.id === agent.harness)?.description,
1198
1317
  login: controls.state.setup?.actions.some(a => a.harness === agent.harness) ?? false,
1199
1318
  autoStart: controls.state.harnesses.some(h => h.id === agent.harness && h.autoStart), configure: false }));
1200
1319
  const modelChoices = localChoices.filter(choice => !choice.login && CONFIGURABLE_HARNESSES.includes(choice.id))
@@ -1444,6 +1563,7 @@ export async function activate(context) {
1444
1563
  sayTasks();
1445
1564
  const unwatchTasks = connection.session.onChange(sayTasks);
1446
1565
  let completed = false;
1566
+ let turnStarted = false;
1447
1567
  try {
1448
1568
  // A new harness transcript is created by input, so binding its identity
1449
1569
  // cannot be a prerequisite for that input. Retry the host save as native
@@ -1453,7 +1573,11 @@ export async function activate(context) {
1453
1573
  // A turn the person stopped is marked stopped by VS Code itself; only an interruption from elsewhere is said.
1454
1574
  if (event.kind === 'turn_interrupted' && token.isCancellationRequested)
1455
1575
  return;
1456
- writeProjected(stream, projectEvent(event, projection), granted, state);
1576
+ // Native submit publishes its initial user echo before `turn_started`.
1577
+ // Subsequent input belongs in the response while this turn is visible.
1578
+ if (event.kind === 'turn_started')
1579
+ turnStarted = true;
1580
+ writeProjected(stream, projectEvent(event, { ...projection, liveUserMessages: turnStarted }), granted, state);
1457
1581
  watch.sweep();
1458
1582
  rememberIdentity(connection);
1459
1583
  }, { throwIfCancelled, onSubmitted: () => {
@@ -1726,15 +1850,26 @@ export async function activate(context) {
1726
1850
  vscode.commands.registerCommand('supercode.frontend.respond', async (answer) => { await answerRequest(answer); }), vscode.commands.registerCommand('supercode.frontend.respondInTurnApproval', (answer) => answerRequest(answer)), vscode.commands.registerCommand('supercode.frontend.status', async (inspect) => {
1727
1851
  if (controls)
1728
1852
  await controls.refresh();
1853
+ const draft = activeDraft();
1854
+ const focusedId = focusedResource ? boundId(focusedResource) : undefined;
1855
+ const focusedSession = controls?.state.sessions.find(session => session.id === focusedId);
1856
+ const selectionSource = draft ? 'draft' : focusedAttached ? 'attachment' : focusedSession ? 'conversation'
1857
+ : focusedResource ? 'unavailable' : 'host';
1858
+ const selection = draft ? draftSelection() : focusedAttached ? undefined : focusedSession?.selection
1859
+ ?? (focusedResource ? undefined : controls?.state.selection);
1729
1860
  if (inspect === true)
1730
1861
  return {
1731
1862
  setupState: setupState(),
1732
1863
  connections: frontends.all.map(c => ({ ...frontends.info(c), permissions: [...c.session.permissions],
1733
1864
  pendingRequests: [...c.session.pendingRequests.values()] })),
1734
- selection: controls?.state.selection, activeSession: controls?.state.activeSession,
1865
+ selection, selectionSource, focusedSession: focusedId ?? null,
1866
+ hostSelection: controls?.state.selection, activeSession: controls?.state.activeSession,
1867
+ focusedSessionResource: focusedResource?.toString() ?? null, focusGeneration,
1735
1868
  busy: Boolean(controls?.state.busy || frontends.all.some(c => c.session.descriptor?.turn_state === 'busy' || c.session.pendingRequests.size)),
1736
1869
  setupHandoff: { hostReady: controls?.state.setup?.ready ?? null, complete: setupHandoffComplete(),
1737
- revealed: setupHandoff ?? null },
1870
+ revealed: setupHandoff ?? null,
1871
+ lastCanceledReveal: canceledSetupReveal ? { id: canceledSetupReveal.id, baseUrl: canceledSetupReveal.baseUrl,
1872
+ generation: canceledSetupReveal.generation, focusResource: canceledSetupReveal.focusResource, live: canceledSetupReveal.live } : null },
1738
1873
  sessions: controls?.state.sessions, permission: currentOptions().permissions, identity: identity(),
1739
1874
  };
1740
1875
  const rows = frontends.all.map((connection) => {
@@ -1886,25 +2021,40 @@ export async function activate(context) {
1886
2021
  // panel on "Loading…" — observed.
1887
2022
  const opening = setTimeout(() => void openResumedSession().catch(error => log.warn(`Could not reveal Chat: ${String(error)}`)), 500);
1888
2023
  context.subscriptions.push({ dispose: () => clearTimeout(opening) });
1889
- // Retry until host readiness has completed both attachment AND native reveal.
1890
- // A transient failure in either step leaves the handoff pending, even if ready=true.
2024
+ // Retry attachment and reveal failures. A native navigation cancellation waits for changed focus/target/liveness
2025
+ // or an explicit refresh; it neither earns a completed handoff nor repeats the same veto every three seconds.
1891
2026
  let setupTimer;
1892
2027
  let setupDisposed = false;
1893
2028
  const pollSetup = () => {
2029
+ if (!controls || setupDisposed) {
2030
+ clearTimeout(setupTimer);
2031
+ setupTimer = undefined;
2032
+ return;
2033
+ }
1894
2034
  const choice = activeDraft()?.choice;
1895
2035
  const awaitingLogin = (choice?.kind === 'signIn' && choice.harness && !setupAgents(controls.state).some(h => h.harness === choice.harness && h.signedIn))
1896
2036
  || [...(setupFlow?.operations.values() ?? [])].some(operation => operation.phase === 'installing' || operation.phase === 'signing-in');
1897
2037
  if ([...(setupFlow?.operations.values() ?? [])].some(operation => operation.phase === 'installing' || operation.phase === 'signing-in'))
1898
2038
  return;
1899
- if (setupTimer || setupDisposed || !controls?.state.setup || (controls.state.setup.ready && setupHandoffComplete() && !awaitingLogin))
2039
+ const connection = controls.state.activeSession ? frontends.get(controls.state.activeSession) : undefined;
2040
+ const canceled = connection && revealCanceledFor({ id: connection.id,
2041
+ baseUrl: controls.state.connection?.SUPERCODE_FRONTEND_URL ?? '', generation: focusGeneration,
2042
+ focusResource: focusedResource?.toString() ?? null,
2043
+ live: Boolean(connection.session.pendingRequests.size || connection.session.descriptor?.turn_state === 'busy') }, connection);
2044
+ if (controls.state.setup?.ready && !awaitingLogin && (setupHandoffComplete() || canceled)) {
2045
+ clearTimeout(setupTimer);
2046
+ setupTimer = undefined;
2047
+ return;
2048
+ }
2049
+ if (setupTimer || !controls.state.setup)
1900
2050
  return;
1901
2051
  setupTimer = setTimeout(() => {
1902
2052
  setupTimer = undefined;
1903
- void refreshSetup().catch(error => log.warn(`Could not refresh sign-in: ${String(error)}`)).finally(pollSetup);
2053
+ void refreshSetup().catch(error => log.warn(`Could not refresh Chat setup: ${String(error)}`)).finally(pollSetup);
1904
2054
  }, 3000);
1905
2055
  };
1906
2056
  pollSetup();
1907
- context.subscriptions.push(controlsChanged.event(pollSetup));
2057
+ context.subscriptions.push(controlsChanged.event(pollSetup), frontends.onDidChange(pollSetup));
1908
2058
  context.subscriptions.push({ dispose: () => { setupDisposed = true; clearTimeout(setupTimer); } });
1909
2059
  return {
1910
2060
  connect: async (options) => {
@@ -1943,7 +2093,7 @@ function oneRowPerToolCall(parts) {
1943
2093
  rows[at] = {
1944
2094
  ...first,
1945
2095
  ...part,
1946
- pastTenseMessage: first.invocationMessage ?? part.pastTenseMessage,
2096
+ pastTenseMessage: first.pastTenseMessage || part.pastTenseMessage,
1947
2097
  invocationMessage: first.invocationMessage ?? part.invocationMessage,
1948
2098
  };
1949
2099
  continue;
@@ -2024,11 +2174,12 @@ function historyText(part) {
2024
2174
  }
2025
2175
  function registerSessions(context, frontends, participant, granted, handler, historyParticipant, controls) {
2026
2176
  const turns = vscode;
2177
+ const failedResources = new Map();
2027
2178
  const items = () => {
2028
2179
  return (controls?.catalogue() ?? []).filter(entry => !frontends.get(entry.id)).map(entry => {
2029
2180
  return { resource: vscode.Uri.from({ scheme: SESSION_SCHEME, authority: entry.id, path: '/conversation' }), label: entry.title,
2030
2181
  description: runtimeIdentity(entry.selection.harness, entry.selection.model, controls?.effective(entry)).name,
2031
- status: vscode.ChatSessionStatus.Completed, timing: { created: entry.created } };
2182
+ status: failedResources.has(entry.id) ? vscode.ChatSessionStatus.Failed : vscode.ChatSessionStatus.Completed, timing: { created: entry.created } };
2032
2183
  });
2033
2184
  };
2034
2185
  const connectedRow = (connection) => ({
@@ -2127,13 +2278,38 @@ function registerSessions(context, frontends, participant, granted, handler, his
2127
2278
  if (token.isCancellationRequested)
2128
2279
  throw new vscode.CancellationError();
2129
2280
  const boundHandler = (request, chatContext, stream, token) => handler(request, { ...chatContext, chatSessionContext: { chatSessionItem: { resource, label: 'Conversation' } } }, stream, token);
2281
+ const failedContent = (error) => {
2282
+ const message = opaqueText(String(error?.message ?? error), 2048);
2283
+ if (failedResources.get(resource.authority) !== message) {
2284
+ failedResources.set(resource.authority, message);
2285
+ refreshItems();
2286
+ }
2287
+ return {
2288
+ history: [
2289
+ new turns.ChatRequestTurn(HISTORY_MARKER_PROMPT, undefined, [], 'supercode', []),
2290
+ new turns.ChatResponseTurn([new vscode.ChatResponseMarkdownPart(`**Conversation recovery failed**\n\n${message}`)], {}, 'supercode'),
2291
+ ],
2292
+ requestHandler: boundHandler,
2293
+ };
2294
+ };
2130
2295
  // Only a native untitled resource can create a conversation. Existing
2131
2296
  // resources must resolve their exact saved binding; there is no fallback.
2132
- const connection = controls ? (resource.path.startsWith('/untitled-') && !controls.bindings(resource) ? undefined : await controls.resolve(resource, token)) : frontends.get(resource.authority);
2297
+ let connection;
2298
+ try {
2299
+ connection = controls ? (resource.path.startsWith('/untitled-') && !controls.bindings(resource) ? undefined : await controls.resolve(resource, token)) : frontends.get(resource.authority);
2300
+ }
2301
+ catch (error) {
2302
+ if (token.isCancellationRequested || error instanceof vscode.CancellationError)
2303
+ throw error;
2304
+ return failedContent(error);
2305
+ }
2306
+ failedResources.delete(resource.authority);
2133
2307
  if (token.isCancellationRequested)
2134
2308
  throw new vscode.CancellationError();
2135
2309
  if (!connection) {
2136
- return { history: [], requestHandler: boundHandler };
2310
+ const draft = resource.path.startsWith('/untitled-') && !controls?.bindings(resource);
2311
+ return draft ? { history: [], requestHandler: boundHandler }
2312
+ : failedContent(frontends.unavailable ?? 'Could not attach to this saved conversation\'s runtime.');
2137
2313
  }
2138
2314
  connection.viewReady = false;
2139
2315
  connection.viewResource = resource.toString();
@@ -2146,7 +2322,8 @@ function registerSessions(context, frontends, participant, granted, handler, his
2146
2322
  // `state.history`), so the copy taken when this window attached is
2147
2323
  // stale as soon as a turn runs. The generated client reuses the
2148
2324
  // already-open event stream, so this is a snapshot, not a reconnect.
2149
- const attached = frontends.get(connection.id) === connection && await connection.session.attach().then(() => true).catch(() => {
2325
+ const attached = frontends.get(connection.id) === connection && await connection.session.attach().then(() => true).catch(error => {
2326
+ connection.session.reportFailure(error);
2150
2327
  // An unreachable runtime replays what this window last saw.
2151
2328
  return false;
2152
2329
  });
@@ -2160,7 +2337,10 @@ function registerSessions(context, frontends, participant, granted, handler, his
2160
2337
  // created. Keep it only as a fallback when no runtime user history is
2161
2338
  // available, rather than trying to align equal-looking prompt text.
2162
2339
  const runtimeHistory = connection.session.history;
2163
- const replay = connection.session.replay.filter(event => event.sequence > connection.session.historyCursor);
2340
+ const replay = [
2341
+ ...connection.session.historyEvents,
2342
+ ...connection.session.replay.filter(event => event.sequence > connection.session.historyCursor),
2343
+ ];
2164
2344
  const hasRuntimeHistory = runtimeHistory.length > 0 || replay.some(event => event.kind === 'user_message');
2165
2345
  const usingSavedHistory = !hasRuntimeHistory && Boolean(connection.persistedHistory?.length);
2166
2346
  const canonicalHistory = usingSavedHistory ? connection.persistedHistory : runtimeHistory;
@@ -2217,6 +2397,8 @@ function registerSessions(context, frontends, participant, granted, handler, his
2217
2397
  }
2218
2398
  if (usingSavedHistory && connection.historyTruncated)
2219
2399
  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));
2400
+ if (connection.session.historyEventsDropped > 0)
2401
+ history.push(new turns.ChatRequestTurn(HISTORY_MARKER_PROMPT, undefined, [], authorId, []), new turns.ChatResponseTurn([new vscode.ChatResponseMarkdownPart(`_${connection.session.historyEventsDropped} earlier runtime events are outside this display snapshot._`)], {}, authorId));
2220
2402
  for (const turn of projected) {
2221
2403
  if (turn.role === 'user') {
2222
2404
  history.push(new turns.ChatRequestTurn(turn.prompt ?? '', undefined, [], authorId, []));
@@ -2224,6 +2406,11 @@ function registerSessions(context, frontends, participant, granted, handler, his
2224
2406
  }
2225
2407
  history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {}, authorId));
2226
2408
  }
2409
+ if (connection.session.failure) {
2410
+ // An explicit runtime marker pairs this notice without presenting
2411
+ // it as a person's prompt. The native busy state stays unchanged.
2412
+ history.push(new turns.ChatRequestTurn(HISTORY_MARKER_PROMPT, undefined, [], authorId, []), new turns.ChatResponseTurn([new vscode.ChatResponseMarkdownPart(`**Conversation recovery failed**\n\n${opaqueText(connection.session.failure.message, 2048)}`)], {}, authorId));
2413
+ }
2227
2414
  const renderedCursor = connection.session.cursor;
2228
2415
  const pendingAtCursor = [...connection.session.pendingRequests.values()];
2229
2416
  if (!live)
@@ -2252,6 +2439,7 @@ function registerSessions(context, frontends, participant, granted, handler, his
2252
2439
  await observeActiveResponse(connection.session, token, (event) => {
2253
2440
  writeProjected(stream, projectEvent(event, {
2254
2441
  connectionId: connection.id,
2442
+ liveUserMessages: true,
2255
2443
  pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
2256
2444
  canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
2257
2445
  }), granted, state);
@@ -2259,6 +2447,11 @@ function registerSessions(context, frontends, participant, granted, handler, his
2259
2447
  controls?.rememberIdentity(connection);
2260
2448
  }, () => { connection.viewReady = true; }, renderedCursor);
2261
2449
  }
2450
+ catch (error) {
2451
+ if (token.isCancellationRequested || error instanceof vscode.CancellationError)
2452
+ throw error;
2453
+ stream.markdown(`\n\n**Conversation stream failed**\n\n${opaqueText(String(error?.message ?? error), 2048)}\n\n`);
2454
+ }
2262
2455
  finally {
2263
2456
  connection.viewReady = connection.session.descriptor?.turn_state !== 'busy' && !connection.session.pendingRequests.size;
2264
2457
  watch?.dispose();
@@ -27,6 +27,7 @@ export interface ManagedChatSession {
27
27
  title: string;
28
28
  created: number;
29
29
  defaultPermission?: 'ask' | 'autoApprove';
30
+ runtimePolicy?: 'default' | 'yolo';
30
31
  }
31
32
  /** Host-owned operations. The host revalidates commands before native terminal execution. */
32
33
  export interface ChatSetupAction {
@@ -75,6 +76,9 @@ export interface HostState {
75
76
  /** The mode a new chat's picker shows. A conversation's own default is its `ManagedChatSession.defaultPermission`;
76
77
  * this is never applied to one that exists. `autoApprove` takes effect only where the runtime can answer approvals. */
77
78
  newChatPermission?: 'ask' | 'autoApprove';
79
+ /** Project policy for the next start/resume; an existing runtime uses its recorded session policy. */
80
+ newChatRuntimePolicy?: 'default' | 'yolo' | null;
81
+ runtimePolicyError?: string | null;
78
82
  }
79
83
  export interface HostSelectionResult extends HostState {
80
84
  connection: Record<string, string>;
@@ -70,9 +70,12 @@ export function runtimeIdentity(harness, model, effective) {
70
70
  const harnessName = harnessDisplayName(harness);
71
71
  const knownModel = effective?.model || (model && model !== harness ? model : '');
72
72
  const modelName = effective?.modelName || { sonnet: 'Claude Sonnet', opus: 'Claude Opus', haiku: 'Claude Haiku' }[knownModel] || knownModel;
73
+ // The provider of a display name ("Claude Opus 4.8"), whose mark is media/<provider>.svg (supercode-ui's catalogue);
74
+ // the catalogue's modelProvider reads model ids.
73
75
  const provider = /claude|sonnet|opus|haiku/i.test(modelName) || (!modelName && harness === 'claude-code') ? 'anthropic'
74
76
  : /^(gpt|o[134]|chatgpt)|\/gpt/i.test(modelName) || (!modelName && harness === 'codex') ? 'openai'
75
- : /gemini/i.test(modelName) || (!modelName && harness === 'gemini') ? 'google' : undefined;
77
+ : /gemini/i.test(modelName) || (!modelName && harness === 'gemini') ? 'google'
78
+ : /grok/i.test(modelName) || (!modelName && harness === 'grok') ? 'xai' : undefined;
76
79
  const effort = effective?.effort ? ` · ${effective.effort}` : '';
77
80
  return { name: modelName ? `${modelName}${effort}` : `Model chosen by ${harnessName}`, provider };
78
81
  }
@@ -1,6 +1,6 @@
1
1
  import type { CancellationToken } from 'vscode';
2
2
  import type { FrontendEvent } from '../vendor/supercode-frontend/generated.mjs';
3
3
  import { type VscodeFrontendSession } from './session.js';
4
- export declare function sessionStatus(session: Pick<VscodeFrontendSession, 'descriptor' | 'pendingRequests'>): 'NeedsInput' | 'InProgress' | 'Completed';
4
+ export declare function sessionStatus(session: Pick<VscodeFrontendSession, 'descriptor' | 'pendingRequests' | 'failure'>): 'NeedsInput' | 'Failed' | 'InProgress' | 'Completed';
5
5
  /** Follow a restored turn, including the interval between snapshot and callback. */
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>;
6
+ export declare function observeActiveResponse(session: Pick<VscodeFrontendSession, 'descriptor' | 'pendingRequests' | 'onEvent' | 'onChange' | 'onFailure' | 'interrupt' | 'actions' | 'replay'>, token: CancellationToken, write: (event: FrontendEvent) => void, onSubscribed?: () => void, afterSequence?: number): Promise<void>;