@volter/supercode-frontend-vscode 0.1.18 → 0.1.21

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/NOTICE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Third-party notice
2
2
 
3
- This optional frontend has **no runtime npm dependency**. It builds against `@types/vscode`
3
+ This optional frontend needs **no runtime npm installation**. It builds against `@types/vscode`
4
4
  (Copyright (c) Microsoft Corporation, MIT) as an exact devDependency for types only; `vscode` itself
5
5
  is supplied by the editor at runtime and is never bundled or redistributed here.
6
6
 
package/README.md CHANGED
@@ -34,65 +34,6 @@ Changed setup resources refresh
34
34
  Code-OSS's cached status history after an install. Hosts without these optional
35
35
  fields keep the existing runtime-attachment behavior.
36
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
-
96
37
  A bundling host can expose a private lifecycle endpoint through
97
38
  `SUPERCODE_FRONTEND_HOST_URL` and `SUPERCODE_FRONTEND_HOST_CREDENTIAL_FILE`.
98
39
  The endpoint must be loopback HTTP and the credential follows the runtime credential-file
@@ -1,6 +1,4 @@
1
- /** Native Chat presentation; the host supplies inventory, terminal actions and project data. */
1
+ /** Native Chat presentation; the host supplies inventory and terminal actions. */
2
2
  import * as vscode from 'vscode';
3
- import type { ChatSetupState, ChatStarterPrompt } from './host-controls.js';
3
+ import type { ChatSetupState } from './host-controls.js';
4
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;
@@ -1,4 +1,4 @@
1
- /** Native Chat presentation; the host supplies inventory, terminal actions and project data. */
1
+ /** Native Chat presentation; the host supplies inventory and terminal actions. */
2
2
  import * as vscode from 'vscode';
