@volter/supercode-frontend-vscode 0.1.50 → 0.1.51

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,74 +11,55 @@ package or npm.
11
11
 
12
12
  ## Native chat controls
13
13
 
14
- When the host has an extension gallery and the workbench install command, setup
15
- also offers **Sign in with ChatGPT** using the official `openai.chatgpt` extension.
16
- Only a click installs it, using `workbench.extensions.installExtension` with
17
- `enable: true` so the workbench waits for registration in the running extension
18
- registry; subsequent clicks reuse it. `chatgpt.openSidebar` then opens its own sign-in view
19
- (the extension does not contribute a public sign-in command). No terminal or
20
- Supercode settings are involved in this path. The choice also appears whenever
21
- the agent picker is shown; it does not interrupt automatic startup or the
22
- sole-local-agent New Session shortcut.
23
- Gallery detection reads the host's `product.json` through `vscode.workspace.fs`
24
- at `vscode.env.appRoot`; hosts without an inspectable gallery configuration hide
25
- the choice. Neither the extension nor its credentials are bundled or copied.
26
-
27
- The welcome's primary action uses `supercode.frontend.beginChatGPTSignIn`:
28
- it starts one retained install/sidebar operation and returns a copied receipt
29
- with its id, timestamps and current phase. This is a launch receipt, not proof
30
- that installation, sidebar presentation or authentication succeeded. A second
31
- begin while that operation is running returns the same id and starts no extra
32
- installation. `supercode.frontend.chatGPTSignInStatus` reads its current phase
33
- locally without waiting for host readiness or installation; pass the operation
34
- id to receive only that operation (a mismatched id returns null).
35
- Phases name the selected work before its awaited command: `checking-gallery`,
36
- `installing-extension`, `opening-sidebar`, then `completed` or `failed`.
37
- Completed means the official command sequence fulfilled, not signed in. Failures
38
- remain in operation status and the existing native error notification/output.
39
- Only the current or most recent operation is retained, for this extension-window
40
- lifetime; reload does not restore or automatically retry it. Runtime status
41
- inspection also includes this snapshot as `chatGPTSignIn`.
42
- The existing `supercode.frontend.signInWithChatGPT` still awaits the same work
43
- with its existing error-notification/fulfillment behavior, and the agent picker
44
- continues to use it. Ordinary relay budgets and draft guards are unchanged.
45
-
46
- The optional host-controls state can include `setup: { ready, actions, reason,
47
- cwd }`. Each action supplies `kind` (`login` or `install`), `harness`, `name` and
48
- the exact `command` revalidated before execution. On a signed-out first open,
49
- Chat shows its native empty-session welcome: one primary **Sign in with ChatGPT**
50
- button for the official extension, followed by **Supercode · local agents** and
51
- each installed agent's own sign-in link, labeled with its reported name. The
52
- `defaultChatParticipant` proposal supplies `additionalWelcomeMessage`; the pinned
53
- Code-OSS `ChatViewWelcomePart`, with the editor's native-welcome-button overlay,
54
- turns the standalone trusted command link into a native button. Stock hosts
55
- render it as an ordinary command link; product release requires the overlay.
56
- Setup creates no transcript turns, status sessions or protocol markers, and displays no terminal commands, filesystem paths or runtime banner.
57
- No installed agents means an explanatory empty state, without invented choices.
58
- Clicking asks the host's authenticated `/setup` door to revalidate `{ kind,
59
- harness }` and return the action plus a structured `{ program, arguments, cwd, env? }`
60
- terminal launch. Absolute executables and an optional host-authored PATH keep
61
- the terminal on the same installation and discovery path as the host's probe.
62
- It runs directly as the integrated terminal's process, without typing a shell
63
- command; credentials and
64
- browser interaction belong to the harness. Failed or cancelled processes can be
65
- retried, and closing a terminal never counts as successful authentication.
66
-
67
- While setup is not ready, Chat refreshes `/state` every three seconds and exposes
68
- **Check again**. When the host returns a ready runtime connection, it attaches
69
- and reveals that conversation without a reload. Polling only finishes after both
70
- attachment and the host's guarded readiness reveal command succeed for that same
71
- session and runtime URL. Readiness/connection work completes before awaiting the
72
- native reveal, which may call back into activation. The optional
73
- `revealReadySessionCommand` checks the observed native conversation throughout
74
- queued navigation; a newer draft or selection cancels that automatic reveal.
75
- Hosts without that capability leave the handoff incomplete and direct the person
76
- to open the conversation from history. A transient attach/reveal failure stays pending and is retried;
77
- **Check again** retries that handoff even when the host already reports ready.
78
- Changed inventory updates welcome metadata in place, without opening another
79
- session or moving focus. Ready connections clear the welcome and reveal the
80
- actual conversation. Hosts without these optional fields keep the existing
81
- runtime-attachment commands.
14
+ When no coding agent is ready, native Chat offers **Sign in with ChatGPT**
15
+ (Codex) and **Sign in with Claude** (Claude Code), with the subtitle
16
+ “Use the AI plan you already pay for.” Both use the coding agent in this Chat.
17
+ A click starts one install/sign-in operation for that agent. The host returns
18
+ its exact structured terminal launch through the authenticated `/setup` door;
19
+ a missing agent installs from its vendor package, then the same click continues
20
+ to login. The frontend never interprets repair prose as a command.
21
+
22
+ The host's `/state` supplies `setup: {ready, actions, agents, reason, cwd}`.
23
+ Each agent supplies `{harness, name, installed, signedIn, account}`; an unknown
24
+ account is `null`. Each action supplies `{kind, harness, name, command}` with
25
+ `kind` equal to `install` or `login`. The host revalidates the chosen action
26
+ before returning `{program, arguments, cwd, env?}`. Credentials remain with
27
+ the coding agent; browser sign-in requires the person's approval.
28
+
29
+ The versioned local `supercode.frontend.setupState` command returns
30
+ `{version:1, visible, title, subtitle, footnote, composerPlaceholder,
31
+ composerEnabled, rows, signedInNotice}`. Rows include both providers with their
32
+ installation/login state, `phase` (`idle`, `installing`, `signing-in`, `failed`,
33
+ `signed-in`), `buttonLabel`, `caption`, `busy`, and `operationId`. A renderer
34
+ uses `visible` and `composerEnabled`, rather than the mere presence of rows,
35
+ to decide whether the welcome or disabled composer belongs in the current
36
+ view. This local read starts no process and makes no host request.
37
+
38
+ `supercode.frontend.beginSetup(harness)` starts work and returns its receipt.
39
+ Duplicate clicks on a busy agent return the same operation. The other agent
40
+ remains usable. `supercode.frontend.cancelSetup(harness, operationId)` cancels
41
+ only the terminal that operation started; a stale operation id does nothing.
42
+ Cancel returns to idle, while process failure shows **Try again** and
43
+ **Sign-in didn't finish.** The full reason is written to the Chat output log.
44
+ A process finishing never proves authentication: the host's native inventory
45
+ must report a signed-in account. Once it does, readiness polling attaches
46
+ and reveals the conversation without requiring another click, including when
47
+ sign-in finishes in a separate terminal.
48
+
49
+ Welcome metadata changes in place on each phase change. Cyclotron's paired
50
+ native renderer uses it to show primary/secondary buttons, disabled spinners,
51
+ one caption/Cancel/error slot per provider, and **Sign in to start a chat**
52
+ in the disabled composer. Stock hosts retain native Markdown links. The
53
+ editor's renderer/build is a separate integration; the frontend package alone
54
+ does not claim those richer controls are installed in every host. A successful
55
+ explicit sign-in publishes **Signed in with ChatGPT** or **Signed in with
56
+ Claude** above the newly opened Chat.
57
+
58
+ Setup creates no transcript turns. The guarded readiness reveal preserves a
59
+ newer draft or selection; a welcome-owned empty draft can give way to the first
60
+ ready conversation. Hosts without the guarded reveal command report the
61
+ limitation. Passive inventory polling retries a transient handoff failure until
62
+ attachment and reveal both succeed for the same conversation.
82
63
 
83
64
  A bundling host can expose a private lifecycle endpoint through
84
65
  `SUPERCODE_FRONTEND_HOST_URL` and `SUPERCODE_FRONTEND_HOST_CREDENTIAL_FILE`.
@@ -89,17 +70,15 @@ and returns its new scoped connection. The host owns validation, process lifetim
89
70
  persistence, and failure handling. Prompts and approvals still use the generated frontend client.
90
71
  The contract is in `src/host-controls.ts`.
91
72
 
92
- With that host, **New Session** / **Chat: New Chat: Choose Agent** asks only
93
- which startable agent to use. It refreshes the host's available choices first;
94
- with one ready local agent and no pending native sign-in choices it opens
95
- immediately, even when the official ChatGPT choice is available. Otherwise the
96
- picker includes the official choice on supported hosts; with no choices it returns
97
- to setup. Picking an agent sends model
98
- and reasoning as empty values, leaving both at the harness's own defaults,
99
- even if the previous conversation used explicit settings. Each agent whose model
100
- and reasoning a chat can set (Claude Code, Codex) is also listed in the picker as **<agent>: model
101
- and reasoning…**, which goes on to the model and effort choices below. Cancelling the agent
102
- picker starts no backend conversation.
73
+ With that host, **New Session** / **Chat: New Chat: Choose Agent** lists all
74
+ coding agents, including Codex and Claude Code when missing or signed out.
75
+ Their rows say **Signed in with ChatGPT/Claude** or **Sign in with
76
+ ChatGPT/Claude…**. A sign-in row starts the same install/login operation as the
77
+ welcome. Selecting a ready agent leaves model and reasoning at its defaults;
78
+ the separate **<agent>: model and reasoning…** rows retain explicit settings.
79
+ The picker is titled **New chat**, with **Choose your coding agent** as its
80
+ placeholder. Cancelling it starts no backend conversation. Initial automatic
81
+ agent selection and last-used selection remain the host's responsibility.
103
82
 
104
83
  The native blank chat opens before the picker appears. First send waits for the
105
84
  choice and the host's new connection through Code-OSS's session-creation handler;
@@ -111,7 +90,7 @@ paths. A failed startup leaves the draft available to retry.
111
90
 
112
91
  The pending agent/model choice belongs to that blank chat's untitled resource
113
92
  and is retired when another conversation opens. Cancelling or opening sign-in
114
- does not choose an agent. Readiness polling and **Check again** still discover
93
+ does not choose an agent. Readiness polling continues to discover
115
94
  agents while a draft is open, updating the welcome and model catalogue without
116
95
  replacing its widget or input. First send refreshes readiness; after a native
117
96
  sign-in choice it uses that agent once ready, otherwise it keeps the draft with
@@ -1,25 +1,40 @@
1
- /** Native Chat presentation; the host supplies inventory and terminal actions. */
1
+ /** Native Chat setup. The host owns inventory, commands and provider credentials. */
2
2
  import * as vscode from 'vscode';
3
- import type { ChatSetupState } from './host-controls.js';
4
- export declare const CHATGPT_SIGN_IN = "supercode.frontend.signInWithChatGPT";
5
- export declare const CHATGPT_SIGN_IN_BEGIN = "supercode.frontend.beginChatGPTSignIn";
6
- export declare const CHATGPT_SIGN_IN_STATUS = "supercode.frontend.chatGPTSignInStatus";
7
- export type ChatGPTSignInWorkPhase = 'checking-gallery' | 'installing-extension' | 'opening-sidebar';
8
- export interface ChatGPTSignInOperation {
3
+ import type { ChatSetupAgent, HostState } from './host-controls.js';
4
+ export declare const SETUP_BEGIN = "supercode.frontend.beginSetup";
5
+ export declare const SETUP_CANCEL = "supercode.frontend.cancelSetup";
6
+ export declare const SETUP_STATE = "supercode.frontend.setupState";
7
+ export type ChatSetupPhase = 'idle' | 'installing' | 'signing-in' | 'failed' | 'signed-in';
8
+ export interface ChatSetupOperation {
9
9
  readonly id: string;
10
- readonly phase: ChatGPTSignInWorkPhase | 'completed' | 'failed';
10
+ readonly harness: string;
11
+ readonly phase: ChatSetupPhase;
11
12
  readonly startedAt: number;
12
13
  readonly updatedAt: number;
13
- readonly finishedAt?: number;
14
14
  }
15
- /** The install command also exists in gallery-less Code-OSS. Fail closed when
16
- * the host's product configuration cannot be inspected through the VS Code API. */
17
- export declare function chatGPTAvailable(): Promise<boolean>;
18
- /** Only called by an explicit click. The gallery and extension own installation
19
- * and authentication; there is no terminal or provider credential handling here. */
20
- export declare function signInWithChatGPT(report?: (phase: ChatGPTSignInWorkPhase) => void): Promise<void>;
21
- /** Empty-chat welcome content, rendered by Code-OSS's ChatViewWelcomePart.
22
- * Only the official extension gets the primary button; native local sign-in
23
- * actions remain ordinary links. Commands and host diagnostic paths stay out
24
- * of the welcome, and the host still revalidates every action on click. */
25
- export declare function setupWelcome(setup: ChatSetupState, chatGPT?: boolean): vscode.MarkdownString;
15
+ export interface ChatSetupProvider extends ChatSetupAgent {
16
+ provider: string;
17
+ phase: ChatSetupPhase;
18
+ buttonLabel: string;
19
+ caption: string;
20
+ busy: boolean;
21
+ operationId: string | null;
22
+ }
23
+ export interface ChatSetupPresentation {
24
+ version: 1;
25
+ visible: boolean;
26
+ title: string;
27
+ subtitle: string;
28
+ footnote: string;
29
+ composerPlaceholder: string;
30
+ composerEnabled: boolean;
31
+ rows: ChatSetupProvider[];
32
+ signedInNotice: string | null;
33
+ }
34
+ /** Older hosts can describe readiness through actions and startable choices.
35
+ * An absent account stays unknown; a launched process never establishes login. */
36
+ export declare function setupAgents(state: HostState): ChatSetupAgent[];
37
+ export declare function setupPresentation(state: HostState, operations: ReadonlyMap<string, ChatSetupOperation>, notice?: string): ChatSetupPresentation;
38
+ /** Stock hosts keep native Markdown. Cyclotron reads the same state through
39
+ * SETUP_STATE to render disabled buttons, progress and the composer in place. */
40
+ export declare function setupWelcome(presentation: ChatSetupPresentation): vscode.MarkdownString;
@@ -1,69 +1,65 @@
1
- /** Native Chat presentation; the host supplies inventory and terminal actions. */
1
+ /** Native Chat setup. The host owns inventory, commands and provider credentials. */
2
2
  import * as vscode from 'vscode';
3
- export const CHATGPT_SIGN_IN = 'supercode.frontend.signInWithChatGPT';
4
- export const CHATGPT_SIGN_IN_BEGIN = 'supercode.frontend.beginChatGPTSignIn';
5
- export const CHATGPT_SIGN_IN_STATUS = 'supercode.frontend.chatGPTSignInStatus';
6
- const CODEX_EXTENSION = 'openai.chatgpt';
7
- const INSTALL_EXTENSION = 'workbench.extensions.installExtension';
8
- /** The install command also exists in gallery-less Code-OSS. Fail closed when
9
- * the host's product configuration cannot be inspected through the VS Code API. */
10
- export async function chatGPTAvailable() {
11
- try {
12
- if (!vscode.env.appRoot || !(await vscode.commands.getCommands(true)).includes(INSTALL_EXTENSION))
13
- return false;
14
- const bytes = await vscode.workspace.fs.readFile(vscode.Uri.joinPath(vscode.Uri.file(vscode.env.appRoot), 'product.json'));
15
- const product = JSON.parse(Buffer.from(bytes).toString('utf8'));
16
- const serviceUrl = product?.extensionsGallery?.serviceUrl;
17
- return typeof serviceUrl === 'string' && /^https?:\/\//.test(serviceUrl);
18
- }
19
- catch {
20
- return false;
21
- }
3
+ export const SETUP_BEGIN = 'supercode.frontend.beginSetup';
4
+ export const SETUP_CANCEL = 'supercode.frontend.cancelSetup';
5
+ export const SETUP_STATE = 'supercode.frontend.setupState';
6
+ const PROVIDERS = [
7
+ { harness: 'codex', name: 'Codex', provider: 'ChatGPT', caption: 'Runs Codex · ChatGPT Plus, Pro or Business' },
8
+ { harness: 'claude-code', name: 'Claude Code', provider: 'Claude', caption: 'Runs Claude Code · Claude Pro or Max' },
9
+ ];
10
+ /** Older hosts can describe readiness through actions and startable choices.
11
+ * An absent account stays unknown; a launched process never establishes login. */
12
+ export function setupAgents(state) {
13
+ if (state.setup?.agents)
14
+ return state.setup.agents.map(agent => ({ ...agent }));
15
+ const ids = new Set([...PROVIDERS.map(p => p.harness), ...state.harnesses.map(h => h.id), ...(state.setup?.actions ?? []).map(a => a.harness)]);
16
+ return [...ids].map(harness => {
17
+ const choice = state.harnesses.find(h => h.id === harness);
18
+ const actions = state.setup?.actions.filter(a => a.harness === harness) ?? [];
19
+ return { harness, name: choice?.name ?? PROVIDERS.find(p => p.harness === harness)?.name ?? actions[0]?.name ?? harness,
20
+ installed: !actions.some(a => a.kind === 'install') && Boolean(choice || actions.some(a => a.kind === 'login')),
21
+ signedIn: choice?.autoStart === true && !actions.some(a => a.kind === 'login'), account: null };
22
+ });
22
23
  }
23
- /** Only called by an explicit click. The gallery and extension own installation
24
- * and authentication; there is no terminal or provider credential handling here. */
25
- export async function signInWithChatGPT(report) {
26
- report?.('checking-gallery');
27
- if (!await chatGPTAvailable())
28
- throw new Error('This host does not provide an extension gallery.');
29
- if (!vscode.extensions.getExtension(CODEX_EXTENSION)) {
30
- // enable makes the workbench await the running extension registry, not
31
- // just the installed inventory, before we dispatch the contributed command.
32
- report?.('installing-extension');
33
- await vscode.commands.executeCommand(INSTALL_EXTENSION, CODEX_EXTENSION, { enable: true });
34
- }
35
- // openai.chatgpt contributes this command, not a public sign-in command.
36
- // Revealing its sidebar presents its own Sign in with ChatGPT when signed out.
37
- report?.('opening-sidebar');
38
- await vscode.commands.executeCommand('chatgpt.openSidebar');
24
+ export function setupPresentation(state, operations, notice) {
25
+ const agents = setupAgents(state);
26
+ const providers = PROVIDERS.map(provider => {
27
+ const agent = agents.find(a => a.harness === provider.harness) ?? { harness: provider.harness, name: provider.name, installed: false, signedIn: false, account: null };
28
+ const operation = operations.get(provider.harness);
29
+ const phase = agent.signedIn ? 'signed-in' : operation?.phase ?? 'idle';
30
+ const busy = phase === 'installing' || phase === 'signing-in';
31
+ return { ...agent, provider: provider.provider, phase,
32
+ buttonLabel: phase === 'installing' ? `Installing ${provider.name}…` : phase === 'signing-in' ? 'Finish signing in in your browser…'
33
+ : phase === 'failed' ? 'Try again' : `Sign in with ${provider.provider}`,
34
+ caption: busy ? 'Cancel' : phase === 'failed' ? "Sign-in didn't finish." : provider.caption,
35
+ busy, operationId: operation?.id ?? null };
36
+ });
37
+ const visible = Boolean(state.setup && !state.setup.ready);
38
+ return { version: 1, visible, title: 'Chat', subtitle: 'Use the AI plan you already pay for.',
39
+ footnote: 'Runs on this computer with your own account.', composerPlaceholder: visible ? 'Sign in to start a chat' : 'Ask a question or describe a task',
40
+ composerEnabled: !visible, rows: providers, signedInNotice: notice ? `Signed in with ${notice}` : null };
39
41
  }
40
- /** Empty-chat welcome content, rendered by Code-OSS's ChatViewWelcomePart.
41
- * Only the official extension gets the primary button; native local sign-in
42
- * actions remain ordinary links. Commands and host diagnostic paths stay out
43
- * of the welcome, and the host still revalidates every action on click. */
44
- export function setupWelcome(setup, chatGPT = false) {
42
+ /** Stock hosts keep native Markdown. Cyclotron reads the same state through
43
+ * SETUP_STATE to render disabled buttons, progress and the composer in place. */
44
+ export function setupWelcome(presentation) {
45
45
  const welcome = new vscode.MarkdownString();
46
- welcome.isTrusted = { enabledCommands: [CHATGPT_SIGN_IN_BEGIN, 'supercode.frontend.setup', 'supercode.frontend.refreshSetup'] };
47
- if (chatGPT) {
48
- welcome.appendMarkdown(`[Sign in with ChatGPT](command:${CHATGPT_SIGN_IN_BEGIN})\n\n`);
49
- welcome.appendMarkdown('Use the official Codex extension.');
46
+ welcome.isTrusted = { enabledCommands: [SETUP_BEGIN, SETUP_CANCEL] };
47
+ welcome.appendMarkdown(`${presentation.subtitle}\n\n`);
48
+ for (const provider of presentation.rows) {
49
+ const args = encodeURIComponent(JSON.stringify([provider.harness]));
50
+ if (provider.busy)
51
+ welcome.appendText(provider.buttonLabel);
52
+ else
53
+ welcome.appendMarkdown(`[${provider.buttonLabel}](command:${SETUP_BEGIN}?${args})`);
50
54
  welcome.appendMarkdown('\n\n');
51
- }
52
- welcome.appendMarkdown('**Supercode · local agents**\n\n');
53
- const login = setup.actions.filter(action => action.kind === 'login');
54
- if (login.length) {
55
- for (const action of login) {
56
- const title = new vscode.MarkdownString().appendText(`Sign in with ${action.name}`).value;
57
- const args = encodeURIComponent(JSON.stringify([action.kind, action.harness]));
58
- welcome.appendMarkdown(`- [${title}](command:supercode.frontend.setup?${args})\n`);
55
+ if (provider.busy) {
56
+ const cancel = encodeURIComponent(JSON.stringify([provider.harness, provider.operationId]));
57
+ welcome.appendMarkdown(`[Cancel](command:${SETUP_CANCEL}?${cancel})`);
59
58
  }
60
- welcome.appendMarkdown('\n');
61
- welcome.appendMarkdown('Each agent opens its own sign-in.');
62
- }
63
- else {
64
- welcome.appendMarkdown('No local agents are ready. Install an agent on this machine to use it with Supercode.');
59
+ else
60
+ welcome.appendText(provider.caption);
61
+ welcome.appendMarkdown('\n\n');
65
62
  }
66
- // Inline so it remains a secondary link, not another primary button.
67
- welcome.appendMarkdown('\n\nAlready signed in? [Check again](command:supercode.frontend.refreshSetup).');
63
+ welcome.appendText(presentation.footnote);
68
64
  return welcome;
69
65
  }
package/dist/extension.js CHANGED
@@ -2,7 +2,8 @@ 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 { CHATGPT_SIGN_IN, CHATGPT_SIGN_IN_BEGIN, CHATGPT_SIGN_IN_STATUS, chatGPTAvailable, setupWelcome, signInWithChatGPT } from './chat-setup.js';
5
+ import { SETUP_BEGIN, SETUP_CANCEL, SETUP_STATE, setupAgents, setupPresentation, setupWelcome } from './chat-setup.js';
6
+ import { ChatSetupFlow } from './setup-flow.js';
6
7
  import * as vscode from 'vscode';
7
8
  import { createFrontendClient } from './client.js';
8
9
  import { readCredentialFile } from './credential.js';
@@ -345,38 +346,6 @@ export async function activate(context) {
345
346
  }
346
347
  }
347
348
  const controls = await ChatHostControls.fromEnvironment();
348
- const offerChatGPT = await chatGPTAvailable();
349
- let signingInWithChatGPT;
350
- let chatGPTSignInOperation;
351
- const signInSnapshot = () => chatGPTSignInOperation ? { ...chatGPTSignInOperation } : null;
352
- const startChatGPTSignIn = () => {
353
- if (signingInWithChatGPT)
354
- return signingInWithChatGPT;
355
- const startedAt = Date.now();
356
- const operation = { id: randomUUID(), startedAt };
357
- const update = (phase) => {
358
- const updatedAt = Date.now();
359
- chatGPTSignInOperation = { ...operation, phase, updatedAt,
360
- ...(phase === 'completed' || phase === 'failed' ? { finishedAt: updatedAt } : {}) };
361
- };
362
- signingInWithChatGPT = signInWithChatGPT(phase => update(phase)).then(() => {
363
- // Fulfilled install/sidebar commands do not establish authentication.
364
- update('completed');
365
- }).catch(error => {
366
- update('failed');
367
- log.error(`Could not open ChatGPT sign-in: ${String(error)}`);
368
- void vscode.window.showErrorMessage(`Could not open ChatGPT sign-in: ${error instanceof Error ? error.message : String(error)}`);
369
- }).finally(() => { signingInWithChatGPT = undefined; });
370
- return signingInWithChatGPT;
371
- };
372
- context.subscriptions.push(
373
- // Preserve the completion-awaiting command used by the agent picker.
374
- vscode.commands.registerCommand(CHATGPT_SIGN_IN, startChatGPTSignIn),
375
- // A click starts one retained task. Its receipt is not install/login success.
376
- vscode.commands.registerCommand(CHATGPT_SIGN_IN_BEGIN, () => {
377
- void startChatGPTSignIn();
378
- return signInSnapshot();
379
- }), vscode.commands.registerCommand(CHATGPT_SIGN_IN_STATUS, (operationId) => operationId === undefined || operationId === chatGPTSignInOperation?.id ? signInSnapshot() : null));
380
349
  const controlsChanged = new vscode.EventEmitter();
381
350
  context.subscriptions.push(controlsChanged);
382
351
  // A person's explicit picker choice: per conversation once it is bound, and for the new chat before then.
@@ -628,6 +597,7 @@ export async function activate(context) {
628
597
  setupHandoff = { id, baseUrl: connection.baseUrl };
629
598
  };
630
599
  let setupRevealed = false;
600
+ let welcomeWasVisible = controls?.state.setup?.ready === false;
631
601
  const revealSetup = async () => {
632
602
  if (!granted.has('chatSessionsProvider') || !controls?.state.setup || controls.state.setup.ready || setupRevealed)
633
603
  return;
@@ -636,6 +606,13 @@ export async function activate(context) {
636
606
  await vscode.commands.executeCommand(`workbench.action.chat.openNewChatSessionInPlace.${SESSION_SCHEME}`, 'sidebar');
637
607
  setupRevealed = true;
638
608
  };
609
+ let refreshingInventory;
610
+ const refreshInventory = () => {
611
+ if (!controls)
612
+ return Promise.resolve();
613
+ refreshingInventory ??= controls.refresh().then(() => { controlsChanged.fire(); }).finally(() => { refreshingInventory = undefined; });
614
+ return refreshingInventory;
615
+ };
639
616
  let refreshingSetup;
640
617
  const refreshSetup = () => {
641
618
  if (refreshingSetup)
@@ -645,9 +622,10 @@ export async function activate(context) {
645
622
  return;
646
623
  const generation = focusGeneration;
647
624
  const focusResource = focusedResource?.toString() ?? null;
625
+ welcomeWasVisible ||= controls.state.setup?.ready === false;
648
626
  // Discover and publish readiness even while a blank chat holds a draft.
649
627
  // Only the automatic reveal is suppressed; discovery must never stall.
650
- await controls.refresh();
628
+ await refreshInventory();
651
629
  const state = controls.state;
652
630
  controlsChanged.fire();
653
631
  if (state.setup?.ready || (!state.setup && state.connection?.SUPERCODE_FRONTEND_URL)) {
@@ -672,7 +650,7 @@ export async function activate(context) {
672
650
  const live = target?.kind === 'managed' && Boolean(frontends.get(target.id)?.session.pendingRequests.size
673
651
  || frontends.get(target.id)?.session.descriptor?.turn_state === 'busy');
674
652
  if (!target || !controls || applying !== applied || target.generation !== focusGeneration
675
- || (focusedResource?.path.startsWith('/untitled-') && !live))
653
+ || (focusedResource?.path.startsWith('/untitled-') && !live && !welcomeWasVisible))
676
654
  return;
677
655
  if (target.kind === 'managed') {
678
656
  if ((controls.state.setup && !controls.state.setup.ready) || controls.state.activeSession !== target.id
@@ -680,45 +658,74 @@ export async function activate(context) {
680
658
  return;
681
659
  if (!setupHandoffComplete())
682
660
  await revealManaged(target, { resource: target.focusResource });
661
+ if (setupHandoffComplete())
662
+ welcomeWasVisible = false;
683
663
  }
684
664
  else if (!controls.state.setup?.ready) {
685
665
  await revealSetup();
686
666
  }
687
667
  });
688
- // Coalesce Check again/polling through actual reveal completion or refusal.
668
+ // Coalesce inventory refresh through actual reveal completion or refusal.
689
669
  refreshingSetup = revealed.finally(() => { refreshingSetup = undefined; });
690
670
  return refreshingSetup;
691
671
  };
692
672
  const setupTerminals = new Map();
693
673
  context.subscriptions.push(vscode.window.onDidCloseTerminal(terminal => {
694
- for (const [key, value] of setupTerminals)
695
- if (value === terminal)
696
- setupTerminals.delete(key);
697
- }));
698
- context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.refreshSetup', refreshSetup));
699
- context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.setup', async (kind, harness) => {
700
- if (!controls)
701
- throw new Error('This host does not expose sign-in.');
702
- const key = `${kind}/${harness}`;
703
- const existing = setupTerminals.get(key);
704
- if (existing && existing.exitStatus === undefined) {
705
- existing.show();
706
- return;
674
+ const resolve = setupTerminals.get(terminal);
675
+ if (resolve) {
676
+ setupTerminals.delete(terminal);
677
+ resolve(terminal.exitStatus?.code);
707
678
  }
708
- await controls.refresh();
709
- const offered = controls.state.setup?.actions.find(a => a.kind === kind && a.harness === harness);
710
- if (!offered) {
711
- await refreshSetup();
712
- throw new Error('This setup action is no longer available.');
713
- }
714
- const action = await controls.setup(kind, harness);
715
- if (action.command !== offered.command)
716
- throw new Error('The setup command changed. Check again before running it.');
717
- const terminal = vscode.window.createTerminal({ name: `${action.name} · ${kind === 'login' ? 'sign in' : 'install'}`,
718
- cwd: action.cwd, shellPath: action.program, shellArgs: action.arguments, env: action.env });
719
- setupTerminals.set(key, terminal);
720
- terminal.show();
721
679
  }));
680
+ const setupFlow = controls ? new ChatSetupFlow({
681
+ state: () => controls.state,
682
+ refresh: async () => {
683
+ await refreshInventory();
684
+ // Inventory settles independently of guarded navigation: a person moving
685
+ // focus does not turn a successful native login into a failed one.
686
+ if (controls.state.setup?.ready && !setupHandoffComplete()) {
687
+ void refreshSetup().catch(error => log.warn(`Could not reveal signed-in Chat: ${String(error)}`));
688
+ }
689
+ },
690
+ changed: () => {
691
+ const choice = activeDraft()?.choice;
692
+ if (choice?.kind === 'signIn' && choice.harness && setupAgents(controls.state).some(agent => agent.harness === choice.harness && agent.signedIn)) {
693
+ activeDraft().choice = { kind: 'agent', selection: { harness: choice.harness, model: '', effort: '' } };
694
+ }
695
+ controlsChanged.fire();
696
+ },
697
+ log: message => log.error(message),
698
+ launch: async (kind, harness, current) => {
699
+ if (!current())
700
+ throw new vscode.CancellationError();
701
+ const offered = controls.state.setup?.actions.find(a => a.kind === kind && a.harness === harness);
702
+ if (!offered)
703
+ throw new Error('This sign-in or install action is unavailable.');
704
+ const action = await controls.setup(kind, harness);
705
+ if (!current())
706
+ throw new vscode.CancellationError();
707
+ if (action.command !== offered.command)
708
+ throw new Error('The sign-in command changed. Try again.');
709
+ const terminal = vscode.window.createTerminal({ name: `${action.name} · ${kind === 'login' ? 'sign in' : 'install'}`,
710
+ cwd: action.cwd, shellPath: action.program, shellArgs: action.arguments, env: action.env });
711
+ const exited = new Promise(resolve => {
712
+ setupTerminals.set(terminal, resolve);
713
+ // The process can finish before the close listener has observed this terminal.
714
+ if (terminal.exitStatus) {
715
+ setupTerminals.delete(terminal);
716
+ resolve(terminal.exitStatus.code);
717
+ }
718
+ });
719
+ terminal.show();
720
+ return { exited, cancel: () => terminal.dispose() };
721
+ },
722
+ }) : undefined;
723
+ const setupState = () => controls ? setupPresentation(controls.state, setupFlow?.operations ?? new Map(), setupFlow?.notice) : null;
724
+ context.subscriptions.push({ dispose: () => setupFlow?.dispose() }, vscode.commands.registerCommand('supercode.frontend.refreshSetup', refreshSetup), vscode.commands.registerCommand(SETUP_STATE, setupState), vscode.commands.registerCommand(SETUP_BEGIN, (harness) => {
725
+ if (!setupFlow)
726
+ throw new Error('This host does not expose sign-in.');
727
+ return setupFlow.begin(harness);
728
+ }), vscode.commands.registerCommand(SETUP_CANCEL, (harness, operationId) => setupFlow?.cancel(harness, operationId) ?? false));
722
729
  const restoring = new Map();
723
730
  const connectionFor = async (resource, activate = true, token) => {
724
731
  if (savedAttachments.has(resource.authority)) {
@@ -943,7 +950,7 @@ export async function activate(context) {
943
950
  throw new Error(`Finish signing in with ${harnessDisplayName(loginHarness)} before sending.`);
944
951
  }
945
952
  if (choice?.kind !== 'agent' && controls.state.setup && !controls.state.setup.ready) {
946
- throw new Error('Sign in before sending; use Check again after signing in.');
953
+ throw new Error('Sign in to start a chat.');
947
954
  }
948
955
  const selection = choice?.kind === 'agent' ? choice.selection
949
956
  : { harness: loginHarness ?? controls.state.selection.harness, model: '', effort: '' };
@@ -1078,41 +1085,31 @@ export async function activate(context) {
1078
1085
  return;
1079
1086
  }
1080
1087
  await controls.refresh();
1081
- // Sign-in choices stay visible alongside agents that can be explicitly started.
1082
- const login = controls.state.setup?.actions.filter(action => action.kind === 'login') ?? [];
1083
- const localChoices = [
1084
- ...login.map(action => ({ label: action.name, id: action.harness, description: 'Sign in…', login: true, autoStart: false, configure: false })),
1085
- ...controls.state.harnesses.filter(h => !login.some(action => action.harness === h.id))
1086
- .map(h => ({ label: h.name, id: h.id, description: h.description, login: false, autoStart: h.autoStart === true, configure: false })),
1087
- ];
1088
- // Each agent whose model and reasoning a chat can set also offers that, right where the agent is chosen.
1088
+ const agents = setupAgents(controls.state);
1089
+ const localChoices = agents.map(agent => ({ label: agent.name, id: agent.harness,
1090
+ description: agent.harness === 'codex' ? (agent.signedIn ? 'Signed in with ChatGPT' : 'Sign in with ChatGPT…')
1091
+ : agent.harness === 'claude-code' ? (agent.signedIn ? 'Signed in with Claude' : 'Sign in with Claude…')
1092
+ : controls.state.harnesses.find(h => h.id === agent.harness)?.description,
1093
+ login: controls.state.setup?.actions.some(a => a.harness === agent.harness) ?? false,
1094
+ autoStart: controls.state.harnesses.some(h => h.id === agent.harness && h.autoStart), configure: false }));
1089
1095
  const modelChoices = localChoices.filter(choice => !choice.login && CONFIGURABLE_HARNESSES.includes(choice.id))
1090
- .map(choice => ({ ...choice, label: `${choice.label}: model and reasoning…`, description: 'Choose the model and reasoning effort', autoStart: false, configure: true }));
1091
- const choices = [
1092
- ...(offerChatGPT ? [{ label: 'Sign in with ChatGPT', id: CHATGPT_SIGN_IN, description: 'Official Codex extension', login: false, autoStart: false, configure: false }] : []),
1093
- ...localChoices,
1094
- ...modelChoices,
1095
- ];
1096
+ .map(choice => ({ ...choice, label: `${choice.label}: model and reasoning…`, description: undefined, autoStart: false, configure: true }));
1097
+ const choices = [...localChoices, ...(modelChoices.length ? [{ label: '', id: '__models__', kind: vscode.QuickPickItemKind.Separator,
1098
+ description: undefined, login: false, autoStart: false, configure: false }, ...modelChoices] : [])];
1096
1099
  if (!choices.length) {
1097
1100
  await revealSetup();
1098
1101
  return;
1099
1102
  }
1100
- // A sole sign-in option still requires a deliberate click before opening its terminal.
1101
- // The official choice must not disable the existing sole-local-agent shortcut.
1102
1103
  await chooseNewChat(async () => {
1103
- const harness = localChoices.length === 1 && localChoices[0].autoStart ? localChoices[0] : await vscode.window.showQuickPick(choices, { title: 'New chat · Agent', placeHolder: 'Choose your coding agent. Its default model and reasoning apply unless you choose them.' });
1104
+ const harness = await vscode.window.showQuickPick(choices, { title: 'New chat', placeHolder: 'Choose your coding agent' });
1104
1105
  if (!harness)
1105
1106
  return { kind: 'cancelled' };
1106
1107
  if (harness.configure) {
1107
1108
  const chosen = await pickModelAndEffort(harness.id);
1108
1109
  return chosen ? { kind: 'agent', selection: { harness: harness.id, ...chosen } } : { kind: 'cancelled' };
1109
1110
  }
1110
- if (harness.id === CHATGPT_SIGN_IN) {
1111
- await vscode.commands.executeCommand(CHATGPT_SIGN_IN);
1112
- return { kind: 'signIn' };
1113
- }
1114
1111
  if (harness.login) {
1115
- await vscode.commands.executeCommand('supercode.frontend.setup', 'login', harness.id);
1112
+ await vscode.commands.executeCommand(SETUP_BEGIN, harness.id);
1116
1113
  return { kind: 'signIn', harness: harness.id };
1117
1114
  }
1118
1115
  return { kind: 'agent', selection: { harness: harness.id, model: '', effort: '' } };
@@ -1447,14 +1444,18 @@ export async function activate(context) {
1447
1444
  if (!granted.has('defaultChatParticipant'))
1448
1445
  return;
1449
1446
  const setup = controls?.state.setup;
1450
- const welcome = setup && !setup.ready ? setupWelcome(setup, offerChatGPT)
1447
+ const presentation = setupState();
1448
+ const welcome = setup && !setup.ready ? setupWelcome(presentation)
1451
1449
  : !controls && frontends.all.length === 0 ? new vscode.MarkdownString(unavailableMarkdown(frontends.unavailable))
1452
1450
  : undefined;
1453
- if (welcomeValue === welcome?.value)
1451
+ const key = JSON.stringify([welcome?.value, presentation]);
1452
+ if (welcomeValue === key)
1454
1453
  return;
1455
- welcomeValue = welcome?.value;
1454
+ welcomeValue = key;
1456
1455
  // Metadata updates merge across RPC; undefined can leave the previous
1457
1456
  // sign-in message behind. An explicit empty value clears it in place.
1457
+ // The native renderer owns the signed-in notice above a ready conversation.
1458
+ // Keeping it in this fallback message would draw it again below that notice.
1458
1459
  for (const participant of participants)
1459
1460
  participant.additionalWelcomeMessage = welcome ?? new vscode.MarkdownString('');
1460
1461
  };
@@ -1615,7 +1616,7 @@ export async function activate(context) {
1615
1616
  await controls.refresh();
1616
1617
  if (inspect === true)
1617
1618
  return {
1618
- chatGPTSignIn: signInSnapshot(),
1619
+ setupState: setupState(),
1619
1620
  connections: frontends.all.map(c => ({ ...frontends.info(c), permissions: [...c.session.permissions],
1620
1621
  pendingRequests: [...c.session.pendingRequests.values()] })),
1621
1622
  selection: controls?.state.selection, activeSession: controls?.state.activeSession,
@@ -1779,7 +1780,10 @@ export async function activate(context) {
1779
1780
  let setupDisposed = false;
1780
1781
  const pollSetup = () => {
1781
1782
  const choice = activeDraft()?.choice;
1782
- const awaitingLogin = choice?.kind === 'signIn' && choice.harness && !controls?.state.harnesses.some(h => h.id === choice.harness && h.autoStart);
1783
+ const awaitingLogin = (choice?.kind === 'signIn' && choice.harness && !setupAgents(controls.state).some(h => h.harness === choice.harness && h.signedIn))
1784
+ || [...(setupFlow?.operations.values() ?? [])].some(operation => operation.phase === 'installing' || operation.phase === 'signing-in');
1785
+ if ([...(setupFlow?.operations.values() ?? [])].some(operation => operation.phase === 'installing' || operation.phase === 'signing-in'))
1786
+ return;
1783
1787
  if (setupTimer || setupDisposed || !controls?.state.setup || (controls.state.setup.ready && setupHandoffComplete() && !awaitingLogin))
1784
1788
  return;
1785
1789
  setupTimer = setTimeout(() => {
@@ -1788,6 +1792,7 @@ export async function activate(context) {
1788
1792
  }, 3000);
1789
1793
  };
1790
1794
  pollSetup();
1795
+ context.subscriptions.push(controlsChanged.event(pollSetup));
1791
1796
  context.subscriptions.push({ dispose: () => { setupDisposed = true; clearTimeout(setupTimer); } });
1792
1797
  return {
1793
1798
  connect: async (options) => {
@@ -35,9 +35,17 @@ export interface ChatSetupAction {
35
35
  name: string;
36
36
  command: string;
37
37
  }
38
+ export interface ChatSetupAgent {
39
+ harness: string;
40
+ name: string;
41
+ installed: boolean;
42
+ signedIn: boolean;
43
+ account: string | null;
44
+ }
38
45
  export interface ChatSetupState {
39
46
  ready: boolean;
40
47
  actions: ChatSetupAction[];
48
+ agents?: ChatSetupAgent[];
41
49
  reason: string | null;
42
50
  cwd: string;
43
51
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { createFrontendClient, type FrontendPermission, type VscodeFrontendConnection } from './client.js';
2
2
  export { readCredentialFile } from './credential.js';
3
- export type { ChatGPTSignInWorkPhase, ChatGPTSignInOperation } from './chat-setup.js';
3
+ export type { ChatSetupPhase, ChatSetupOperation, ChatSetupProvider, ChatSetupPresentation } from './chat-setup.js';
4
+ export type { ChatSetupAgent } from './host-controls.js';
4
5
  export { grantedProposals, PROPOSAL_EFFECTS, REQUESTED_PROPOSALS, type ProposalName, } from './proposals.js';
5
6
  export { EventSequenceGapError, LEASE_ERROR_NAMES, RequestNotPendingError, TURN_TERMINAL_KINDS, VscodeFrontendSession, type VscodeFrontendActions, type VscodeRuntimeClient, } from './session.js';
6
7
  export { HISTORY_MARKER_PROMPT, VSCODE_CHAT_LIMITS, acceptedDecisions, diffPaths, fence, opaqueText, projectEvent, projectPendingRequests, projectHistory, projectReplayTurns, requestAnswers, requestNeedsTypedReply, requestSubject, requestSummary, resolutionSentence, resolveParts, toolSubject, typedAnswer, type ChatAnswer, type ChatPart, type ProjectedPart, type ProjectedTurn, type ProjectionContext, } from './view.js';
@@ -0,0 +1,28 @@
1
+ import type { HostState } from './host-controls.js';
2
+ import { type ChatSetupOperation } from './chat-setup.js';
3
+ export interface SetupProcess {
4
+ exited: Promise<number | undefined>;
5
+ cancel(): void;
6
+ }
7
+ export interface SetupFlowHost {
8
+ state(): HostState;
9
+ refresh(): Promise<void>;
10
+ launch(kind: 'login' | 'install', harness: string, current: () => boolean): Promise<SetupProcess>;
11
+ changed(): void;
12
+ log(message: string): void;
13
+ }
14
+ export declare class ChatSetupFlow {
15
+ private readonly host;
16
+ readonly operations: Map<string, ChatSetupOperation>;
17
+ private processes;
18
+ private disposed;
19
+ notice: string | undefined;
20
+ constructor(host: SetupFlowHost);
21
+ begin(harness: string): ChatSetupOperation;
22
+ cancel(harness: string, id: string): boolean;
23
+ dispose(): void;
24
+ private current;
25
+ private update;
26
+ private refresh;
27
+ private run;
28
+ }
@@ -0,0 +1,127 @@
1
+ /** One user-requested install/sign-in operation per agent. No credentials cross this boundary. */
2
+ import { randomUUID } from 'node:crypto';
3
+ import { setupAgents } from './chat-setup.js';
4
+ export class ChatSetupFlow {
5
+ host;
6
+ operations = new Map();
7
+ processes = new Map();
8
+ disposed = false;
9
+ notice;
10
+ constructor(host) {
11
+ this.host = host;
12
+ }
13
+ begin(harness) {
14
+ if (this.disposed)
15
+ throw new Error('Chat setup has closed.');
16
+ const existing = this.operations.get(harness);
17
+ if (existing?.phase === 'installing' || existing?.phase === 'signing-in')
18
+ return { ...existing };
19
+ const agent = setupAgents(this.host.state()).find(a => a.harness === harness);
20
+ if (!agent)
21
+ throw new Error('That coding agent is unavailable.');
22
+ const now = Date.now();
23
+ const operation = { id: randomUUID(), harness, phase: agent.signedIn ? 'signed-in' : agent.installed ? 'signing-in' : 'installing', startedAt: now, updatedAt: now };
24
+ this.operations.set(harness, operation);
25
+ this.host.changed();
26
+ if (!agent.signedIn)
27
+ void this.run(operation);
28
+ return { ...operation };
29
+ }
30
+ cancel(harness, id) {
31
+ const operation = this.operations.get(harness);
32
+ if (!operation || operation.id !== id || !['installing', 'signing-in'].includes(operation.phase))
33
+ return false;
34
+ // Retire ownership before disposing the terminal: its close callback is not a failed sign-in.
35
+ this.operations.delete(harness);
36
+ this.processes.get(id)?.cancel();
37
+ this.processes.delete(id);
38
+ this.host.changed();
39
+ return true;
40
+ }
41
+ dispose() {
42
+ this.disposed = true;
43
+ for (const operation of this.operations.values())
44
+ this.processes.get(operation.id)?.cancel();
45
+ this.processes.clear();
46
+ }
47
+ current(operation) {
48
+ return !this.disposed && this.operations.get(operation.harness)?.id === operation.id;
49
+ }
50
+ update(operation, phase) {
51
+ if (!this.current(operation))
52
+ return;
53
+ this.operations.set(operation.harness, { ...operation, phase, updatedAt: Date.now() });
54
+ this.host.changed();
55
+ }
56
+ async refresh(operation) {
57
+ await this.host.refresh();
58
+ if (!this.current(operation))
59
+ throw new Error('Sign-in cancelled.');
60
+ }
61
+ async run(operation) {
62
+ const current = () => this.current(operation);
63
+ let process;
64
+ try {
65
+ await this.refresh(operation);
66
+ let agent = setupAgents(this.host.state()).find(a => a.harness === operation.harness);
67
+ if (!agent?.installed) {
68
+ this.update(operation, 'installing');
69
+ process = await this.host.launch('install', operation.harness, current);
70
+ if (!current()) {
71
+ process.cancel();
72
+ return;
73
+ }
74
+ this.processes.set(operation.id, process);
75
+ const exitCode = await process.exited;
76
+ this.processes.delete(operation.id);
77
+ if (!current())
78
+ return;
79
+ if (exitCode !== 0)
80
+ throw new Error(`Installation ended ${exitCode === undefined ? 'without an exit status' : `with exit code ${exitCode}`}.`);
81
+ await this.refresh(operation);
82
+ agent = setupAgents(this.host.state()).find(a => a.harness === operation.harness);
83
+ if (!agent?.installed)
84
+ throw new Error('Installation completed, but the coding agent is not available to this editor.');
85
+ }
86
+ if (!agent.signedIn) {
87
+ this.update(operation, 'signing-in');
88
+ process = await this.host.launch('login', operation.harness, current);
89
+ if (!current()) {
90
+ process.cancel();
91
+ return;
92
+ }
93
+ this.processes.set(operation.id, process);
94
+ let ended = false;
95
+ let exitCode;
96
+ void process.exited.then(code => { ended = true; exitCode = code; });
97
+ // Readiness comes only from the host's native inventory. A successful
98
+ // process exit, a browser opening, and a protocol handshake are not login.
99
+ while (current()) {
100
+ await this.refresh(operation);
101
+ if (setupAgents(this.host.state()).some(a => a.harness === operation.harness && a.signedIn))
102
+ break;
103
+ if (ended)
104
+ throw new Error(`Sign-in ended ${exitCode === undefined ? 'without an exit status' : `with exit code ${exitCode}`}, without a signed-in account.`);
105
+ await new Promise(resolve => setTimeout(resolve, 1000));
106
+ }
107
+ }
108
+ if (!current())
109
+ return;
110
+ this.notice = operation.harness === 'codex' ? 'ChatGPT' : operation.harness === 'claude-code' ? 'Claude' : undefined;
111
+ this.update(operation, 'signed-in');
112
+ // Retire only the terminal this click launched. Its native inventory has
113
+ // already confirmed login; closing it never supplies that confirmation.
114
+ process?.cancel();
115
+ }
116
+ catch (error) {
117
+ if (current()) {
118
+ this.host.log(`Could not sign in with ${operation.harness}: ${String(error)}`);
119
+ this.update(operation, 'failed');
120
+ process?.cancel();
121
+ }
122
+ }
123
+ finally {
124
+ this.processes.delete(operation.id);
125
+ }
126
+ }
127
+ }
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.50",
4
+ "version": "0.1.51",
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",