@volter/supercode-frontend-vscode 0.1.16 → 0.1.18

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
@@ -11,6 +11,88 @@ package or npm.
11
11
 
12
12
  ## Native chat controls
13
13
 
14
+ The optional host-controls state can include `setup: { ready, actions, reason,
15
+ cwd }`. Each action supplies `kind` (`login` or `install`), `harness`, `name` and
16
+ the exact `command` shown before execution. Chat renders native buttons, with
17
+ **Sign in with ChatGPT** for Codex and the reported name for other harnesses.
18
+ Clicking asks the host's authenticated `/setup` door to revalidate `{ kind,
19
+ harness }` and return the action plus a structured `{ program, arguments, cwd, env? }`
20
+ terminal launch. Absolute executables and an optional host-authored PATH keep
21
+ the terminal on the same installation and discovery path as the host's probe.
22
+ It runs directly as the integrated terminal's process, without typing a shell
23
+ command; credentials and
24
+ browser interaction belong to the harness. Failed or cancelled processes can be
25
+ retried, and closing a terminal never counts as successful authentication.
26
+
27
+ While setup is not ready, Chat refreshes `/state` every three seconds and exposes
28
+ **Check again**. When the host returns a ready runtime connection, it attaches
29
+ and reveals that conversation without a reload. Polling only finishes after both
30
+ attachment and the host's native reveal command succeed for that same session
31
+ and runtime URL. A transient attach/reveal failure stays pending and is retried;
32
+ **Check again** retries that handoff even when the host already reports ready.
33
+ Changed setup resources refresh
34
+ Code-OSS's cached status history after an install. Hosts without these optional
35
+ fields keep the existing runtime-attachment behavior.
36
+
37
+ `starterPrompts?: {label: string; prompt: string}[]` is project data supplied by
38
+ the host. An eligible new conversation exposes at most three short command links
39
+ in the default participant's native welcome content, outside the transcript.
40
+ The host's `fillInputCommand` receives the exact native session resource and full
41
+ prompt and must return `{sessionResource, input}` after filling that existing
42
+ widget, without submitting. A missing or mismatched receipt is an error.
43
+ The untargeted partial-query command is not used. Links never send or write a turn.
44
+ Presentation and clicks use the same `starterEligibility` calculation; runtime
45
+ and host state changes update the welcome message, and a user turn clears it.
46
+ Native Chat also hides its welcome surface immediately when a request is added.
47
+ Eligibility requires a successful attachment, an idle runtime, no pending request,
48
+ no saved/canonical non-system messages or truncated saved history, and no observed
49
+ user turn. Accepted-input events (`user_message`, `turn_started`) and busy snapshots
50
+ record turn evidence that survives re-attachment and replay eviction. Setup/status
51
+ events can advance cursors without disqualifying a new conversation; system-only
52
+ history also stays eligible. The checks run again after
53
+ project data loads and before filling; a failed attachment never offers starters.
54
+ `supercode.frontend.status(true)` includes each connection's `starterEligibility`:
55
+ attachment state, turn state, pending count, saved-history presence/truncation/count
56
+ and roles, canonical count/roles, sticky turn evidence, both cursors, replay count
57
+ and kinds, active host ID, host busy state, connection identity check, and rejection
58
+ reasons. `lastStarterAttempt` preserves the exact post-attachment guard snapshot
59
+ and native binding from the latest click. These contain no message bodies or tokens.
60
+ A rejected click names its failed conditions instead of a generic new/idle error.
61
+ The pinned Chat session API exposes immutable history, without withdrawal or
62
+ live enablement of a rendered command part. Starters therefore never enter history.
63
+ `defaultChatParticipant.additionalWelcomeMessage` supplies trusted Markdown with
64
+ only `supercode.frontend.fillStarter` allowed; labels are escaped and command
65
+ arguments are encoded. Hosts without that proposal get no starter offer rather
66
+ than permanent transcript buttons. Existing setup actions keep their native status
67
+ presentation. No new workbench API or editor-side contribution is required.
68
+ The pinned Code-OSS `f16dc165` has no `suggestedPrompts` field in either
69
+ `vscode.proposed.defaultChatParticipant.d.ts` or the internal
70
+ `IChatViewWelcomeContent`. `ChatViewWelcomePart` renders `additionalMessage` as
71
+ Markdown; its optional `firstLinkToButton` converts only one anchor and the
72
+ participant welcome never enables that option. The editor's native overlay can
73
+ render standalone trusted command links as welcome buttons without a new extension
74
+ API; other hosts keep Markdown links. The two introductory lines live together in
75
+ this extension's `package.json`: `chatSessions.welcomeMessage` and
76
+ `supercodeFrontendWelcome.starterIntroduction`. Hosts own their project prompt data.
77
+ If `starterPromptsPending` is true, the project configuration is still loading.
78
+ Empty-conversation resolution refreshes the host until that declaration arrives
79
+ (cancellable, with a 30-second timeout), so native Chat does not cache an empty
80
+ offer before the project adapter loads.
81
+
82
+ W32/W37 passed starter filling and W37 passed healthy refresh with draft retention.
83
+ W44 passed a real boost turn and exposed a presentation defect: transcript starter
84
+ buttons remained after the turn even though the guard rejected them. W50 passed
85
+ the native welcome fix at `29d8fac99`: all three pointer fills, one real HUD turn,
86
+ first-turn disappearance, return-to-idle suppression, and no starters when reopening
87
+ the earlier conversation with seven history entries. The shorter introductory copy
88
+ is source-only; it needs a frontend build plus a workbench overlay copy.
89
+ The accompanying native button and New Session repairs require a Code-OSS compile.
90
+ W50 also found that the generic native New Session action
91
+ bypasses this provider's creation command; its owning host must route that action
92
+ through `chatSessions/newSession` rather than opening an unbound untitled resource.
93
+ Signed-out/install transitions and failed-discovery/attachment recovery remain
94
+ unverified live. A published frontend and rebuilt workbench are needed for npx users.
95
+
14
96
  A bundling host can expose a private lifecycle endpoint through
15
97
  `SUPERCODE_FRONTEND_HOST_URL` and `SUPERCODE_FRONTEND_HOST_CREDENTIAL_FILE`.
16
98
  The endpoint must be loopback HTTP and the credential follows the runtime credential-file
@@ -20,11 +102,25 @@ and returns its new scoped connection. The host owns validation, process lifetim
20
102
  persistence, and failure handling. Prompts and approvals still use the generated frontend client.
21
103
  The contract is in `src/host-controls.ts`.
22
104
 
23
- With that host, **Chat: New Chat: Choose Harness, Model and Reasoning** chooses
24
- configuration before opening a separate conversation. A conversation's harness, model and
25
- reasoning settings remain fixed: this integration does not advertise hot swapping. Native
26
- Chat displays its selected model and provider icon inside the composer and its supported
27
- approval policy below it. The response author identifies the harness.
105
+ With that host, **New Session** / **Chat: New Chat: Choose Agent** asks only
106
+ which startable agent to use. It refreshes the host's available choices first;
107
+ with one agent it opens immediately, and with none it returns to setup. Model
108
+ and reasoning are sent as empty values, leaving both at the harness's own defaults,
109
+ even if the previous conversation used explicit settings. Cancelling the agent
110
+ picker creates nothing.
111
+
112
+ The composer's native **Model settings** control offers **Choose model and reasoning
113
+ for a new chat…** for Claude Code and Codex. This optional path retains the model
114
+ list, exact model ID entry and reasoning choices. Only completing the choices
115
+ starts a separate conversation; cancelling either leaves the current one intact.
116
+ A conversation's harness, model and reasoning remain fixed: this integration does
117
+ not advertise hot swapping. The built-in model picker still displays the selected
118
+ model and provider icon; nondelegating session providers cannot add its Manage
119
+ Models action, so configuration uses the native session-options command door.
120
+ The response author identifies the harness. Other harnesses keep their own defaults.
121
+
122
+ This simplified creation flow is source-only pending frontend compilation and a
123
+ live check of zero/one picker, cancellation, defaults and explicit configuration.
28
124
 
29
125
  The host publishes durable conversation IDs and their exact harness history identities.
30
126
  `POST /open` resumes only the requested saved conversation; `POST /remember` binds its
@@ -0,0 +1,6 @@
1
+ /** Native Chat presentation; the host supplies inventory, terminal actions and project data. */
2
+ import * as vscode from 'vscode';
3
+ import type { ChatSetupState, ChatStarterPrompt } from './host-controls.js';
4
+ export declare function setupParts(setup: ChatSetupState): Array<vscode.ChatResponseMarkdownPart | vscode.ChatResponseCommandButtonPart>;
5
+ /** Welcome content is owned by the empty-state renderer, never transcript history. */
6
+ export declare function starterWelcome(prompts: readonly ChatStarterPrompt[], session: string, resource: string, introduction: string): vscode.MarkdownString;
@@ -0,0 +1,29 @@
1
+ /** Native Chat presentation; the host supplies inventory, terminal actions and project data. */
2
+ import * as vscode from 'vscode';
3
+ export function setupParts(setup) {
4
+ const parts = [
5
+ new vscode.ChatResponseMarkdownPart('Sign in to use Chat. Complete sign-in in the terminal and browser.'),
6
+ ];
7
+ for (const action of setup.actions) {
8
+ const title = action.kind === 'install' ? `Install ${action.name}`
9
+ : action.harness === 'codex' ? 'Sign in with ChatGPT' : `Sign in with ${action.name}`;
10
+ parts.push(new vscode.ChatResponseMarkdownPart(`Runs: \`${action.command.replace(/`/g, '\\`')}\``));
11
+ parts.push(new vscode.ChatResponseCommandButtonPart({ title, command: 'supercode.frontend.setup', arguments: [action.kind, action.harness] }));
12
+ }
13
+ if (!setup.actions.length) {
14
+ parts.push(new vscode.ChatResponseMarkdownPart(setup.reason || 'No sign-in action is available.'));
15
+ }
16
+ parts.push(new vscode.ChatResponseCommandButtonPart({ title: 'Check again', command: 'supercode.frontend.refreshSetup' }));
17
+ return parts;
18
+ }
19
+ /** Welcome content is owned by the empty-state renderer, never transcript history. */
20
+ export function starterWelcome(prompts, session, resource, introduction) {
21
+ const message = new vscode.MarkdownString().appendText(introduction).appendMarkdown('\n\n');
22
+ message.isTrusted = { enabledCommands: ['supercode.frontend.fillStarter'] };
23
+ for (const [index, starter] of prompts.slice(0, 3).entries()) {
24
+ const args = encodeURIComponent(JSON.stringify([session, index, resource]));
25
+ message.appendMarkdown('[').appendText(starter.label)
26
+ .appendMarkdown(`](command:supercode.frontend.fillStarter?${args})\n\n`);
27
+ }
28
+ return message;
29
+ }
package/dist/extension.js CHANGED
@@ -2,6 +2,7 @@ import { resolveSessionBinding } from './bindings.js';
2
2
  import { withTextContext } from './context.js';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { ChatHostControls, runtimeIdentity, harnessDisplayName, turnPermission } from './host-controls.js';
