@volter/supercode-frontend-vscode 0.1.49 → 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 +59 -80
- package/dist/chat-setup.d.ts +35 -20
- package/dist/chat-setup.js +56 -60
- package/dist/extension.js +104 -101
- package/dist/host-controls.d.ts +8 -2
- package/dist/host-controls.js +0 -2
- package/dist/index.d.ts +2 -1
- package/dist/session.js +2 -1
- package/dist/setup-flow.d.ts +28 -0
- package/dist/setup-flow.js +127 -0
- package/dist/view.js +7 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,74 +11,55 @@ package or npm.
|
|
|
11
11
|
|
|
12
12
|
## Native chat controls
|
|
13
13
|
|
|
14
|
-
When
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
installation
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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**
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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
|
package/dist/chat-setup.d.ts
CHANGED
|
@@ -1,25 +1,40 @@
|
|
|
1
|
-
/** Native Chat
|
|
1
|
+
/** Native Chat setup. The host owns inventory, commands and provider credentials. */
|
|
2
2
|
import * as vscode from 'vscode';
|
|
3
|
-
import type {
|
|
4
|
-
export declare const
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const
|
|
7
|
-
export type
|
|
8
|
-
export interface
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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;
|
package/dist/chat-setup.js
CHANGED
|
@@ -1,69 +1,65 @@
|
|
|
1
|
-
/** Native Chat
|
|
1
|
+
/** Native Chat setup. The host owns inventory, commands and provider credentials. */
|
|
2
2
|
import * as vscode from 'vscode';
|
|
3
|
-
export const
|
|
4
|
-
export const
|
|
5
|
-
export const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
/**
|
|
41
|
-
*
|
|
42
|
-
|
|
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: [
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
@@ -515,12 +484,6 @@ export async function activate(context) {
|
|
|
515
484
|
const picked = selection.model ? controls?.state.modelsByHarness?.[selection.harness]?.find(model => model.id === selection.model) : undefined;
|
|
516
485
|
return { model: selection.model || configured.model, modelName: selection.model ? picked?.name ?? selection.model : configured.modelName, effort: selection.effort || configured.effort };
|
|
517
486
|
};
|
|
518
|
-
const effectiveNow = () => {
|
|
519
|
-
if (focusedAttached)
|
|
520
|
-
return null;
|
|
521
|
-
const selection = draftSelection();
|
|
522
|
-
return selection ? draftEffective(selection) : controls?.state.effective ?? null;
|
|
523
|
-
};
|
|
524
487
|
const identity = () => {
|
|
525
488
|
if (focusedAttached)
|
|
526
489
|
return runtimeIdentity(focusedAttached.session.descriptor?.source_harness, focusedAttached.session.descriptor?.model);
|
|
@@ -634,6 +597,7 @@ export async function activate(context) {
|
|
|
634
597
|
setupHandoff = { id, baseUrl: connection.baseUrl };
|
|
635
598
|
};
|
|
636
599
|
let setupRevealed = false;
|
|
600
|
+
let welcomeWasVisible = controls?.state.setup?.ready === false;
|
|
637
601
|
const revealSetup = async () => {
|
|
638
602
|
if (!granted.has('chatSessionsProvider') || !controls?.state.setup || controls.state.setup.ready || setupRevealed)
|
|
639
603
|
return;
|
|
@@ -642,6 +606,13 @@ export async function activate(context) {
|
|
|
642
606
|
await vscode.commands.executeCommand(`workbench.action.chat.openNewChatSessionInPlace.${SESSION_SCHEME}`, 'sidebar');
|
|
643
607
|
setupRevealed = true;
|
|
644
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
|
+
};
|
|
645
616
|
let refreshingSetup;
|
|
646
617
|
const refreshSetup = () => {
|
|
647
618
|
if (refreshingSetup)
|
|
@@ -651,9 +622,10 @@ export async function activate(context) {
|
|
|
651
622
|
return;
|
|
652
623
|
const generation = focusGeneration;
|
|
653
624
|
const focusResource = focusedResource?.toString() ?? null;
|
|
625
|
+
welcomeWasVisible ||= controls.state.setup?.ready === false;
|
|
654
626
|
// Discover and publish readiness even while a blank chat holds a draft.
|
|
655
627
|
// Only the automatic reveal is suppressed; discovery must never stall.
|
|
656
|
-
await
|
|
628
|
+
await refreshInventory();
|
|
657
629
|
const state = controls.state;
|
|
658
630
|
controlsChanged.fire();
|
|
659
631
|
if (state.setup?.ready || (!state.setup && state.connection?.SUPERCODE_FRONTEND_URL)) {
|
|
@@ -678,7 +650,7 @@ export async function activate(context) {
|
|
|
678
650
|
const live = target?.kind === 'managed' && Boolean(frontends.get(target.id)?.session.pendingRequests.size
|
|
679
651
|
|| frontends.get(target.id)?.session.descriptor?.turn_state === 'busy');
|
|
680
652
|
if (!target || !controls || applying !== applied || target.generation !== focusGeneration
|
|
681
|
-
|| (focusedResource?.path.startsWith('/untitled-') && !live))
|
|
653
|
+
|| (focusedResource?.path.startsWith('/untitled-') && !live && !welcomeWasVisible))
|
|
682
654
|
return;
|
|
683
655
|
if (target.kind === 'managed') {
|
|
684
656
|
if ((controls.state.setup && !controls.state.setup.ready) || controls.state.activeSession !== target.id
|
|
@@ -686,45 +658,74 @@ export async function activate(context) {
|
|
|
686
658
|
return;
|
|
687
659
|
if (!setupHandoffComplete())
|
|
688
660
|
await revealManaged(target, { resource: target.focusResource });
|
|
661
|
+
if (setupHandoffComplete())
|
|
662
|
+
welcomeWasVisible = false;
|
|
689
663
|
}
|
|
690
664
|
else if (!controls.state.setup?.ready) {
|
|
691
665
|
await revealSetup();
|
|
692
666
|
}
|
|
693
667
|
});
|
|
694
|
-
// Coalesce
|
|
668
|
+
// Coalesce inventory refresh through actual reveal completion or refusal.
|
|
695
669
|
refreshingSetup = revealed.finally(() => { refreshingSetup = undefined; });
|
|
696
670
|
return refreshingSetup;
|
|
697
671
|
};
|
|
698
672
|
const setupTerminals = new Map();
|
|
699
673
|
context.subscriptions.push(vscode.window.onDidCloseTerminal(terminal => {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.refreshSetup', refreshSetup));
|
|
705
|
-
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.setup', async (kind, harness) => {
|
|
706
|
-
if (!controls)
|
|
707
|
-
throw new Error('This host does not expose sign-in.');
|
|
708
|
-
const key = `${kind}/${harness}`;
|
|
709
|
-
const existing = setupTerminals.get(key);
|
|
710
|
-
if (existing && existing.exitStatus === undefined) {
|
|
711
|
-
existing.show();
|
|
712
|
-
return;
|
|
674
|
+
const resolve = setupTerminals.get(terminal);
|
|
675
|
+
if (resolve) {
|
|
676
|
+
setupTerminals.delete(terminal);
|
|
677
|
+
resolve(terminal.exitStatus?.code);
|
|
713
678
|
}
|
|
714
|
-
await controls.refresh();
|
|
715
|
-
const offered = controls.state.setup?.actions.find(a => a.kind === kind && a.harness === harness);
|
|
716
|
-
if (!offered) {
|
|
717
|
-
await refreshSetup();
|
|
718
|
-
throw new Error('This setup action is no longer available.');
|
|
719
|
-
}
|
|
720
|
-
const action = await controls.setup(kind, harness);
|
|
721
|
-
if (action.command !== offered.command)
|
|
722
|
-
throw new Error('The setup command changed. Check again before running it.');
|
|
723
|
-
const terminal = vscode.window.createTerminal({ name: `${action.name} · ${kind === 'login' ? 'sign in' : 'install'}`,
|
|
724
|
-
cwd: action.cwd, shellPath: action.program, shellArgs: action.arguments, env: action.env });
|
|
725
|
-
setupTerminals.set(key, terminal);
|
|
726
|
-
terminal.show();
|
|
727
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));
|
|
728
729
|
const restoring = new Map();
|
|
729
730
|
const connectionFor = async (resource, activate = true, token) => {
|
|
730
731
|
if (savedAttachments.has(resource.authority)) {
|
|
@@ -949,7 +950,7 @@ export async function activate(context) {
|
|
|
949
950
|
throw new Error(`Finish signing in with ${harnessDisplayName(loginHarness)} before sending.`);
|
|
950
951
|
}
|
|
951
952
|
if (choice?.kind !== 'agent' && controls.state.setup && !controls.state.setup.ready) {
|
|
952
|
-
throw new Error('Sign in
|
|
953
|
+
throw new Error('Sign in to start a chat.');
|
|
953
954
|
}
|
|
954
955
|
const selection = choice?.kind === 'agent' ? choice.selection
|
|
955
956
|
: { harness: loginHarness ?? controls.state.selection.harness, model: '', effort: '' };
|
|
@@ -1084,41 +1085,31 @@ export async function activate(context) {
|
|
|
1084
1085
|
return;
|
|
1085
1086
|
}
|
|
1086
1087
|
await controls.refresh();
|
|
1087
|
-
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
// 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 }));
|
|
1095
1095
|
const modelChoices = localChoices.filter(choice => !choice.login && CONFIGURABLE_HARNESSES.includes(choice.id))
|
|
1096
|
-
.map(choice => ({ ...choice, label: `${choice.label}: model and reasoning…`, description:
|
|
1097
|
-
const choices = [
|
|
1098
|
-
|
|
1099
|
-
...localChoices,
|
|
1100
|
-
...modelChoices,
|
|
1101
|
-
];
|
|
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] : [])];
|
|
1102
1099
|
if (!choices.length) {
|
|
1103
1100
|
await revealSetup();
|
|
1104
1101
|
return;
|
|
1105
1102
|
}
|
|
1106
|
-
// A sole sign-in option still requires a deliberate click before opening its terminal.
|
|
1107
|
-
// The official choice must not disable the existing sole-local-agent shortcut.
|
|
1108
1103
|
await chooseNewChat(async () => {
|
|
1109
|
-
const harness =
|
|
1104
|
+
const harness = await vscode.window.showQuickPick(choices, { title: 'New chat', placeHolder: 'Choose your coding agent' });
|
|
1110
1105
|
if (!harness)
|
|
1111
1106
|
return { kind: 'cancelled' };
|
|
1112
1107
|
if (harness.configure) {
|
|
1113
1108
|
const chosen = await pickModelAndEffort(harness.id);
|
|
1114
1109
|
return chosen ? { kind: 'agent', selection: { harness: harness.id, ...chosen } } : { kind: 'cancelled' };
|
|
1115
1110
|
}
|
|
1116
|
-
if (harness.id === CHATGPT_SIGN_IN) {
|
|
1117
|
-
await vscode.commands.executeCommand(CHATGPT_SIGN_IN);
|
|
1118
|
-
return { kind: 'signIn' };
|
|
1119
|
-
}
|
|
1120
1111
|
if (harness.login) {
|
|
1121
|
-
await vscode.commands.executeCommand(
|
|
1112
|
+
await vscode.commands.executeCommand(SETUP_BEGIN, harness.id);
|
|
1122
1113
|
return { kind: 'signIn', harness: harness.id };
|
|
1123
1114
|
}
|
|
1124
1115
|
return { kind: 'agent', selection: { harness: harness.id, model: '', effort: '' } };
|
|
@@ -1204,8 +1195,15 @@ export async function activate(context) {
|
|
|
1204
1195
|
const connection = connectionId ? frontends.get(connectionId) : managed();
|
|
1205
1196
|
if (!connection)
|
|
1206
1197
|
return { stopped: false, reason: 'no conversation' };
|
|
1207
|
-
|
|
1208
|
-
|
|
1198
|
+
try {
|
|
1199
|
+
await stopConnection(connection);
|
|
1200
|
+
return { stopped: true, connectionId: connection.id };
|
|
1201
|
+
}
|
|
1202
|
+
catch (error) {
|
|
1203
|
+
const reason = String(error?.message ?? error);
|
|
1204
|
+
log.warn(`Stop could not interrupt: ${reason}`);
|
|
1205
|
+
return { stopped: false, connectionId: connection.id, reason };
|
|
1206
|
+
}
|
|
1209
1207
|
}));
|
|
1210
1208
|
const outsideTurns = setInterval(answerOutsideTurns, 2_000);
|
|
1211
1209
|
context.subscriptions.push({ dispose: () => clearInterval(outsideTurns) });
|
|
@@ -1394,9 +1392,6 @@ export async function activate(context) {
|
|
|
1394
1392
|
stream.markdown(unavailableMarkdown(frontends.unavailable));
|
|
1395
1393
|
return {};
|
|
1396
1394
|
}
|
|
1397
|
-
// While a request is pending, a typed message is its answer — this is the
|
|
1398
|
-
// path for the answers no button can carry (`allow for session`, `cancel`,
|
|
1399
|
-
// and an elicitation's free-text content).
|
|
1400
1395
|
const pending = pendingRequestOf(connection);
|
|
1401
1396
|
// A message is never taken as an approval: one sent while an approval waits would otherwise allow the tool and
|
|
1402
1397
|
// never reach the agent. It is refused, with what to do, like a send during any running turn.
|
|
@@ -1449,14 +1444,18 @@ export async function activate(context) {
|
|
|
1449
1444
|
if (!granted.has('defaultChatParticipant'))
|
|
1450
1445
|
return;
|
|
1451
1446
|
const setup = controls?.state.setup;
|
|
1452
|
-
const
|
|
1447
|
+
const presentation = setupState();
|
|
1448
|
+
const welcome = setup && !setup.ready ? setupWelcome(presentation)
|
|
1453
1449
|
: !controls && frontends.all.length === 0 ? new vscode.MarkdownString(unavailableMarkdown(frontends.unavailable))
|
|
1454
1450
|
: undefined;
|
|
1455
|
-
|
|
1451
|
+
const key = JSON.stringify([welcome?.value, presentation]);
|
|
1452
|
+
if (welcomeValue === key)
|
|
1456
1453
|
return;
|
|
1457
|
-
welcomeValue =
|
|
1454
|
+
welcomeValue = key;
|
|
1458
1455
|
// Metadata updates merge across RPC; undefined can leave the previous
|
|
1459
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.
|
|
1460
1459
|
for (const participant of participants)
|
|
1461
1460
|
participant.additionalWelcomeMessage = welcome ?? new vscode.MarkdownString('');
|
|
1462
1461
|
};
|
|
@@ -1617,7 +1616,7 @@ export async function activate(context) {
|
|
|
1617
1616
|
await controls.refresh();
|
|
1618
1617
|
if (inspect === true)
|
|
1619
1618
|
return {
|
|
1620
|
-
|
|
1619
|
+
setupState: setupState(),
|
|
1621
1620
|
connections: frontends.all.map(c => ({ ...frontends.info(c), permissions: [...c.session.permissions],
|
|
1622
1621
|
pendingRequests: [...c.session.pendingRequests.values()] })),
|
|
1623
1622
|
selection: controls?.state.selection, activeSession: controls?.state.activeSession,
|
|
@@ -1781,7 +1780,10 @@ export async function activate(context) {
|
|
|
1781
1780
|
let setupDisposed = false;
|
|
1782
1781
|
const pollSetup = () => {
|
|
1783
1782
|
const choice = activeDraft()?.choice;
|
|
1784
|
-
const awaitingLogin = choice?.kind === 'signIn' && choice.harness && !controls
|
|
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;
|
|
1785
1787
|
if (setupTimer || setupDisposed || !controls?.state.setup || (controls.state.setup.ready && setupHandoffComplete() && !awaitingLogin))
|
|
1786
1788
|
return;
|
|
1787
1789
|
setupTimer = setTimeout(() => {
|
|
@@ -1790,6 +1792,7 @@ export async function activate(context) {
|
|
|
1790
1792
|
}, 3000);
|
|
1791
1793
|
};
|
|
1792
1794
|
pollSetup();
|
|
1795
|
+
context.subscriptions.push(controlsChanged.event(pollSetup));
|
|
1793
1796
|
context.subscriptions.push({ dispose: () => { setupDisposed = true; clearTimeout(setupTimer); } });
|
|
1794
1797
|
return {
|
|
1795
1798
|
connect: async (options) => {
|
package/dist/host-controls.d.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -102,7 +110,5 @@ export declare function runtimeIdentity(harness: string | null | undefined, mode
|
|
|
102
110
|
name: string;
|
|
103
111
|
provider: string | undefined;
|
|
104
112
|
};
|
|
105
|
-
/** The approval mode as the Chat names it. */
|
|
106
|
-
export declare function permissionName(value: 'ask' | 'autoApprove'): string;
|
|
107
113
|
/** Native pickers need a nonempty id; the host uses an empty model for its default. */
|
|
108
114
|
export declare function modelSelection(value: string): string;
|
package/dist/host-controls.js
CHANGED
|
@@ -76,7 +76,5 @@ export function runtimeIdentity(harness, model, effective) {
|
|
|
76
76
|
const effort = effective?.effort ? ` · ${effective.effort}` : '';
|
|
77
77
|
return { name: modelName ? `${modelName}${effort}` : `Model chosen by ${harnessName}`, provider };
|
|
78
78
|
}
|
|
79
|
-
/** The approval mode as the Chat names it. */
|
|
80
|
-
export function permissionName(value) { return value === 'autoApprove' ? 'Auto approve' : 'Ask for approval'; }
|
|
81
79
|
/** Native pickers need a nonempty id; the host uses an empty model for its default. */
|
|
82
80
|
export function modelSelection(value) { return value === "__default__" ? "" : value; }
|
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 {
|
|
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';
|
package/dist/session.js
CHANGED
|
@@ -133,7 +133,8 @@ export class VscodeFrontendSession {
|
|
|
133
133
|
// forever with nothing to answer it (2026-10-07). Compaction happens only at a turn boundary, so a request raised
|
|
134
134
|
// AFTER the replay's last boundary and not resolved since is still open, idle or not; only older ones are dropped.
|
|
135
135
|
if (staged.descriptor?.turn_state !== 'busy' && staged.descriptor?.source_harness !== 'codex') {
|
|
136
|
-
|
|
136
|
+
// A turn's END, not `turn_completed`: that one is a model round-trip and fires mid-turn.
|
|
137
|
+
const BOUNDARIES = new Set(['turn_succeeded', 'turn_failed', 'turn_interrupted']);
|
|
137
138
|
let lastBoundary = -1;
|
|
138
139
|
const raisedAt = new Map();
|
|
139
140
|
for (const event of snapshot.replay) {
|
|
@@ -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/dist/view.js
CHANGED
|
@@ -99,6 +99,8 @@ export function toolSubject(argumentsText) {
|
|
|
99
99
|
return undefined;
|
|
100
100
|
}
|
|
101
101
|
const code = (value) => `\`${value.replace(/`/g, "'")}\``;
|
|
102
|
+
/** Free text in a title, which the Chat renders as Markdown: its own `*`, `_`, `[` and the like stay characters. */
|
|
103
|
+
const plain = (value) => value.replace(/[\\`*_{}[\]()<>#+!|~]/g, '\\$&');
|
|
102
104
|
const basename = (path) => path.split(/[\\/]/).filter(Boolean).pop() ?? path;
|
|
103
105
|
/**
|
|
104
106
|
* A tool row's titles the way VS Code's own agent writes them: what the call does, in the present while it runs
|
|
@@ -130,7 +132,7 @@ export function toolTitles(toolName, argumentsText) {
|
|
|
130
132
|
if (['bash', 'shell', 'exec_command', 'local_shell', 'powershell'].includes(name)) {
|
|
131
133
|
const description = field('description');
|
|
132
134
|
if (description)
|
|
133
|
-
return both(description, description);
|
|
135
|
+
return both(plain(description), plain(description));
|
|
134
136
|
const command = field('command', 'cmd');
|
|
135
137
|
return command ? both(`Running ${code(command)}`, `Ran ${code(command)}`) : both(`Running a command`, `Ran a command`);
|
|
136
138
|
}
|
|
@@ -157,7 +159,7 @@ export function toolTitles(toolName, argumentsText) {
|
|
|
157
159
|
if (name === 'webfetch') {
|
|
158
160
|
const url = field('url');
|
|
159
161
|
if (url)
|
|
160
|
-
return both(`Fetching ${url}`, `Fetched ${url}`);
|
|
162
|
+
return both(`Fetching ${plain(url)}`, `Fetched ${plain(url)}`);
|
|
161
163
|
}
|
|
162
164
|
if (name === 'websearch' || name === 'web_search') {
|
|
163
165
|
const query = field('query');
|
|
@@ -167,16 +169,16 @@ export function toolTitles(toolName, argumentsText) {
|
|
|
167
169
|
if (['task', 'agent'].includes(name)) {
|
|
168
170
|
const description = field('description');
|
|
169
171
|
if (description)
|
|
170
|
-
return both(`Running a subagent: ${description}`, `Ran a subagent: ${description}`);
|
|
172
|
+
return both(`Running a subagent: ${plain(description)}`, `Ran a subagent: ${plain(description)}`);
|
|
171
173
|
}
|
|
172
174
|
if (name === 'todowrite' || name === 'update_plan')
|
|
173
175
|
return both('Updating the plan', 'Updated the plan');
|
|
174
176
|
// Claude Code names an MCP tool `mcp__<server>__<tool>`; VS Code writes "Ran `tool` – server".
|
|
175
177
|
const mcp = /^mcp__(.+?)__(.+)$/.exec(toolName);
|
|
176
178
|
if (mcp)
|
|
177
|
-
return both(`Running ${code(mcp[2])} – ${mcp[1]}`, `Ran ${code(mcp[2])} – ${mcp[1]}`);
|
|
179
|
+
return both(`Running ${code(mcp[2])} – ${plain(mcp[1])}`, `Ran ${code(mcp[2])} – ${plain(mcp[1])}`);
|
|
178
180
|
const subject = toolSubject(argumentsText);
|
|
179
|
-
return subject ? both(`Running ${code(toolName)} · ${subject}`, `Ran ${code(toolName)} · ${subject}`) : both(`Running ${code(toolName)}`, `Ran ${code(toolName)}`);
|
|
181
|
+
return subject ? both(`Running ${code(toolName)} · ${plain(subject)}`, `Ran ${code(toolName)} · ${plain(subject)}`) : both(`Running ${code(toolName)}`, `Ran ${code(toolName)}`);
|
|
180
182
|
}
|
|
181
183
|
/** The command line of a shell tool call, read from its own arguments; undefined for any other tool. */
|
|
182
184
|
export function shellCommand(toolName, argumentsText) {
|
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.
|
|
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",
|