3
3
  export function setupParts(setup) {
4
4
  const parts = [
@@ -16,14 +16,3 @@ export function setupParts(setup) {
16
16
  parts.push(new vscode.ChatResponseCommandButtonPart({ title: 'Check again', command: 'supercode.frontend.refreshSetup' }));
17
17
  return parts;
18
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,7 +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
+ import { setupParts } from './chat-setup.js';
6
6
  import * as vscode from 'vscode';
7
7
  import { createFrontendClient } from './client.js';
8
8
  import { readCredentialFile } from './credential.js';
@@ -33,44 +33,6 @@ const ENV = Object.freeze({
33
33
  */
34
34
  unavailable: 'SUPERCODE_FRONTEND_UNAVAILABLE',
35
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
- }
74
36
  function normalizePermissions(value, fallback) {
75
37
  const raw = Array.isArray(value)
76
38
  ? value.map(String)
@@ -363,24 +325,6 @@ export async function activate(context) {
363
325
  const boundId = (resource) => resolveSessionBinding(resource, controls?.state.sessions ?? [], resourceBindings);
364
326
  let applying = Promise.resolve();
365
327
  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();
384
328
  // Runtimes attached by address ("Set Runtime Token", "Attach to Runtime")
385
329
  // beside a host's managed conversation. The host's catalogue does not know
386
330
  // them, so they open as what they are: attachments, shown as their runtime
@@ -389,28 +333,6 @@ export async function activate(context) {
389
333
  const managed = () => frontends.all.find((connection) => !attachedIds.has(connection.id));
390
334
  // The attached runtime the person is looking at: the Chat names its harness, not the host's.
391
335
  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));
414
336
  let approvalAvailable = false;
415
337
  context.subscriptions.push(frontends.onDidChange(() => {
416
338
  const active = managed();
@@ -573,36 +495,6 @@ export async function activate(context) {
573
495
  setupTerminals.set(key, terminal);
574
496
  terminal.show();
575
497
  }));
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
- }));
606
498
  const connectionFor = async (resource) => {
607
499
  if (!controls) {
608
500
  const connection = frontends.get(resource.authority);
@@ -705,24 +597,6 @@ export async function activate(context) {
705
597
  catalogue: () => controls?.state.sessions ?? [],
706
598
  bindings: boundId,
707
599
  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
- },
726
600
  };
727
601
  context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.activateSession', async (resource) => { await connectionFor(vscode.Uri.parse(resource)); }));
728
602
  context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.openSession', async (id) => {
@@ -748,16 +622,25 @@ export async function activate(context) {
748
622
  return;
749
623
  }
750
624
  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;
625
+ // Sign-in choices stay visible alongside agents that can be explicitly started.
626
+ const login = controls.state.setup?.actions.filter(action => action.kind === 'login') ?? [];
627
+ const choices = [
628
+ ...login.map(action => ({ label: action.name, id: action.harness, description: 'Sign in…', login: true, autoStart: false })),
629
+ ...controls.state.harnesses.filter(h => !login.some(action => action.harness === h.id))
630
+ .map(h => ({ label: h.name, id: h.id, description: h.description, login: false, autoStart: h.autoStart === true })),
631
+ ];
754
632
  if (!choices.length) {
755
633
  await revealSetup();
756
634
  return;
757
635
  }
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.' });
636
+ // A sole sign-in option still requires a deliberate click before opening its terminal.
637
+ const harness = choices.length === 1 && choices[0].autoStart ? choices[0] : await vscode.window.showQuickPick(choices, { title: 'New chat · Agent', placeHolder: 'Choose your coding agent. Model and reasoning use its defaults.' });
759
638
  if (!harness)
760
639
  return;
640
+ if (harness.login) {
641
+ await vscode.commands.executeCommand('supercode.frontend.setup', 'login', harness.id);
642
+ return;
643
+ }
761
644
  await enqueueSelection({ harness: harness.id, model: '', effort: '' });
762
645
  }));
763
646
  context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.configureModel', async (resource, sessionId) => {
@@ -874,9 +757,14 @@ export async function activate(context) {
874
757
  // and an elicitation's free-text content).
875
758
  const pending = pendingRequestOf(connection);
876
759
  if (pending) {
877
- const answer = typedAnswer(pending, request.prompt);
878
- await connection.session.respond(answer);
879
- stream.markdown(`_answered request #${pending.id}: ${opaqueText(answer, 256)}_`);
760
+ try {
761
+ const answer = typedAnswer(pending, request.prompt);
762
+ await connection.session.respond(answer);
763
+ stream.markdown(`_answered request #${pending.id}: ${opaqueText(answer, 256)}_`);
764
+ }
765
+ catch (error) {
766
+ stream.markdown(`Could not answer request #${pending.id}: ${opaqueText(String(error.message), 512)}`);
767
+ }
880
768
  return {};
881
769
  }
882
770
  if (request.prompt.trim().length === 0) {
@@ -904,8 +792,6 @@ export async function activate(context) {
904
792
  const participant = vscode.chat.createChatParticipant(id, handler);
905
793
  participant.iconPath = icon();
906
794
  participants.push(participant);
907
- welcomeParticipants.add(participant);
908
- updateStarterWelcome();
909
795
  context.subscriptions.push(participant);
910
796
  }
911
797
  catch (error) {
@@ -928,8 +814,6 @@ export async function activate(context) {
928
814
  ? 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)
929
815
  : vscode.chat.createChatParticipant(SESSION_SCHEME, handler);
930
816
  participant.iconPath = icon();
931
- welcomeParticipants.add(participant);
932
- updateStarterWelcome();
933
817
  return participant;
934
818
  };
935
819
  let sessionParticipant = makeParticipant();
@@ -938,7 +822,6 @@ export async function activate(context) {
938
822
  if (participantKey() === previousIdentity)
939
823
  return;
940
824
  previousIdentity = participantKey();
941
- welcomeParticipants.delete(sessionParticipant);
942
825
  sessionParticipant.dispose();
943
826
  sessionParticipant = makeParticipant();
944
827
  for (const p of participants)
@@ -971,17 +854,13 @@ export async function activate(context) {
971
854
  return;
972
855
  }
973
856
  let response = answer.response;
974
- if (answer.promptForContent) {
975
- const content = await vscode.window.showInputBox({
976
- title: 'Volter Harness runtime request',
977
- prompt: 'The runtime asked for a value; a button cannot carry one.',
978
- ignoreFocusOut: true,
979
- });
980
- if (content === undefined)
981
- return;
982
- response = { ...response, content };
983
- }
984
857
  try {
858
+ if (answer.promptForContent) {
859
+ const content = await requestContent(request);
860
+ if (content === undefined)
861
+ return;
862
+ response = { ...response, content };
863
+ }
985
864
  await connection.session.respond(response);
986
865
  void vscode.window.setStatusBarMessage(`Volter Harness: ${resolutionSentence(response, request, answer.requestId)}`, 4000);
987
866
  }
@@ -1042,8 +921,7 @@ export async function activate(context) {
1042
921
  if (inspect === true)
1043
922
  return {
1044
923
  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 })),
924
+ pendingRequests: [...c.session.pendingRequests.values()] })),
1047
925
  selection: controls?.state.selection, activeSession: controls?.state.activeSession,
