@volter/supercode-frontend-vscode 0.1.16 → 0.1.18
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 +101 -5
- package/dist/chat-setup.d.ts +6 -0
- package/dist/chat-setup.js +29 -0
- package/dist/extension.js +301 -16
- package/dist/host-controls.d.ts +32 -0
- package/dist/host-controls.js +3 -0
- package/dist/session.d.ts +2 -0
- package/dist/session.js +10 -0
- package/dist/view.js +53 -9
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -11,6 +11,88 @@ package or npm.
|
|
|
11
11
|
|
|
12
12
|
## Native chat controls
|
|
13
13
|
|
|
14
|
+
The optional host-controls state can include `setup: { ready, actions, reason,
|
|
15
|
+
cwd }`. Each action supplies `kind` (`login` or `install`), `harness`, `name` and
|
|
16
|
+
the exact `command` shown before execution. Chat renders native buttons, with
|
|
17
|
+
**Sign in with ChatGPT** for Codex and the reported name for other harnesses.
|
|
18
|
+
Clicking asks the host's authenticated `/setup` door to revalidate `{ kind,
|
|
19
|
+
harness }` and return the action plus a structured `{ program, arguments, cwd, env? }`
|
|
20
|
+
terminal launch. Absolute executables and an optional host-authored PATH keep
|
|
21
|
+
the terminal on the same installation and discovery path as the host's probe.
|
|
22
|
+
It runs directly as the integrated terminal's process, without typing a shell
|
|
23
|
+
command; credentials and
|
|
24
|
+
browser interaction belong to the harness. Failed or cancelled processes can be
|
|
25
|
+
retried, and closing a terminal never counts as successful authentication.
|
|
26
|
+
|
|
27
|
+
While setup is not ready, Chat refreshes `/state` every three seconds and exposes
|
|
28
|
+
**Check again**. When the host returns a ready runtime connection, it attaches
|
|
29
|
+
and reveals that conversation without a reload. Polling only finishes after both
|
|
30
|
+
attachment and the host's native reveal command succeed for that same session
|
|
31
|
+
and runtime URL. A transient attach/reveal failure stays pending and is retried;
|
|
32
|
+
**Check again** retries that handoff even when the host already reports ready.
|
|
33
|
+
Changed setup resources refresh
|
|
34
|
+
Code-OSS's cached status history after an install. Hosts without these optional
|
|
35
|
+
fields keep the existing runtime-attachment behavior.
|
|
36
|
+
|
|
37
|
+
`starterPrompts?: {label: string; prompt: string}[]` is project data supplied by
|
|
38
|
+
the host. An eligible new conversation exposes at most three short command links
|
|
39
|
+
in the default participant's native welcome content, outside the transcript.
|
|
40
|
+
The host's `fillInputCommand` receives the exact native session resource and full
|
|
41
|
+
prompt and must return `{sessionResource, input}` after filling that existing
|
|
42
|
+
widget, without submitting. A missing or mismatched receipt is an error.
|
|
43
|
+
The untargeted partial-query command is not used. Links never send or write a turn.
|
|
44
|
+
Presentation and clicks use the same `starterEligibility` calculation; runtime
|
|
45
|
+
and host state changes update the welcome message, and a user turn clears it.
|
|
46
|
+
Native Chat also hides its welcome surface immediately when a request is added.
|
|
47
|
+
Eligibility requires a successful attachment, an idle runtime, no pending request,
|
|
48
|
+
no saved/canonical non-system messages or truncated saved history, and no observed
|
|
49
|
+
user turn. Accepted-input events (`user_message`, `turn_started`) and busy snapshots
|
|
50
|
+
record turn evidence that survives re-attachment and replay eviction. Setup/status
|
|
51
|
+
events can advance cursors without disqualifying a new conversation; system-only
|
|
52
|
+
history also stays eligible. The checks run again after
|
|
53
|
+
project data loads and before filling; a failed attachment never offers starters.
|
|
54
|
+
`supercode.frontend.status(true)` includes each connection's `starterEligibility`:
|
|
55
|
+
attachment state, turn state, pending count, saved-history presence/truncation/count
|
|
56
|
+
and roles, canonical count/roles, sticky turn evidence, both cursors, replay count
|
|
57
|
+
and kinds, active host ID, host busy state, connection identity check, and rejection
|
|
58
|
+
reasons. `lastStarterAttempt` preserves the exact post-attachment guard snapshot
|
|
59
|
+
and native binding from the latest click. These contain no message bodies or tokens.
|
|
60
|
+
A rejected click names its failed conditions instead of a generic new/idle error.
|
|
61
|
+
The pinned Chat session API exposes immutable history, without withdrawal or
|
|
62
|
+
live enablement of a rendered command part. Starters therefore never enter history.
|
|
63
|
+
`defaultChatParticipant.additionalWelcomeMessage` supplies trusted Markdown with
|
|
64
|
+
only `supercode.frontend.fillStarter` allowed; labels are escaped and command
|
|
65
|
+
arguments are encoded. Hosts without that proposal get no starter offer rather
|
|
66
|
+
than permanent transcript buttons. Existing setup actions keep their native status
|
|
67
|
+
presentation. No new workbench API or editor-side contribution is required.
|
|
68
|
+
The pinned Code-OSS `f16dc165` has no `suggestedPrompts` field in either
|
|
69
|
+
`vscode.proposed.defaultChatParticipant.d.ts` or the internal
|
|
70
|
+
`IChatViewWelcomeContent`. `ChatViewWelcomePart` renders `additionalMessage` as
|
|
71
|
+
Markdown; its optional `firstLinkToButton` converts only one anchor and the
|
|
72
|
+
participant welcome never enables that option. The editor's native overlay can
|
|
73
|
+
render standalone trusted command links as welcome buttons without a new extension
|
|
74
|
+
API; other hosts keep Markdown links. The two introductory lines live together in
|
|
75
|
+
this extension's `package.json`: `chatSessions.welcomeMessage` and
|
|
76
|
+
`supercodeFrontendWelcome.starterIntroduction`. Hosts own their project prompt data.
|
|
77
|
+
If `starterPromptsPending` is true, the project configuration is still loading.
|
|
78
|
+
Empty-conversation resolution refreshes the host until that declaration arrives
|
|
79
|
+
(cancellable, with a 30-second timeout), so native Chat does not cache an empty
|
|
80
|
+
offer before the project adapter loads.
|
|
81
|
+
|
|
82
|
+
W32/W37 passed starter filling and W37 passed healthy refresh with draft retention.
|
|
83
|
+
W44 passed a real boost turn and exposed a presentation defect: transcript starter
|
|
84
|
+
buttons remained after the turn even though the guard rejected them. W50 passed
|
|
85
|
+
the native welcome fix at `29d8fac99`: all three pointer fills, one real HUD turn,
|
|
86
|
+
first-turn disappearance, return-to-idle suppression, and no starters when reopening
|
|
87
|
+
the earlier conversation with seven history entries. The shorter introductory copy
|
|
88
|
+
is source-only; it needs a frontend build plus a workbench overlay copy.
|
|
89
|
+
The accompanying native button and New Session repairs require a Code-OSS compile.
|
|
90
|
+
W50 also found that the generic native New Session action
|
|
91
|
+
bypasses this provider's creation command; its owning host must route that action
|
|
92
|
+
through `chatSessions/newSession` rather than opening an unbound untitled resource.
|
|
93
|
+
Signed-out/install transitions and failed-discovery/attachment recovery remain
|
|
94
|
+
unverified live. A published frontend and rebuilt workbench are needed for npx users.
|
|
95
|
+
|
|
14
96
|
A bundling host can expose a private lifecycle endpoint through
|
|
15
97
|
`SUPERCODE_FRONTEND_HOST_URL` and `SUPERCODE_FRONTEND_HOST_CREDENTIAL_FILE`.
|
|
16
98
|
The endpoint must be loopback HTTP and the credential follows the runtime credential-file
|
|
@@ -20,11 +102,25 @@ and returns its new scoped connection. The host owns validation, process lifetim
|
|
|
20
102
|
persistence, and failure handling. Prompts and approvals still use the generated frontend client.
|
|
21
103
|
The contract is in `src/host-controls.ts`.
|
|
22
104
|
|
|
23
|
-
With that host, **Chat: New Chat: Choose
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
105
|
+
With that host, **New Session** / **Chat: New Chat: Choose Agent** asks only
|
|
106
|
+
which startable agent to use. It refreshes the host's available choices first;
|
|
107
|
+
with one agent it opens immediately, and with none it returns to setup. Model
|
|
108
|
+
and reasoning are sent as empty values, leaving both at the harness's own defaults,
|
|
109
|
+
even if the previous conversation used explicit settings. Cancelling the agent
|
|
110
|
+
picker creates nothing.
|
|
111
|
+
|
|
112
|
+
The composer's native **Model settings** control offers **Choose model and reasoning
|
|
113
|
+
for a new chat…** for Claude Code and Codex. This optional path retains the model
|
|
114
|
+
list, exact model ID entry and reasoning choices. Only completing the choices
|
|
115
|
+
starts a separate conversation; cancelling either leaves the current one intact.
|
|
116
|
+
A conversation's harness, model and reasoning remain fixed: this integration does
|
|
117
|
+
not advertise hot swapping. The built-in model picker still displays the selected
|
|
118
|
+
model and provider icon; nondelegating session providers cannot add its Manage
|
|
119
|
+
Models action, so configuration uses the native session-options command door.
|
|
120
|
+
The response author identifies the harness. Other harnesses keep their own defaults.
|
|
121
|
+
|
|
122
|
+
This simplified creation flow is source-only pending frontend compilation and a
|
|
123
|
+
live check of zero/one picker, cancellation, defaults and explicit configuration.
|
|
28
124
|
|
|
29
125
|
The host publishes durable conversation IDs and their exact harness history identities.
|
|
30
126
|
`POST /open` resumes only the requested saved conversation; `POST /remember` binds its
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Native Chat presentation; the host supplies inventory, terminal actions and project data. */
|
|
2
|
+
import * as vscode from 'vscode';
|
|
3
|
+
import type { ChatSetupState, ChatStarterPrompt } from './host-controls.js';
|
|
4
|
+
export declare function setupParts(setup: ChatSetupState): Array<vscode.ChatResponseMarkdownPart | vscode.ChatResponseCommandButtonPart>;
|
|
5
|
+
/** Welcome content is owned by the empty-state renderer, never transcript history. */
|
|
6
|
+
export declare function starterWelcome(prompts: readonly ChatStarterPrompt[], session: string, resource: string, introduction: string): vscode.MarkdownString;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Native Chat presentation; the host supplies inventory, terminal actions and project data. */
|
|
2
|
+
import * as vscode from 'vscode';
|
|
3
|
+
export function setupParts(setup) {
|
|
4
|
+
const parts = [
|
|
5
|
+
new vscode.ChatResponseMarkdownPart('Sign in to use Chat. Complete sign-in in the terminal and browser.'),
|
|
6
|
+
];
|
|
7
|
+
for (const action of setup.actions) {
|
|
8
|
+
const title = action.kind === 'install' ? `Install ${action.name}`
|
|
9
|
+
: action.harness === 'codex' ? 'Sign in with ChatGPT' : `Sign in with ${action.name}`;
|
|
10
|
+
parts.push(new vscode.ChatResponseMarkdownPart(`Runs: \`${action.command.replace(/`/g, '\\`')}\``));
|
|
11
|
+
parts.push(new vscode.ChatResponseCommandButtonPart({ title, command: 'supercode.frontend.setup', arguments: [action.kind, action.harness] }));
|
|
12
|
+
}
|
|
13
|
+
if (!setup.actions.length) {
|
|
14
|
+
parts.push(new vscode.ChatResponseMarkdownPart(setup.reason || 'No sign-in action is available.'));
|
|
15
|
+
}
|
|
16
|
+
parts.push(new vscode.ChatResponseCommandButtonPart({ title: 'Check again', command: 'supercode.frontend.refreshSetup' }));
|
|
17
|
+
return parts;
|
|
18
|
+
}
|
|
19
|
+
/** Welcome content is owned by the empty-state renderer, never transcript history. */
|
|
20
|
+
export function starterWelcome(prompts, session, resource, introduction) {
|
|
21
|
+
const message = new vscode.MarkdownString().appendText(introduction).appendMarkdown('\n\n');
|
|
22
|
+
message.isTrusted = { enabledCommands: ['supercode.frontend.fillStarter'] };
|
|
23
|
+
for (const [index, starter] of prompts.slice(0, 3).entries()) {
|
|
24
|
+
const args = encodeURIComponent(JSON.stringify([session, index, resource]));
|
|
25
|
+
message.appendMarkdown('[').appendText(starter.label)
|
|
26
|
+
.appendMarkdown(`](command:supercode.frontend.fillStarter?${args})\n\n`);
|
|
27
|
+
}
|
|
28
|
+
return message;
|
|
29
|
+
}
|
package/dist/extension.js
CHANGED
|
@@ -2,6 +2,7 @@ import { resolveSessionBinding } from './bindings.js';
|
|
|
2
2
|
import { withTextContext } from './context.js';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { ChatHostControls, runtimeIdentity, harnessDisplayName, turnPermission } from './host-controls.js';
|
|
5
|
+
import { setupParts, starterWelcome } from './chat-setup.js';
|
|
5
6
|
import * as vscode from 'vscode';
|
|
6
7
|
import { createFrontendClient } from './client.js';
|
|
7
8
|
import { readCredentialFile } from './credential.js';
|
|
@@ -32,6 +33,44 @@ const ENV = Object.freeze({
|
|
|
32
33
|
*/
|
|
33
34
|
unavailable: 'SUPERCODE_FRONTEND_UNAVAILABLE',
|
|
34
35
|
});
|
|
36
|
+
/** Transport progress includes setup events; it is not a user-turn count. */
|
|
37
|
+
function conversationEligibility(connection) {
|
|
38
|
+
const session = connection.session;
|
|
39
|
+
const inputs = {
|
|
40
|
+
attachmentState: session.connection,
|
|
41
|
+
turnState: session.descriptor?.turn_state ?? null,
|
|
42
|
+
pendingRequestCount: session.pendingRequests.size,
|
|
43
|
+
historyTruncated: connection.historyTruncated ?? false,
|
|
44
|
+
persistedHistoryLoaded: connection.persistedHistory !== undefined,
|
|
45
|
+
persistedHistoryCount: connection.persistedHistory?.length ?? 0,
|
|
46
|
+
persistedHistoryRoles: connection.persistedHistory?.map(message => message.role) ?? [],
|
|
47
|
+
canonicalHistoryCount: session.history.length,
|
|
48
|
+
canonicalHistoryRoles: session.history.map(message => message.role),
|
|
49
|
+
hasUserTurn: session.hasUserTurn,
|
|
50
|
+
historyCursor: session.historyCursor,
|
|
51
|
+
eventCursor: session.cursor,
|
|
52
|
+
replayLength: session.replay.length,
|
|
53
|
+
replayKinds: [...new Set(session.replay.map(event => event.kind))],
|
|
54
|
+
nativeEventKinds: [...new Set(session.replay.filter(event => event.kind === 'native_event')
|
|
55
|
+
.map(event => event.payload?.kind).filter(kind => typeof kind === 'string'))],
|
|
56
|
+
};
|
|
57
|
+
const reasons = [];
|
|
58
|
+
if (inputs.attachmentState !== 'connected')
|
|
59
|
+
reasons.push(`attachmentState=${inputs.attachmentState}`);
|
|
60
|
+
if (inputs.turnState !== 'idle')
|
|
61
|
+
reasons.push(`turnState=${inputs.turnState}`);
|
|
62
|
+
if (inputs.pendingRequestCount)
|
|
63
|
+
reasons.push(`pendingRequestCount=${inputs.pendingRequestCount}`);
|
|
64
|
+
if (inputs.historyTruncated)
|
|
65
|
+
reasons.push('saved history is truncated');
|
|
66
|
+
if (inputs.persistedHistoryRoles.some(role => role !== 'system'))
|
|
67
|
+
reasons.push('saved history contains a turn');
|
|
68
|
+
if (inputs.canonicalHistoryRoles.some(role => role !== 'system'))
|
|
69
|
+
reasons.push('canonical history contains a turn');
|
|
70
|
+
if (inputs.hasUserTurn)
|
|
71
|
+
reasons.push('a user turn has been observed');
|
|
72
|
+
return { ...inputs, eligible: reasons.length === 0, reasons };
|
|
73
|
+
}
|
|
35
74
|
function normalizePermissions(value, fallback) {
|
|
36
75
|
const raw = Array.isArray(value)
|
|
37
76
|
? value.map(String)
|
|
@@ -324,6 +363,24 @@ export async function activate(context) {
|
|
|
324
363
|
const boundId = (resource) => resolveSessionBinding(resource, controls?.state.sessions ?? [], resourceBindings);
|
|
325
364
|
let applying = Promise.resolve();
|
|
326
365
|
const frontends = new SupercodeFrontends();
|
|
366
|
+
const starterEligibility = (connection) => {
|
|
367
|
+
const conversation = conversationEligibility(connection);
|
|
368
|
+
const activeSession = controls?.state.activeSession ?? null;
|
|
369
|
+
const hostBusy = Boolean(controls?.state.busy);
|
|
370
|
+
const connectionMatches = frontends.get(connection.id) === connection;
|
|
371
|
+
const reasons = [...conversation.reasons];
|
|
372
|
+
if (!controls)
|
|
373
|
+
reasons.push('host controls unavailable');
|
|
374
|
+
if (activeSession !== connection.id)
|
|
375
|
+
reasons.push(`activeSession=${activeSession}; expected ${connection.id}`);
|
|
376
|
+
if (hostBusy)
|
|
377
|
+
reasons.push('hostBusy=true');
|
|
378
|
+
if (!connectionMatches)
|
|
379
|
+
reasons.push('connection was replaced');
|
|
380
|
+
return { ...conversation, controlsAvailable: Boolean(controls), connectionId: connection.id, activeSession, hostBusy, connectionMatches,
|
|
381
|
+
eligible: reasons.length === 0, reasons };
|
|
382
|
+
};
|
|
383
|
+
const lastStarterAttempts = new Map();
|
|
327
384
|
// Runtimes attached by address ("Set Runtime Token", "Attach to Runtime")
|
|
328
385
|
// beside a host's managed conversation. The host's catalogue does not know
|
|
329
386
|
// them, so they open as what they are: attachments, shown as their runtime
|
|
@@ -332,6 +389,28 @@ export async function activate(context) {
|
|
|
332
389
|
const managed = () => frontends.all.find((connection) => !attachedIds.has(connection.id));
|
|
333
390
|
// The attached runtime the person is looking at: the Chat names its harness, not the host's.
|
|
334
391
|
let focusedAttached;
|
|
392
|
+
const welcomeParticipants = new Set();
|
|
393
|
+
const welcomeValues = new WeakMap();
|
|
394
|
+
const starterResources = new Map();
|
|
395
|
+
const updateStarterWelcome = () => {
|
|
396
|
+
if (!granted.has('defaultChatParticipant'))
|
|
397
|
+
return;
|
|
398
|
+
const connection = managed();
|
|
399
|
+
const prompts = controls?.state.starterPrompts ?? [];
|
|
400
|
+
// This is the click guard's exact state, including host activity and connection
|
|
401
|
+
// identity. A later idle state must not revive an offer after a user turn.
|
|
402
|
+
const message = connection && !focusedAttached && !controls?.state.starterPromptsPending
|
|
403
|
+
&& prompts.length && starterEligibility(connection).eligible
|
|
404
|
+
? starterWelcome(prompts, connection.id, starterResources.get(connection.id) ?? sessionResource(connection).toString(), context.extension.packageJSON.supercodeFrontendWelcome.starterIntroduction)
|
|
405
|
+
: new vscode.MarkdownString('');
|
|
406
|
+
for (const participant of welcomeParticipants) {
|
|
407
|
+
if (welcomeValues.get(participant) === message.value)
|
|
408
|
+
continue;
|
|
409
|
+
participant.additionalWelcomeMessage = message;
|
|
410
|
+
welcomeValues.set(participant, message.value);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
context.subscriptions.push(frontends.onDidChange(updateStarterWelcome), controlsChanged.event(updateStarterWelcome));
|
|
335
414
|
let approvalAvailable = false;
|
|
336
415
|
context.subscriptions.push(frontends.onDidChange(() => {
|
|
337
416
|
const active = managed();
|
|
@@ -372,7 +451,7 @@ export async function activate(context) {
|
|
|
372
451
|
statusIcon: new vscode.ThemeIcon(provider === 'anthropic' ? 'claude' : provider === 'openai' ? 'openai' : provider === 'google' ? 'google-gemini' : 'sparkle'),
|
|
373
452
|
maxInputTokens: 0, maxOutputTokens: 0, capabilities: {},
|
|
374
453
|
targetChatSessionType: SESSION_SCHEME, isUserSelectable: true, isDefault: model.id === selection.model,
|
|
375
|
-
tooltip: `${model.name} via ${harnessDisplayName(selection.harness)}${selection.effort ? ` · ${selection.effort} reasoning` : ''}. Model is fixed for this conversation. Use
|
|
454
|
+
tooltip: `${model.name} via ${harnessDisplayName(selection.harness)}${selection.effort ? ` · ${selection.effort} reasoning` : ''}. Model is fixed for this conversation. Use Model settings to choose model and reasoning for a new conversation.`,
|
|
376
455
|
};
|
|
377
456
|
});
|
|
378
457
|
},
|
|
@@ -392,8 +471,11 @@ export async function activate(context) {
|
|
|
392
471
|
throw new Error('The requested conversation has no runtime connection.');
|
|
393
472
|
const id = controls.state.activeSession;
|
|
394
473
|
const existing = frontends.get(id);
|
|
395
|
-
if (existing && existing.baseUrl === env.SUPERCODE_FRONTEND_URL)
|
|
474
|
+
if (existing && existing.baseUrl === env.SUPERCODE_FRONTEND_URL && existing.session.connection !== 'closed') {
|
|
475
|
+
if (existing.session.connection !== 'connected')
|
|
476
|
+
await existing.session.attach();
|
|
396
477
|
return existing;
|
|
478
|
+
}
|
|
397
479
|
for (const connection of frontends.all.filter((entry) => !attachedIds.has(entry.id)))
|
|
398
480
|
await frontends.disconnect(connection.id);
|
|
399
481
|
const connection = await frontends.connect({ connectionId: id, baseUrl: env.SUPERCODE_FRONTEND_URL,
|
|
@@ -403,12 +485,124 @@ export async function activate(context) {
|
|
|
403
485
|
throw new Error('Could not connect to the requested conversation.');
|
|
404
486
|
return connection;
|
|
405
487
|
};
|
|
488
|
+
let setupHandoff;
|
|
489
|
+
const setupHandoffComplete = () => {
|
|
490
|
+
const id = controls?.state.activeSession;
|
|
491
|
+
const connection = id ? frontends.get(id) : undefined;
|
|
492
|
+
return Boolean(setupHandoff && setupHandoff.id === id
|
|
493
|
+
&& setupHandoff.baseUrl === controls?.state.connection?.SUPERCODE_FRONTEND_URL
|
|
494
|
+
&& connection?.baseUrl === setupHandoff.baseUrl && connection.session.connection === 'connected');
|
|
495
|
+
};
|
|
406
496
|
const revealManaged = async () => {
|
|
407
497
|
const id = controls?.state.activeSession;
|
|
408
498
|
if (!id || !controls?.state.openSessionCommand)
|
|
409
499
|
throw new Error('The host cannot reveal this conversation.');
|
|
410
500
|
await vscode.commands.executeCommand(controls.state.openSessionCommand, vscode.Uri.from({ scheme: SESSION_SCHEME, authority: id, path: '/conversation' }).toString());
|
|
501
|
+
const connection = frontends.get(id);
|
|
502
|
+
if (controls.state.activeSession !== id || !connection || connection.session.connection !== 'connected'
|
|
503
|
+
|| connection.baseUrl !== controls.state.connection?.SUPERCODE_FRONTEND_URL) {
|
|
504
|
+
throw new Error('The conversation changed or disconnected before setup could reveal it.');
|
|
505
|
+
}
|
|
506
|
+
// Host readiness alone is not completion: the native reveal must succeed too.
|
|
507
|
+
setupHandoff = { id, baseUrl: connection.baseUrl };
|
|
508
|
+
};
|
|
509
|
+
let setupFingerprint = '';
|
|
510
|
+
const revealSetup = async () => {
|
|
511
|
+
if (!controls?.state.setup || controls.state.setup.ready || !controls.state.openSessionCommand)
|
|
512
|
+
return;
|
|
513
|
+
const fingerprint = JSON.stringify(controls.state.setup);
|
|
514
|
+
if (setupFingerprint === fingerprint)
|
|
515
|
+
return;
|
|
516
|
+
setupFingerprint = fingerprint;
|
|
517
|
+
// A changed resource avoids Code-OSS reusing cached status history after an install.
|
|
518
|
+
try {
|
|
519
|
+
await vscode.commands.executeCommand(controls.state.openSessionCommand, vscode.Uri.from({ scheme: SESSION_SCHEME, path: UNAVAILABLE_PATH, query: encodeURIComponent(fingerprint) }).toString());
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
setupFingerprint = '';
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
let refreshingSetup;
|
|
527
|
+
const refreshSetup = () => {
|
|
528
|
+
refreshingSetup ??= (async () => {
|
|
529
|
+
if (!controls)
|
|
530
|
+
return;
|
|
531
|
+
await controls.refresh();
|
|
532
|
+
if (controls.state.setup?.ready) {
|
|
533
|
+
await connectManaged();
|
|
534
|
+
controlsChanged.fire();
|
|
535
|
+
if (!setupHandoffComplete())
|
|
536
|
+
await revealManaged();
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
setupHandoff = undefined;
|
|
540
|
+
controlsChanged.fire();
|
|
541
|
+
await revealSetup();
|
|
542
|
+
}
|
|
543
|
+
})().finally(() => { refreshingSetup = undefined; });
|
|
544
|
+
return refreshingSetup;
|
|
411
545
|
};
|
|
546
|
+
const setupTerminals = new Map();
|
|
547
|
+
context.subscriptions.push(vscode.window.onDidCloseTerminal(terminal => {
|
|
548
|
+
for (const [key, value] of setupTerminals)
|
|
549
|
+
if (value === terminal)
|
|
550
|
+
setupTerminals.delete(key);
|
|
551
|
+
}));
|
|
552
|
+
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.refreshSetup', refreshSetup));
|
|
553
|
+
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.setup', async (kind, harness) => {
|
|
554
|
+
if (!controls)
|
|
555
|
+
throw new Error('This host does not expose sign-in.');
|
|
556
|
+
const key = `${kind}/${harness}`;
|
|
557
|
+
const existing = setupTerminals.get(key);
|
|
558
|
+
if (existing && existing.exitStatus === undefined) {
|
|
559
|
+
existing.show();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
await controls.refresh();
|
|
563
|
+
const offered = controls.state.setup?.actions.find(a => a.kind === kind && a.harness === harness);
|
|
564
|
+
if (!offered) {
|
|
565
|
+
await refreshSetup();
|
|
566
|
+
throw new Error('This setup action is no longer available.');
|
|
567
|
+
}
|
|
568
|
+
const action = await controls.setup(kind, harness);
|
|
569
|
+
if (action.command !== offered.command)
|
|
570
|
+
throw new Error('The setup command changed. Check again before running it.');
|
|
571
|
+
const terminal = vscode.window.createTerminal({ name: `${action.name} · ${kind === 'login' ? 'sign in' : 'install'}`,
|
|
572
|
+
cwd: action.cwd, shellPath: action.program, shellArgs: action.arguments, env: action.env });
|
|
573
|
+
setupTerminals.set(key, terminal);
|
|
574
|
+
terminal.show();
|
|
575
|
+
}));
|
|
576
|
+
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.fillStarter', async (session, index, nativeResource) => {
|
|
577
|
+
if (!controls || !Number.isInteger(index) || index < 0 || index > 2)
|
|
578
|
+
throw new Error('Invalid starter selection.');
|
|
579
|
+
const connection = frontends.get(session);
|
|
580
|
+
if (!connection)
|
|
581
|
+
throw new Error(`Starter prompt unavailable: connection ${session} is absent.`);
|
|
582
|
+
const resource = nativeResource ? vscode.Uri.parse(nativeResource)
|
|
583
|
+
: vscode.Uri.from({ scheme: SESSION_SCHEME, authority: session, path: '/conversation' });
|
|
584
|
+
if (boundId(resource) !== session)
|
|
585
|
+
throw new Error('The starter has no matching conversation binding.');
|
|
586
|
+
await connection.session.attach();
|
|
587
|
+
const guard = starterEligibility(connection);
|
|
588
|
+
lastStarterAttempts.set(session, { at: new Date().toISOString(), resource: resource.toString(),
|
|
589
|
+
boundSession: boundId(resource), guard });
|
|
590
|
+
while (lastStarterAttempts.size > 50)
|
|
591
|
+
lastStarterAttempts.delete(lastStarterAttempts.keys().next().value);
|
|
592
|
+
if (!guard.eligible)
|
|
593
|
+
throw new Error(`Starter prompt unavailable: ${guard.reasons.join('; ')}.`);
|
|
594
|
+
const prompt = controls.state.starterPrompts?.[index]?.prompt;
|
|
595
|
+
if (!prompt)
|
|
596
|
+
throw new Error('This starter is no longer available.');
|
|
597
|
+
if (!controls.state.fillInputCommand)
|
|
598
|
+
throw new Error('This host does not support filling a conversation draft.');
|
|
599
|
+
// The host addresses the existing native widget, never the default participant or last focus.
|
|
600
|
+
const receipt = await vscode.commands.executeCommand(controls.state.fillInputCommand, resource.toString(), prompt);
|
|
601
|
+
if (!receipt || receipt.sessionResource !== resource.toString() || receipt.input !== prompt) {
|
|
602
|
+
throw new Error('Chat did not confirm the requested draft.');
|
|
603
|
+
}
|
|
604
|
+
return receipt;
|
|
605
|
+
}));
|
|
412
606
|
const connectionFor = async (resource) => {
|
|
413
607
|
if (!controls) {
|
|
414
608
|
const connection = frontends.get(resource.authority);
|
|
@@ -477,6 +671,16 @@ export async function activate(context) {
|
|
|
477
671
|
const permissionItems = [{ id: 'ask', name: canApprove ? 'Ask for approval' : 'Harness permissions', description: canApprove ? 'Show each approval requested by the harness.' : 'Approval controls are unavailable for this harness.' },
|
|
478
672
|
...((canApprove || (!active && resource && currentOptions(resource).permissions === 'autoApprove')) ? [{ id: 'autoApprove', name: 'Auto approve', description: 'Allow tool approval requests for this chat. Harness sandbox rules still apply.' }] : [])];
|
|
479
673
|
const groups = [{ id: 'permissions', name: 'Permissions', kind: 'permissions', items: permissionItems, selected: permissionItems.find(i => i.id === (resource ? currentOptions(resource).permissions : 'ask')) }];
|
|
674
|
+
const selected = controls?.state.sessions.find(session => session.id === id);
|
|
675
|
+
if (selected && ['claude-code', 'codex'].includes(selected.selection.harness)) {
|
|
676
|
+
// Native session-option commands remain reachable for nondelegating providers,
|
|
677
|
+
// whose built-in model picker deliberately omits Manage Models. Do not advertise
|
|
678
|
+
// hot swapping: the command opens a separate conversation after both choices.
|
|
679
|
+
// A locked selected item hides the entire native dropdown, including commands.
|
|
680
|
+
const item = { id: 'settings', name: 'Model settings' };
|
|
681
|
+
groups.push({ id: 'modelSettings', name: 'Model', items: [item], selected: item,
|
|
682
|
+
commands: [{ command: 'supercode.frontend.configureModel', title: 'Choose model and reasoning for a new chat…', arguments: [selected.id] }] });
|
|
683
|
+
}
|
|
480
684
|
return groups;
|
|
481
685
|
},
|
|
482
686
|
setPermission: (resource, value) => {
|
|
@@ -491,7 +695,7 @@ export async function activate(context) {
|
|
|
491
695
|
create: async (inputState) => {
|
|
492
696
|
if (!controls)
|
|
493
697
|
throw new Error('The host does not expose conversation creation.');
|
|
494
|
-
await enqueueSelection({
|
|
698
|
+
await enqueueSelection({ harness: controls.state.selection.harness, model: '', effort: '' }, false);
|
|
495
699
|
const connection = frontends.get(controls.state.activeSession);
|
|
496
700
|
savePermission(connection.id, turnPermission(inputState.groups.find(group => group.id === 'permissions')?.selected?.id));
|
|
497
701
|
return connection;
|
|
@@ -500,6 +704,25 @@ export async function activate(context) {
|
|
|
500
704
|
attached: (connection) => attachedIds.has(connection.id),
|
|
501
705
|
catalogue: () => controls?.state.sessions ?? [],
|
|
502
706
|
bindings: boundId,
|
|
707
|
+
setup: () => controls?.state.setup,
|
|
708
|
+
prepareStarters: async (session, resource, token) => {
|
|
709
|
+
// Project configuration can arrive after the runtime connects. Wait for
|
|
710
|
+
// its real answer before publishing the conversation's welcome actions.
|
|
711
|
+
const deadline = Date.now() + 30_000;
|
|
712
|
+
await controls?.refresh();
|
|
713
|
+
while (controls?.state.starterPromptsPending) {
|
|
714
|
+
if (token.isCancellationRequested)
|
|
715
|
+
throw new vscode.CancellationError();
|
|
716
|
+
if (Date.now() >= deadline)
|
|
717
|
+
throw new Error('Project configuration is still loading. Reopen Chat once the project is ready.');
|
|
718
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
719
|
+
await controls.refresh();
|
|
720
|
+
}
|
|
721
|
+
if (token.isCancellationRequested)
|
|
722
|
+
throw new vscode.CancellationError();
|
|
723
|
+
starterResources.set(session, resource);
|
|
724
|
+
controlsChanged.fire();
|
|
725
|
+
},
|
|
503
726
|
};
|
|
504
727
|
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.activateSession', async (resource) => { await connectionFor(vscode.Uri.parse(resource)); }));
|
|
505
728
|
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.openSession', async (id) => {
|
|
@@ -524,15 +747,34 @@ export async function activate(context) {
|
|
|
524
747
|
void vscode.window.showInformationMessage('This host does not expose chat configuration.');
|
|
525
748
|
return;
|
|
526
749
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
750
|
+
await controls.refresh();
|
|
751
|
+
// The host supplies startable agents, including installation/authentication checks.
|
|
752
|
+
// An absent choice is not permission to guess a harness or start another runtime.
|
|
753
|
+
const choices = controls.state.harnesses;
|
|
754
|
+
if (!choices.length) {
|
|
755
|
+
await revealSetup();
|
|
530
756
|
return;
|
|
531
|
-
if (harness.id !== selection.harness) {
|
|
532
|
-
selection.harness = harness.id;
|
|
533
|
-
selection.model = '';
|
|
534
|
-
selection.effort = '';
|
|
535
757
|
}
|
|
758
|
+
const harness = choices.length === 1 ? choices[0] : await vscode.window.showQuickPick(choices.map(h => ({ label: h.name, id: h.id })), { title: 'New chat · Agent', placeHolder: 'Choose your coding agent. Model and reasoning use its defaults.' });
|
|
759
|
+
if (!harness)
|
|
760
|
+
return;
|
|
761
|
+
await enqueueSelection({ harness: harness.id, model: '', effort: '' });
|
|
762
|
+
}));
|
|
763
|
+
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.configureModel', async (resource, sessionId) => {
|
|
764
|
+
if (!controls || focusedAttached)
|
|
765
|
+
throw new Error('Model settings require a host-managed conversation.');
|
|
766
|
+
await controls.refresh();
|
|
767
|
+
// The native session-options picker prepends its exact resource to command arguments.
|
|
768
|
+
const bound = resource ? boundId(resource) : undefined;
|
|
769
|
+
if (resource && (!bound || (sessionId && bound !== sessionId)))
|
|
770
|
+
throw new Error('Model settings have no matching conversation binding.');
|
|
771
|
+
const id = bound ?? controls.state.activeSession;
|
|
772
|
+
const selected = controls.state.sessions.find(session => session.id === id);
|
|
773
|
+
if (!selected || selected.id !== controls.state.activeSession)
|
|
774
|
+
throw new Error('Open this conversation before choosing model settings.');
|
|
775
|
+
const selection = { ...selected.selection };
|
|
776
|
+
if (!['claude-code', 'codex'].includes(selection.harness))
|
|
777
|
+
throw new Error('This harness uses its own model defaults; configuration is not advertised.');
|
|
536
778
|
if (['claude-code', 'codex'].includes(selection.harness)) {
|
|
537
779
|
const choices = controls.state.modelsByHarness?.[selection.harness] ?? [{ id: '', name: 'Harness default' }];
|
|
538
780
|
const picked = await vscode.window.showQuickPick([...choices.map(model => ({ label: model.name, id: model.id })), { label: 'Enter an exact model ID…', id: '__custom__' }], { title: 'New chat · Model', placeHolder: 'Model availability depends on your harness account.' });
|
|
@@ -608,6 +850,11 @@ export async function activate(context) {
|
|
|
608
850
|
const handler = async (request, chatContext, stream, token) => {
|
|
609
851
|
await applying;
|
|
610
852
|
const requestedResource = chatContext.chatSessionContext?.chatSessionItem?.resource;
|
|
853
|
+
if (controls?.state.setup && !controls.state.setup.ready) {
|
|
854
|
+
for (const part of setupParts(controls.state.setup))
|
|
855
|
+
stream.push(part);
|
|
856
|
+
return {};
|
|
857
|
+
}
|
|
611
858
|
const connection = requestedResource
|
|
612
859
|
? await connectionFor(requestedResource)
|
|
613
860
|
: controls ? undefined : managed();
|
|
@@ -657,6 +904,8 @@ export async function activate(context) {
|
|
|
657
904
|
const participant = vscode.chat.createChatParticipant(id, handler);
|
|
658
905
|
participant.iconPath = icon();
|
|
659
906
|
participants.push(participant);
|
|
907
|
+
welcomeParticipants.add(participant);
|
|
908
|
+
updateStarterWelcome();
|
|
660
909
|
context.subscriptions.push(participant);
|
|
661
910
|
}
|
|
662
911
|
catch (error) {
|
|
@@ -679,6 +928,8 @@ export async function activate(context) {
|
|
|
679
928
|
? vscode.chat.createDynamicChatParticipant(SESSION_SCHEME, { name: current.name.replace(/[^a-zA-Z0-9]/g, ''), fullName: current.name, publisherName: 'Volter', description: 'Ask a question or describe a task' }, handler)
|
|
680
929
|
: vscode.chat.createChatParticipant(SESSION_SCHEME, handler);
|
|
681
930
|
participant.iconPath = icon();
|
|
931
|
+
welcomeParticipants.add(participant);
|
|
932
|
+
updateStarterWelcome();
|
|
682
933
|
return participant;
|
|
683
934
|
};
|
|
684
935
|
let sessionParticipant = makeParticipant();
|
|
@@ -687,6 +938,7 @@ export async function activate(context) {
|
|
|
687
938
|
if (participantKey() === previousIdentity)
|
|
688
939
|
return;
|
|
689
940
|
previousIdentity = participantKey();
|
|
941
|
+
welcomeParticipants.delete(sessionParticipant);
|
|
690
942
|
sessionParticipant.dispose();
|
|
691
943
|
sessionParticipant = makeParticipant();
|
|
692
944
|
for (const p of participants)
|
|
@@ -788,7 +1040,16 @@ export async function activate(context) {
|
|
|
788
1040
|
if (controls)
|
|
789
1041
|
await controls.refresh();
|
|
790
1042
|
if (inspect === true)
|
|
791
|
-
return {
|
|
1043
|
+
return {
|
|
1044
|
+
connections: frontends.all.map(c => ({ ...frontends.info(c), permissions: [...c.session.permissions],
|
|
1045
|
+
pendingRequests: [...c.session.pendingRequests.values()], starterEligibility: starterEligibility(c),
|
|
1046
|
+
lastStarterAttempt: lastStarterAttempts.get(c.id) ?? null })),
|
|
1047
|
+
selection: controls?.state.selection, activeSession: controls?.state.activeSession,
|
|
1048
|
+
busy: Boolean(controls?.state.busy || frontends.all.some(c => c.session.descriptor?.turn_state === 'busy' || c.session.pendingRequests.size)),
|
|
1049
|
+
setupHandoff: { hostReady: controls?.state.setup?.ready ?? null, complete: setupHandoffComplete(),
|
|
1050
|
+
revealed: setupHandoff ?? null },
|
|
1051
|
+
sessions: controls?.state.sessions, permission: currentOptions().permissions, identity: identity(),
|
|
1052
|
+
};
|
|
792
1053
|
const rows = frontends.all.map((connection) => {
|
|
793
1054
|
const info = frontends.info(connection);
|
|
794
1055
|
return `${info.label} · ${info.baseUrl} · ${info.model ?? 'unknown model'} · ${info.turnState ?? 'unattached'}`;
|
|
@@ -875,6 +1136,10 @@ export async function activate(context) {
|
|
|
875
1136
|
if (!granted.has('chatSessionsProvider'))
|
|
876
1137
|
return;
|
|
877
1138
|
if (controls) {
|
|
1139
|
+
if (controls.state.setup && !controls.state.setup.ready) {
|
|
1140
|
+
await revealSetup();
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
878
1143
|
try {
|
|
879
1144
|
await revealManaged();
|
|
880
1145
|
}
|
|
@@ -906,8 +1171,21 @@ export async function activate(context) {
|
|
|
906
1171
|
// NOT awaited: this opens an editor, and activation must return before the
|
|
907
1172
|
// workbench will run one. Awaiting it here left activation hanging and the
|
|
908
1173
|
// panel on "Loading…" — observed.
|
|
909
|
-
const opening = setTimeout(() => void openResumedSession(), 500);
|
|
1174
|
+
const opening = setTimeout(() => void openResumedSession().catch(error => log.warn(`Could not reveal Chat: ${String(error)}`)), 500);
|
|
910
1175
|
context.subscriptions.push({ dispose: () => clearTimeout(opening) });
|
|
1176
|
+
// Retry until host readiness has completed both attachment AND native reveal.
|
|
1177
|
+
// A transient failure in either step leaves the handoff pending, even if ready=true.
|
|
1178
|
+
let setupTimer;
|
|
1179
|
+
let setupDisposed = false;
|
|
1180
|
+
const pollSetup = () => {
|
|
1181
|
+
if (setupDisposed || !controls?.state.setup || (controls.state.setup.ready && setupHandoffComplete()))
|
|
1182
|
+
return;
|
|
1183
|
+
setupTimer = setTimeout(() => {
|
|
1184
|
+
void refreshSetup().catch(error => log.warn(`Could not refresh sign-in: ${String(error)}`)).finally(pollSetup);
|
|
1185
|
+
}, 3000);
|
|
1186
|
+
};
|
|
1187
|
+
pollSetup();
|
|
1188
|
+
context.subscriptions.push({ dispose: () => { setupDisposed = true; clearTimeout(setupTimer); } });
|
|
911
1189
|
return {
|
|
912
1190
|
connect: async (options) => {
|
|
913
1191
|
const connection = await frontends.connect(options, log);
|
|
@@ -1053,7 +1331,7 @@ function registerSessions(context, frontends, participant, granted, handler, con
|
|
|
1053
1331
|
const unavailableItem = () => {
|
|
1054
1332
|
if (!frontends.unavailable || frontends.all.length > 0)
|
|
1055
1333
|
return undefined;
|
|
1056
|
-
const item = controller.createChatSessionItem(unavailableResource(), 'Volter Harness · no runtime');
|
|
1334
|
+
const item = controller.createChatSessionItem(unavailableResource(), controls?.setup() ? 'Sign in to Chat' : 'Volter Harness · no runtime');
|
|
1057
1335
|
item.description = frontends.unavailable.split('\n')[0];
|
|
1058
1336
|
item.status = vscode.ChatSessionStatus.Failed;
|
|
1059
1337
|
return item;
|
|
@@ -1122,10 +1400,10 @@ function registerSessions(context, frontends, participant, granted, handler, con
|
|
|
1122
1400
|
// the session opens, with no request behind it and nothing to wait
|
|
1123
1401
|
// for — which is the whole point, since the person has not typed.
|
|
1124
1402
|
return {
|
|
1125
|
-
title: 'Volter Harness · no runtime',
|
|
1403
|
+
title: controls?.setup() ? 'Sign in to Chat' : 'Volter Harness · no runtime',
|
|
1126
1404
|
history: [
|
|
1127
1405
|
new turns.ChatRequestTurn(STATUS_MARKER_PROMPT, undefined, [], participant.id, []),
|
|
1128
|
-
new turns.ChatResponseTurn([new vscode.ChatResponseMarkdownPart(unavailableMarkdown(frontends.unavailable))], {}, participant.id),
|
|
1406
|
+
new turns.ChatResponseTurn(controls?.setup() ? setupParts(controls.setup()) : [new vscode.ChatResponseMarkdownPart(unavailableMarkdown(frontends.unavailable))], {}, participant.id),
|
|
1129
1407
|
],
|
|
1130
1408
|
requestHandler: boundHandler,
|
|
1131
1409
|
};
|
|
@@ -1145,8 +1423,9 @@ function registerSessions(context, frontends, participant, granted, handler, con
|
|
|
1145
1423
|
// `state.history`), so the copy taken when this window attached is
|
|
1146
1424
|
// stale as soon as a turn runs. The generated client reuses the
|
|
1147
1425
|
// already-open event stream, so this is a snapshot, not a reconnect.
|
|
1148
|
-
await connection.session.attach().catch(() => {
|
|
1426
|
+
const attached = await connection.session.attach().then(() => true).catch(() => {
|
|
1149
1427
|
// An unreachable runtime replays what this window last saw.
|
|
1428
|
+
return false;
|
|
1150
1429
|
});
|
|
1151
1430
|
// The attach snapshot IS the session's history: the runtime's own
|
|
1152
1431
|
// canonical transcript, replayed as turns.
|
|
@@ -1184,6 +1463,12 @@ function registerSessions(context, frontends, participant, granted, handler, con
|
|
|
1184
1463
|
}
|
|
1185
1464
|
history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {}, participant.id));
|
|
1186
1465
|
}
|
|
1466
|
+
// The public API cannot withdraw a rendered history command part. Keep
|
|
1467
|
+
// starters in the default participant's native welcome content instead:
|
|
1468
|
+
// Chat hides that whole surface as soon as the first request is added.
|
|
1469
|
+
if (attached && history.length === 0 && !controls?.attached(connection) && controls) {
|
|
1470
|
+
await controls.prepareStarters(connection.id, resource.toString(), _token);
|
|
1471
|
+
}
|
|
1187
1472
|
const busy = connection.session.descriptor?.turn_state === 'busy';
|
|
1188
1473
|
return {
|
|
1189
1474
|
title: connection.label,
|
package/dist/host-controls.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ export interface ChatChoice {
|
|
|
10
10
|
name: string;
|
|
11
11
|
description?: string;
|
|
12
12
|
}
|
|
13
|
+
export interface ChatStarterPrompt {
|
|
14
|
+
label: string;
|
|
15
|
+
prompt: string;
|
|
16
|
+
}
|
|
13
17
|
export interface ManagedChatSession {
|
|
14
18
|
id: string;
|
|
15
19
|
selection: ChatSelection;
|
|
@@ -17,20 +21,42 @@ export interface ManagedChatSession {
|
|
|
17
21
|
title: string;
|
|
18
22
|
created: number;
|
|
19
23
|
}
|
|
24
|
+
/** Host-owned operations. The frontend displays commands before the person runs them. */
|
|
25
|
+
export interface ChatSetupAction {
|
|
26
|
+
kind: 'login' | 'install';
|
|
27
|
+
harness: string;
|
|
28
|
+
name: string;
|
|
29
|
+
command: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ChatSetupState {
|
|
32
|
+
ready: boolean;
|
|
33
|
+
actions: ChatSetupAction[];
|
|
34
|
+
reason: string | null;
|
|
35
|
+
cwd: string;
|
|
36
|
+
}
|
|
20
37
|
export interface HostState {
|
|
21
38
|
history?: ChatMessage[];
|
|
22
39
|
historyTruncated?: boolean;
|
|
23
40
|
activeSession: string | null;
|
|
24
41
|
openSessionCommand?: string;
|
|
42
|
+
/** Fill the composer of an exact native session; returns {sessionResource, input}. */
|
|
43
|
+
fillInputCommand?: string;
|
|
44
|
+
/** The native renderer supports a leading response with no visible user request. */
|
|
45
|
+
responseOnlyHistory?: boolean;
|
|
25
46
|
sessions: ManagedChatSession[];
|
|
26
47
|
selection: ChatSelection;
|
|
27
48
|
connection?: Record<string, string>;
|
|
49
|
+
/** Agents the host can start now (installation, authentication and capability checked). */
|
|
28
50
|
harnesses: ChatChoice[];
|
|
29
51
|
models: ChatChoice[];
|
|
30
52
|
modelsByHarness?: Record<string, ChatChoice[]>;
|
|
31
53
|
busy: boolean;
|
|
32
54
|
actualModel?: string | null;
|
|
33
55
|
configurable: boolean;
|
|
56
|
+
setup?: ChatSetupState;
|
|
57
|
+
starterPrompts?: ChatStarterPrompt[];
|
|
58
|
+
/** The host's project configuration has not finished loading yet. */
|
|
59
|
+
starterPromptsPending?: boolean;
|
|
34
60
|
}
|
|
35
61
|
export interface HostSelectionResult extends HostState {
|
|
36
62
|
connection: Record<string, string>;
|
|
@@ -45,6 +71,12 @@ export declare class ChatHostControls {
|
|
|
45
71
|
select(selection: ChatSelection): Promise<HostSelectionResult>;
|
|
46
72
|
remember(id: string, nativeId: string): Promise<void>;
|
|
47
73
|
open(id: string): Promise<HostSelectionResult>;
|
|
74
|
+
setup(kind: ChatSetupAction['kind'], harness: string): Promise<ChatSetupAction & {
|
|
75
|
+
cwd: string;
|
|
76
|
+
program: string;
|
|
77
|
+
arguments: string[];
|
|
78
|
+
env?: Record<string, string>;
|
|
79
|
+
}>;
|
|
48
80
|
private request;
|
|
49
81
|
}
|
|
50
82
|
/** Approval policy is captured for one turn, never inferred from connection privileges. */
|
package/dist/host-controls.js
CHANGED
|
@@ -31,6 +31,9 @@ export class ChatHostControls {
|
|
|
31
31
|
this.state = result;
|
|
32
32
|
return result;
|
|
33
33
|
}
|
|
34
|
+
async setup(kind, harness) {
|
|
35
|
+
return this.request('/setup', { kind, harness });
|
|
36
|
+
}
|
|
34
37
|
async request(path, selection) {
|
|
35
38
|
const response = await fetch(this.baseUrl + path, {
|
|
36
39
|
method: selection ? 'POST' : 'GET', headers: { Authorization: `Bearer ${this.credential}`, 'Content-Type': 'application/json' },
|
package/dist/session.d.ts
CHANGED
|
@@ -77,6 +77,8 @@ export declare class VscodeFrontendSession {
|
|
|
77
77
|
history: ChatMessage[];
|
|
78
78
|
historyCursor: number;
|
|
79
79
|
cursor: number;
|
|
80
|
+
/** Semantic evidence, retained even when replay is evicted or re-attached. */
|
|
81
|
+
hasUserTurn: boolean;
|
|
80
82
|
/**
|
|
81
83
|
* Every event this attachment has seen, in order — NOT just the last
|
|
82
84
|
* snapshot's. A re-attach asks `afterSequence: cursor`, so its `replay` is
|
package/dist/session.js
CHANGED
|
@@ -44,6 +44,8 @@ export class VscodeFrontendSession {
|
|
|
44
44
|
history = [];
|
|
45
45
|
historyCursor = 0;
|
|
46
46
|
cursor = 0;
|
|
47
|
+
/** Semantic evidence, retained even when replay is evicted or re-attached. */
|
|
48
|
+
hasUserTurn = false;
|
|
47
49
|
/**
|
|
48
50
|
* Every event this attachment has seen, in order — NOT just the last
|
|
49
51
|
* snapshot's. A re-attach asks `afterSequence: cursor`, so its `replay` is
|
|
@@ -103,6 +105,8 @@ export class VscodeFrontendSession {
|
|
|
103
105
|
descriptor: structuredClone(snapshot.descriptor),
|
|
104
106
|
pendingRequests: new Map([...this.pendingRequests].map(([id, request]) => [id, structuredClone(request)])),
|
|
105
107
|
cursor: Math.max(this.cursor, snapshot.history_cursor),
|
|
108
|
+
hasUserTurn: this.hasUserTurn || snapshot.descriptor.turn_state === 'busy'
|
|
109
|
+
|| snapshot.history.some(message => message.role !== 'system'),
|
|
106
110
|
};
|
|
107
111
|
// Validate and project the whole attachment before any observable field
|
|
108
112
|
// moves or a live listener is registered.
|
|
@@ -125,6 +129,7 @@ export class VscodeFrontendSession {
|
|
|
125
129
|
this.appendTranscript(structuredClone(snapshot.replay));
|
|
126
130
|
this.pendingRequests = staged.pendingRequests;
|
|
127
131
|
this.cursor = staged.cursor;
|
|
132
|
+
this.hasUserTurn = staged.hasUserTurn;
|
|
128
133
|
this.connection = 'connected';
|
|
129
134
|
this.attachedAtMs ??= Date.now();
|
|
130
135
|
if (!this.#subscribed) {
|
|
@@ -252,6 +257,11 @@ export class VscodeFrontendSession {
|
|
|
252
257
|
throw new EventSequenceGapError(state.cursor + 1, event.sequence);
|
|
253
258
|
}
|
|
254
259
|
state.cursor = event.sequence;
|
|
260
|
+
// Hosted runtimes publish user_message + turn_started when accepting input.
|
|
261
|
+
// Native init/status notifications also consume sequence numbers, but are
|
|
262
|
+
// not user turns. Keep this independent of the bounded replay buffer.
|
|
263
|
+
if (BUSY_KINDS.has(event.kind))
|
|
264
|
+
state.hasUserTurn = true;
|
|
255
265
|
const payload = (event.payload ?? {});
|
|
256
266
|
if (event.kind === 'request' && payload.type === 'request') {
|
|
257
267
|
const request = payload.request;
|
package/dist/view.js
CHANGED
|
@@ -639,6 +639,9 @@ export function projectReplayTurns(events, granted, context) {
|
|
|
639
639
|
*/
|
|
640
640
|
export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
641
641
|
const turns = [];
|
|
642
|
+
// Keep results inside the native invocation, as the live stream does. A
|
|
643
|
+
// separate Markdown result bypasses the editor's collapsed tool-step group.
|
|
644
|
+
const toolRows = new Map();
|
|
642
645
|
const pushAssistant = (parts) => {
|
|
643
646
|
const last = turns[turns.length - 1];
|
|
644
647
|
if (last && last.role === 'assistant' && last.parts) {
|
|
@@ -659,6 +662,7 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
659
662
|
for (const message of history) {
|
|
660
663
|
const body = messageText(message);
|
|
661
664
|
if (message.role === 'user') {
|
|
665
|
+
toolRows.clear();
|
|
662
666
|
turns.push({ role: 'user', prompt: clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters) });
|
|
663
667
|
continue;
|
|
664
668
|
}
|
|
@@ -674,14 +678,21 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
674
678
|
pushAssistant([{ part: 'markdown', value: clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters) }]);
|
|
675
679
|
}
|
|
676
680
|
for (const call of message.tool_calls ?? []) {
|
|
681
|
+
const subject = toolSubject(call.function.arguments);
|
|
682
|
+
const label = clamp(stripControls(subject ? `${call.function.name} · ${subject}` : call.function.name), VSCODE_CHAT_LIMITS.titleCharacters);
|
|
683
|
+
const row = {
|
|
684
|
+
part: 'tool',
|
|
685
|
+
toolName: call.function.name,
|
|
686
|
+
toolCallId: call.id,
|
|
687
|
+
invocationMessage: label,
|
|
688
|
+
pastTenseMessage: label,
|
|
689
|
+
inputText: clamp(stripControls(parseArguments(call.function.arguments)), VSCODE_CHAT_LIMITS.partCharacters),
|
|
690
|
+
isComplete: true,
|
|
691
|
+
};
|
|
692
|
+
if (call.id)
|
|
693
|
+
toolRows.set(call.id, row);
|
|
677
694
|
pushAssistant(resolveParts([{
|
|
678
|
-
emit:
|
|
679
|
-
part: 'tool',
|
|
680
|
-
toolName: call.function.name,
|
|
681
|
-
toolCallId: call.id,
|
|
682
|
-
invocationMessage: clamp(call.function.name, VSCODE_CHAT_LIMITS.titleCharacters),
|
|
683
|
-
isComplete: true,
|
|
684
|
-
},
|
|
695
|
+
emit: row,
|
|
685
696
|
needs: 'chatParticipantAdditions',
|
|
686
697
|
fallback: [{
|
|
687
698
|
part: 'markdown',
|
|
@@ -696,10 +707,43 @@ export function projectHistory(history, granted, pendingToolNames = new Set()) {
|
|
|
696
707
|
// interruption. Only the final matching tool is affected, never old history.
|
|
697
708
|
const pending = message === history.at(-1) && pendingToolNames.has(message.name ?? '')
|
|
698
709
|
&& body === '[no tool result recorded — turn interrupted]';
|
|
710
|
+
if (pending) {
|
|
711
|
+
// The pinned editor serializes history tool parts as complete even if
|
|
712
|
+
// isComplete=false. Keep this explicit marker for matched AND orphan
|
|
713
|
+
// results; the active response separately restores the approval controls.
|
|
714
|
+
pushAssistant([{ part: 'markdown', value: '**Awaiting approval** — no tool result yet.' }]);
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (granted.has('chatParticipantAdditions')) {
|
|
718
|
+
const row = message.tool_call_id ? toolRows.get(message.tool_call_id) : undefined;
|
|
719
|
+
const output = clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters);
|
|
720
|
+
if (row) {
|
|
721
|
+
row.resultText = output;
|
|
722
|
+
// Canonical ChatMessage has no general error flag. Only this known
|
|
723
|
+
// synthetic marker establishes interruption; do not guess from prose.
|
|
724
|
+
if (body === '[no tool result recorded — turn interrupted]')
|
|
725
|
+
row.isError = true;
|
|
726
|
+
toolRows.delete(message.tool_call_id);
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
// A bounded transcript can retain a result whose call was omitted.
|
|
730
|
+
// Preserve it as an inspectable tool row without inventing its input.
|
|
731
|
+
const label = clamp(stripControls(message.name || 'Tool result'), VSCODE_CHAT_LIMITS.titleCharacters);
|
|
732
|
+
pushAssistant([{
|
|
733
|
+
part: 'tool',
|
|
734
|
+
toolName: message.name || 'tool',
|
|
735
|
+
toolCallId: message.tool_call_id || `history-result-${history.indexOf(message)}`,
|
|
736
|
+
invocationMessage: label,
|
|
737
|
+
pastTenseMessage: label,
|
|
738
|
+
isComplete: true,
|
|
739
|
+
resultText: output,
|
|
740
|
+
}]);
|
|
741
|
+
}
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
699
744
|
pushAssistant([{
|
|
700
745
|
part: 'markdown',
|
|
701
|
-
value:
|
|
702
|
-
: `**Tool result${message.name ? ` · ${message.name}` : ''}**\n\n${fence(clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters))}`,
|
|
746
|
+
value: `**Tool result${message.name ? ` · ${message.name}` : ''}**\n\n${fence(clamp(stripControls(body), VSCODE_CHAT_LIMITS.partCharacters))}`,
|
|
703
747
|
}]);
|
|
704
748
|
}
|
|
705
749
|
if (turns.length <= VSCODE_CHAT_LIMITS.historyTurns)
|
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.18",
|
|
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",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"description": "Conversations with your AI assistant",
|
|
85
85
|
"icon": "$(server-process)",
|
|
86
86
|
"welcomeTitle": "Chat",
|
|
87
|
-
"welcomeMessage": "
|
|
87
|
+
"welcomeMessage": "Write a message to your coding agent.",
|
|
88
88
|
"inputPlaceholder": "Ask a question or describe a task",
|
|
89
89
|
"order": 1,
|
|
90
90
|
"capabilities": {
|
|
@@ -140,9 +140,14 @@
|
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
142
|
"command": "supercode.frontend.configure",
|
|
143
|
-
"title": "New Chat: Choose
|
|
143
|
+
"title": "New Chat: Choose Agent",
|
|
144
144
|
"category": "Chat",
|
|
145
145
|
"icon": "$(add)"
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"command": "supercode.frontend.configureModel",
|
|
149
|
+
"title": "Model and Reasoning: Open New Chat",
|
|
150
|
+
"category": "Chat"
|
|
146
151
|
}
|
|
147
152
|
],
|
|
148
153
|
"configuration": {
|
|
@@ -191,6 +196,9 @@
|
|
|
191
196
|
"@types/vscode": "1.138.0",
|
|
192
197
|
"typescript": "5.6.3"
|
|
193
198
|
},
|
|
199
|
+
"supercodeFrontendWelcome": {
|
|
200
|
+
"starterIntroduction": "Choose an idea to edit before sending:"
|
|
201
|
+
},
|
|
194
202
|
"supercodeFrontendContract": {
|
|
195
203
|
"name": "@volter/supercode-frontend",
|
|
196
204
|
"version": "0.2.7",
|