@volter/supercode-frontend-vscode 0.1.58 → 0.1.61
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/NOTICE.md +1 -1
- package/README.md +112 -10
- package/dist/chat-setup.js +3 -2
- package/dist/extension.js +248 -44
- package/dist/host-controls.d.ts +15 -0
- package/dist/host-controls.js +12 -1
- package/dist/native-session.d.ts +2 -2
- package/dist/native-session.js +10 -1
- package/dist/session.d.ts +9 -0
- package/dist/session.js +134 -22
- package/dist/setup-flow.js +10 -2
- package/dist/view.d.ts +4 -2
- package/dist/view.js +34 -10
- package/dist/workbench-tools.d.ts +13 -0
- package/dist/workbench-tools.js +81 -0
- package/media/anthropic.svg +1 -1
- package/media/google.svg +1 -1
- package/media/openai.svg +1 -1
- package/media/xai.svg +1 -0
- package/package.json +22 -3
- package/vendor/supercode-frontend/README.md +27 -0
- package/vendor/supercode-frontend/client.d.mts +7 -1
- package/vendor/supercode-frontend/client.mjs +85 -11
- package/vendor/supercode-frontend/contract.json +7 -2
- package/vendor/supercode-frontend/generated-client.d.mts +1 -1
- package/vendor/supercode-frontend/generated-client.mjs +1 -1
- package/vendor/supercode-frontend/generated.d.mts +7 -0
- package/vendor/supercode-frontend/package.json +1 -1
package/dist/extension.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveSessionBinding } from './bindings.js';
|
|
2
2
|
import { withTextContext } from './context.js';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { WorkbenchTools } from './workbench-tools.js';
|
|
4
5
|
import { ChatHostControls, runtimeIdentity, harnessDisplayName, turnPermission } from './host-controls.js';
|
|
5
6
|
import { SETUP_BEGIN, SETUP_CANCEL, SETUP_STATE, setupAgents, setupPresentation, setupWelcome } from './chat-setup.js';
|
|
6
7
|
import { ChatSetupFlow } from './setup-flow.js';
|
|
@@ -108,8 +109,10 @@ class SupercodeFrontends {
|
|
|
108
109
|
const { baseUrl, token } = options;
|
|
109
110
|
const id = options.connectionId ?? options.clientId ?? `vscode-${randomUUID()}`;
|
|
110
111
|
const existing = this.#connections.get(id);
|
|
111
|
-
if (existing)
|
|
112
|
-
|
|
112
|
+
if (existing) {
|
|
113
|
+
await existing.controlling;
|
|
114
|
+
return this.#connections.get(id) === existing && existing.session.connection !== 'closed' ? existing : undefined;
|
|
115
|
+
}
|
|
113
116
|
const permissions = normalizePermissions(options.permissions, ['observe']);
|
|
114
117
|
const client = createFrontendClient({
|
|
115
118
|
baseUrl,
|
|
@@ -121,9 +124,9 @@ class SupercodeFrontends {
|
|
|
121
124
|
// The generated client opens SSE before the snapshot RPC; `attach` is
|
|
122
125
|
// therefore the whole race-free attachment and `follow` only drains it.
|
|
123
126
|
await session.attach();
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
+
// Taking control must not hold up observation of an attached snapshot.
|
|
128
|
+
// Actions share this acquisition; connect still awaits it before returning.
|
|
129
|
+
const controlling = session.takeControlAndHold().catch((error) => {
|
|
127
130
|
log.warn(`Volter Harness could not take control of ${baseUrl}: ${String(error?.message ?? error)}`);
|
|
128
131
|
});
|
|
129
132
|
// The runtime's stream can end under a live attachment: an HTTP client's
|
|
@@ -135,14 +138,17 @@ class SupercodeFrontends {
|
|
|
135
138
|
const stillWanted = () => connection !== undefined && this.#connections.get(id) === connection && session.connection !== 'closed';
|
|
136
139
|
const following = (async () => {
|
|
137
140
|
for (let attempt = 0;; attempt += 1) {
|
|
141
|
+
let failure;
|
|
138
142
|
try {
|
|
139
143
|
await session.follow();
|
|
140
144
|
}
|
|
141
145
|
catch (error) {
|
|
146
|
+
failure = error;
|
|
142
147
|
log.warn(`Volter Harness event stream ended: ${String(error?.message ?? error)}`);
|
|
143
148
|
}
|
|
144
149
|
if (!stillWanted())
|
|
145
150
|
return;
|
|
151
|
+
session.reportFailure(failure ?? new Error('The conversation event stream ended before this attachment closed. Reconnecting to the runtime; its turn may still be running.'));
|
|
146
152
|
await new Promise((resolve) => setTimeout(resolve, Math.min(15_000, 500 * 2 ** Math.min(attempt, 5))));
|
|
147
153
|
if (!stillWanted())
|
|
148
154
|
return;
|
|
@@ -162,6 +168,7 @@ class SupercodeFrontends {
|
|
|
162
168
|
baseUrl,
|
|
163
169
|
session,
|
|
164
170
|
following,
|
|
171
|
+
controlling,
|
|
165
172
|
};
|
|
166
173
|
session.onChange(() => this.#changed.fire());
|
|
167
174
|
this.#connections.set(id, connection);
|
|
@@ -169,6 +176,9 @@ class SupercodeFrontends {
|
|
|
169
176
|
this.setUnavailable(undefined);
|
|
170
177
|
log.info(`Volter Harness attached: ${connection.label} (${baseUrl}) as ${permissions.join(',')}` +
|
|
171
178
|
` · ${session.history.length} history messages`);
|
|
179
|
+
await controlling;
|
|
180
|
+
if (this.#connections.get(id) !== connection || session.connection === 'closed')
|
|
181
|
+
return undefined;
|
|
172
182
|
return connection;
|
|
173
183
|
}
|
|
174
184
|
async disconnect(id) {
|
|
@@ -351,6 +361,16 @@ export async function activate(context) {
|
|
|
351
361
|
const controls = await ChatHostControls.fromEnvironment();
|
|
352
362
|
const controlsChanged = new vscode.EventEmitter();
|
|
353
363
|
context.subscriptions.push(controlsChanged);
|
|
364
|
+
// The workbench's tools for the host's runtimes, on the endpoint the host names (ADR 0255).
|
|
365
|
+
if (controls) {
|
|
366
|
+
const workbenchTools = new WorkbenchTools();
|
|
367
|
+
context.subscriptions.push(workbenchTools);
|
|
368
|
+
const serveTools = (state) => {
|
|
369
|
+
workbenchTools.serve(state.workbenchTools).catch(error => log.warn(`Workbench tools unavailable: ${String(error)}`));
|
|
370
|
+
};
|
|
371
|
+
context.subscriptions.push({ dispose: controls.onState(serveTools) });
|
|
372
|
+
serveTools(controls.state);
|
|
373
|
+
}
|
|
354
374
|
// A person's explicit picker choice: per conversation once it is bound, and for the new chat before then.
|
|
355
375
|
let permission;
|
|
356
376
|
const permissions = new Map(context.workspaceState.get('supercode.sessionPermissions', []));
|
|
@@ -383,13 +403,26 @@ export async function activate(context) {
|
|
|
383
403
|
// protocol (an ACP agent) keeps asking until a host names it.
|
|
384
404
|
return ['claude-code', 'codex'].includes(harness) && (!connection || canAnswer(connection)) ? 'autoApprove' : 'ask';
|
|
385
405
|
};
|
|
386
|
-
//
|
|
387
|
-
const
|
|
406
|
+
// Runtime YOLO is the host's recorded launch policy, never an approval-picker choice.
|
|
407
|
+
const yoloFor = (id) => {
|
|
408
|
+
const session = controls?.state.sessions.find(entry => entry.id === id);
|
|
409
|
+
return !attachedIds.has(id) && session?.runtimePolicy === 'yolo'
|
|
410
|
+
&& ['claude-code', 'codex'].includes(session.selection.harness);
|
|
411
|
+
};
|
|
412
|
+
// A saved Ask choice cannot turn prompts back on in a runtime already launched with bypass flags.
|
|
413
|
+
const approvalFor = (id) => {
|
|
414
|
+
if (!yoloFor(id))
|
|
415
|
+
return permissions.get(id) ?? defaultFor(id);
|
|
416
|
+
const connection = frontends.get(id);
|
|
417
|
+
return !connection || canAnswer(connection) ? 'autoApprove' : 'ask';
|
|
418
|
+
};
|
|
388
419
|
// A picker value becomes the person's choice only when it is one: it differs from the default the picker showed
|
|
389
420
|
// (`shown`; null when the person already picked it), or replaces an earlier choice. Echoes of the displayed default
|
|
390
421
|
// (the picker re-applied when a conversation opens) are not saved, and neither is an Auto approve the conversation
|
|
391
422
|
// cannot answer: the picker offers it only where it can, so there it is a default shown before the harness was known.
|
|
392
423
|
const choosePermission = (id, value, shown = defaultFor(id)) => {
|
|
424
|
+
if (yoloFor(id))
|
|
425
|
+
return;
|
|
393
426
|
const connection = frontends.get(id);
|
|
394
427
|
if (value === 'autoApprove' && connection && !canAnswer(connection))
|
|
395
428
|
return;
|
|
@@ -405,6 +438,11 @@ export async function activate(context) {
|
|
|
405
438
|
let focusedResource;
|
|
406
439
|
let focusGeneration = 0;
|
|
407
440
|
const activeDraft = () => pendingDraft?.resource && pendingDraft.resource.toString() === focusedResource?.toString() ? pendingDraft : undefined;
|
|
441
|
+
const draftChoiceRefusal = () => {
|
|
442
|
+
const draft = activeDraft();
|
|
443
|
+
return draft && (draft.choosing || !draft.choice || draft.choice.kind === 'cancelled')
|
|
444
|
+
? 'Choose your coding agent before sending. Your draft is saved; send it after choosing.' : undefined;
|
|
445
|
+
};
|
|
408
446
|
const draftSelection = () => {
|
|
409
447
|
const choice = activeDraft()?.choice;
|
|
410
448
|
return choice?.kind === 'agent' ? choice.selection : undefined;
|
|
@@ -624,9 +662,11 @@ export async function activate(context) {
|
|
|
624
662
|
provideTokenCount: async () => 0,
|
|
625
663
|
}));
|
|
626
664
|
}
|
|
665
|
+
const newChatYolo = () => controls?.state.newChatRuntimePolicy === 'yolo'
|
|
666
|
+
&& ['claude-code', 'codex'].includes(draftSelection()?.harness ?? controls.state.selection.harness);
|
|
627
667
|
const currentOptions = (resource) => {
|
|
628
668
|
const id = resource ? boundId(resource) : activeDraft() ? undefined : controls?.state.activeSession;
|
|
629
|
-
return { permissions: id ? approvalFor(id) : permission ?? newChatDefault() };
|
|
669
|
+
return { permissions: id ? yoloFor(id) ? 'yolo' : approvalFor(id) : newChatYolo() ? 'yolo' : permission ?? newChatDefault() };
|
|
630
670
|
};
|
|
631
671
|
const savedConnections = new Map();
|
|
632
672
|
const saveManagedHistory = async (connection) => {
|
|
@@ -658,6 +698,12 @@ export async function activate(context) {
|
|
|
658
698
|
return connection;
|
|
659
699
|
};
|
|
660
700
|
let setupHandoff;
|
|
701
|
+
let canceledSetupReveal;
|
|
702
|
+
const revealCanceledFor = (basis, connection) => Boolean(canceledSetupReveal
|
|
703
|
+
&& canceledSetupReveal.id === basis.id && canceledSetupReveal.baseUrl === basis.baseUrl
|
|
704
|
+
&& canceledSetupReveal.generation === basis.generation && canceledSetupReveal.focusResource === basis.focusResource
|
|
705
|
+
&& canceledSetupReveal.live === basis.live && canceledSetupReveal.connection.deref() === connection
|
|
706
|
+
&& connection.session.connection === 'connected');
|
|
661
707
|
const setupHandoffComplete = () => {
|
|
662
708
|
const id = controls?.state.activeSession;
|
|
663
709
|
const connection = id ? frontends.get(id) : undefined;
|
|
@@ -725,6 +771,7 @@ export async function activate(context) {
|
|
|
725
771
|
}
|
|
726
772
|
else {
|
|
727
773
|
setupHandoff = undefined;
|
|
774
|
+
canceledSetupReveal = undefined;
|
|
728
775
|
controlsChanged.fire();
|
|
729
776
|
return { kind: 'setup', generation };
|
|
730
777
|
}
|
|
@@ -745,8 +792,27 @@ export async function activate(context) {
|
|
|
745
792
|
if ((controls.state.setup && !controls.state.setup.ready) || controls.state.activeSession !== target.id
|
|
746
793
|
|| controls.state.connection?.SUPERCODE_FRONTEND_URL !== target.baseUrl)
|
|
747
794
|
return;
|
|
748
|
-
if (!setupHandoffComplete())
|
|
749
|
-
|
|
795
|
+
if (!setupHandoffComplete()) {
|
|
796
|
+
const connection = frontends.get(target.id);
|
|
797
|
+
const basis = { ...target, live };
|
|
798
|
+
if (!connection || revealCanceledFor(basis, connection))
|
|
799
|
+
return;
|
|
800
|
+
try {
|
|
801
|
+
await revealManaged(target, { resource: target.focusResource });
|
|
802
|
+
canceledSetupReveal = undefined;
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
// The native focus guard canceled this navigation. Repeating the same request cannot earn new focus;
|
|
806
|
+
// attachment remains real and the reveal remains incomplete. A changed basis or explicit refresh retries.
|
|
807
|
+
if (!(error instanceof vscode.CancellationError)
|
|
808
|
+
&& !(error instanceof Error && error.name === 'Canceled' && error.message === 'Canceled'))
|
|
809
|
+
throw error;
|
|
810
|
+
canceledSetupReveal = { ...basis, connection: new WeakRef(connection) };
|
|
811
|
+
log.debug('Automatic Chat reveal canceled for the current conversation and focus.');
|
|
812
|
+
controlsChanged.fire();
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
750
816
|
if (setupHandoffComplete())
|
|
751
817
|
welcomeWasVisible = false;
|
|
752
818
|
}
|
|
@@ -795,6 +861,41 @@ export async function activate(context) {
|
|
|
795
861
|
throw new vscode.CancellationError();
|
|
796
862
|
if (action.command !== offered.command)
|
|
797
863
|
throw new Error('The sign-in command changed. Try again.');
|
|
864
|
+
if (kind === 'install') {
|
|
865
|
+
// A task observes native process exit without closing the terminal that
|
|
866
|
+
// holds its diagnostics. Each click gets its own panel and identity;
|
|
867
|
+
// cancellation terminates only the execution returned for this task.
|
|
868
|
+
const operation = randomUUID();
|
|
869
|
+
const task = new vscode.Task({ type: 'supercode-setup', operation }, vscode.workspace.getWorkspaceFolder(vscode.Uri.file(action.cwd)) ?? vscode.TaskScope.Workspace, `${action.name} · install`, 'supercode', new vscode.ProcessExecution(action.program, action.arguments, { cwd: action.cwd, env: action.env }));
|
|
870
|
+
task.presentationOptions = { reveal: vscode.TaskRevealKind.Always, panel: vscode.TaskPanelKind.New, clear: false, close: false };
|
|
871
|
+
const listeners = [];
|
|
872
|
+
let finished = false;
|
|
873
|
+
let resolveExit;
|
|
874
|
+
const exited = new Promise(resolve => { resolveExit = resolve; });
|
|
875
|
+
const finish = (code) => {
|
|
876
|
+
if (finished)
|
|
877
|
+
return;
|
|
878
|
+
finished = true;
|
|
879
|
+
for (const listener of listeners)
|
|
880
|
+
listener.dispose();
|
|
881
|
+
resolveExit(code);
|
|
882
|
+
};
|
|
883
|
+
const owns = (execution) => execution.task.definition.type === 'supercode-setup'
|
|
884
|
+
&& execution.task.definition.operation === operation;
|
|
885
|
+
listeners.push(vscode.tasks.onDidEndTaskProcess(event => { if (owns(event.execution))
|
|
886
|
+
finish(event.exitCode); }), vscode.tasks.onDidEndTask(event => { if (owns(event.execution))
|
|
887
|
+
finish(undefined); }));
|
|
888
|
+
let execution;
|
|
889
|
+
try {
|
|
890
|
+
execution = await vscode.tasks.executeTask(task);
|
|
891
|
+
}
|
|
892
|
+
catch (error) {
|
|
893
|
+
finish(undefined);
|
|
894
|
+
throw error;
|
|
895
|
+
}
|
|
896
|
+
return { exited, cancel: () => { if (!finished)
|
|
897
|
+
execution.terminate(); } };
|
|
898
|
+
}
|
|
798
899
|
const terminal = vscode.window.createTerminal({ name: `${action.name} · ${kind === 'login' ? 'sign in' : 'install'}`,
|
|
799
900
|
cwd: action.cwd, shellPath: action.program, shellArgs: action.arguments, env: action.env });
|
|
800
901
|
const exited = new Promise(resolve => {
|
|
@@ -810,7 +911,10 @@ export async function activate(context) {
|
|
|
810
911
|
},
|
|
811
912
|
}) : undefined;
|
|
812
913
|
const setupState = () => controls ? setupPresentation(controls.state, setupFlow?.operations ?? new Map(), setupFlow?.notice) : null;
|
|
813
|
-
context.subscriptions.push({ dispose: () => setupFlow?.dispose() }, vscode.commands.registerCommand('supercode.frontend.refreshSetup',
|
|
914
|
+
context.subscriptions.push({ dispose: () => setupFlow?.dispose() }, vscode.commands.registerCommand('supercode.frontend.refreshSetup', () => {
|
|
915
|
+
canceledSetupReveal = undefined;
|
|
916
|
+
return refreshSetup();
|
|
917
|
+
}), vscode.commands.registerCommand(SETUP_STATE, setupState), vscode.commands.registerCommand(SETUP_BEGIN, (harness) => {
|
|
814
918
|
if (!setupFlow)
|
|
815
919
|
throw new Error('This host does not expose sign-in.');
|
|
816
920
|
return setupFlow.begin(harness);
|
|
@@ -1014,7 +1118,7 @@ export async function activate(context) {
|
|
|
1014
1118
|
if (draft && draft.choosing === picking)
|
|
1015
1119
|
draft.choosing = undefined;
|
|
1016
1120
|
if (draft?.choice?.kind === 'signIn') {
|
|
1017
|
-
void refreshSetup().catch(error => log.warn(`Could not refresh
|
|
1121
|
+
void refreshSetup().catch(error => log.warn(`Could not refresh Chat setup: ${String(error)}`)).finally(pollSetup);
|
|
1018
1122
|
}
|
|
1019
1123
|
}
|
|
1020
1124
|
};
|
|
@@ -1028,12 +1132,16 @@ export async function activate(context) {
|
|
|
1028
1132
|
if (busy)
|
|
1029
1133
|
throw new Error(busy);
|
|
1030
1134
|
const draft = activeDraft();
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
1033
|
-
|
|
1034
|
-
if (
|
|
1135
|
+
// Refuse before native materialization accepts the input. A pending picker
|
|
1136
|
+
// must not hold an invisible send that starts when the choice later ends.
|
|
1137
|
+
const choiceRefusal = draftChoiceRefusal();
|
|
1138
|
+
if (choiceRefusal) {
|
|
1139
|
+
void vscode.window.showInformationMessage(choiceRefusal);
|
|
1035
1140
|
throw new vscode.CancellationError();
|
|
1141
|
+
}
|
|
1036
1142
|
await controls.refresh();
|
|
1143
|
+
if (draft && activeDraft() !== draft)
|
|
1144
|
+
throw new vscode.CancellationError();
|
|
1037
1145
|
controlsChanged.fire();
|
|
1038
1146
|
const choice = draft?.choice;
|
|
1039
1147
|
const loginHarness = choice?.kind === 'signIn' ? choice.harness : undefined;
|
|
@@ -1051,7 +1159,8 @@ export async function activate(context) {
|
|
|
1051
1159
|
// The picker shows the host's default until someone changes it; only a change is the person's choice.
|
|
1052
1160
|
const selected = inputState.groups.find(group => group.id === 'permissions')?.selected?.id;
|
|
1053
1161
|
const picked = selected === 'ask' || selected === 'autoApprove' ? selected : permission ?? newChatDefault();
|
|
1054
|
-
|
|
1162
|
+
if (!yoloFor(connection.id))
|
|
1163
|
+
choosePermission(connection.id, picked, permission !== undefined ? null : newChatDefault());
|
|
1055
1164
|
permission = undefined;
|
|
1056
1165
|
if (pendingDraft === draft) {
|
|
1057
1166
|
pendingDraft = undefined;
|
|
@@ -1074,8 +1183,11 @@ export async function activate(context) {
|
|
|
1074
1183
|
// the default harness decides; only with none known does the new-chat default stand in for it
|
|
1075
1184
|
: controls?.state.selection.harness ? ['claude-code', 'codex'].includes(controls.state.selection.harness) : newChatDefault() === 'autoApprove');
|
|
1076
1185
|
// An unbound input is a new chat, not the active conversation left behind it.
|
|
1077
|
-
const
|
|
1078
|
-
const
|
|
1186
|
+
const isYolo = id ? yoloFor(id) : newChatYolo();
|
|
1187
|
+
const selectedPermission = isYolo ? 'yolo' : resource ? currentOptions(resource).permissions : permission ?? newChatDefault();
|
|
1188
|
+
const permissionItems = isYolo ? [{ id: 'yolo', name: 'YOLO', description: id
|
|
1189
|
+
? 'This runtime runs without approval prompts. Change the project Chat runtime policy for its next start or resume.'
|
|
1190
|
+
: 'Project policy: start native Claude Code or Codex without approval prompts or the Codex sandbox.' }] : [{ 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.' },
|
|
1079
1191
|
...((canApprove || (!active && resource && currentOptions(resource).permissions === 'autoApprove')) ? [{ id: 'autoApprove', name: 'Auto approve', description: 'Approve the requests the harness would ask you about, including requests to run outside its sandbox. Its deny rules still apply.' }] : [])];
|
|
1080
1192
|
const groups = [{ id: 'permissions', name: 'Permissions', kind: 'permissions', items: permissionItems, selected: permissionItems.find(i => i.id === selectedPermission) ?? permissionItems[0] }];
|
|
1081
1193
|
const selected = controls?.state.sessions.find(session => session.id === id);
|
|
@@ -1094,24 +1206,34 @@ export async function activate(context) {
|
|
|
1094
1206
|
},
|
|
1095
1207
|
setPermission: (resource, value) => {
|
|
1096
1208
|
const id = boundId(resource);
|
|
1209
|
+
if (value === 'yolo' && !id && newChatYolo())
|
|
1210
|
+
return;
|
|
1097
1211
|
if (controls && !id)
|
|
1098
1212
|
throw new Error('This conversation has no session binding.');
|
|
1213
|
+
if (id && yoloFor(id))
|
|
1214
|
+
return;
|
|
1099
1215
|
if (id)
|
|
1100
1216
|
choosePermission(id, turnPermission(value));
|
|
1101
1217
|
else
|
|
1102
1218
|
permission = turnPermission(value);
|
|
1103
1219
|
},
|
|
1104
|
-
// A send that makes no conversation
|
|
1105
|
-
//
|
|
1106
|
-
//
|
|
1220
|
+
// A send that makes no conversation keeps its refusal for the draft handler.
|
|
1221
|
+
// Materialization can stop before that callback, so creation also shows the
|
|
1222
|
+
// failure directly instead of relying on a rejected native callback (#1282).
|
|
1107
1223
|
create: async (inputState) => {
|
|
1224
|
+
const refusedKey = activeDraft()?.resource.toString() ?? inputState.sessionResource?.toString() ?? '';
|
|
1108
1225
|
try {
|
|
1109
1226
|
return await createConversation(inputState);
|
|
1110
1227
|
}
|
|
1111
1228
|
catch (error) {
|
|
1112
1229
|
if (error instanceof vscode.CancellationError)
|
|
1113
1230
|
throw error;
|
|
1114
|
-
|
|
1231
|
+
const reason = String(error?.message ?? error);
|
|
1232
|
+
refusedSends.set(refusedKey, reason);
|
|
1233
|
+
// Materialization can end before the native workbench calls a request
|
|
1234
|
+
// handler. Surface the original launch error on this path as well.
|
|
1235
|
+
log.error(`Chat creation failed: ${reason}`);
|
|
1236
|
+
void vscode.window.showErrorMessage(`Chat could not start: ${reason}. No message was sent.`);
|
|
1115
1237
|
return undefined;
|
|
1116
1238
|
}
|
|
1117
1239
|
},
|
|
@@ -1125,6 +1247,10 @@ export async function activate(context) {
|
|
|
1125
1247
|
finishIdentitySave: (connection) => finishIdentitySave(connection, true),
|
|
1126
1248
|
};
|
|
1127
1249
|
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.activateSession', async (resource) => { await connectionFor(vscode.Uri.parse(resource)); }));
|
|
1250
|
+
// A paired native host can explain Enter when its model/options precondition
|
|
1251
|
+
// prevents the submit command from reaching session materialization at all.
|
|
1252
|
+
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.sendRefusal', () => draftChoiceRefusal()
|
|
1253
|
+
?? (controls?.state.setup?.ready === false ? 'Finish setting up your coding agent before sending. Your draft is saved.' : undefined)));
|
|
1128
1254
|
// A background task the chat named (sayTasks in runTurnInto), stopped through the harness's own control.
|
|
1129
1255
|
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.stopBackgroundTask', async (connectionId, taskId) => {
|
|
1130
1256
|
const connection = frontends.get(connectionId);
|
|
@@ -1165,10 +1291,12 @@ export async function activate(context) {
|
|
|
1165
1291
|
// with a new chat's draft open, a permission chosen is the draft's, never the conversation behind it
|
|
1166
1292
|
if (supplied?.permissions) {
|
|
1167
1293
|
const id = activeDraft() ? undefined : controls?.state.activeSession;
|
|
1168
|
-
if (id)
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1294
|
+
if (!(id ? yoloFor(id) : supplied.permissions === 'yolo' && newChatYolo())) {
|
|
1295
|
+
if (id)
|
|
1296
|
+
choosePermission(id, turnPermission(supplied.permissions));
|
|
1297
|
+
else
|
|
1298
|
+
permission = turnPermission(supplied.permissions);
|
|
1299
|
+
}
|
|
1172
1300
|
controlsChanged.fire();
|
|
1173
1301
|
return currentOptions();
|
|
1174
1302
|
}
|
|
@@ -1192,9 +1320,11 @@ export async function activate(context) {
|
|
|
1192
1320
|
?? controls.state.selection.harness;
|
|
1193
1321
|
const agents = setupAgents(controls.state);
|
|
1194
1322
|
const localChoices = agents.map(agent => ({ label: agent.name, id: agent.harness,
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1323
|
+
// An agent the host's account signs in (its launch carries that account's route) names that account.
|
|
1324
|
+
description: agent.signedIn && agent.account ? `Signed in with ${agent.account}`
|
|
1325
|
+
: agent.harness === 'codex' ? (agent.signedIn ? 'Signed in with ChatGPT' : 'Sign in with ChatGPT…')
|
|
1326
|
+
: agent.harness === 'claude-code' ? (agent.signedIn ? 'Signed in with Claude' : 'Sign in with Claude…')
|
|
1327
|
+
: controls.state.harnesses.find(h => h.id === agent.harness)?.description,
|
|
1198
1328
|
login: controls.state.setup?.actions.some(a => a.harness === agent.harness) ?? false,
|
|
1199
1329
|
autoStart: controls.state.harnesses.some(h => h.id === agent.harness && h.autoStart), configure: false }));
|
|
1200
1330
|
const modelChoices = localChoices.filter(choice => !choice.login && CONFIGURABLE_HARNESSES.includes(choice.id))
|
|
@@ -1444,6 +1574,7 @@ export async function activate(context) {
|
|
|
1444
1574
|
sayTasks();
|
|
1445
1575
|
const unwatchTasks = connection.session.onChange(sayTasks);
|
|
1446
1576
|
let completed = false;
|
|
1577
|
+
let turnStarted = false;
|
|
1447
1578
|
try {
|
|
1448
1579
|
// A new harness transcript is created by input, so binding its identity
|
|
1449
1580
|
// cannot be a prerequisite for that input. Retry the host save as native
|
|
@@ -1453,7 +1584,11 @@ export async function activate(context) {
|
|
|
1453
1584
|
// A turn the person stopped is marked stopped by VS Code itself; only an interruption from elsewhere is said.
|
|
1454
1585
|
if (event.kind === 'turn_interrupted' && token.isCancellationRequested)
|
|
1455
1586
|
return;
|
|
1456
|
-
|
|
1587
|
+
// Native submit publishes its initial user echo before `turn_started`.
|
|
1588
|
+
// Subsequent input belongs in the response while this turn is visible.
|
|
1589
|
+
if (event.kind === 'turn_started')
|
|
1590
|
+
turnStarted = true;
|
|
1591
|
+
writeProjected(stream, projectEvent(event, { ...projection, liveUserMessages: turnStarted }), granted, state);
|
|
1457
1592
|
watch.sweep();
|
|
1458
1593
|
rememberIdentity(connection);
|
|
1459
1594
|
}, { throwIfCancelled, onSubmitted: () => {
|
|
@@ -1726,15 +1861,26 @@ export async function activate(context) {
|
|
|
1726
1861
|
vscode.commands.registerCommand('supercode.frontend.respond', async (answer) => { await answerRequest(answer); }), vscode.commands.registerCommand('supercode.frontend.respondInTurnApproval', (answer) => answerRequest(answer)), vscode.commands.registerCommand('supercode.frontend.status', async (inspect) => {
|
|
1727
1862
|
if (controls)
|
|
1728
1863
|
await controls.refresh();
|
|
1864
|
+
const draft = activeDraft();
|
|
1865
|
+
const focusedId = focusedResource ? boundId(focusedResource) : undefined;
|
|
1866
|
+
const focusedSession = controls?.state.sessions.find(session => session.id === focusedId);
|
|
1867
|
+
const selectionSource = draft ? 'draft' : focusedAttached ? 'attachment' : focusedSession ? 'conversation'
|
|
1868
|
+
: focusedResource ? 'unavailable' : 'host';
|
|
1869
|
+
const selection = draft ? draftSelection() : focusedAttached ? undefined : focusedSession?.selection
|
|
1870
|
+
?? (focusedResource ? undefined : controls?.state.selection);
|
|
1729
1871
|
if (inspect === true)
|
|
1730
1872
|
return {
|
|
1731
1873
|
setupState: setupState(),
|
|
1732
1874
|
connections: frontends.all.map(c => ({ ...frontends.info(c), permissions: [...c.session.permissions],
|
|
1733
1875
|
pendingRequests: [...c.session.pendingRequests.values()] })),
|
|
1734
|
-
selection
|
|
1876
|
+
selection, selectionSource, focusedSession: focusedId ?? null,
|
|
1877
|
+
hostSelection: controls?.state.selection, activeSession: controls?.state.activeSession,
|
|
1878
|
+
focusedSessionResource: focusedResource?.toString() ?? null, focusGeneration,
|
|
1735
1879
|
busy: Boolean(controls?.state.busy || frontends.all.some(c => c.session.descriptor?.turn_state === 'busy' || c.session.pendingRequests.size)),
|
|
1736
1880
|
setupHandoff: { hostReady: controls?.state.setup?.ready ?? null, complete: setupHandoffComplete(),
|
|
1737
|
-
revealed: setupHandoff ?? null
|
|
1881
|
+
revealed: setupHandoff ?? null,
|
|
1882
|
+
lastCanceledReveal: canceledSetupReveal ? { id: canceledSetupReveal.id, baseUrl: canceledSetupReveal.baseUrl,
|
|
1883
|
+
generation: canceledSetupReveal.generation, focusResource: canceledSetupReveal.focusResource, live: canceledSetupReveal.live } : null },
|
|
1738
1884
|
sessions: controls?.state.sessions, permission: currentOptions().permissions, identity: identity(),
|
|
1739
1885
|
};
|
|
1740
1886
|
const rows = frontends.all.map((connection) => {
|
|
@@ -1886,25 +2032,40 @@ export async function activate(context) {
|
|
|
1886
2032
|
// panel on "Loading…" — observed.
|
|
1887
2033
|
const opening = setTimeout(() => void openResumedSession().catch(error => log.warn(`Could not reveal Chat: ${String(error)}`)), 500);
|
|
1888
2034
|
context.subscriptions.push({ dispose: () => clearTimeout(opening) });
|
|
1889
|
-
// Retry
|
|
1890
|
-
//
|
|
2035
|
+
// Retry attachment and reveal failures. A native navigation cancellation waits for changed focus/target/liveness
|
|
2036
|
+
// or an explicit refresh; it neither earns a completed handoff nor repeats the same veto every three seconds.
|
|
1891
2037
|
let setupTimer;
|
|
1892
2038
|
let setupDisposed = false;
|
|
1893
2039
|
const pollSetup = () => {
|
|
2040
|
+
if (!controls || setupDisposed) {
|
|
2041
|
+
clearTimeout(setupTimer);
|
|
2042
|
+
setupTimer = undefined;
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
1894
2045
|
const choice = activeDraft()?.choice;
|
|
1895
2046
|
const awaitingLogin = (choice?.kind === 'signIn' && choice.harness && !setupAgents(controls.state).some(h => h.harness === choice.harness && h.signedIn))
|
|
1896
2047
|
|| [...(setupFlow?.operations.values() ?? [])].some(operation => operation.phase === 'installing' || operation.phase === 'signing-in');
|
|
1897
2048
|
if ([...(setupFlow?.operations.values() ?? [])].some(operation => operation.phase === 'installing' || operation.phase === 'signing-in'))
|
|
1898
2049
|
return;
|
|
1899
|
-
|
|
2050
|
+
const connection = controls.state.activeSession ? frontends.get(controls.state.activeSession) : undefined;
|
|
2051
|
+
const canceled = connection && revealCanceledFor({ id: connection.id,
|
|
2052
|
+
baseUrl: controls.state.connection?.SUPERCODE_FRONTEND_URL ?? '', generation: focusGeneration,
|
|
2053
|
+
focusResource: focusedResource?.toString() ?? null,
|
|
2054
|
+
live: Boolean(connection.session.pendingRequests.size || connection.session.descriptor?.turn_state === 'busy') }, connection);
|
|
2055
|
+
if (controls.state.setup?.ready && !awaitingLogin && (setupHandoffComplete() || canceled)) {
|
|
2056
|
+
clearTimeout(setupTimer);
|
|
2057
|
+
setupTimer = undefined;
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
if (setupTimer || !controls.state.setup)
|
|
1900
2061
|
return;
|
|
1901
2062
|
setupTimer = setTimeout(() => {
|
|
1902
2063
|
setupTimer = undefined;
|
|
1903
|
-
void refreshSetup().catch(error => log.warn(`Could not refresh
|
|
2064
|
+
void refreshSetup().catch(error => log.warn(`Could not refresh Chat setup: ${String(error)}`)).finally(pollSetup);
|
|
1904
2065
|
}, 3000);
|
|
1905
2066
|
};
|
|
1906
2067
|
pollSetup();
|
|
1907
|
-
context.subscriptions.push(controlsChanged.event(pollSetup));
|
|
2068
|
+
context.subscriptions.push(controlsChanged.event(pollSetup), frontends.onDidChange(pollSetup));
|
|
1908
2069
|
context.subscriptions.push({ dispose: () => { setupDisposed = true; clearTimeout(setupTimer); } });
|
|
1909
2070
|
return {
|
|
1910
2071
|
connect: async (options) => {
|
|
@@ -1943,7 +2104,7 @@ function oneRowPerToolCall(parts) {
|
|
|
1943
2104
|
rows[at] = {
|
|
1944
2105
|
...first,
|
|
1945
2106
|
...part,
|
|
1946
|
-
pastTenseMessage: first.
|
|
2107
|
+
pastTenseMessage: first.pastTenseMessage || part.pastTenseMessage,
|
|
1947
2108
|
invocationMessage: first.invocationMessage ?? part.invocationMessage,
|
|
1948
2109
|
};
|
|
1949
2110
|
continue;
|
|
@@ -2024,11 +2185,12 @@ function historyText(part) {
|
|
|
2024
2185
|
}
|
|
2025
2186
|
function registerSessions(context, frontends, participant, granted, handler, historyParticipant, controls) {
|
|
2026
2187
|
const turns = vscode;
|
|
2188
|
+
const failedResources = new Map();
|
|
2027
2189
|
const items = () => {
|
|
2028
2190
|
return (controls?.catalogue() ?? []).filter(entry => !frontends.get(entry.id)).map(entry => {
|
|
2029
2191
|
return { resource: vscode.Uri.from({ scheme: SESSION_SCHEME, authority: entry.id, path: '/conversation' }), label: entry.title,
|
|
2030
2192
|
description: runtimeIdentity(entry.selection.harness, entry.selection.model, controls?.effective(entry)).name,
|
|
2031
|
-
status: vscode.ChatSessionStatus.Completed, timing: { created: entry.created } };
|
|
2193
|
+
status: failedResources.has(entry.id) ? vscode.ChatSessionStatus.Failed : vscode.ChatSessionStatus.Completed, timing: { created: entry.created } };
|
|
2032
2194
|
});
|
|
2033
2195
|
};
|
|
2034
2196
|
const connectedRow = (connection) => ({
|
|
@@ -2127,13 +2289,38 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2127
2289
|
if (token.isCancellationRequested)
|
|
2128
2290
|
throw new vscode.CancellationError();
|
|
2129
2291
|
const boundHandler = (request, chatContext, stream, token) => handler(request, { ...chatContext, chatSessionContext: { chatSessionItem: { resource, label: 'Conversation' } } }, stream, token);
|
|
2292
|
+
const failedContent = (error) => {
|
|
2293
|
+
const message = opaqueText(String(error?.message ?? error), 2048);
|
|
2294
|
+
if (failedResources.get(resource.authority) !== message) {
|
|
2295
|
+
failedResources.set(resource.authority, message);
|
|
2296
|
+
refreshItems();
|
|
2297
|
+
}
|
|
2298
|
+
return {
|
|
2299
|
+
history: [
|
|
2300
|
+
new turns.ChatRequestTurn(HISTORY_MARKER_PROMPT, undefined, [], 'supercode', []),
|
|
2301
|
+
new turns.ChatResponseTurn([new vscode.ChatResponseMarkdownPart(`**Conversation recovery failed**\n\n${message}`)], {}, 'supercode'),
|
|
2302
|
+
],
|
|
2303
|
+
requestHandler: boundHandler,
|
|
2304
|
+
};
|
|
2305
|
+
};
|
|
2130
2306
|
// Only a native untitled resource can create a conversation. Existing
|
|
2131
2307
|
// resources must resolve their exact saved binding; there is no fallback.
|
|
2132
|
-
|
|
2308
|
+
let connection;
|
|
2309
|
+
try {
|
|
2310
|
+
connection = controls ? (resource.path.startsWith('/untitled-') && !controls.bindings(resource) ? undefined : await controls.resolve(resource, token)) : frontends.get(resource.authority);
|
|
2311
|
+
}
|
|
2312
|
+
catch (error) {
|
|
2313
|
+
if (token.isCancellationRequested || error instanceof vscode.CancellationError)
|
|
2314
|
+
throw error;
|
|
2315
|
+
return failedContent(error);
|
|
2316
|
+
}
|
|
2317
|
+
failedResources.delete(resource.authority);
|
|
2133
2318
|
if (token.isCancellationRequested)
|
|
2134
2319
|
throw new vscode.CancellationError();
|
|
2135
2320
|
if (!connection) {
|
|
2136
|
-
|
|
2321
|
+
const draft = resource.path.startsWith('/untitled-') && !controls?.bindings(resource);
|
|
2322
|
+
return draft ? { history: [], requestHandler: boundHandler }
|
|
2323
|
+
: failedContent(frontends.unavailable ?? 'Could not attach to this saved conversation\'s runtime.');
|
|
2137
2324
|
}
|
|
2138
2325
|
connection.viewReady = false;
|
|
2139
2326
|
connection.viewResource = resource.toString();
|
|
@@ -2146,7 +2333,8 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2146
2333
|
// `state.history`), so the copy taken when this window attached is
|
|
2147
2334
|
// stale as soon as a turn runs. The generated client reuses the
|
|
2148
2335
|
// already-open event stream, so this is a snapshot, not a reconnect.
|
|
2149
|
-
const attached = frontends.get(connection.id) === connection && await connection.session.attach().then(() => true).catch(
|
|
2336
|
+
const attached = frontends.get(connection.id) === connection && await connection.session.attach().then(() => true).catch(error => {
|
|
2337
|
+
connection.session.reportFailure(error);
|
|
2150
2338
|
// An unreachable runtime replays what this window last saw.
|
|
2151
2339
|
return false;
|
|
2152
2340
|
});
|
|
@@ -2160,7 +2348,10 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2160
2348
|
// created. Keep it only as a fallback when no runtime user history is
|
|
2161
2349
|
// available, rather than trying to align equal-looking prompt text.
|
|
2162
2350
|
const runtimeHistory = connection.session.history;
|
|
2163
|
-
const replay =
|
|
2351
|
+
const replay = [
|
|
2352
|
+
...connection.session.historyEvents,
|
|
2353
|
+
...connection.session.replay.filter(event => event.sequence > connection.session.historyCursor),
|
|
2354
|
+
];
|
|
2164
2355
|
const hasRuntimeHistory = runtimeHistory.length > 0 || replay.some(event => event.kind === 'user_message');
|
|
2165
2356
|
const usingSavedHistory = !hasRuntimeHistory && Boolean(connection.persistedHistory?.length);
|
|
2166
2357
|
const canonicalHistory = usingSavedHistory ? connection.persistedHistory : runtimeHistory;
|
|
@@ -2217,6 +2408,8 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2217
2408
|
}
|
|
2218
2409
|
if (usingSavedHistory && connection.historyTruncated)
|
|
2219
2410
|
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));
|
|
2411
|
+
if (connection.session.historyEventsDropped > 0)
|
|
2412
|
+
history.push(new turns.ChatRequestTurn(HISTORY_MARKER_PROMPT, undefined, [], authorId, []), new turns.ChatResponseTurn([new vscode.ChatResponseMarkdownPart(`_${connection.session.historyEventsDropped} earlier runtime events are outside this display snapshot._`)], {}, authorId));
|
|
2220
2413
|
for (const turn of projected) {
|
|
2221
2414
|
if (turn.role === 'user') {
|
|
2222
2415
|
history.push(new turns.ChatRequestTurn(turn.prompt ?? '', undefined, [], authorId, []));
|
|
@@ -2224,6 +2417,11 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2224
2417
|
}
|
|
2225
2418
|
history.push(new turns.ChatResponseTurn(historyParts(oneRowPerToolCall(turn.parts ?? [])), {}, authorId));
|
|
2226
2419
|
}
|
|
2420
|
+
if (connection.session.failure) {
|
|
2421
|
+
// An explicit runtime marker pairs this notice without presenting
|
|
2422
|
+
// it as a person's prompt. The native busy state stays unchanged.
|
|
2423
|
+
history.push(new turns.ChatRequestTurn(HISTORY_MARKER_PROMPT, undefined, [], authorId, []), new turns.ChatResponseTurn([new vscode.ChatResponseMarkdownPart(`**Conversation recovery failed**\n\n${opaqueText(connection.session.failure.message, 2048)}`)], {}, authorId));
|
|
2424
|
+
}
|
|
2227
2425
|
const renderedCursor = connection.session.cursor;
|
|
2228
2426
|
const pendingAtCursor = [...connection.session.pendingRequests.values()];
|
|
2229
2427
|
if (!live)
|
|
@@ -2252,6 +2450,7 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2252
2450
|
await observeActiveResponse(connection.session, token, (event) => {
|
|
2253
2451
|
writeProjected(stream, projectEvent(event, {
|
|
2254
2452
|
connectionId: connection.id,
|
|
2453
|
+
liveUserMessages: true,
|
|
2255
2454
|
pendingRequest: (id) => connection.session.pendingRequests.get(id) ?? connection.session.resolvedRequests.get(id),
|
|
2256
2455
|
canRespond: Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve')),
|
|
2257
2456
|
}), granted, state);
|
|
@@ -2259,6 +2458,11 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
2259
2458
|
controls?.rememberIdentity(connection);
|
|
2260
2459
|
}, () => { connection.viewReady = true; }, renderedCursor);
|
|
2261
2460
|
}
|
|
2461
|
+
catch (error) {
|
|
2462
|
+
if (token.isCancellationRequested || error instanceof vscode.CancellationError)
|
|
2463
|
+
throw error;
|
|
2464
|
+
stream.markdown(`\n\n**Conversation stream failed**\n\n${opaqueText(String(error?.message ?? error), 2048)}\n\n`);
|
|
2465
|
+
}
|
|
2262
2466
|
finally {
|
|
2263
2467
|
connection.viewReady = connection.session.descriptor?.turn_state !== 'busy' && !connection.session.pendingRequests.size;
|
|
2264
2468
|
watch?.dispose();
|
package/dist/host-controls.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface ManagedChatSession {
|
|
|
27
27
|
title: string;
|
|
28
28
|
created: number;
|
|
29
29
|
defaultPermission?: 'ask' | 'autoApprove';
|
|
30
|
+
runtimePolicy?: 'default' | 'yolo';
|
|
30
31
|
}
|
|
31
32
|
/** Host-owned operations. The host revalidates commands before native terminal execution. */
|
|
32
33
|
export interface ChatSetupAction {
|
|
@@ -75,15 +76,29 @@ export interface HostState {
|
|
|
75
76
|
/** The mode a new chat's picker shows. A conversation's own default is its `ManagedChatSession.defaultPermission`;
|
|
76
77
|
* this is never applied to one that exists. `autoApprove` takes effect only where the runtime can answer approvals. */
|
|
77
78
|
newChatPermission?: 'ask' | 'autoApprove';
|
|
79
|
+
/** Project policy for the next start/resume; an existing runtime uses its recorded session policy. */
|
|
80
|
+
newChatRuntimePolicy?: 'default' | 'yolo' | null;
|
|
81
|
+
runtimePolicyError?: string | null;
|
|
82
|
+
/** The workbench tools this host's runtimes reach (ADR 0255): the tag they carry, and the socket and token the host
|
|
83
|
+
* named, where their relay (`supercode mcp workbench`) calls them. Absent, they reach none. */
|
|
84
|
+
workbenchTools?: WorkbenchToolsEndpoint;
|
|
85
|
+
}
|
|
86
|
+
export interface WorkbenchToolsEndpoint {
|
|
87
|
+
tag: string;
|
|
88
|
+
socket: string;
|
|
89
|
+
token: string;
|
|
78
90
|
}
|
|
79
91
|
export interface HostSelectionResult extends HostState {
|
|
80
92
|
connection: Record<string, string>;
|
|
81
93
|
}
|
|
82
94
|
export declare class ChatHostControls {
|
|
95
|
+
#private;
|
|
83
96
|
readonly baseUrl: string;
|
|
84
97
|
private readonly credential;
|
|
85
98
|
state: HostState;
|
|
86
99
|
private updating;
|
|
100
|
+
/** Call `listener` with each state the host replies with; returns the unsubscribe. */
|
|
101
|
+
onState(listener: (state: HostState) => void): () => void;
|
|
87
102
|
private constructor();
|
|
88
103
|
static fromEnvironment(env?: NodeJS.ProcessEnv): Promise<ChatHostControls | undefined>;
|
|
89
104
|
refresh(): Promise<void>;
|