5
+ import { setupParts, starterWelcome } from './chat-setup.js';
5
6
  import * as vscode from 'vscode';
6
7
  import { createFrontendClient } from './client.js';
7
8
  import { readCredentialFile } from './credential.js';
@@ -32,6 +33,44 @@ const ENV = Object.freeze({
32
33
  */
33
34
  unavailable: 'SUPERCODE_FRONTEND_UNAVAILABLE',
34
35
  });
36
+ /** Transport progress includes setup events; it is not a user-turn count. */
37
+ function conversationEligibility(connection) {
38
+ const session = connection.session;
39
+ const inputs = {
40
+ attachmentState: session.connection,
41
+ turnState: session.descriptor?.turn_state ?? null,
42
+ pendingRequestCount: session.pendingRequests.size,
43
+ historyTruncated: connection.historyTruncated ?? false,
44
+ persistedHistoryLoaded: connection.persistedHistory !== undefined,
45
+ persistedHistoryCount: connection.persistedHistory?.length ?? 0,
46
+ persistedHistoryRoles: connection.persistedHistory?.map(message => message.role) ?? [],
47
+ canonicalHistoryCount: session.history.length,
48
+ canonicalHistoryRoles: session.history.map(message => message.role),
49
+ hasUserTurn: session.hasUserTurn,
50
+ historyCursor: session.historyCursor,
51
+ eventCursor: session.cursor,
52
+ replayLength: session.replay.length,
53
+ replayKinds: [...new Set(session.replay.map(event => event.kind))],
54
+ nativeEventKinds: [...new Set(session.replay.filter(event => event.kind === 'native_event')
55
+ .map(event => event.payload?.kind).filter(kind => typeof kind === 'string'))],
56
+ };
57
+ const reasons = [];
58
+ if (inputs.attachmentState !== 'connected')
59
+ reasons.push(`attachmentState=${inputs.attachmentState}`);
60
+ if (inputs.turnState !== 'idle')
61
+ reasons.push(`turnState=${inputs.turnState}`);
62
+ if (inputs.pendingRequestCount)
63
+ reasons.push(`pendingRequestCount=${inputs.pendingRequestCount}`);
64
+ if (inputs.historyTruncated)
65
+ reasons.push('saved history is truncated');
66
+ if (inputs.persistedHistoryRoles.some(role => role !== 'system'))
67
+ reasons.push('saved history contains a turn');
68
+ if (inputs.canonicalHistoryRoles.some(role => role !== 'system'))
69
+ reasons.push('canonical history contains a turn');
70
+ if (inputs.hasUserTurn)
71
+ reasons.push('a user turn has been observed');
72
+ return { ...inputs, eligible: reasons.length === 0, reasons };
73
+ }
35
74
  function normalizePermissions(value, fallback) {
36
75
  const raw = Array.isArray(value)
37
76
  ? value.map(String)
@@ -324,6 +363,24 @@ export async function activate(context) {
324
363
  const boundId = (resource) => resolveSessionBinding(resource, controls?.state.sessions ?? [], resourceBindings);
325
364
  let applying = Promise.resolve();
326
365
  const frontends = new SupercodeFrontends();
366
+ const starterEligibility = (connection) => {
367
+ const conversation = conversationEligibility(connection);
368
+ const activeSession = controls?.state.activeSession ?? null;
369
+ const hostBusy = Boolean(controls?.state.busy);
370
+ const connectionMatches = frontends.get(connection.id) === connection;
371
+ const reasons = [...conversation.reasons];
372
+ if (!controls)
373
+ reasons.push('host controls unavailable');
374
+ if (activeSession !== connection.id)
375
+ reasons.push(`activeSession=${activeSession}; expected ${connection.id}`);
376
+ if (hostBusy)
377
+ reasons.push('hostBusy=true');
378
+ if (!connectionMatches)
379
+ reasons.push('connection was replaced');
380
+ return { ...conversation, controlsAvailable: Boolean(controls), connectionId: connection.id, activeSession, hostBusy, connectionMatches,
381
+ eligible: reasons.length === 0, reasons };
382
+ };
383
+ const lastStarterAttempts = new Map();
327
384
  // Runtimes attached by address ("Set Runtime Token", "Attach to Runtime")
328
385
  // beside a host's managed conversation. The host's catalogue does not know
329
386
  // them, so they open as what they are: attachments, shown as their runtime
@@ -332,6 +389,28 @@ export async function activate(context) {
332
389
  const managed = () => frontends.all.find((connection) => !attachedIds.has(connection.id));
333
390
  // The attached runtime the person is looking at: the Chat names its harness, not the host's.
334
391
  let focusedAttached;
392
+ const welcomeParticipants = new Set();
393
+ const welcomeValues = new WeakMap();
394
+ const starterResources = new Map();
395
+ const updateStarterWelcome = () => {
396
+ if (!granted.has('defaultChatParticipant'))
397
+ return;
398
+ const connection = managed();
399
+ const prompts = controls?.state.starterPrompts ?? [];
400
+ // This is the click guard's exact state, including host activity and connection
401
+ // identity. A later idle state must not revive an offer after a user turn.
402
+ const message = connection && !focusedAttached && !controls?.state.starterPromptsPending
403
+ && prompts.length && starterEligibility(connection).eligible
404
+ ? starterWelcome(prompts, connection.id, starterResources.get(connection.id) ?? sessionResource(connection).toString(), context.extension.packageJSON.supercodeFrontendWelcome.starterIntroduction)
405
+ : new vscode.MarkdownString('');
406
+ for (const participant of welcomeParticipants) {
407
+ if (welcomeValues.get(participant) === message.value)
408
+ continue;
409
+ participant.additionalWelcomeMessage = message;
410
+ welcomeValues.set(participant, message.value);
411
+ }
412
+ };
413
+ context.subscriptions.push(frontends.onDidChange(updateStarterWelcome), controlsChanged.event(updateStarterWelcome));
335
414
  let approvalAvailable = false;
336
415
  context.subscriptions.push(frontends.onDidChange(() => {
337
416
  const active = managed();
@@ -372,7 +451,7 @@ export async function activate(context) {
372
451
  statusIcon: new vscode.ThemeIcon(provider === 'anthropic' ? 'claude' : provider === 'openai' ? 'openai' : provider === 'google' ? 'google-gemini' : 'sparkle'),
373
452
  maxInputTokens: 0, maxOutputTokens: 0, capabilities: {},
374
453
  targetChatSessionType: SESSION_SCHEME, isUserSelectable: true, isDefault: model.id === selection.model,
375
- tooltip: `${model.name} via ${harnessDisplayName(selection.harness)}${selection.effort ? ` · ${selection.effort} reasoning` : ''}. Model is fixed for this conversation. Use New Chat to choose another model.`,
454
+ tooltip: `${model.name} via ${harnessDisplayName(selection.harness)}${selection.effort ? ` · ${selection.effort} reasoning` : ''}. Model is fixed for this conversation. Use Model settings to choose model and reasoning for a new conversation.`,
376
455
  };
377
456
  });
378
457
  },
@@ -392,8 +471,11 @@ export async function activate(context) {
392
471
  throw new Error('The requested conversation has no runtime connection.');
393
472
  const id = controls.state.activeSession;
394
473
  const existing = frontends.get(id);
395
- if (existing && existing.baseUrl === env.SUPERCODE_FRONTEND_URL)
474
+ if (existing && existing.baseUrl === env.SUPERCODE_FRONTEND_URL && existing.session.connection !== 'closed') {
475
+ if (existing.session.connection !== 'connected')
476
+ await existing.session.attach();
396
477
  return existing;
478
+ }
397
479
  for (const connection of frontends.all.filter((entry) => !attachedIds.has(entry.id)))
398
480
  await frontends.disconnect(connection.id);
399
481
  const connection = await frontends.connect({ connectionId: id, baseUrl: env.SUPERCODE_FRONTEND_URL,
@@ -403,12 +485,124 @@ export async function activate(context) {
403
485
  throw new Error('Could not connect to the requested conversation.');
404
486
  return connection;
405
487
  };
488
+ let setupHandoff;
489
+ const setupHandoffComplete = () => {
490
+ const id = controls?.state.activeSession;
491
+ const connection = id ? frontends.get(id) : undefined;
492
+ return Boolean(setupHandoff && setupHandoff.id === id
493
+ && setupHandoff.baseUrl === controls?.state.connection?.SUPERCODE_FRONTEND_URL
494
+ && connection?.baseUrl === setupHandoff.baseUrl && connection.session.connection === 'connected');
495
+ };
406
496
  const revealManaged = async () => {
407
497
  const id = controls?.state.activeSession;
408
498
  if (!id || !controls?.state.openSessionCommand)
409
499
  throw new Error('The host cannot reveal this conversation.');
410
500
  await vscode.commands.executeCommand(controls.state.openSessionCommand, vscode.Uri.from({ scheme: SESSION_SCHEME, authority: id, path: '/conversation' }).toString());
501
+ const connection = frontends.get(id);
502
+ if (controls.state.activeSession !== id || !connection || connection.session.connection !== 'connected'
503
+ || connection.baseUrl !== controls.state.connection?.SUPERCODE_FRONTEND_URL) {
504
+ throw new Error('The conversation changed or disconnected before setup could reveal it.');
505
+ }
506
+ // Host readiness alone is not completion: the native reveal must succeed too.
507
+ setupHandoff = { id, baseUrl: connection.baseUrl };
508
+ };
509
+ let setupFingerprint = '';
510
+ const revealSetup = async () => {
511
+ if (!controls?.state.setup || controls.state.setup.ready || !controls.state.openSessionCommand)
512
+ return;
513
+ const fingerprint = JSON.stringify(controls.state.setup);
514
+ if (setupFingerprint === fingerprint)
515
+ return;
516
+ setupFingerprint = fingerprint;
517
+ // A changed resource avoids Code-OSS reusing cached status history after an install.
518
+ try {
519
+ await vscode.commands.executeCommand(controls.state.openSessionCommand, vscode.Uri.from({ scheme: SESSION_SCHEME, path: UNAVAILABLE_PATH, query: encodeURIComponent(fingerprint) }).toString());
520
+ }
521
+ catch (error) {
522
+ setupFingerprint = '';
523
+ throw error;
524
+ }
525
+ };
526
+ let refreshingSetup;
527
+ const refreshSetup = () => {
528
+ refreshingSetup ??= (async () => {
529
+ if (!controls)
530
+ return;
531
+ await controls.refresh();
532
+ if (controls.state.setup?.ready) {
533
+ await connectManaged();
534
+ controlsChanged.fire();
535
+ if (!setupHandoffComplete())
536
+ await revealManaged();
537
+ }
538
+ else {
539
+ setupHandoff = undefined;
540
+ controlsChanged.fire();
541
+ await revealSetup();
542
+ }
543
+ })().finally(() => { refreshingSetup = undefined; });
544
+ return refreshingSetup;
411
545
  };
546
+ const setupTerminals = new Map();
547
+ context.subscriptions.push(vscode.window.onDidCloseTerminal(terminal => {
548
+ for (const [key, value] of setupTerminals)
549
+ if (value === terminal)
550
+ setupTerminals.delete(key);
551
+ }));
552
+ context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.refreshSetup', refreshSetup));
553
+ context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.setup', async (kind, harness) => {
554
+ if (!controls)
555
+ throw new Error('This host does not expose sign-in.');
556
+ const key = `${kind}/${harness}`;
557
+ const existing = setupTerminals.get(key);
558
+ if (existing && existing.exitStatus === undefined) {
559
+ existing.show();
560
+ return;
561
+ }
562
+ await controls.refresh();
563
+ const offered = controls.state.setup?.actions.find(a => a.kind === kind && a.harness === harness);
564
+ if (!offered) {
565
+ await refreshSetup();
566
+ throw new Error('This setup action is no longer available.');
567
+ }
568
+ const action = await controls.setup(kind, harness);
569
+ if (action.command !== offered.command)
570
+ throw new Error('The setup command changed. Check again before running it.');
571
+ const terminal = vscode.window.createTerminal({ name: `${action.name} · ${kind === 'login' ? 'sign in' : 'install'}`,
572
+ cwd: action.cwd, shellPath: action.program, shellArgs: action.arguments, env: action.env });
573
+ setupTerminals.set(key, terminal);
574
+ terminal.show();
575
+ }));
576
+ context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.fillStarter', async (session, index, nativeResource) => {
577
+ if (!controls || !Number.isInteger(index) || index < 0 || index > 2)
578
+ throw new Error('Invalid starter selection.');
579
+ const connection = frontends.get(session);
580
+ if (!connection)
581
+ throw new Error(`Starter prompt unavailable: connection ${session} is absent.`);
582
+ const resource = nativeResource ? vscode.Uri.parse(nativeResource)
583
+ : vscode.Uri.from({ scheme: SESSION_SCHEME, authority: session, path: '/conversation' });
584
+ if (boundId(resource) !== session)
585
+ throw new Error('The starter has no matching conversation binding.');
586
+ await connection.session.attach();
587
+ const guard = starterEligibility(connection);
588
+ lastStarterAttempts.set(session, { at: new Date().toISOString(), resource: resource.toString(),
589
+ boundSession: boundId(resource), guard });
590
+ while (lastStarterAttempts.size > 50)
591
+ lastStarterAttempts.delete(lastStarterAttempts.keys().next().value);
592
+ if (!guard.eligible)
593
+ throw new Error(`Starter prompt unavailable: ${guard.reasons.join('; ')}.`);
594
+ const prompt = controls.state.starterPrompts?.[index]?.prompt;
595
+ if (!prompt)
596
+ throw new Error('This starter is no longer available.');
597
+ if (!controls.state.fillInputCommand)
598
+ throw new Error('This host does not support filling a conversation draft.');
599
+ // The host addresses the existing native widget, never the default participant or last focus.
600
+ const receipt = await vscode.commands.executeCommand(controls.state.fillInputCommand, resource.toString(), prompt);
601
+ if (!receipt || receipt.sessionResource !== resource.toString() || receipt.input !== prompt) {
602
+ throw new Error('Chat did not confirm the requested draft.');
603
+ }
604
+ return receipt;
605
+ }));
412
606
  const connectionFor = async (resource) => {
413
607
  if (!controls) {
414
608
  const connection = frontends.get(resource.authority);
@@ -477,6 +671,16 @@ export async function activate(context) {
477
671
  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.' },
478
672
  ...((canApprove || (!active && resource && currentOptions(resource).permissions === 'autoApprove')) ? [{ id: 'autoApprove', name: 'Auto approve', description: 'Allow tool approval requests for this chat. Harness sandbox rules still apply.' }] : [])];
479
673
  const groups = [{ id: 'permissions', name: 'Permissions', kind: 'permissions', items: permissionItems, selected: permissionItems.find(i => i.id === (resource ? currentOptions(resource).permissions : 'ask')) }];
674
+ const selected = controls?.state.sessions.find(session => session.id === id);
675
+ if (selected && ['claude-code', 'codex'].includes(selected.selection.harness)) {
676
+ // Native session-option commands remain reachable for nondelegating providers,
677
+ // whose built-in model picker deliberately omits Manage Models. Do not advertise
678
+ // hot swapping: the command opens a separate conversation after both choices.
679
+ // A locked selected item hides the entire native dropdown, including commands.
680
+ const item = { id: 'settings', name: 'Model settings' };
681
+ groups.push({ id: 'modelSettings', name: 'Model', items: [item], selected: item,
682
+ commands: [{ command: 'supercode.frontend.configureModel', title: 'Choose model and reasoning for a new chat…', arguments: [selected.id] }] });
683
+ }
480
684
  return groups;
481
685
  },
482
686
  setPermission: (resource, value) => {
@@ -491,7 +695,7 @@ export async function activate(context) {
491
695
  create: async (inputState) => {
492
696
  if (!controls)
493
697
  throw new Error('The host does not expose conversation creation.');
494
- await enqueueSelection({ ...controls.state.selection }, false);
698
+ await enqueueSelection({ harness: controls.state.selection.harness, model: '', effort: '' }, false);
495
699
  const connection = frontends.get(controls.state.activeSession);
496
700
  savePermission(connection.id, turnPermission(inputState.groups.find(group => group.id === 'permissions')?.selected?.id));
497
701
  return connection;
@@ -500,6 +704,25 @@ export async function activate(context) {
500
704
  attached: (connection) => attachedIds.has(connection.id),
501
705
  catalogue: () => controls?.state.sessions ?? [],
502
706
  bindings: boundId,
707
+ setup: () => controls?.state.setup,
708
+ prepareStarters: async (session, resource, token) => {
709
+ // Project configuration can arrive after the runtime connects. Wait for
710
+ // its real answer before publishing the conversation's welcome actions.
711
+ const deadline = Date.now() + 30_000;
712
+ await controls?.refresh();
713
+ while (controls?.state.starterPromptsPending) {
714
+ if (token.isCancellationRequested)
715
+ throw new vscode.CancellationError();
716
+ if (Date.now() >= deadline)
717
+ throw new Error('Project configuration is still loading. Reopen Chat once the project is ready.');
718
+ await new Promise(resolve => setTimeout(resolve, 250));
719
+ await controls.refresh();
720
+ }
721
+ if (token.isCancellationRequested)
722
+ throw new vscode.CancellationError();
723
+ starterResources.set(session, resource);
724
+ controlsChanged.fire();
725
+ },
503
726
  };
504
727
  context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.activateSession', async (resource) => { await connectionFor(vscode.Uri.parse(resource)); }));
505
728
  context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.openSession', async (id) => {
@@ -524,15 +747,34 @@ export async function activate(context) {
524
747
  void vscode.window.showInformationMessage('This host does not expose chat configuration.');
525
748
  return;
526
749
  }
527
- const selection = { ...controls.state.selection };
528
- const harness = await vscode.window.showQuickPick(controls.state.harnesses.map(h => ({ label: h.name, id: h.id })), { title: 'New chat · Harness', placeHolder: 'Choose the harness for a new conversation. Existing chats keep their harness and history.' });
529
- if (!harness)
750
+ await controls.refresh();
751
+ // The host supplies startable agents, including installation/authentication checks.
752
+ // An absent choice is not permission to guess a harness or start another runtime.
753
+ const choices = controls.state.harnesses;
754
+ if (!choices.length) {
755
+ await revealSetup();
530
756
  return;
531
- if (harness.id !== selection.harness) {
532
- selection.harness = harness.id;
533
- selection.model = '';
534
- selection.effort = '';
535
757
  }
758
+ const harness = choices.length === 1 ? choices[0] : await vscode.window.showQuickPick(choices.map(h => ({ label: h.name, id: h.id })), { title: 'New chat · Agent', placeHolder: 'Choose your coding agent. Model and reasoning use its defaults.' });
759
+ if (!harness)
760
+ return;
761
+ await enqueueSelection({ harness: harness.id, model: '', effort: '' });
762
+ }));
763
+ context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.configureModel', async (resource, sessionId) => {
764
+ if (!controls || focusedAttached)
765
+ throw new Error('Model settings require a host-managed conversation.');
766
+ await controls.refresh();
767
+ // The native session-options picker prepends its exact resource to command arguments.
768
+ const bound = resource ? boundId(resource) : undefined;
769
+ if (resource && (!bound || (sessionId && bound !== sessionId)))
770
+ throw new Error('Model settings have no matching conversation binding.');
771
+ const id = bound ?? controls.state.activeSession;
772
+ const selected = controls.state.sessions.find(session => session.id === id);
773
+ if (!selected || selected.id !== controls.state.activeSession)
774
+ throw new Error('Open this conversation before choosing model settings.');
775
+ const selection = { ...selected.selection };
776
+ if (!['claude-code', 'codex'].includes(selection.harness))
777
+ throw new Error('This harness uses its own model defaults; configuration is not advertised.');
536
778
  if (['claude-code', 'codex'].includes(selection.harness)) {
537
779
  const choices = controls.state.modelsByHarness?.[selection.harness] ?? [{ id: '', name: 'Harness default' }];
538
780
  const picked = await vscode.window.showQuickPick([...choices.map(model => ({ label: model.name, id: model.id })), { label: 'Enter an exact model ID…', id: '__custom__' }], { title: 'New chat · Model', placeHolder: 'Model availability depends on your harness account.' });
@@ -608,6 +850,11 @@ export async function activate(context) {
608
850
  const handler = async (request, chatContext, stream, token) => {
609
851
  await applying;
610
852
  const requestedResource = chatContext.chatSessionContext?.chatSessionItem?.resource;
853
+ if (controls?.state.setup && !controls.state.setup.ready) {
854
+ for (const part of setupParts(controls.state.setup))
855
+ stream.push(part);
856
+ return {};
857
+ }
611
858
  const connection = requestedResource
612
859
  ? await connectionFor(requestedResource)
613
860
  : controls ? undefined : managed();
@@ -657,6 +904,8 @@ export async function activate(context) {
657
904
  const participant = vscode.chat.createChatParticipant(id, handler);
658
905
  participant.iconPath = icon();
659
906
  participants.push(participant);
907
+ welcomeParticipants.add(participant);
908
+ updateStarterWelcome();
660
909
  context.subscriptions.push(participant);
661
910
  }
662
911
  catch (error) {
@@ -679,6 +928,8 @@ export async function activate(context) {
679
928
  ? vscode.chat.createDynamicChatParticipant(SESSION_SCHEME, { name: current.name.replace(/[^a-zA-Z0-9]/g, ''), fullName: current.name, publisherName: 'Volter', description: 'Ask a question or describe a task' }, handler)
680
929
  : vscode.chat.createChatParticipant(SESSION_SCHEME, handler);
681
930
  participant.iconPath = icon();
931
+ welcomeParticipants.add(participant);
932
+ updateStarterWelcome();
682
933
  return participant;
683
934
  };
684
935
  let sessionParticipant = makeParticipant();
@@ -687,6 +938,7 @@ export async function activate(context) {
687
938
  if (participantKey() === previousIdentity)
688
939
  return;
689
940
  previousIdentity = participantKey();
941
+ welcomeParticipants.delete(sessionParticipant);
690
942
  sessionParticipant.dispose();
691
943
  sessionParticipant = makeParticipant();
692
944
  for (const p of participants)
@@ -788,7 +1040,16 @@ export async function activate(context) {
788
1040
  if (controls)
789
1041
  await controls.refresh();
790
1042
  if (inspect === true)
791
- return { connections: frontends.all.map(c => ({ ...frontends.info(c), permissions: [...c.session.permissions], pendingRequests: [...c.session.pendingRequests.values()] })), selection: controls?.state.selection, activeSession: controls?.state.activeSession, busy: Boolean(controls?.state.busy || frontends.all.some(c => c.session.descriptor?.turn_state === 'busy' || c.session.pendingRequests.size)), sessions: controls?.state.sessions, permission: currentOptions().permissions, identity: identity() };
1043
+ return {
1044
+ connections: frontends.all.map(c => ({ ...frontends.info(c), permissions: [...c.session.permissions],
1045
+ pendingRequests: [...c.session.pendingRequests.values()], starterEligibility: starterEligibility(c),
1046
+ lastStarterAttempt: lastStarterAttempts.get(c.id) ?? null })),
1047
+ selection: controls?.state.selection, activeSession: controls?.state.activeSession,
1048
+ busy: Boolean(controls?.state.busy || frontends.all.some(c => c.session.descriptor?.turn_state === 'busy' || c.session.pendingRequests.size)),
1049
+ setupHandoff: { hostReady: controls?.state.setup?.ready ?? null, complete: setupHandoffComplete(),
1050
+ revealed: setupHandoff ?? null },
1051
+ sessions: controls?.state.sessions, permission: currentOptions().permissions, identity: identity(),
1052
+ };
792
1053
  const rows = frontends.all.map((connection) => {
793
1054
  const info = frontends.info(connection);
794
1055
  return `${info.label} · ${info.baseUrl} · ${info.model ?? 'unknown model'} · ${info.turnState ?? 'unattached'}`;
@@ -875,6 +1136,10 @@ export async function activate(context) {
875
1136
  if (!granted.has('chatSessionsProvider'))
876
1137
  return;
877
1138
  if (controls) {
1139
+ if (controls.state.setup && !controls.state.setup.ready) {
1140
+ await revealSetup();
1141
+ return;
1142
+ }
878
1143
  try {
879
1144
  await revealManaged();
880
1145
  }
@@ -906,8 +1171,21 @@ export async function activate(context) {
906
1171
  // NOT awaited: this opens an editor, and activation must return before the
907
1172
  // workbench will run one. Awaiting it here left activation hanging and the
908
1173
  // panel on "Loading…" — observed.
909
- const opening = setTimeout(() => void openResumedSession(), 500);
1174
+ const opening = setTimeout(() => void openResumedSession().catch(error => log.warn(`Could not reveal Chat: ${String(error)}`)), 500);
910
1175
  context.subscriptions.push({ dispose: () => clearTimeout(opening) });
1176
+ // Retry until host readiness has completed both attachment AND native reveal.
1177
+ // A transient failure in either step leaves the handoff pending, even if ready=true.
1178
+ let setupTimer;
1179
+ let setupDisposed = false;
1180
+ const pollSetup = () => {
1181
+ if (setupDisposed || !controls?.state.setup || (controls.state.setup.ready && setupHandoffComplete()))
1182
+ return;
1183
+ setupTimer = setTimeout(() => {
1184
+ void refreshSetup().catch(error => log.warn(`Could not refresh sign-in: ${String(error)}`)).finally(pollSetup);
1185
+ }, 3000);
1186
+ };
1187
+ pollSetup();
1188
+ context.subscriptions.push({ dispose: () => { setupDisposed = true; clearTimeout(setupTimer); } });
911
1189
  return {
912
1190
  connect: async (options) => {
913
1191
  const connection = await frontends.connect(options, log);
@@ -1053,7 +1331,7 @@ function registerSessions(context, frontends, participant, granted, handler, con
1053
1331
  const unavailableItem = () => {
1054
1332
  if (!frontends.unavailable || frontends.all.length > 0)
1055
1333
  return undefined;
1056
- const item = controller.createChatSessionItem(unavailableResource(), 'Volter Harness · no runtime');
1334
+ const item = controller.createChatSessionItem(unavailableResource(), controls?.setup() ? 'Sign in to Chat' : 'Volter Harness · no runtime');
1057
1335
  item.description = frontends.unavailable.split('\n')[0];
1058
1336
  item.status = vscode.ChatSessionStatus.Failed;
1059
1337
  return item;
@@ -1122,10 +1400,10 @@ function registerSessions(context, frontends, participant, granted, handler, con
1122
1400
  // the session opens, with no request behind it and nothing to wait
1123
1401
  // for — which is the whole point, since the person has not typed.
1124
1402
  return {
1125
- title: 'Volter Harness · no runtime',
1403
+ title: controls?.setup() ? 'Sign in to Chat' : 'Volter Harness · no runtime',
1126
1404
  history: [
1127
1405
  new turns.ChatRequestTurn(STATUS_MARKER_PROMPT, undefined, [], participant.id, []),
1128
- new turns.ChatResponseTurn([new vscode.ChatResponseMarkdownPart(unavailableMarkdown(frontends.unavailable))], {}, participant.id),
1406
+ new turns.ChatResponseTurn(controls?.setup() ? setupParts(controls.setup()) : [new vscode.ChatResponseMarkdownPart(unavailableMarkdown(frontends.unavailable))], {}, participant.id),
1129
1407
  ],
1130
1408
  requestHandler: boundHandler,
1131
1409
  };
@@ -1145,8 +1423,9 @@ function registerSessions(context, frontends, participant, granted, handler, con
1145
1423
  // `state.history`), so the copy taken when this window attached is
1146
1424
  // stale as soon as a turn runs. The generated client reuses the
1147
1425
  // already-open event stream, so this is a snapshot, not a reconnect.
1148
- await connection.session.attach().catch(() => {
1426
+ const attached = await connection.session.attach().then(() => true).catch(() => {
1149
1427
  // An unreachable runtime replays what this window last saw.
1428
+ return false;
1150
1429
  });
1151
1430
  // The attach snapshot IS the session's history: the runtime's own
1152
1431
  // canonical transcript, replayed as turns.
@@ -1184,6 +1463,12 @@ function registerSessions(context, frontends, participant, granted, handler, con
1184
1463
  }
1185
1464
  history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {}, participant.id));
1186
1465
  }
1466
+ // The public API cannot withdraw a rendered history command part. Keep
1467
+ // starters in the default participant's native welcome content instead:
1468
+ // Chat hides that whole surface as soon as the first request is added.
1469
+ if (attached && history.length === 0 && !controls?.attached(connection) && controls) {
1470
+ await controls.prepareStarters(connection.id, resource.toString(), _token);
1471
+ }
1187
1472
  const busy = connection.session.descriptor?.turn_state === 'busy';
1188
1473
  return {
1189
1474
  title: connection.label,
@@ -10,6 +10,10 @@ export interface ChatChoice {
10
10
  name: string;
11
11
  description?: string;
12
12
  }
13
+ export interface ChatStarterPrompt {
14
+ label: string;
15
+ prompt: string;
16
+ }
13
17
  export interface ManagedChatSession {
14
18
  id: string;
15
19
  selection: ChatSelection;
@@ -17,20 +21,42 @@ export interface ManagedChatSession {
17
21
  title: string;
18
22
  created: number;
19
23
  }
24
+ /** Host-owned operations. The frontend displays commands before the person runs them. */
25
+ export interface ChatSetupAction {
26
+ kind: 'login' | 'install';
27
+ harness: string;
28
+ name: string;
29
+ command: string;
30
+ }
31
+ export interface ChatSetupState {
32
+ ready: boolean;
33
+ actions: ChatSetupAction[];
34
+ reason: string | null;
35
+ cwd: string;
36
+ }
20
37
  export interface HostState {
21
38
  history?: ChatMessage[];
22
39
  historyTruncated?: boolean;
23
40
  activeSession: string | null;
24
41
  openSessionCommand?: string;
42
+ /** Fill the composer of an exact native session; returns {sessionResource, input}. */
43
+ fillInputCommand?: string;
44
+ /** The native renderer supports a leading response with no visible user request. */
45
+ responseOnlyHistory?: boolean;
25
46
  sessions: ManagedChatSession[];
26
47
  selection: ChatSelection;
27
48
  connection?: Record<string, string>;
49
+ /** Agents the host can start now (installation, authentication and capability checked). */
28
50
  harnesses: ChatChoice[];
29
51
  models: ChatChoice[];
30
52
  modelsByHarness?: Record<string, ChatChoice[]>;
31
53
  busy: boolean;
32
54
  actualModel?: string | null;
33
55
  configurable: boolean;
56
+ setup?: ChatSetupState;
57
+ starterPrompts?: ChatStarterPrompt[];
58
+ /** The host's project configuration has not finished loading yet. */
59
+ starterPromptsPending?: boolean;
34
60
  }
35
61
  export interface HostSelectionResult extends HostState {
36
62
  connection: Record<string, string>;
@@ -45,6 +71,12 @@ export declare class ChatHostControls {
45
71
  select(selection: ChatSelection): Promise<HostSelectionResult>;
46
72
  remember(id: string, nativeId: string): Promise<void>;
47
73
  open(id: string): Promise<HostSelectionResult>;
74
+ setup(kind: ChatSetupAction['kind'], harness: string): Promise<ChatSetupAction & {
75
+ cwd: string;
76
+ program: string;
77
+ arguments: string[];
78
+ env?: Record<string, string>;
79
+ }>;
48
80
  private request;
49
81
  }
50
82
  /** Approval policy is captured for one turn, never inferred from connection privileges. */
@@ -31,6 +31,9 @@ export class ChatHostControls {
31
31
  this.state = result;
32
32
  return result;
33
33
  }
34
+ async setup(kind, harness) {
35
+ return this.request('/setup', { kind, harness });
36
+ }
34
37
  async request(path, selection) {
35
38
  const response = await fetch(this.baseUrl + path, {
36
39
  method: selection ? 'POST' : 'GET', headers: { Authorization: `Bearer ${this.credential}`, 'Content-Type': 'application/json' },
package/dist/session.d.ts CHANGED
@@ -77,6 +77,8 @@ export declare class VscodeFrontendSession {
77
77
  history: ChatMessage[];
78
78
  historyCursor: number;
79
79
  cursor: number;
80
+ /** Semantic evidence, retained even when replay is evicted or re-attached. */
81
+ hasUserTurn: boolean;
80
82
  /**
81
83
  * Every event this attachment has seen, in order — NOT just the last
82
84
  * snapshot's. A re-attach asks `afterSequence: cursor`, so its `replay` is
package/dist/session.js CHANGED
@@ -44,6 +44,8 @@ export class VscodeFrontendSession {
44
44
  history = [];
45
45
  historyCursor = 0;
46
46
  cursor = 0;
47
+ /** Semantic evidence, retained even when replay is evicted or re-attached. */
48
+ hasUserTurn = false;
47
49
  /**
48
50
  * Every event this attachment has seen, in order — NOT just the last
49
51
  * snapshot's. A re-attach asks `afterSequence: cursor`, so its `replay` is
@@ -103,6 +105,8 @@ export class VscodeFrontendSession {
103
105
  descriptor: structuredClone(snapshot.descriptor),
104
106
  pendingRequests: new Map([...this.pendingRequests].map(([id, request]) => [id, structuredClone(request)])),
105
107
  cursor: Math.max(this.cursor, snapshot.history_cursor),
108
+ hasUserTurn: this.hasUserTurn || snapshot.descriptor.turn_state === 'busy'
109
+ || snapshot.history.some(message => message.role !== 'system'),
106
110
  };
107
111
  // Validate and project the whole attachment before any observable field
108
112
  // moves or a live listener is registered.
@@ -125,6 +129,7 @@ export class VscodeFrontendSession {
125
129
  this.appendTranscript(structuredClone(snapshot.replay));
126
130
  this.pendingRequests = staged.pendingRequests;
127
131
  this.cursor = staged.cursor;
132
+ this.hasUserTurn = staged.hasUserTurn;
128
133
  this.connection = 'connected';
129
134
  this.attachedAtMs ??= Date.now();
130
135
  if (!this.#subscribed) {
@@ -252,6 +257,11 @@ export class VscodeFrontendSession {
252
257
  throw new EventSequenceGapError(state.cursor + 1, event.sequence);
253
258
  }
254
259
  state.cursor = event.sequence;
260
+ // Hosted runtimes publish user_message + turn_started when accepting input.
261
+ // Native init/status notifications also consume sequence numbers, but are
262
+ // not user turns. Keep this independent of the bounded replay buffer.
263
+ if (BUSY_KINDS.has(event.kind))
264
+ state.hasUserTurn = true;
255
265
  const payload = (event.payload ?? {});
256
266
  if (event.kind === 'request' && payload.type === 'request') {
257
267
  const request = payload.request;
package/dist/view.js CHANGED
@@ -639,6 +639,9 @@ export function projectReplayTurns(events, granted, context) {
639
639
  */
640
640
  export function projectHistory(history, granted, pendingToolNames = new Set()) {
641
641
  const turns = [];
642
+ // Keep results inside the native invocation, as the live stream does. A
643
+ // separate Markdown result bypasses the editor's collapsed tool-step group.
644
+ const toolRows = new Map();
642
645
  const pushAssistant = (parts) => {
643
646
  const last = turns[turns.length - 1];
644
647
  if (last && last.role === 'assistant' && last.parts) {
@@ -659,6 +662,7 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
659
662
  for (const message of history) {
660
663
  const body = messageText(message);
661
664
  if (message.role === 'user') {
665
+ toolRows.clear();
662
666
  turns.push({ role: 'user', prompt: clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters) });
663
667
  continue;
664
668
  }
@@ -674,14 +678,21 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
674
678
  pushAssistant([{ part: 'markdown', value: clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters) }]);
675
679
  }
676
680
  for (const call of message.tool_calls ?? []) {
681
+ const subject = toolSubject(call.function.arguments);
682
+ const label = clamp(stripControls(subject ? `${call.function.name} · ${subject}` : call.function.name), VSCODE_CHAT_LIMITS.titleCharacters);
683
+ const row = {
684
+ part: 'tool',
685
+ toolName: call.function.name,
686
+ toolCallId: call.id,
687
+ invocationMessage: label,
688
+ pastTenseMessage: label,
689
+ inputText: clamp(stripControls(parseArguments(call.function.arguments)), VSCODE_CHAT_LIMITS.partCharacters),
690
+ isComplete: true,
691
+ };
692
+ if (call.id)
693
+ toolRows.set(call.id, row);
677
694
  pushAssistant(resolveParts([{
678
- emit: {
679
- part: 'tool',
680
- toolName: call.function.name,
681
- toolCallId: call.id,
682
- invocationMessage: clamp(call.function.name, VSCODE_CHAT_LIMITS.titleCharacters),
683
- isComplete: true,
684
- },
695
+ emit: row,
685
696
  needs: 'chatParticipantAdditions',
686
697
  fallback: [{
687
698
  part: 'markdown',
@@ -696,10 +707,43 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
696
707
  // interruption. Only the final matching tool is affected, never old history.
697
708
  const pending = message === history.at(-1) && pendingToolNames.has(message.name ?? '')
698
709
  && body === '[no tool result recorded — turn interrupted]';
710
+ if (pending) {
711
+ // The pinned editor serializes history tool parts as complete even if
712
+ // isComplete=false. Keep this explicit marker for matched AND orphan
713
+ // results; the active response separately restores the approval controls.
714
+ pushAssistant([{ part: 'markdown', value: '**Awaiting approval** — no tool result yet.' }]);
715
+ continue;
716
+ }
717
+ if (granted.has('chatParticipantAdditions')) {
718
+ const row = message.tool_call_id ? toolRows.get(message.tool_call_id) : undefined;
719
+ const output = clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters);
720
+ if (row) {
721
+ row.resultText = output;
722
+ // Canonical ChatMessage has no general error flag. Only this known
723
+ // synthetic marker establishes interruption; do not guess from prose.
724
+ if (body === '[no tool result recorded — turn interrupted]')
725
+ row.isError = true;
726
+ toolRows.delete(message.tool_call_id);
727
+ }
728
+ else {
729
+ // A bounded transcript can retain a result whose call was omitted.
730
+ // Preserve it as an inspectable tool row without inventing its input.
731
+ const label = clamp(stripControls(message.name || 'Tool result'), VSCODE_CHAT_LIMITS.titleCharacters);
732
+ pushAssistant([{
733
+ part: 'tool',
734
+ toolName: message.name || 'tool',
735
+ toolCallId: message.tool_call_id || `history-result-${history.indexOf(message)}`,
736
+ invocationMessage: label,
737
+ pastTenseMessage: label,
738
+ isComplete: true,
739
+ resultText: output,
740
+ }]);
741
+ }
742
+ continue;
743
+ }
699
744
  pushAssistant([{
700
745
  part: 'markdown',
701
- value: pending ? '**Awaiting approval** — no tool result yet.'
702
- : `**Tool result${message.name ? ` · ${message.name}` : ''}**\n\n${fence(clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters))}`,
746
+ value: `**Tool result${message.name ? ` · ${message.name}` : ''}**\n\n${fence(clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters))}`,
703
747
  }]);
704
748
  }
705
749
  if (turns.length <= VSCODE_CHAT_LIMITS.historyTurns)
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.16",
4
+ "version": "0.1.18",
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",
@@ -84,7 +84,7 @@
84
84
  "description": "Conversations with your AI assistant",
85
85
  "icon": "$(server-process)",
86
86
  "welcomeTitle": "Chat",
87
- "welcomeMessage": "Ask a question or continue a saved conversation.",
87
+ "welcomeMessage": "Write a message to your coding agent.",
88
88
  "inputPlaceholder": "Ask a question or describe a task",
89
89
  "order": 1,
90
90
  "capabilities": {
@@ -140,9 +140,14 @@
140
140
  },
141
141
  {
142
142
  "command": "supercode.frontend.configure",
143
- "title": "New Chat: Choose Harness, Model and Reasoning",
143
+ "title": "New Chat: Choose Agent",
144
144
  "category": "Chat",
145
145
  "icon": "$(add)"
146
+ },
147
+ {
148
+ "command": "supercode.frontend.configureModel",
149
+ "title": "Model and Reasoning: Open New Chat",
150
+ "category": "Chat"
146
151
  }
147
152
  ],
148
153
  "configuration": {
@@ -191,6 +196,9 @@
191
196
  "@types/vscode": "1.138.0",
192
197
  "typescript": "5.6.3"
193
198
  },
199
+ "supercodeFrontendWelcome": {
200
+ "starterIntroduction": "Choose an idea to edit before sending:"
201
+ },
194
202
  "supercodeFrontendContract": {
195
203
  "name": "@volter/supercode-frontend",
196
204
  "version": "0.2.7",