1048
926
  busy: Boolean(controls?.state.busy || frontends.all.some(c => c.session.descriptor?.turn_state === 'busy' || c.session.pendingRequests.size)),
1049
927
  setupHandoff: { hostReady: controls?.state.setup?.ready ?? null, complete: setupHandoffComplete(),
@@ -1463,12 +1341,6 @@ function registerSessions(context, frontends, participant, granted, handler, con
1463
1341
  }
1464
1342
  history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {}, participant.id));
1465
1343
  }
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
- }
1472
1344
  const busy = connection.session.descriptor?.turn_state === 'busy';
1473
1345
  return {
1474
1346
  title: connection.label,
@@ -1495,3 +1367,133 @@ function registerSessions(context, frontends, participant, granted, handler, con
1495
1367
  },
1496
1368
  }, participant, { supportsInterruptions: true }));
1497
1369
  }
1370
+ /** Collect each question through the existing Chat answer button, preserving ids and secrets. */
1371
+ async function requestContent(request) {
1372
+ const payload = request.payload;
1373
+ if (Array.isArray(payload?.questions)) {
1374
+ const answers = Object.create(null);
1375
+ for (const value of payload.questions) {
1376
+ if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.id !== 'string')
1377
+ continue;
1378
+ const question = value;
1379
+ const options = Array.isArray(question.options) ? question.options.flatMap(option => {
1380
+ if (!option || typeof option !== 'object' || Array.isArray(option) || typeof option.label !== 'string')
1381
+ return [];
1382
+ return [{ label: option.label, description: typeof option.description === 'string' ? option.description : undefined, custom: false }];
1383
+ }) : [];
1384
+ let answer;
1385
+ if (options.length) {
1386
+ if (question.isOther === true)
1387
+ options.push({ label: 'Write an answer…', description: undefined, custom: true });
1388
+ const chosen = await vscode.window.showQuickPick(options, { title: String(question.header ?? 'Codex question'), placeHolder: String(question.question ?? ''), ignoreFocusOut: true });
1389
+ if (!chosen)
1390
+ return undefined;
1391
+ if (!chosen.custom)
1392
+ answer = chosen.label;
1393
+ }
1394
+ if (answer === undefined) {
1395
+ answer = await vscode.window.showInputBox({ title: String(question.header ?? 'Codex question'), prompt: String(question.question ?? ''), password: question.isSecret === true, ignoreFocusOut: true });
1396
+ }
1397
+ if (answer === undefined)
1398
+ return undefined;
1399
+ answers[value.id] = answer;
1400
+ }
1401
+ return answers;
1402
+ }
1403
+ const schema = payload?.requested_schema;
1404
+ if (schema === undefined || schema === null) {
1405
+ return vscode.window.showInputBox({ title: 'Runtime request', prompt: 'Enter your answer.', ignoreFocusOut: true });
1406
+ }
1407
+ const showSchema = async () => {
1408
+ const document = await vscode.workspace.openTextDocument({ language: 'json', content: JSON.stringify(schema, null, 2) });
1409
+ await vscode.window.showTextDocument(document, { preview: true, preserveFocus: true });
1410
+ };
1411
+ const structuredInput = async () => {
1412
+ await showSchema();
1413
+ const typed = await vscode.window.showInputBox({ title: 'Runtime request', prompt: 'Enter your answer as JSON.', ignoreFocusOut: true,
1414
+ validateInput: value => { try {
1415
+ JSON.parse(value);
1416
+ return undefined;
1417
+ }
1418
+ catch {
1419
+ return 'Enter valid JSON';
1420
+ } } });
1421
+ return typed === undefined ? undefined : JSON.parse(typed);
1422
+ };
1423
+ const form = objectValue(schema);
1424
+ const properties = objectValue(form?.properties);
1425
+ if (!properties)
1426
+ return structuredInput();
1427
+ const content = Object.create(null);
1428
+ const required = Array.isArray(form?.required) ? form.required : [];
1429
+ for (const [name, spec] of Object.entries(properties)) {
1430
+ const field = objectValue(spec) ?? {};
1431
+ const title = typeof field.title === 'string' ? field.title : name;
1432
+ const description = typeof field.description === 'string' ? field.description : undefined;
1433
+ if (!required.includes(name)) {
1434
+ const choice = await vscode.window.showQuickPick(['Provide a value', 'Omit this field'], { title: `${title} (optional)`, ignoreFocusOut: true });
1435
+ if (!choice)
1436
+ return undefined;
1437
+ if (choice === 'Omit this field')
1438
+ continue;
1439
+ }
1440
+ const multiple = field.type === 'array';
1441
+ const options = fieldChoices(multiple ? objectValue(field.items) ?? {} : field);
1442
+ if (options.length) {
1443
+ if (multiple) {
1444
+ const selected = await vscode.window.showQuickPick(options, { title, placeHolder: description, canPickMany: true, ignoreFocusOut: true });
1445
+ if (!selected)
1446
+ return undefined;
1447
+ content[name] = selected.map(choice => choice.value);
1448
+ }
1449
+ else {
1450
+ const selected = await vscode.window.showQuickPick(options, { title, placeHolder: description, ignoreFocusOut: true });
1451
+ if (!selected)
1452
+ return undefined;
1453
+ content[name] = selected.value;
1454
+ }
1455
+ }
1456
+ else {
1457
+ const parse = (value) => {
1458
+ if (field.type === 'number' || field.type === 'integer') {
1459
+ if (!value.trim() || !Number.isFinite(Number(value)))
1460
+ throw new Error('Enter a number');
1461
+ return Number(value);
1462
+ }
1463
+ if (field.type === 'array' || field.type === 'object')
1464
+ return JSON.parse(value);
1465
+ return value;
1466
+ };
1467
+ const typed = await vscode.window.showInputBox({ title, prompt: description, ignoreFocusOut: true,
1468
+ validateInput: value => { try {
1469
+ parse(value);
1470
+ return undefined;
1471
+ }
1472
+ catch {
1473
+ return field.type === 'number' || field.type === 'integer' ? 'Enter a number' : 'Enter valid JSON';
1474
+ } } });
1475
+ if (typed === undefined)
1476
+ return undefined;
1477
+ content[name] = parse(typed);
1478
+ }
1479
+ }
1480
+ return content;
1481
+ }
1482
+ function objectValue(value) {
1483
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
1484
+ }
1485
+ function fieldChoices(field) {
1486
+ if (Array.isArray(field.enum))
1487
+ return field.enum.map((value, index) => ({ value,
1488
+ label: Array.isArray(field.enumNames) && typeof field.enumNames[index] === 'string' ? field.enumNames[index] : String(value) }));
1489
+ const variants = Array.isArray(field.oneOf) ? field.oneOf : field.anyOf;
1490
+ if (Array.isArray(variants))
1491
+ return variants.flatMap(value => {
1492
+ const option = objectValue(value);
1493
+ return option && Object.hasOwn(option, 'const') ? [{ value: option.const,
1494
+ label: typeof option.title === 'string' ? option.title : String(option.const) }] : [];
1495
+ });
1496
+ if (field.type === 'boolean')
1497
+ return [{ label: 'True', value: true }, { label: 'False', value: false }];
1498
+ return [];
1499
+ }
@@ -9,10 +9,7 @@ export interface ChatChoice {
9
9
  id: string;
10
10
  name: string;
11
11
  description?: string;
12
- }
13
- export interface ChatStarterPrompt {
14
- label: string;
15
- prompt: string;
12
+ autoStart?: boolean;
16
13
  }
