@volter/supercode-frontend-vscode 0.1.50 → 0.1.52
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 +75 -80
- package/dist/chat-setup.d.ts +35 -20
- package/dist/chat-setup.js +56 -60
- package/dist/extension.js +166 -113
- package/dist/host-controls.d.ts +8 -0
- package/dist/index.d.ts +2 -1
- package/dist/native-session.d.ts +1 -1
- package/dist/native-session.js +7 -2
- package/dist/session.d.ts +2 -1
- package/dist/session.js +8 -3
- package/dist/setup-flow.d.ts +28 -0
- package/dist/setup-flow.js +127 -0
- package/dist/view.d.ts +1 -9
- package/dist/view.js +14 -12
- 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
|
|
@@ -414,6 +393,22 @@ are read only for backwards compatibility.
|
|
|
414
393
|
Restored response cancellation interrupts the runtime and waits for acknowledgement.
|
|
415
394
|
Session-list updates consistently prioritize pending input over busy status.
|
|
416
395
|
|
|
396
|
+
Reload restoration joins the runtime's canonical history to events after its
|
|
397
|
+
`history_cursor`. A separately loaded native transcript is a fallback when the
|
|
398
|
+
runtime has no user history; it never replaces the live event tail. The final
|
|
399
|
+
running response is replayed through `activeResponseCallback`, with each pending
|
|
400
|
+
request drawn once from the snapshot and later events followed by sequence.
|
|
401
|
+
An idle runtime with a pending request also keeps this callback open until the
|
|
402
|
+
request is resolved. Standalone requests use the explicit runtime-history marker
|
|
403
|
+
instead of reopening an earlier answer. Managed conversations save their native
|
|
404
|
+
identity before sending input, as well as when a turn ends.
|
|
405
|
+
Stop is observed during the save and checked before input, including a lease
|
|
406
|
+
renewal retry. Retained request/decision records with no originating replay
|
|
407
|
+
prompt appear in a separate runtime-history turn.
|
|
408
|
+
The [reload investigation](../../docs/evidence/chat-pending-approval-reload-2026-10-08.md)
|
|
409
|
+
separates the retained first-user failure from this source change; a live
|
|
410
|
+
candidate reload and approval click remain unmeasured.
|
|
411
|
+
|
|
417
412
|
The pinned Code-OSS host needs a resource-scoping repair in its input-state change
|
|
418
413
|
routing: stock code broadcasts changes to all input states of a provider. The
|
|
419
414
|
Volter overlay guards this fix with a native regression test. The former legacy
|
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,14 +2,15 @@ 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';
|
|
9
10
|
import { grantedProposals, PROPOSAL_EFFECTS, REQUESTED_PROPOSALS } from './proposals.js';
|
|
10
11
|
import { observeActiveResponse, sessionStatus } from './native-session.js';
|
|
11
12
|
import { RequestNotPendingError, VscodeFrontendSession } from './session.js';
|
|
12
|
-
import { acceptedDecisions, opaqueText, projectEvent, projectPendingRequests, projectHistory, projectReplayTurns, resolveParts, resolutionSentence, typedAnswer, } from './view.js';
|
|
13
|
+
import { acceptedDecisions, opaqueText, projectEvent, projectPendingRequests, projectHistory, projectReplayTurns, HISTORY_MARKER_PROMPT, resolveParts, resolutionSentence, typedAnswer, } from './view.js';
|
|
13
14
|
const SESSION_SCHEME = 'supercode';
|
|
14
15
|
/** The harnesses whose model and reasoning effort a new chat can set (the host passes them to the CLI). */
|
|
15
16
|
const CONFIGURABLE_HARNESSES = ['claude-code', 'codex'];
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
|
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
|
-
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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:
|
|
1091
|
-
const choices = [
|
|
1092
|
-
|
|
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 =
|
|
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(
|
|
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: '' } };
|
|
@@ -1298,6 +1295,11 @@ export async function activate(context) {
|
|
|
1298
1295
|
} };
|
|
1299
1296
|
};
|
|
1300
1297
|
const runTurnInto = async (connection, prompt, stream, token, approval) => {
|
|
1298
|
+
const throwIfCancelled = () => {
|
|
1299
|
+
if (token.isCancellationRequested)
|
|
1300
|
+
throw new vscode.CancellationError();
|
|
1301
|
+
};
|
|
1302
|
+
throwIfCancelled();
|
|
1301
1303
|
if (approval() === 'autoApprove' && !canAnswer(connection)) {
|
|
1302
1304
|
throw new Error('This harness does not support automatic approval through Chat. Select Harness permissions.');
|
|
1303
1305
|
}
|
|
@@ -1338,13 +1340,20 @@ export async function activate(context) {
|
|
|
1338
1340
|
const unwatchTasks = connection.session.onChange(sayTasks);
|
|
1339
1341
|
let completed = false;
|
|
1340
1342
|
try {
|
|
1343
|
+
// Observe Stop throughout the save, then check the token before input.
|
|
1344
|
+
// A cancellation while this idle runtime saves cannot be handled by an
|
|
1345
|
+
// interrupt alone: there is no running turn to interrupt yet.
|
|
1346
|
+
if (controls && !attachedIds.has(connection.id) && connection.session.descriptor?.session_id) {
|
|
1347
|
+
await controls.remember(connection.id, connection.session.descriptor.session_id);
|
|
1348
|
+
}
|
|
1349
|
+
throwIfCancelled();
|
|
1341
1350
|
await connection.session.runTurn(prompt, (event) => {
|
|
1342
1351
|
// A turn the person stopped is marked stopped by VS Code itself; only an interruption from elsewhere is said.
|
|
1343
1352
|
if (event.kind === 'turn_interrupted' && token.isCancellationRequested)
|
|
1344
1353
|
return;
|
|
1345
1354
|
writeProjected(stream, projectEvent(event, projection), granted, state);
|
|
1346
1355
|
watch.sweep();
|
|
1347
|
-
});
|
|
1356
|
+
}, { throwIfCancelled });
|
|
1348
1357
|
completed = true;
|
|
1349
1358
|
}
|
|
1350
1359
|
finally {
|
|
@@ -1447,14 +1456,18 @@ export async function activate(context) {
|
|
|
1447
1456
|
if (!granted.has('defaultChatParticipant'))
|
|
1448
1457
|
return;
|
|
1449
1458
|
const setup = controls?.state.setup;
|
|
1450
|
-
const
|
|
1459
|
+
const presentation = setupState();
|
|
1460
|
+
const welcome = setup && !setup.ready ? setupWelcome(presentation)
|
|
1451
1461
|
: !controls && frontends.all.length === 0 ? new vscode.MarkdownString(unavailableMarkdown(frontends.unavailable))
|
|
1452
1462
|
: undefined;
|
|
1453
|
-
|
|
1463
|
+
const key = JSON.stringify([welcome?.value, presentation]);
|
|
1464
|
+
if (welcomeValue === key)
|
|
1454
1465
|
return;
|
|
1455
|
-
welcomeValue =
|
|
1466
|
+
welcomeValue = key;
|
|
1456
1467
|
// Metadata updates merge across RPC; undefined can leave the previous
|
|
1457
1468
|
// sign-in message behind. An explicit empty value clears it in place.
|
|
1469
|
+
// The native renderer owns the signed-in notice above a ready conversation.
|
|
1470
|
+
// Keeping it in this fallback message would draw it again below that notice.
|
|
1458
1471
|
for (const participant of participants)
|
|
1459
1472
|
participant.additionalWelcomeMessage = welcome ?? new vscode.MarkdownString('');
|
|
1460
1473
|
};
|
|
@@ -1615,7 +1628,7 @@ export async function activate(context) {
|
|
|
1615
1628
|
await controls.refresh();
|
|
1616
1629
|
if (inspect === true)
|
|
1617
1630
|
return {
|
|
1618
|
-
|
|
1631
|
+
setupState: setupState(),
|
|
1619
1632
|
connections: frontends.all.map(c => ({ ...frontends.info(c), permissions: [...c.session.permissions],
|
|
1620
1633
|
pendingRequests: [...c.session.pendingRequests.values()] })),
|
|
1621
1634
|
selection: controls?.state.selection, activeSession: controls?.state.activeSession,
|
|
@@ -1779,7 +1792,10 @@ export async function activate(context) {
|
|
|
1779
1792
|
let setupDisposed = false;
|
|
1780
1793
|
const pollSetup = () => {
|
|
1781
1794
|
const choice = activeDraft()?.choice;
|
|
1782
|
-
const awaitingLogin = choice?.kind === 'signIn' && choice.harness && !controls
|
|
1795
|
+
const awaitingLogin = (choice?.kind === 'signIn' && choice.harness && !setupAgents(controls.state).some(h => h.harness === choice.harness && h.signedIn))
|
|
1796
|
+
|| [...(setupFlow?.operations.values() ?? [])].some(operation => operation.phase === 'installing' || operation.phase === 'signing-in');
|
|
1797
|
+
if ([...(setupFlow?.operations.values() ?? [])].some(operation => operation.phase === 'installing' || operation.phase === 'signing-in'))
|
|
1798
|
+
return;
|
|
1783
1799
|
if (setupTimer || setupDisposed || !controls?.state.setup || (controls.state.setup.ready && setupHandoffComplete() && !awaitingLogin))
|
|
1784
1800
|
return;
|
|
1785
1801
|
setupTimer = setTimeout(() => {
|
|
@@ -1788,6 +1804,7 @@ export async function activate(context) {
|
|
|
1788
1804
|
}, 3000);
|
|
1789
1805
|
};
|
|
1790
1806
|
pollSetup();
|
|
1807
|
+
context.subscriptions.push(controlsChanged.event(pollSetup));
|
|
1791
1808
|
context.subscriptions.push({ dispose: () => { setupDisposed = true; clearTimeout(setupTimer); } });
|
|
1792
1809
|
return {
|
|
1793
1810
|
connect: async (options) => {
|
|
@@ -2021,12 +2038,16 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2021
2038
|
// canonical transcript, replayed as turns.
|
|
2022
2039
|
const history = [];
|
|
2023
2040
|
const authorId = historyParticipant(connection);
|
|
2024
|
-
//
|
|
2025
|
-
//
|
|
2026
|
-
//
|
|
2027
|
-
//
|
|
2028
|
-
//
|
|
2029
|
-
const
|
|
2041
|
+
// History and replay are one runtime snapshot, joined by its cursor.
|
|
2042
|
+
// A host's separately loaded native transcript has no replay cursor;
|
|
2043
|
+
// selecting it instead hid every turn started since the runtime was
|
|
2044
|
+
// created. Keep it only as a fallback when no runtime user history is
|
|
2045
|
+
// available, rather than trying to align equal-looking prompt text.
|
|
2046
|
+
const runtimeHistory = connection.session.history;
|
|
2047
|
+
const replay = connection.session.replay.filter(event => event.sequence > connection.session.historyCursor);
|
|
2048
|
+
const hasRuntimeHistory = runtimeHistory.length > 0 || replay.some(event => event.kind === 'user_message');
|
|
2049
|
+
const usingSavedHistory = !hasRuntimeHistory && Boolean(connection.persistedHistory?.length);
|
|
2050
|
+
const canonicalHistory = usingSavedHistory ? connection.persistedHistory : runtimeHistory;
|
|
2030
2051
|
const pendingToolNames = new Set();
|
|
2031
2052
|
if (connection.session.descriptor?.turn_state === 'busy') {
|
|
2032
2053
|
for (const request of connection.session.pendingRequests.values()) {
|
|
@@ -2038,14 +2059,44 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2038
2059
|
pendingToolNames.add(name);
|
|
2039
2060
|
}
|
|
2040
2061
|
}
|
|
2041
|
-
const projected =
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2062
|
+
const projected = projectReplayTurns(replay.filter(event => {
|
|
2063
|
+
if (event.kind !== 'request')
|
|
2064
|
+
return true;
|
|
2065
|
+
const payload = event.payload;
|
|
2066
|
+
return !payload?.request || !connection.session.pendingRequests.has(payload.request.id);
|
|
2067
|
+
}), granted, {
|
|
2068
|
+
connectionId: connection.id,
|
|
2069
|
+
pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
|
|
2070
|
+
canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
|
|
2071
|
+
}, projectHistory(canonicalHistory, granted, pendingToolNames));
|
|
2072
|
+
const live = connection.session.descriptor?.turn_state === 'busy' || connection.session.pendingRequests.size > 0;
|
|
2073
|
+
const activeParts = [];
|
|
2074
|
+
if (live) {
|
|
2075
|
+
// Standalone requests have no user turn. Give their live response
|
|
2076
|
+
// an explicit runtime marker instead of reopening an old answer.
|
|
2077
|
+
let lastBoundary = -1;
|
|
2078
|
+
let lastUser = -1;
|
|
2079
|
+
for (const event of replay) {
|
|
2080
|
+
if (['turn_succeeded', 'turn_failed', 'turn_interrupted'].includes(event.kind))
|
|
2081
|
+
lastBoundary = event.sequence;
|
|
2082
|
+
if (event.kind === 'user_message')
|
|
2083
|
+
lastUser = event.sequence;
|
|
2084
|
+
}
|
|
2085
|
+
if (!projected.some(turn => turn.role === 'user')
|
|
2086
|
+
|| (connection.session.descriptor?.turn_state !== 'busy' && lastUser <= lastBoundary)) {
|
|
2087
|
+
projected.push({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
|
|
2088
|
+
}
|
|
2089
|
+
// Leave the final request without a historical response, so the
|
|
2090
|
+
// callback creates its response from live progress. Rehydrated
|
|
2091
|
+
// responses can be stale; their command buttons are disabled.
|
|
2092
|
+
const last = projected.at(-1);
|
|
2093
|
+
if (last?.role === 'assistant') {
|
|
2094
|
+
activeParts.push(...(projected.pop()?.parts ?? []).map(part => part.part === 'answers'
|
|
2095
|
+
? { part: 'markdown', value: `**${part.title}**\n\n${part.message}` }
|
|
2096
|
+
: part));
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
if (usingSavedHistory && connection.historyTruncated)
|
|
2049
2100
|
history.push(new turns.ChatRequestTurn('[Earlier conversation history]', undefined, [], authorId, []), new turns.ChatResponseTurn([new vscode.ChatResponseMarkdownPart('_Showing the latest 120 saved messages. Earlier context remains in the harness session._')], {}, authorId));
|
|
2050
2101
|
for (const turn of projected) {
|
|
2051
2102
|
if (turn.role === 'user') {
|
|
@@ -2054,16 +2105,16 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2054
2105
|
}
|
|
2055
2106
|
history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {}, authorId));
|
|
2056
2107
|
}
|
|
2057
|
-
const busy = connection.session.descriptor?.turn_state === 'busy';
|
|
2058
2108
|
const renderedCursor = connection.session.cursor;
|
|
2059
|
-
|
|
2109
|
+
const pendingAtCursor = [...connection.session.pendingRequests.values()];
|
|
2110
|
+
if (!live)
|
|
2060
2111
|
connection.viewReady = attached;
|
|
2061
2112
|
return {
|
|
2062
2113
|
title: connection.label,
|
|
2063
2114
|
options: controls?.attached(connection) ? undefined : controls?.currentOptions(resource),
|
|
2064
2115
|
history,
|
|
2065
2116
|
requestHandler: boundHandler,
|
|
2066
|
-
activeResponseCallback:
|
|
2117
|
+
activeResponseCallback: live
|
|
2067
2118
|
? async (stream, token) => {
|
|
2068
2119
|
// A turn re-joined after a reload has lost the handler that ran it, so its approvals are
|
|
2069
2120
|
// answered here under the conversation's mode (t_39dac582): Auto approve answers the cards
|
|
@@ -2071,22 +2122,24 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2071
2122
|
const watch = controls && !controls.attached(connection) ? controls.watchApprovals(connection, stream, token) : undefined;
|
|
2072
2123
|
try {
|
|
2073
2124
|
const state = { toolInputs: new Map(), toolNames: new Map(), toolTitles: new Map(), toolCommands: new Map(), quiet: watch?.answers };
|
|
2074
|
-
|
|
2125
|
+
for (const part of oneRowPerToolCall(activeParts))
|
|
2126
|
+
writePart(stream, part, state);
|
|
2127
|
+
writeProjected(stream, projectPendingRequests(pendingAtCursor.filter(request => connection.session.pendingRequests.has(request.id)), {
|
|
2075
2128
|
connectionId: connection.id,
|
|
2076
|
-
canRespond: connection.session.descriptor?.actions.respond
|
|
2129
|
+
canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
|
|
2077
2130
|
}), granted, state);
|
|
2078
2131
|
watch?.sweep();
|
|
2079
2132
|
await observeActiveResponse(connection.session, token, (event) => {
|
|
2080
2133
|
writeProjected(stream, projectEvent(event, {
|
|
2081
2134
|
connectionId: connection.id,
|
|
2082
2135
|
pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
|
|
2083
|
-
canRespond: connection.session.descriptor?.actions.respond
|
|
2136
|
+
canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
|
|
2084
2137
|
}), granted, state);
|
|
2085
2138
|
watch?.sweep();
|
|
2086
2139
|
}, () => { connection.viewReady = true; }, renderedCursor);
|
|
2087
2140
|
}
|
|
2088
2141
|
finally {
|
|
2089
|
-
connection.viewReady = connection.session.descriptor?.turn_state !== 'busy';
|
|
2142
|
+
connection.viewReady = connection.session.descriptor?.turn_state !== 'busy' && !connection.session.pendingRequests.size;
|
|
2090
2143
|
watch?.dispose();
|
|
2091
2144
|
}
|
|
2092
2145
|
}
|
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
|
}
|
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/native-session.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ import type { FrontendEvent } from '../vendor/supercode-frontend/generated.mjs';
|
|
|
3
3
|
import { type VscodeFrontendSession } from './session.js';
|
|
4
4
|
export declare function sessionStatus(session: Pick<VscodeFrontendSession, 'descriptor' | 'pendingRequests'>): 'NeedsInput' | 'InProgress' | 'Completed';
|
|
5
5
|
/** Follow a restored turn, including the interval between snapshot and callback. */
|
|
6
|
-
export declare function observeActiveResponse(session: Pick<VscodeFrontendSession, 'descriptor' | 'onEvent' | 'interrupt' | 'actions' | 'replay'>, token: CancellationToken, write: (event: FrontendEvent) => void, onSubscribed?: () => void, afterSequence?: number): Promise<void>;
|
|
6
|
+
export declare function observeActiveResponse(session: Pick<VscodeFrontendSession, 'descriptor' | 'pendingRequests' | 'onEvent' | 'interrupt' | 'actions' | 'replay'>, token: CancellationToken, write: (event: FrontendEvent) => void, onSubscribed?: () => void, afterSequence?: number): Promise<void>;
|
package/dist/native-session.js
CHANGED
|
@@ -34,7 +34,12 @@ export async function observeActiveResponse(session, token, write, onSubscribed,
|
|
|
34
34
|
reject(error);
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
|
-
if (
|
|
37
|
+
if (event.kind === 'runtime_disconnected') {
|
|
38
|
+
resolve();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const ended = TURN_TERMINAL_KINDS.includes(event.kind) || event.kind === 'request_resolved';
|
|
42
|
+
if (!cancelling && ended && session.descriptor?.turn_state !== 'busy' && !session.pendingRequests.size)
|
|
38
43
|
resolve();
|
|
39
44
|
};
|
|
40
45
|
unsubscribe = session.onEvent(consume);
|
|
@@ -47,7 +52,7 @@ export async function observeActiveResponse(session, token, write, onSubscribed,
|
|
|
47
52
|
cancellation = token.onCancellationRequested(cancel);
|
|
48
53
|
if (token.isCancellationRequested)
|
|
49
54
|
cancel();
|
|
50
|
-
else if (session.descriptor?.turn_state !== 'busy')
|
|
55
|
+
else if (session.descriptor?.turn_state !== 'busy' && !session.pendingRequests.size)
|
|
51
56
|
resolve();
|
|
52
57
|
});
|
|
53
58
|
}
|
package/dist/session.d.ts
CHANGED
|
@@ -147,7 +147,7 @@ export declare class VscodeFrontendSession {
|
|
|
147
147
|
actions(): VscodeFrontendActions;
|
|
148
148
|
/** Run one controller action, re-taking an expired lease exactly once. */
|
|
149
149
|
private withControl;
|
|
150
|
-
submit(prompt: string): Promise<void>;
|
|
150
|
+
submit(prompt: string, throwIfCancelled?: () => void): Promise<void>;
|
|
151
151
|
steer(prompt: string): Promise<void>;
|
|
152
152
|
interrupt(): Promise<void>;
|
|
153
153
|
/** Stop one of the harness's own background tasks, by the id it listed it with. */
|
|
@@ -164,6 +164,7 @@ export declare class VscodeFrontendSession {
|
|
|
164
164
|
*/
|
|
165
165
|
runTurn(prompt: string, onEvent: (event: FrontendEvent) => void, options?: {
|
|
166
166
|
onCancel?: (cancel: () => void) => void;
|
|
167
|
+
throwIfCancelled?: () => void;
|
|
167
168
|
}): Promise<FrontendEvent | null>;
|
|
168
169
|
private require;
|
|
169
170
|
private notify;
|
package/dist/session.js
CHANGED
|
@@ -356,11 +356,16 @@ export class VscodeFrontendSession {
|
|
|
356
356
|
return action();
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
|
-
async submit(prompt) {
|
|
359
|
+
async submit(prompt, throwIfCancelled) {
|
|
360
|
+
throwIfCancelled?.();
|
|
360
361
|
this.require('submit');
|
|
361
362
|
if (prompt.length === 0)
|
|
362
363
|
throw new TypeError('prompt must not be empty');
|
|
363
|
-
await this.withControl(() =>
|
|
364
|
+
await this.withControl(() => {
|
|
365
|
+
// The same guard covers a send retried after an awaited lease renewal.
|
|
366
|
+
throwIfCancelled?.();
|
|
367
|
+
return this.client.sendInput(prompt);
|
|
368
|
+
});
|
|
364
369
|
}
|
|
365
370
|
async steer(prompt) {
|
|
366
371
|
this.require('steer');
|
|
@@ -474,7 +479,7 @@ export class VscodeFrontendSession {
|
|
|
474
479
|
void this.interrupt().catch((error) => fail?.(error));
|
|
475
480
|
});
|
|
476
481
|
try {
|
|
477
|
-
await this.submit(prompt);
|
|
482
|
+
await this.submit(prompt, options.throwIfCancelled);
|
|
478
483
|
return await finished;
|
|
479
484
|
}
|
|
480
485
|
finally {
|
|
@@ -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.d.ts
CHANGED
|
@@ -170,14 +170,6 @@ export interface ProjectedTurn {
|
|
|
170
170
|
/** Rendered parts for an assistant turn. */
|
|
171
171
|
parts?: ChatPart[];
|
|
172
172
|
}
|
|
173
|
-
/**
|
|
174
|
-
* A HOSTED harness runtime's transcript as chat turns.
|
|
175
|
-
*
|
|
176
|
-
* `HostedHarnessRuntime::attach` returns `history: Vec::new()` and puts the
|
|
177
|
-
* whole conversation in `replay` — the canonical ChatMessage history belongs to
|
|
178
|
-
* supercode's own agent loop, and a hosted harness has none. So a resumed
|
|
179
|
-
* Claude Code session's turns are here or nowhere.
|
|
180
|
-
*/
|
|
181
173
|
/**
|
|
182
174
|
* A "user" message no person typed: Claude Code's `<task-notification>` (a background task reporting back) or the
|
|
183
175
|
* editor's automatic tripwire nudge. Neither is shown as the person's words. A task notification is said as one
|
|
@@ -186,7 +178,7 @@ export interface ProjectedTurn {
|
|
|
186
178
|
export declare function automaticPrompt(body: string): {
|
|
187
179
|
notice?: string;
|
|
188
180
|
} | undefined;
|
|
189
|
-
export declare function projectReplayTurns(events: readonly FrontendEvent[], granted: ReadonlySet<string>, context: ProjectionContext): ProjectedTurn[];
|
|
181
|
+
export declare function projectReplayTurns(events: readonly FrontendEvent[], granted: ReadonlySet<string>, context: ProjectionContext, history?: readonly ProjectedTurn[]): ProjectedTurn[];
|
|
190
182
|
/**
|
|
191
183
|
* The attach snapshot's canonical history as chat turns. Bounded: when older
|
|
192
184
|
* turns are omitted the view says so, the way the browser observer does.
|
package/dist/view.js
CHANGED
|
@@ -650,14 +650,6 @@ function messageText(message) {
|
|
|
650
650
|
.filter(Boolean)
|
|
651
651
|
.join('\n');
|
|
652
652
|
}
|
|
653
|
-
/**
|
|
654
|
-
* A HOSTED harness runtime's transcript as chat turns.
|
|
655
|
-
*
|
|
656
|
-
* `HostedHarnessRuntime::attach` returns `history: Vec::new()` and puts the
|
|
657
|
-
* whole conversation in `replay` — the canonical ChatMessage history belongs to
|
|
658
|
-
* supercode's own agent loop, and a hosted harness has none. So a resumed
|
|
659
|
-
* Claude Code session's turns are here or nowhere.
|
|
660
|
-
*/
|
|
661
653
|
/**
|
|
662
654
|
* A "user" message no person typed: Claude Code's `<task-notification>` (a background task reporting back) or the
|
|
663
655
|
* editor's automatic tripwire nudge. Neither is shown as the person's words. A task notification is said as one
|
|
@@ -675,8 +667,13 @@ export function automaticPrompt(body) {
|
|
|
675
667
|
return {};
|
|
676
668
|
return undefined;
|
|
677
669
|
}
|
|
678
|
-
export function projectReplayTurns(events, granted, context) {
|
|
679
|
-
|
|
670
|
+
export function projectReplayTurns(events, granted, context, history = []) {
|
|
671
|
+
// Join canonical history with its sequenced tail. Retained request/decision
|
|
672
|
+
// events can be re-sequenced past history_cursor with no originating turn.
|
|
673
|
+
// Before the replay supplies a prompt, give its parts their own runtime
|
|
674
|
+
// history turn rather than attributing old approvals to the latest answer.
|
|
675
|
+
const turns = history.map(turn => ({ ...turn, parts: turn.parts?.slice() }));
|
|
676
|
+
let replayTurn = false;
|
|
680
677
|
let streamedText;
|
|
681
678
|
for (const event of events) {
|
|
682
679
|
if (event.kind === 'user_message') {
|
|
@@ -686,8 +683,10 @@ export function projectReplayTurns(events, granted, context) {
|
|
|
686
683
|
if (automatic) {
|
|
687
684
|
if (!automatic.notice)
|
|
688
685
|
continue;
|
|
689
|
-
if (
|
|
686
|
+
if (!replayTurn) {
|
|
690
687
|
turns.push({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
|
|
688
|
+
replayTurn = true;
|
|
689
|
+
}
|
|
691
690
|
const last = turns[turns.length - 1];
|
|
692
691
|
const notice = { part: 'markdown', value: `\n\n${automatic.notice}\n\n` };
|
|
693
692
|
if (last.role === 'assistant' && last.parts)
|
|
@@ -697,6 +696,7 @@ export function projectReplayTurns(events, granted, context) {
|
|
|
697
696
|
continue;
|
|
698
697
|
}
|
|
699
698
|
turns.push({ role: 'user', prompt: clamp(stripControls(text(payload.text)), VSCODE_CHAT_LIMITS.partCharacters) });
|
|
699
|
+
replayTurn = true;
|
|
700
700
|
continue;
|
|
701
701
|
}
|
|
702
702
|
const parts = resolveParts(projectEvent(event, context), granted);
|
|
@@ -712,8 +712,10 @@ export function projectReplayTurns(events, granted, context) {
|
|
|
712
712
|
else {
|
|
713
713
|
streamedText = undefined;
|
|
714
714
|
}
|
|
715
|
-
if (
|
|
715
|
+
if (!replayTurn) {
|
|
716
716
|
turns.push({ role: 'user', prompt: HISTORY_MARKER_PROMPT });
|
|
717
|
+
replayTurn = true;
|
|
718
|
+
}
|
|
717
719
|
const last = turns[turns.length - 1];
|
|
718
720
|
if (last.role === 'assistant' && last.parts)
|
|
719
721
|
last.parts.push(...parts);
|
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.52",
|
|
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",
|