@volter/supercode-frontend-vscode 0.1.28 → 0.1.29

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
@@ -73,12 +73,29 @@ picker includes the official choice on supported hosts; with no choices it retur
73
73
  to setup. Model
74
74
  and reasoning are sent as empty values, leaving both at the harness's own defaults,
75
75
  even if the previous conversation used explicit settings. Cancelling the agent
76
- picker creates nothing.
76
+ picker starts no backend conversation.
77
+
78
+ The native blank chat opens before the picker appears. First send waits for the
79
+ choice and the host's new connection through Code-OSS's session-creation handler;
80
+ the composer keeps the draft until the request is accepted. There is no later
81
+ conversation swap that can hide a message sent while the agent starts. This follows
82
+ the pinned workbench's [first-send materialization](https://github.com/volter-ai/code-oss/blob/f16dc165c0dffe701a7bbf59aefb1c662b206cee/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts#L1272)
83
+ and [draft acceptance](https://github.com/volter-ai/code-oss/blob/f16dc165c0dffe701a7bbf59aefb1c662b206cee/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts#L3493)
84
+ paths. A failed startup leaves the draft available to retry.
85
+
86
+ The pending agent/model choice belongs to that blank chat's untitled resource
87
+ and is retired when another conversation opens. Cancelling or opening sign-in
88
+ does not choose an agent. Readiness polling and **Check again** still discover
89
+ agents while a draft is open, updating the welcome and model catalogue without
90
+ replacing its widget or input. First send refreshes readiness; after a native
91
+ sign-in choice it uses that agent once ready, otherwise it keeps the draft with
92
+ a visible sign-in error.
77
93
 
78
94
  The composer's native **Model settings** control offers **Choose model and reasoning
79
95
  for a new chat…** for Claude Code and Codex. This optional path retains the model
80
96
  list, exact model ID entry and reasoning choices. Only completing the choices
81
- starts a separate conversation; cancelling either leaves the current one intact.
97
+ configures the new blank chat; its backend starts on first send. Cancelling a
98
+ choice starts no backend and leaves the blank chat available.
82
99
  A conversation's harness, model and reasoning remain fixed: this integration does
83
100
  not advertise hot swapping. The built-in model picker still displays the selected
84
101
  model and provider icon; nondelegating session providers cannot add its Manage
@@ -93,7 +110,12 @@ The host publishes durable conversation IDs and their exact harness history iden
93
110
  reported native session ID to discovered history. Unknown or missing bindings fail explicitly,
94
111
  never falling back to the currently active runtime. The host may provide an `openSessionCommand`
95
112
  to reveal an exact resource through its native widget service. Cached native widgets should
96
- notify `supercode.frontend.activateSession` when focused. The frontend owns no processes.
113
+ notify `supercode.frontend.focusSession` on every native focused-resource change,
114
+ including untitled resources and leaving harness Chat (`undefined` when there is
115
+ no focused session). This retires abandoned draft choices even when returning to
116
+ the same cached conversation. Managed resources then notify
117
+ `supercode.frontend.activateSession` to restore their exact backend. The frontend
118
+ owns no processes.
97
119
 
98
120
  The permission group offers **Ask for approval** and, on an answerable interactive attachment,
99
121
  **Auto approve**. Policies are scoped by conversation and captured for each turn; automatic
package/dist/extension.js CHANGED
@@ -321,6 +321,21 @@ export async function activate(context) {
321
321
  const resourceBindings = new Map(context.workspaceState.get('supercode.sessionBindings', []));
322
322
  const boundId = (resource) => resolveSessionBinding(resource, controls?.state.sessions ?? [], resourceBindings);
323
323
  let applying = Promise.resolve();
324
+ let pendingDraft;
325
+ let focusedResource;
326
+ const activeDraft = () => pendingDraft?.resource && pendingDraft.resource.toString() === focusedResource?.toString() ? pendingDraft : undefined;
327
+ const draftSelection = () => {
328
+ const choice = activeDraft()?.choice;
329
+ return choice?.kind === 'agent' ? choice.selection : undefined;
330
+ };
331
+ const focusSession = (resource) => {
332
+ const changed = focusedResource?.toString() !== resource?.toString();
333
+ focusedResource = resource;
334
+ if (pendingDraft?.resource && pendingDraft.resource.toString() !== resource?.toString())
335
+ pendingDraft = undefined;
336
+ if (changed)
337
+ controlsChanged.fire();
338
+ };
324
339
  const frontends = new SupercodeFrontends();
325
340
  // Runtimes attached by address ("Set Runtime Token", "Attach to Runtime")
326
341
  // beside a host's managed conversation. The host's catalogue does not know
@@ -343,6 +358,9 @@ export async function activate(context) {
343
358
  if (focusedAttached)
344
359
  return runtimeIdentity(focusedAttached.session.descriptor?.source_harness, focusedAttached.session.descriptor?.model);
345
360
  const descriptor = managed()?.session.descriptor;
361
+ const selection = draftSelection();
362
+ if (selection)
363
+ return runtimeIdentity(selection.harness, selection.model);
346
364
  return runtimeIdentity(controls?.state.selection.harness ?? descriptor?.source_harness, controls?.state.selection.model || controls?.state.actualModel || descriptor?.model);
347
365
  };
348
366
  const icon = () => {
@@ -357,7 +375,9 @@ export async function activate(context) {
357
375
  provideLanguageModelChatInformation: () => {
358
376
  if (controls.state.setup && !controls.state.setup.ready)
359
377
  return [];
360
- const { selection, models } = controls.state;
378
+ const draft = draftSelection();
379
+ const selection = draft ?? controls.state.selection;
380
+ const models = draft ? controls.state.modelsByHarness?.[selection.harness] ?? [] : controls.state.models;
361
381
  // A running harness does not advertise hot model changes. Configuration
362
382
  // belongs to the explicit new-conversation flow.
363
383
  const choices = models.filter(model => model.id === selection.model);
@@ -401,7 +421,7 @@ export async function activate(context) {
401
421
  await frontends.disconnect(connection.id);
402
422
  const connection = await frontends.connect({ connectionId: id, baseUrl: env.SUPERCODE_FRONTEND_URL,
403
423
  token: await readCredentialFile(env.SUPERCODE_FRONTEND_CREDENTIAL_FILE), clientId: env.SUPERCODE_FRONTEND_CLIENT_ID,
404
- permissions: normalizePermissions(env.SUPERCODE_FRONTEND_PERMISSIONS, ['observe']), label: identity().name }, log);
424
+ permissions: normalizePermissions(env.SUPERCODE_FRONTEND_PERMISSIONS, ['observe']), label: runtimeIdentity(controls.state.selection.harness, controls.state.selection.model || controls.state.actualModel).name }, log);
405
425
  if (!connection)
406
426
  throw new Error('Could not connect to the requested conversation.');
407
427
  return connection;
@@ -441,18 +461,23 @@ export async function activate(context) {
441
461
  refreshingSetup ??= (async () => {
442
462
  if (!controls)
443
463
  return;
464
+ // Discover and publish readiness even while a blank chat holds a draft.
465
+ // Only the automatic reveal is suppressed; discovery must never stall.
444
466
  await controls.refresh();
467
+ controlsChanged.fire();
468
+ const preserveDraft = focusedResource?.path.startsWith('/untitled-');
445
469
  if (controls.state.setup?.ready) {
446
470
  setupRevealed = false;
447
471
  await connectManaged();
448
472
  controlsChanged.fire();
449
- if (!setupHandoffComplete())
473
+ if (!preserveDraft && !setupHandoffComplete())
450
474
  await revealManaged();
451
475
  }
452
476
  else {
453
477
  setupHandoff = undefined;
454
478
  controlsChanged.fire();
455
- await revealSetup();
479
+ if (!preserveDraft)
480
+ await revealSetup();
456
481
  }
457
482
  })().finally(() => { refreshingSetup = undefined; });
458
483
  return refreshingSetup;
@@ -488,6 +513,7 @@ export async function activate(context) {
488
513
  terminal.show();
489
514
  }));
490
515
  const connectionFor = async (resource) => {
516
+ focusSession(resource);
491
517
  if (!controls) {
492
518
  const connection = frontends.get(resource.authority);
493
519
  if (!connection)
@@ -525,7 +551,7 @@ export async function activate(context) {
525
551
  controlsChanged.fire();
526
552
  return connection;
527
553
  };
528
- async function applySelection(selection, openChat = true) {
554
+ async function applySelection(selection) {
529
555
  if (!controls)
530
556
  throw new Error('This host does not expose harness or model configuration.');
531
557
  const active = managed();
@@ -534,13 +560,61 @@ export async function activate(context) {
534
560
  await controls.select(selection);
535
561
  await connectManaged();
536
562
  controlsChanged.fire();
537
- if (openChat)
538
- await revealManaged();
539
563
  }
540
- const enqueueSelection = (selection, openChat = true) => {
541
- const pending = applying.then(() => applySelection(selection, openChat));
542
- applying = pending.catch(() => undefined);
543
- return pending;
564
+ const openNewChat = async () => {
565
+ if (!controls || !granted.has('chatSessionsProvider'))
566
+ throw new Error('This host does not expose native conversation creation.');
567
+ const active = managed();
568
+ if (active?.session.descriptor?.turn_state === 'busy' || active?.session.pendingRequests.size)
569
+ throw new Error('Finish or cancel the current turn before starting a new chat.');
570
+ // Code-OSS materializes this untitled session on first send, awaiting our
571
+ // newChatSessionItemHandler before accepting/clearing the draft. Never boot
572
+ // beside the old widget and reveal another resource after it accepts input.
573
+ if (!controls.state.openSessionCommand)
574
+ throw new Error('The host cannot reveal a new conversation.');
575
+ // Use Code-OSS's untitled URI format and its existing native widget bridge.
576
+ // Blank sessions resolve content lazily, so a content-provider callback
577
+ // cannot be used to discover which resource owns the pending selection.
578
+ const draft = { resource: vscode.Uri.from({ scheme: SESSION_SCHEME, path: `/untitled-${randomUUID()}` }) };
579
+ pendingDraft = draft;
580
+ focusedResource = draft.resource;
581
+ focusedAttached = undefined;
582
+ controlsChanged.fire();
583
+ await vscode.commands.executeCommand(controls.state.openSessionCommand, draft.resource.toString());
584
+ if (pendingDraft !== draft)
585
+ throw new vscode.CancellationError();
586
+ return draft;
587
+ };
588
+ const chooseNewChat = async (pick) => {
589
+ if (pendingDraft?.choosing)
590
+ throw new Error('Finish choosing an agent before starting another chat.');
591
+ // Open before the picker: a send can beat its result across the extension-
592
+ // host boundary. Materialization awaits this exact draft's choice too.
593
+ const draft = await openNewChat();
594
+ const picking = (async () => {
595
+ const choice = await pick();
596
+ if (activeDraft() !== draft)
597
+ throw new vscode.CancellationError();
598
+ draft.choice = choice;
599
+ controlsChanged.fire();
600
+ if (choice.kind !== 'agent')
601
+ throw new vscode.CancellationError();
602
+ })();
603
+ draft.choosing = picking;
604
+ try {
605
+ await picking;
606
+ }
607
+ catch (error) {
608
+ if (!(error instanceof vscode.CancellationError))
609
+ throw error;
610
+ }
611
+ finally {
612
+ if (draft.choosing === picking)
613
+ draft.choosing = undefined;
614
+ if (draft.choice?.kind === 'signIn') {
615
+ void refreshSetup().catch(error => log.warn(`Could not refresh sign-in: ${String(error)}`)).finally(pollSetup);
616
+ }
617
+ }
544
618
  };
545
619
  // Publish capability-honest input groups only after the handoff descriptor is available.
546
620
  if (controls?.state.connection)
@@ -561,7 +635,7 @@ export async function activate(context) {
561
635
  if (selected && ['claude-code', 'codex'].includes(selected.selection.harness)) {
562
636
  // Native session-option commands remain reachable for nondelegating providers,
563
637
  // whose built-in model picker deliberately omits Manage Models. Do not advertise
564
- // hot swapping: the command opens a separate conversation after both choices.
638
+ // hot swapping: the command configures a separate native blank chat.
565
639
  // A locked selected item hides the entire native dropdown, including commands.
566
640
  const item = { id: 'settings', name: 'Model settings' };
567
641
  groups.push({ id: 'modelSettings', name: 'Model', items: [item], selected: item,
@@ -581,17 +655,45 @@ export async function activate(context) {
581
655
  create: async (inputState) => {
582
656
  if (!controls)
583
657
  throw new Error('The host does not expose conversation creation.');
584
- await enqueueSelection({ harness: controls.state.selection.harness, model: '', effort: '' }, false);
658
+ const draft = activeDraft();
659
+ await draft?.choosing;
660
+ if (draft && activeDraft() !== draft)
661
+ throw new vscode.CancellationError();
662
+ await controls.refresh();
663
+ controlsChanged.fire();
664
+ const choice = draft?.choice;
665
+ const loginHarness = choice?.kind === 'signIn' ? choice.harness : undefined;
666
+ if (loginHarness && !controls.state.harnesses.some(h => h.id === loginHarness && h.autoStart)) {
667
+ throw new Error(`Finish signing in with ${harnessDisplayName(loginHarness)} before sending. Your draft is kept.`);
668
+ }
669
+ if (choice?.kind !== 'agent' && controls.state.setup && !controls.state.setup.ready) {
670
+ throw new Error('Sign in before sending. Your draft is kept; use Check again after signing in.');
671
+ }
672
+ const selection = choice?.kind === 'agent' ? choice.selection
673
+ : { harness: loginHarness ?? controls.state.selection.harness, model: '', effort: '' };
674
+ const pending = applying.then(() => applySelection(selection));
675
+ applying = pending.catch(() => undefined);
676
+ await pending;
585
677
  const connection = frontends.get(controls.state.activeSession);
586
678
  savePermission(connection.id, turnPermission(inputState.groups.find(group => group.id === 'permissions')?.selected?.id));
679
+ if (pendingDraft === draft) {
680
+ pendingDraft = undefined;
681
+ controlsChanged.fire();
682
+ }
587
683
  return connection;
588
684
  },
589
685
  resolve: connectionFor,
686
+ focus: focusSession,
590
687
  attached: (connection) => attachedIds.has(connection.id),
591
688
  catalogue: () => controls?.state.sessions ?? [],
592
689
  bindings: boundId,
593
690
  };
594
691
  context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.activateSession', async (resource) => { await connectionFor(vscode.Uri.parse(resource)); }));
692
+ // The host's native focus event is authoritative even when a cached model
693
+ // skips content resolution. Untitled/non-harness focus never starts a runtime.
694
+ context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.focusSession', (resource) => {
695
+ focusSession(resource ? vscode.Uri.parse(resource) : undefined);
696
+ }));
595
697
  context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.openSession', async (id) => {
596
698
  await connectionFor(vscode.Uri.from({ scheme: SESSION_SCHEME, authority: id, path: '/conversation' }));
597
699
  await revealManaged();
@@ -607,7 +709,7 @@ export async function activate(context) {
607
709
  return currentOptions();
608
710
  }
609
711
  if (supplied && typeof supplied.harness === 'string') {
610
- await enqueueSelection(supplied);
712
+ await chooseNewChat(async () => ({ kind: 'agent', selection: supplied }));
611
713
  return currentOptions();
612
714
  }
613
715
  if (!controls) {
@@ -632,18 +734,20 @@ export async function activate(context) {
632
734
  }
633
735
  // A sole sign-in option still requires a deliberate click before opening its terminal.
634
736
  // The official choice must not disable the existing sole-local-agent shortcut.
635
- const harness = localChoices.length === 1 && localChoices[0].autoStart ? localChoices[0] : await vscode.window.showQuickPick(choices, { title: 'New chat · Agent', placeHolder: 'Choose your coding agent. Model and reasoning use its defaults.' });
636
- if (!harness)
637
- return;
638
- if (harness.id === CHATGPT_SIGN_IN) {
639
- await vscode.commands.executeCommand(CHATGPT_SIGN_IN);
640
- return;
641
- }
642
- if (harness.login) {
643
- await vscode.commands.executeCommand('supercode.frontend.setup', 'login', harness.id);
644
- return;
645
- }
646
- await enqueueSelection({ harness: harness.id, model: '', effort: '' });
737
+ await chooseNewChat(async () => {
738
+ const harness = localChoices.length === 1 && localChoices[0].autoStart ? localChoices[0] : await vscode.window.showQuickPick(choices, { title: 'New chat · Agent', placeHolder: 'Choose your coding agent. Model and reasoning use its defaults.' });
739
+ if (!harness)
740
+ return { kind: 'cancelled' };
741
+ if (harness.id === CHATGPT_SIGN_IN) {
742
+ await vscode.commands.executeCommand(CHATGPT_SIGN_IN);
743
+ return { kind: 'signIn' };
744
+ }
745
+ if (harness.login) {
746
+ await vscode.commands.executeCommand('supercode.frontend.setup', 'login', harness.id);
747
+ return { kind: 'signIn', harness: harness.id };
748
+ }
749
+ return { kind: 'agent', selection: { harness: harness.id, model: '', effort: '' } };
750
+ });
647
751
  }));
648
752
  context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.configureModel', async (resource, sessionId) => {
649
753
  if (!controls || focusedAttached)
@@ -660,25 +764,25 @@ export async function activate(context) {
660
764
  const selection = { ...selected.selection };
661
765
  if (!['claude-code', 'codex'].includes(selection.harness))
662
766
  throw new Error('This harness uses its own model defaults; configuration is not advertised.');
663
- if (['claude-code', 'codex'].includes(selection.harness)) {
767
+ await chooseNewChat(async () => {
664
768
  const choices = controls.state.modelsByHarness?.[selection.harness] ?? [{ id: '', name: 'Harness default' }];
665
769
  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.' });
666
770
  if (!picked)
667
- return;
771
+ return { kind: 'cancelled' };
668
772
  if (picked.id === '__custom__') {
669
773
  const model = await vscode.window.showInputBox({ title: 'New chat · Exact model', prompt: 'Model ID or CLI alias supported by this harness account.', value: selection.model });
670
774
  if (model === undefined)
671
- return;
775
+ return { kind: 'cancelled' };
672
776
  selection.model = model.trim();
673
777
  }
674
778
  else
675
779
  selection.model = picked.id;
676
780
  const effort = await vscode.window.showQuickPick([{ label: 'Harness default', id: '' }, ...['low', 'medium', 'high'].map(id => ({ label: id, id }))], { title: 'New chat · Reasoning effort' });
677
781
  if (!effort)
678
- return;
782
+ return { kind: 'cancelled' };
679
783
  selection.effort = effort.id;
680
- }
681
- await enqueueSelection(selection);
784
+ return { kind: 'agent', selection };
785
+ });
682
786
  }));
683
787
  context.subscriptions.push({ dispose: () => frontends.dispose() });
684
788
  const runTurnInto = async (connection, prompt, stream, token, approval) => {
@@ -804,8 +908,10 @@ export async function activate(context) {
804
908
  if (welcomeValue === welcome?.value)
805
909
  return;
806
910
  welcomeValue = welcome?.value;
911
+ // Metadata updates merge across RPC; undefined can leave the previous
912
+ // sign-in message behind. An explicit empty value clears it in place.
807
913
  for (const participant of participants)
808
- participant.additionalWelcomeMessage = welcome;
914
+ participant.additionalWelcomeMessage = welcome ?? new vscode.MarkdownString('');
809
915
  };
810
916
  updateWelcome();
811
917
  context.subscriptions.push(controlsChanged.event(updateWelcome), frontends.onDidChange(updateWelcome));
@@ -817,7 +923,7 @@ export async function activate(context) {
817
923
  // canDelegate is disabled to avoid a generic workbench-owned registration.
818
924
  // A participant names the harness; the native model picker names the model.
819
925
  // Keep the author stable when a model changes during request dispatch.
820
- const participantName = () => harnessDisplayName(focusedAttached?.session.descriptor?.source_harness ?? controls?.state.selection.harness ?? managed()?.session.descriptor?.source_harness);
926
+ const participantName = () => harnessDisplayName(focusedAttached?.session.descriptor?.source_harness ?? draftSelection()?.harness ?? controls?.state.selection.harness ?? managed()?.session.descriptor?.source_harness);
821
927
  const participantKey = () => `${participantName()}/${identity().provider ?? ''}`;
822
928
  const makeParticipant = () => {
823
929
  const current = { name: participantName() };
@@ -1067,9 +1173,12 @@ export async function activate(context) {
1067
1173
  let setupTimer;
1068
1174
  let setupDisposed = false;
1069
1175
  const pollSetup = () => {
1070
- if (setupDisposed || !controls?.state.setup || (controls.state.setup.ready && setupHandoffComplete()))
1176
+ const choice = activeDraft()?.choice;
1177
+ const awaitingLogin = choice?.kind === 'signIn' && choice.harness && !controls?.state.harnesses.some(h => h.id === choice.harness && h.autoStart);
1178
+ if (setupTimer || setupDisposed || !controls?.state.setup || (controls.state.setup.ready && setupHandoffComplete() && !awaitingLogin))
1071
1179
  return;
1072
1180
  setupTimer = setTimeout(() => {
1181
+ setupTimer = undefined;
1073
1182
  void refreshSetup().catch(error => log.warn(`Could not refresh sign-in: ${String(error)}`)).finally(pollSetup);
1074
1183
  }, 3000);
1075
1184
  };
@@ -1261,6 +1370,7 @@ function registerSessions(context, frontends, participant, granted, handler, con
1261
1370
  }
1262
1371
  context.subscriptions.push(vscode.chat.registerChatSessionContentProvider(SESSION_SCHEME, {
1263
1372
  provideChatSessionContent: async (resource, _token, { inputState }) => {
1373
+ controls?.focus(resource);
1264
1374
  const boundHandler = (request, chatContext, stream, token) => handler(request, { ...chatContext, chatSessionContext: { chatSessionItem: { resource, label: 'Conversation' } } }, stream, token);
1265
1375
  // Only a native untitled resource can create a conversation. Existing
1266
1376
  // resources must resolve their exact saved binding; there is no fallback.
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.28",
4
+ "version": "0.1.29",
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",