17
14
  export interface ManagedChatSession {
18
15
  id: string;
@@ -39,8 +36,6 @@ export interface HostState {
39
36
  historyTruncated?: boolean;
40
37
  activeSession: string | null;
41
38
  openSessionCommand?: string;
42
- /** Fill the composer of an exact native session; returns {sessionResource, input}. */
43
- fillInputCommand?: string;
44
39
  /** The native renderer supports a leading response with no visible user request. */
45
40
  responseOnlyHistory?: boolean;
46
41
  sessions: ManagedChatSession[];
@@ -54,9 +49,6 @@ export interface HostState {
54
49
  actualModel?: string | null;
55
50
  configurable: boolean;
56
51
  setup?: ChatSetupState;
57
- starterPrompts?: ChatStarterPrompt[];
58
- /** The host's project configuration has not finished loading yet. */
59
- starterPromptsPending?: boolean;
60
52
  }
61
53
  export interface HostSelectionResult extends HostState {
62
54
  connection: Record<string, string>;
package/dist/session.d.ts CHANGED
@@ -77,8 +77,6 @@ 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;
82
80
  /**
83
81
  * Every event this attachment has seen, in order — NOT just the last
84
82
  * snapshot's. A re-attach asks `afterSequence: cursor`, so its `replay` is
package/dist/session.js CHANGED
@@ -44,8 +44,6 @@ 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;
49
47
  /**
50
48
  * Every event this attachment has seen, in order — NOT just the last
51
49
  * snapshot's. A re-attach asks `afterSequence: cursor`, so its `replay` is
@@ -105,8 +103,6 @@ export class VscodeFrontendSession {
105
103
  descriptor: structuredClone(snapshot.descriptor),
106
104
  pendingRequests: new Map([...this.pendingRequests].map(([id, request]) => [id, structuredClone(request)])),
107
105
  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'),
110
106
  };
111
107
  // Validate and project the whole attachment before any observable field
112
108
  // moves or a live listener is registered.
@@ -121,7 +117,9 @@ export class VscodeFrontendSession {
121
117
  // dead id. An approval can only be outstanding while a turn is running,
122
118
  // because `decide_approval` blocks the agent loop, so an idle runtime has
123
119
  // none no matter what its replay still carries.
124
- if (staged.descriptor?.turn_state !== 'busy')
120
+ // Hosted Codex also receives standalone MCP elicitations. Its host retains
121
+ // request/resolved pairs without the native broker's turn compaction.
122
+ if (staged.descriptor?.turn_state !== 'busy' && staged.descriptor?.source_harness !== 'codex')
125
123
  staged.pendingRequests.clear();
126
124
  this.descriptor = staged.descriptor;
127
125
  this.history = structuredClone(snapshot.history);
@@ -129,7 +127,6 @@ export class VscodeFrontendSession {
129
127
  this.appendTranscript(structuredClone(snapshot.replay));
130
128
  this.pendingRequests = staged.pendingRequests;
131
129
  this.cursor = staged.cursor;
132
- this.hasUserTurn = staged.hasUserTurn;
133
130
  this.connection = 'connected';
134
131
  this.attachedAtMs ??= Date.now();
135
132
  if (!this.#subscribed) {
@@ -257,11 +254,6 @@ export class VscodeFrontendSession {
257
254
  throw new EventSequenceGapError(state.cursor + 1, event.sequence);
258
255
  }
259
256
  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;
265
257
  const payload = (event.payload ?? {});
266
258
  if (event.kind === 'request' && payload.type === 'request') {
267
259
  const request = payload.request;
@@ -278,6 +270,11 @@ export class VscodeFrontendSession {
278
270
  }
279
271
  state.pendingRequests.delete(payload.request_id);
280
272
  }
273
+ if (event.kind === 'runtime_disconnected') {
274
+ state.pendingRequests.clear();
275
+ if (state.descriptor)
276
+ state.descriptor = { ...state.descriptor, connection_state: 'shutting_down', turn_state: 'idle', actions: { ...state.descriptor.actions, submit: false, steer: false, interrupt: false, respond: false } };
277
+ }
281
278
  if (state === this) {
282
279
  if (BUSY_KINDS.has(event.kind))
283
280
  this.lastTurnStartedMs = Date.now();
package/dist/view.js CHANGED
@@ -111,8 +111,10 @@ export function requestSummary(request) {
111
111
  // naming a runtime uses and falls back to bounded JSON.
112
112
  for (const field of ['subject', 'message', 'tool', 'tool_name', 'summary', 'prompt', 'reason']) {
113
113
  const value = fields[field];
114
- if (typeof value === 'string')
115
- return clamp(stripControls(value), VSCODE_CHAT_LIMITS.partCharacters);
114
+ if (typeof value === 'string') {
115
+ const url = typeof fields.url === 'string' ? `\n\n${fields.url}` : '';
116
+ return clamp(stripControls(value + url), VSCODE_CHAT_LIMITS.partCharacters);
117
+ }
116
118
  }
117
119
  }
118
120
  return opaqueText(payload);
@@ -121,6 +123,18 @@ export function requestSummary(request) {
121
123
  export function requestAnswers(request, connectionId) {
122
124
  const base = { connectionId, requestId: request.id };
123
125
  if (request.kind === 'approval') {
126
+ const options = request.payload && typeof request.payload === 'object' && !Array.isArray(request.payload)
127
+ ? request.payload.options : undefined;
128
+ if (Array.isArray(options)) {
129
+ return options.flatMap(option => {
130
+ if (!option || typeof option !== 'object' || Array.isArray(option) || typeof option.id !== 'string' || typeof option.label !== 'string')
131
+ return [];
132
+ const response = ['allow', 'allow_for_session', 'deny'].includes(option.id)
133
+ ? { kind: 'approval', request_id: request.id, decision: option.id }
134
+ : { kind: 'other', request_id: request.id, action: 'accept', content: { option_id: option.id } };
135
+ return [{ ...base, label: option.label, response }];
136
+ });
137
+ }
124
138
  const all = [
125
139
  { ...base, label: 'Allow', response: { kind: 'approval', request_id: request.id, decision: 'allow' } },
126
140
  {
@@ -161,6 +175,9 @@ export function requestAnswers(request, connectionId) {
161
175
  export function typedAnswer(request, reply) {
162
176
  const normalized = reply.trim().toLowerCase();
163
177
  if (request.kind === 'approval') {
178
+ if (['cancel', 'allow_always', 'execpolicy'].includes(normalized) || /^network_\d+$/.test(normalized)) {
179
+ return { kind: 'other', request_id: request.id, action: 'accept', content: { option_id: normalized } };
180
+ }
164
181
  if (['allow for session', 'allow_for_session', 'session'].includes(normalized)) {
165
182
  return { kind: 'approval', request_id: request.id, decision: 'allow_for_session' };
166
183
  }
@@ -194,6 +211,8 @@ export function requestNeedsTypedReply(request) {
194
211
  const payload = request.payload;
195
212
  if (!payload || typeof payload !== 'object' || Array.isArray(payload))
196
213
  return false;
214
+ if (Array.isArray(payload.questions))
215
+ return true;
197
216
  const schema = payload.requested_schema;
198
217
  return schema !== undefined && schema !== null;
199
218
  }
@@ -211,6 +230,12 @@ export function resolutionSentence(response, request, requestId) {
211
230
  const verb = { allow: 'Allowed', allow_for_session: 'Allowed for session', deny: 'Denied' }[response.decision];
212
231
  return `${verb ?? 'Resolved'}${suffix}`;
213
232
  }
233
+ if (response.kind === 'other' && response.content && typeof response.content === 'object' && !Array.isArray(response.content) && typeof response.content.option_id === 'string') {
234
+ const options = request?.payload && typeof request.payload === 'object' && !Array.isArray(request.payload) ? request.payload.options : undefined;
235
+ const option = Array.isArray(options) ? options.find(value => value && typeof value === 'object' && !Array.isArray(value) && value.id === response.content.option_id) : undefined;
236
+ if (option && typeof option === 'object' && !Array.isArray(option) && typeof option.label === 'string')
237
+ return `Selected “${option.label}” for${suffix}`;
238
+ }
214
239
  const verb = { accept: 'Accepted', decline: 'Declined', cancel: 'Cancelled' }[response.action];
215
240
  const content = typeof response.content === 'string' && response.content.length > 0
216
241
  ? ` — ${clamp(stripControls(response.content), 160)}`
@@ -419,7 +444,7 @@ export function projectEvent(event, context) {
419
444
  // runtime is left blocked. A preferred path that silently swallows
420
445
  // the answer is worse than a plain one that works.
421
446
  // A command button runs inside the live turn and carries its own answer,
422
- // so all three approval decisions are offered. It cannot be withdrawn
447
+ // so the runtime's offered answers remain usable. It cannot be withdrawn
423
448
  // once pushed (`chatCommandContentPart` enables on `!isStale` and has no
424
449
  // used-state), so `supercode.frontend.respond` refuses a request that is
425
450
  // no longer pending and SAYS so instead of sending a stale answer.
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.18",
4
+ "version": "0.1.21",
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",
@@ -23,7 +23,7 @@
23
23
  "media"
24
24
  ],
25
25
  "scripts": {
26
- "build": "node scripts/sync-sdk.mjs && node scripts/fetch-icon.mjs && tsc -p tsconfig.build.json",
26
+ "build": "node -e \"require('node:fs').rmSync('dist', {recursive:true, force:true})\" && node scripts/sync-sdk.mjs && node scripts/fetch-icon.mjs && tsc -p tsconfig.build.json",
27
27
  "extension": "npm run build && node scripts/pack-extension.mjs",
28
28
  "prepack": "npm run build"
29
29
  },
@@ -196,9 +196,6 @@
196
196
  "@types/vscode": "1.138.0",
197
197
  "typescript": "5.6.3"
198
198
  },
199
- "supercodeFrontendWelcome": {
200
- "starterIntroduction": "Choose an idea to edit before sending:"
201
- },
202
199
  "supercodeFrontendContract": {
203
200
  "name": "@volter/supercode-frontend",
204
201
  "version": "0.2.7",