@volter/supercode-frontend-vscode 0.1.35 → 0.1.37
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 +24 -7
- package/dist/extension.js +157 -48
- package/dist/host-controls.d.ts +6 -1
- package/dist/host-controls.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,9 +94,11 @@ which startable agent to use. It refreshes the host's available choices first;
|
|
|
94
94
|
with one ready local agent and no pending native sign-in choices it opens
|
|
95
95
|
immediately, even when the official ChatGPT choice is available. Otherwise the
|
|
96
96
|
picker includes the official choice on supported hosts; with no choices it returns
|
|
97
|
-
to setup.
|
|
98
|
-
and reasoning
|
|
99
|
-
even if the previous conversation used explicit settings.
|
|
97
|
+
to setup. Picking an agent sends model
|
|
98
|
+
and reasoning as empty values, leaving both at the harness's own defaults,
|
|
99
|
+
even if the previous conversation used explicit settings. Each agent whose model
|
|
100
|
+
and reasoning a chat can set (Claude Code, Codex) is also listed in the picker as **<agent>: model
|
|
101
|
+
and reasoning…**, which goes on to the model and effort choices below. Cancelling the agent
|
|
100
102
|
picker starts no backend conversation.
|
|
101
103
|
|
|
102
104
|
The native blank chat opens before the picker appears. First send waits for the
|
|
@@ -116,8 +118,14 @@ sign-in choice it uses that agent once ready, otherwise it keeps the draft with
|
|
|
116
118
|
a visible sign-in error.
|
|
117
119
|
|
|
118
120
|
The composer's native **Model settings** control offers **Choose model and reasoning
|
|
119
|
-
for a new chat…** for Claude Code and Codex
|
|
120
|
-
|
|
121
|
+
for a new chat…** for Claude Code and Codex, and **Chat: Model and Reasoning: Open New
|
|
122
|
+
Chat** does the same from anywhere (with no conversation open it asks for the agent
|
|
123
|
+
first). The models and each model's reasoning efforts are the host's
|
|
124
|
+
`modelsByHarness` choices, which a host reads from the harness itself (Model Editor:
|
|
125
|
+
Claude Code's `--help`, Codex's models cache), so levels such as `xhigh` and `max`
|
|
126
|
+
appear as the harness offers them; a host that sends no efforts gets low/medium/high.
|
|
127
|
+
Exact model ID entry remains. When a turn ends, its reply notes the model it ran on
|
|
128
|
+
(as the harness's replies name it) and the effort it was given. Only completing the choices
|
|
121
129
|
configures the new blank chat; its backend starts on first send. Cancelling a
|
|
122
130
|
choice starts no backend and leaves the blank chat available.
|
|
123
131
|
A conversation's harness, model and reasoning remain fixed: this integration does
|
|
@@ -142,8 +150,17 @@ the same cached conversation. Managed resources then notify
|
|
|
142
150
|
owns no processes.
|
|
143
151
|
|
|
144
152
|
The permission group offers **Ask for approval** and, on an answerable interactive attachment,
|
|
145
|
-
**Auto approve**. Policies are scoped by conversation and
|
|
146
|
-
|
|
153
|
+
**Auto approve**. Policies are scoped by conversation and read throughout the turn: switching to
|
|
154
|
+
**Auto approve** while a turn runs answers the approvals already waiting and every later one.
|
|
155
|
+
A lifecycle host may declare `defaultPermission: "autoApprove"` for conversations no one has
|
|
156
|
+
picked a mode for (Model Editor does); it applies to Claude Code and Codex where the runtime answers approvals
|
|
157
|
+
through Chat, every other harness keeps its own prompts, and a person's explicit pick always
|
|
158
|
+
wins. Only a pick that differs from the default (or replaces an earlier pick) is saved, so the
|
|
159
|
+
default is never stored as a choice (a pick equal to the default follows the default if it later
|
|
160
|
+
changes), and an Auto approve is never stored for a conversation that cannot answer. Conversations opened before 0.1.36 saved their picker's
|
|
161
|
+
**Ask** when opened and keep it. Auto approve approves the requests the harness would ask you about, including requests to run
|
|
162
|
+
outside its sandbox (Codex's approval requests are its sandbox escalations); the harness's deny
|
|
163
|
+
rules still apply. Each request it answers is noted in the turn ("auto-approved: …"). Unsupported modes are refused. Unanswerable
|
|
147
164
|
harnesses show **Harness permissions**. Previously answered requests cannot affect another
|
|
148
165
|
connection. Native command-button fallback limitations are documented below.
|
|
149
166
|
|
package/dist/extension.js
CHANGED
|
@@ -11,6 +11,8 @@ import { observeActiveResponse, sessionStatus } from './native-session.js';
|
|
|
11
11
|
import { RequestNotPendingError, VscodeFrontendSession } from './session.js';
|
|
12
12
|
import { acceptedDecisions, opaqueText, projectEvent, projectPendingRequests, projectHistory, projectReplayTurns, resolveParts, resolutionSentence, typedAnswer, } from './view.js';
|
|
13
13
|
const SESSION_SCHEME = 'supercode';
|
|
14
|
+
/** The harnesses whose model and reasoning effort a new chat can set (the host passes them to the CLI). */
|
|
15
|
+
const CONFIGURABLE_HARNESSES = ['claude-code', 'codex'];
|
|
14
16
|
const PARTICIPANT_ID = 'supercode.chat';
|
|
15
17
|
const DEFAULT_PARTICIPANT_ID = 'supercode.chat.default';
|
|
16
18
|
/** `package.json` gates the `isDefault` participant on this key. */
|
|
@@ -356,11 +358,45 @@ export async function activate(context) {
|
|
|
356
358
|
}), vscode.commands.registerCommand(CHATGPT_SIGN_IN_STATUS, (operationId) => operationId === undefined || operationId === chatGPTSignInOperation?.id ? signInSnapshot() : null));
|
|
357
359
|
const controlsChanged = new vscode.EventEmitter();
|
|
358
360
|
context.subscriptions.push(controlsChanged);
|
|
359
|
-
|
|
361
|
+
// A person's explicit picker choice: per conversation once it is bound, and for the new chat before then.
|
|
362
|
+
let permission;
|
|
360
363
|
const permissions = new Map(context.workspaceState.get('supercode.sessionPermissions', []));
|
|
361
364
|
const savePermission = (id, value) => {
|
|
362
365
|
permissions.set(id, value);
|
|
363
366
|
void Promise.resolve(context.workspaceState.update('supercode.sessionPermissions', [...permissions])).catch(error => log.error(`Could not persist conversation permissions: ${String(error)}`));
|
|
367
|
+
for (const sweep of runningTurns.get(id) ?? [])
|
|
368
|
+
sweep();
|
|
369
|
+
};
|
|
370
|
+
// The approval sweeps of each conversation's running turns, so a picker change reaches a turn already running.
|
|
371
|
+
const runningTurns = new Map();
|
|
372
|
+
// The host's default for a conversation no one has chosen for (Model Editor declares Auto approve).
|
|
373
|
+
const hostDefault = () => controls?.state.defaultPermission === 'autoApprove' ? 'autoApprove' : 'ask';
|
|
374
|
+
const canAnswer = (connection) => Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve'));
|
|
375
|
+
// The host's default for one conversation: Auto approve only for a conversation the host manages and only
|
|
376
|
+
// where this Chat can answer the runtime's approvals, read from its live connection, or, while it is not
|
|
377
|
+
// connected, from the harness the host records for it (hosted Claude Code and Codex answer through Chat).
|
|
378
|
+
// A runtime attached by address and every other harness keep their own prompts.
|
|
379
|
+
const defaultFor = (id) => {
|
|
380
|
+
if (hostDefault() !== 'autoApprove' || attachedIds.has(id))
|
|
381
|
+
return 'ask';
|
|
382
|
+
const connection = frontends.get(id);
|
|
383
|
+
const harness = controls?.state.sessions.find(session => session.id === id)?.selection.harness ?? '';
|
|
384
|
+
// Only the harnesses whose approvals this default was decided for; a runtime that answers through another
|
|
385
|
+
// protocol (an ACP agent) keeps asking until a host names it.
|
|
386
|
+
return ['claude-code', 'codex'].includes(harness) && (!connection || canAnswer(connection)) ? 'autoApprove' : 'ask';
|
|
387
|
+
};
|
|
388
|
+
// What governs a conversation's approvals: the person's choice, else the host's default.
|
|
389
|
+
const approvalFor = (id) => permissions.get(id) ?? defaultFor(id);
|
|
390
|
+
// A picker value becomes the person's choice only when it is one: it differs from the default the picker showed
|
|
391
|
+
// (`shown`; null when the person already picked it), or replaces an earlier choice. Echoes of the displayed default
|
|
392
|
+
// (the picker re-applied when a conversation opens) are not saved, and neither is an Auto approve the conversation
|
|
393
|
+
// cannot answer: the picker offers it only where it can, so there it is a default shown before the harness was known.
|
|
394
|
+
const choosePermission = (id, value, shown = defaultFor(id)) => {
|
|
395
|
+
const connection = frontends.get(id);
|
|
396
|
+
if (value === 'autoApprove' && connection && !canAnswer(connection))
|
|
397
|
+
return;
|
|
398
|
+
if (permissions.has(id) || value !== shown)
|
|
399
|
+
savePermission(id, value);
|
|
364
400
|
};
|
|
365
401
|
const resourceBindings = new Map(context.workspaceState.get('supercode.sessionBindings', []));
|
|
366
402
|
const boundId = (resource) => resolveSessionBinding(resource, controls?.state.sessions ?? [], resourceBindings);
|
|
@@ -452,7 +488,7 @@ export async function activate(context) {
|
|
|
452
488
|
}
|
|
453
489
|
const currentOptions = (resource) => {
|
|
454
490
|
const id = resource ? boundId(resource) : controls?.state.activeSession;
|
|
455
|
-
return { permissions: id ?
|
|
491
|
+
return { permissions: id ? approvalFor(id) : permission ?? hostDefault() };
|
|
456
492
|
};
|
|
457
493
|
const savedConnections = new Map();
|
|
458
494
|
const saveManagedHistory = async (connection) => {
|
|
@@ -705,6 +741,44 @@ export async function activate(context) {
|
|
|
705
741
|
throw new vscode.CancellationError();
|
|
706
742
|
return draft;
|
|
707
743
|
};
|
|
744
|
+
// A new chat's model and reasoning effort, offered from what the harness itself lists (the host reads its models
|
|
745
|
+
// and each model's efforts from the harness). Escape cancels; "Harness default" passes nothing, so the harness's
|
|
746
|
+
// own settings decide. A host that sends no efforts gets the levels every configurable harness takes.
|
|
747
|
+
// A single pick that opens on the current value (showQuickPick's `picked` applies only to multi-select).
|
|
748
|
+
const pickWithCurrent = (items, title, placeholder, currentId) => {
|
|
749
|
+
const quick = vscode.window.createQuickPick();
|
|
750
|
+
quick.title = title;
|
|
751
|
+
quick.placeholder = placeholder;
|
|
752
|
+
quick.items = items;
|
|
753
|
+
quick.activeItems = items.filter(item => item.id === currentId);
|
|
754
|
+
return new Promise(resolve => {
|
|
755
|
+
quick.onDidAccept(() => resolve(quick.selectedItems[0]));
|
|
756
|
+
quick.onDidHide(() => resolve(undefined));
|
|
757
|
+
quick.show();
|
|
758
|
+
}).finally(() => quick.dispose());
|
|
759
|
+
};
|
|
760
|
+
const pickModelAndEffort = async (harness, current) => {
|
|
761
|
+
const choices = controls?.state.modelsByHarness?.[harness] ?? [{ id: '', name: 'Harness default' }];
|
|
762
|
+
const name = harnessDisplayName(harness);
|
|
763
|
+
const items = [...choices.map(model => ({ label: model.name, description: [model.id, model.description].filter(Boolean).join(' · ') || undefined, id: model.id, efforts: model.efforts })),
|
|
764
|
+
{ label: 'Enter an exact model ID…', description: undefined, id: '__custom__', efforts: choices.find(model => model.id === '')?.efforts }];
|
|
765
|
+
const picked = await pickWithCurrent(items, `New chat · ${name} model`, 'Model availability depends on your harness account.', current?.model ?? '');
|
|
766
|
+
if (!picked)
|
|
767
|
+
return;
|
|
768
|
+
let model = picked.id;
|
|
769
|
+
if (picked.id === '__custom__') {
|
|
770
|
+
const typed = await vscode.window.showInputBox({ title: `New chat · ${name} exact model`, prompt: 'Model ID or CLI alias supported by this harness account.', value: current?.model });
|
|
771
|
+
if (typed === undefined)
|
|
772
|
+
return;
|
|
773
|
+
model = typed.trim();
|
|
774
|
+
}
|
|
775
|
+
const levels = picked.efforts ?? ['low', 'medium', 'high'];
|
|
776
|
+
const efforts = [{ label: 'Harness default', id: '' }, ...levels.map(id => ({ label: id, id }))];
|
|
777
|
+
const effort = levels.length === 0 ? efforts[0] : await pickWithCurrent(efforts, `New chat · ${name} reasoning effort`, model ? `For ${model}` : 'For the harness default model', current?.effort ?? '');
|
|
778
|
+
if (!effort)
|
|
779
|
+
return;
|
|
780
|
+
return { model, effort: effort.id };
|
|
781
|
+
};
|
|
708
782
|
const chooseNewChat = async (pick) => {
|
|
709
783
|
if (pendingDraft?.choosing)
|
|
710
784
|
throw new Error('Finish choosing an agent before starting another chat.');
|
|
@@ -749,8 +823,8 @@ export async function activate(context) {
|
|
|
749
823
|
const active = resource ? (id ? frontends.get(id) : undefined) : managed();
|
|
750
824
|
const canApprove = active?.session.descriptor?.actions.respond && active.session.permissions.has('approve');
|
|
751
825
|
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.' },
|
|
752
|
-
...((canApprove || (!active && resource && currentOptions(resource).permissions === 'autoApprove')) ? [{ id: 'autoApprove', name: 'Auto approve', description: '
|
|
753
|
-
const groups = [{ id: 'permissions', name: 'Permissions', kind: 'permissions', items: permissionItems, selected: permissionItems.find(i => i.id ===
|
|
826
|
+
...((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.' }] : [])];
|
|
827
|
+
const groups = [{ id: 'permissions', name: 'Permissions', kind: 'permissions', items: permissionItems, selected: permissionItems.find(i => i.id === currentOptions(resource).permissions) }];
|
|
754
828
|
const selected = controls?.state.sessions.find(session => session.id === id);
|
|
755
829
|
if (selected && ['claude-code', 'codex'].includes(selected.selection.harness)) {
|
|
756
830
|
// Native session-option commands remain reachable for nondelegating providers,
|
|
@@ -768,7 +842,7 @@ export async function activate(context) {
|
|
|
768
842
|
if (controls && !id)
|
|
769
843
|
throw new Error('This conversation has no session binding.');
|
|
770
844
|
if (id)
|
|
771
|
-
|
|
845
|
+
choosePermission(id, turnPermission(value));
|
|
772
846
|
else
|
|
773
847
|
permission = turnPermission(value);
|
|
774
848
|
},
|
|
@@ -794,7 +868,10 @@ export async function activate(context) {
|
|
|
794
868
|
const pending = applying.then(() => applySelection(selection));
|
|
795
869
|
applying = pending.then(() => undefined, () => undefined);
|
|
796
870
|
const connection = await pending;
|
|
797
|
-
|
|
871
|
+
// The picker shows the host's default until someone changes it; only a change is the person's choice.
|
|
872
|
+
const picked = turnPermission(inputState.groups.find(group => group.id === 'permissions')?.selected?.id);
|
|
873
|
+
choosePermission(connection.id, picked, permission !== undefined ? null : hostDefault());
|
|
874
|
+
permission = undefined;
|
|
798
875
|
if (pendingDraft === draft) {
|
|
799
876
|
pendingDraft = undefined;
|
|
800
877
|
controlsChanged.fire();
|
|
@@ -825,7 +902,7 @@ export async function activate(context) {
|
|
|
825
902
|
if (supplied?.permissions) {
|
|
826
903
|
const id = controls?.state.activeSession;
|
|
827
904
|
if (id)
|
|
828
|
-
|
|
905
|
+
choosePermission(id, turnPermission(supplied.permissions));
|
|
829
906
|
else
|
|
830
907
|
permission = turnPermission(supplied.permissions);
|
|
831
908
|
controlsChanged.fire();
|
|
@@ -843,13 +920,17 @@ export async function activate(context) {
|
|
|
843
920
|
// Sign-in choices stay visible alongside agents that can be explicitly started.
|
|
844
921
|
const login = controls.state.setup?.actions.filter(action => action.kind === 'login') ?? [];
|
|
845
922
|
const localChoices = [
|
|
846
|
-
...login.map(action => ({ label: action.name, id: action.harness, description: 'Sign in…', login: true, autoStart: false })),
|
|
923
|
+
...login.map(action => ({ label: action.name, id: action.harness, description: 'Sign in…', login: true, autoStart: false, configure: false })),
|
|
847
924
|
...controls.state.harnesses.filter(h => !login.some(action => action.harness === h.id))
|
|
848
|
-
.map(h => ({ label: h.name, id: h.id, description: h.description, login: false, autoStart: h.autoStart === true })),
|
|
925
|
+
.map(h => ({ label: h.name, id: h.id, description: h.description, login: false, autoStart: h.autoStart === true, configure: false })),
|
|
849
926
|
];
|
|
927
|
+
// Each agent whose model and reasoning a chat can set also offers that, right where the agent is chosen.
|
|
928
|
+
const modelChoices = localChoices.filter(choice => !choice.login && CONFIGURABLE_HARNESSES.includes(choice.id))
|
|
929
|
+
.map(choice => ({ ...choice, label: `${choice.label}: model and reasoning…`, description: 'Choose the model and reasoning effort', autoStart: false, configure: true }));
|
|
850
930
|
const choices = [
|
|
851
|
-
...(offerChatGPT ? [{ label: 'Sign in with ChatGPT', id: CHATGPT_SIGN_IN, description: 'Official Codex extension', login: false, autoStart: false }] : []),
|
|
931
|
+
...(offerChatGPT ? [{ label: 'Sign in with ChatGPT', id: CHATGPT_SIGN_IN, description: 'Official Codex extension', login: false, autoStart: false, configure: false }] : []),
|
|
852
932
|
...localChoices,
|
|
933
|
+
...modelChoices,
|
|
853
934
|
];
|
|
854
935
|
if (!choices.length) {
|
|
855
936
|
await revealSetup();
|
|
@@ -858,9 +939,13 @@ export async function activate(context) {
|
|
|
858
939
|
// A sole sign-in option still requires a deliberate click before opening its terminal.
|
|
859
940
|
// The official choice must not disable the existing sole-local-agent shortcut.
|
|
860
941
|
await chooseNewChat(async () => {
|
|
861
|
-
const harness = localChoices.length === 1 && localChoices[0].autoStart ? localChoices[0] : await vscode.window.showQuickPick(choices, { title: 'New chat · Agent', placeHolder: 'Choose your coding agent.
|
|
942
|
+
const harness = localChoices.length === 1 && localChoices[0].autoStart ? localChoices[0] : await vscode.window.showQuickPick(choices, { title: 'New chat · Agent', placeHolder: 'Choose your coding agent. Its default model and reasoning apply unless you choose them.' });
|
|
862
943
|
if (!harness)
|
|
863
944
|
return { kind: 'cancelled' };
|
|
945
|
+
if (harness.configure) {
|
|
946
|
+
const chosen = await pickModelAndEffort(harness.id);
|
|
947
|
+
return chosen ? { kind: 'agent', selection: { harness: harness.id, ...chosen } } : { kind: 'cancelled' };
|
|
948
|
+
}
|
|
864
949
|
if (harness.id === CHATGPT_SIGN_IN) {
|
|
865
950
|
await vscode.commands.executeCommand(CHATGPT_SIGN_IN);
|
|
866
951
|
return { kind: 'signIn' };
|
|
@@ -880,36 +965,29 @@ export async function activate(context) {
|
|
|
880
965
|
const bound = resource ? boundId(resource) : undefined;
|
|
881
966
|
if (resource && (!bound || (sessionId && bound !== sessionId)))
|
|
882
967
|
throw new Error('Model settings have no matching conversation binding.');
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
968
|
+
// The conversation it was opened from sets the agent and the current values; with none open (a fresh project,
|
|
969
|
+
// the command palette) the person picks the agent first. Either way the choice opens a new chat.
|
|
970
|
+
const selected = controls.state.sessions.find(session => session.id === (bound ?? controls.state.activeSession));
|
|
971
|
+
// An agent that still needs sign-in is set up from the agent picker first, as there.
|
|
972
|
+
const needsSignIn = new Set((controls.state.setup?.actions ?? []).filter(action => action.kind === 'login').map(action => action.harness));
|
|
973
|
+
const configurable = controls.state.harnesses.filter(harness => CONFIGURABLE_HARNESSES.includes(harness.id) && !needsSignIn.has(harness.id));
|
|
974
|
+
if (!configurable.length && !(selected && CONFIGURABLE_HARNESSES.includes(selected.selection.harness)))
|
|
975
|
+
throw new Error('No agent on this machine takes a model or reasoning choice here; its own defaults apply.');
|
|
890
976
|
await chooseNewChat(async () => {
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
if (picked.id === '__custom__') {
|
|
896
|
-
const model = await vscode.window.showInputBox({ title: 'New chat · Exact model', prompt: 'Model ID or CLI alias supported by this harness account.', value: selection.model });
|
|
897
|
-
if (model === undefined)
|
|
977
|
+
let harness = selected && CONFIGURABLE_HARNESSES.includes(selected.selection.harness) ? selected.selection.harness : undefined;
|
|
978
|
+
if (!harness) {
|
|
979
|
+
const agent = configurable.length === 1 ? configurable[0] : await vscode.window.showQuickPick(configurable.map(choice => ({ ...choice, label: choice.name })), { title: 'New chat · Agent', placeHolder: 'Choose the agent whose model and reasoning to set.' });
|
|
980
|
+
if (!agent)
|
|
898
981
|
return { kind: 'cancelled' };
|
|
899
|
-
|
|
982
|
+
harness = agent.id;
|
|
900
983
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
const effort = await vscode.window.showQuickPick([{ label: 'Harness default', id: '' }, ...['low', 'medium', 'high'].map(id => ({ label: id, id }))], { title: 'New chat · Reasoning effort' });
|
|
904
|
-
if (!effort)
|
|
905
|
-
return { kind: 'cancelled' };
|
|
906
|
-
selection.effort = effort.id;
|
|
907
|
-
return { kind: 'agent', selection };
|
|
984
|
+
const chosen = await pickModelAndEffort(harness, selected?.selection.harness === harness ? selected.selection : undefined);
|
|
985
|
+
return chosen ? { kind: 'agent', selection: { harness, ...chosen } } : { kind: 'cancelled' };
|
|
908
986
|
});
|
|
909
987
|
}));
|
|
910
988
|
context.subscriptions.push({ dispose: () => frontends.dispose() });
|
|
911
989
|
const runTurnInto = async (connection, prompt, stream, token, approval) => {
|
|
912
|
-
if (approval === 'autoApprove' && (
|
|
990
|
+
if (approval() === 'autoApprove' && !canAnswer(connection)) {
|
|
913
991
|
throw new Error('This harness does not support automatic approval through Chat. Select Harness permissions.');
|
|
914
992
|
}
|
|
915
993
|
const answered = new Set();
|
|
@@ -925,25 +1003,49 @@ export async function activate(context) {
|
|
|
925
1003
|
// this view reports.
|
|
926
1004
|
});
|
|
927
1005
|
});
|
|
1006
|
+
// Read at every event and on every picker change, so switching to Auto approve mid-turn
|
|
1007
|
+
// answers the approvals already waiting and the ones after them.
|
|
1008
|
+
const sweep = () => {
|
|
1009
|
+
if (approval() !== 'autoApprove' || token.isCancellationRequested || !canAnswer(connection))
|
|
1010
|
+
return;
|
|
1011
|
+
for (const pending of connection.session.pendingRequests.values()) {
|
|
1012
|
+
if (pending.kind !== 'approval' || answered.has(pending.id))
|
|
1013
|
+
continue;
|
|
1014
|
+
const decisions = acceptedDecisions(pending);
|
|
1015
|
+
if (decisions && !decisions.includes('allow'))
|
|
1016
|
+
continue;
|
|
1017
|
+
answered.add(pending.id);
|
|
1018
|
+
// Its card is already in the stream (the event that raised it was written first), so say it was answered.
|
|
1019
|
+
const payload = pending.payload && typeof pending.payload === 'object' && !Array.isArray(pending.payload) ? pending.payload : {};
|
|
1020
|
+
const named = [payload.title, payload.tool, payload.tool_name].find((value) => typeof value === 'string');
|
|
1021
|
+
// The answer can settle after the turn's response has closed, when writing to it throws: the note is then dropped.
|
|
1022
|
+
const note = (text) => { try {
|
|
1023
|
+
stream.markdown(text);
|
|
1024
|
+
}
|
|
1025
|
+
catch {
|
|
1026
|
+
log.info(text.trim());
|
|
1027
|
+
} };
|
|
1028
|
+
void connection.session.respond({ kind: 'approval', request_id: pending.id, decision: 'allow' }).then(() => {
|
|
1029
|
+
note(`\n_auto-approved: ${opaqueText(named ?? `request #${pending.id}`, 200)}_\n`);
|
|
1030
|
+
}, error => {
|
|
1031
|
+
note(`\nApproval failed: ${String(error.message || error)}. Use the approval buttons to answer.\n`);
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
const sweeps = runningTurns.get(connection.id) ?? new Set();
|
|
1036
|
+
runningTurns.set(connection.id, sweeps.add(sweep));
|
|
1037
|
+
let completed = false;
|
|
928
1038
|
try {
|
|
929
1039
|
await connection.session.runTurn(prompt, (event) => {
|
|
930
1040
|
writeProjected(stream, projectEvent(event, projection), granted, state);
|
|
931
|
-
|
|
932
|
-
for (const pending of connection.session.pendingRequests.values()) {
|
|
933
|
-
if (pending.kind !== 'approval' || answered.has(pending.id))
|
|
934
|
-
continue;
|
|
935
|
-
const decisions = acceptedDecisions(pending);
|
|
936
|
-
if (decisions && !decisions.includes('allow'))
|
|
937
|
-
continue;
|
|
938
|
-
answered.add(pending.id);
|
|
939
|
-
void connection.session.respond({ kind: 'approval', request_id: pending.id, decision: 'allow' }).catch(error => {
|
|
940
|
-
stream.markdown(`\nApproval failed: ${String(error.message || error)}. Use the approval buttons to answer.\n`);
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
}
|
|
1041
|
+
sweep();
|
|
944
1042
|
});
|
|
1043
|
+
completed = true;
|
|
945
1044
|
}
|
|
946
1045
|
finally {
|
|
1046
|
+
sweeps.delete(sweep);
|
|
1047
|
+
if (!sweeps.size && runningTurns.get(connection.id) === sweeps)
|
|
1048
|
+
runningTurns.delete(connection.id);
|
|
947
1049
|
cancellation.dispose();
|
|
948
1050
|
if (controls && !attachedIds.has(connection.id)) {
|
|
949
1051
|
if (connection.session.descriptor?.session_id)
|
|
@@ -951,6 +1053,12 @@ export async function activate(context) {
|
|
|
951
1053
|
await controls.refresh().catch(error => log.warn(`Could not refresh chat identity: ${String(error)}`));
|
|
952
1054
|
controlsChanged.fire();
|
|
953
1055
|
}
|
|
1056
|
+
// The model the turn ran on: what the harness's own replies name (the host watches them), else what the
|
|
1057
|
+
// runtime reports; and the effort it was given.
|
|
1058
|
+
const ran = (controls?.state.activeSession === connection.id ? controls.state.actualModel : undefined) || connection.session.descriptor?.model;
|
|
1059
|
+
const effort = controls?.state.sessions.find(session => session.id === connection.id)?.selection.effort;
|
|
1060
|
+
if (completed && ran && !token.isCancellationRequested)
|
|
1061
|
+
stream.markdown(`\n\n_Ran on ${opaqueText(ran, 120)}${effort ? ` · ${opaqueText(effort, 24)} effort` : ''}_`);
|
|
954
1062
|
}
|
|
955
1063
|
};
|
|
956
1064
|
const handler = async (request, chatContext, stream, token) => {
|
|
@@ -993,7 +1101,8 @@ export async function activate(context) {
|
|
|
993
1101
|
stream.markdown('_nothing to submit_');
|
|
994
1102
|
return {};
|
|
995
1103
|
}
|
|
996
|
-
const
|
|
1104
|
+
const native = controls ? undefined : turnPermission(request.permissionLevel);
|
|
1105
|
+
const approval = () => native ?? approvalFor(connection.id);
|
|
997
1106
|
const attached = [];
|
|
998
1107
|
for (const reference of request.references) {
|
|
999
1108
|
const value = reference.value;
|
package/dist/host-controls.d.ts
CHANGED
|
@@ -5,11 +5,13 @@ export interface ChatSelection {
|
|
|
5
5
|
model: string;
|
|
6
6
|
effort: string;
|
|
7
7
|
}
|
|
8
|
+
/** `efforts`: the reasoning efforts a model takes, as the host read them from the harness (absent from older hosts). */
|
|
8
9
|
export interface ChatChoice {
|
|
9
10
|
id: string;
|
|
10
11
|
name: string;
|
|
11
12
|
description?: string;
|
|
12
13
|
autoStart?: boolean;
|
|
14
|
+
efforts?: string[];
|
|
13
15
|
}
|
|
14
16
|
export interface ManagedChatSession {
|
|
15
17
|
id: string;
|
|
@@ -49,6 +51,9 @@ export interface HostState {
|
|
|
49
51
|
actualModel?: string | null;
|
|
50
52
|
configurable: boolean;
|
|
51
53
|
setup?: ChatSetupState;
|
|
54
|
+
/** The approval mode of a conversation no one has picked one for. `autoApprove` takes effect only
|
|
55
|
+
* where the runtime can answer approvals through Chat; absent, every conversation asks. */
|
|
56
|
+
defaultPermission?: 'ask' | 'autoApprove';
|
|
52
57
|
}
|
|
53
58
|
export interface HostSelectionResult extends HostState {
|
|
54
59
|
connection: Record<string, string>;
|
|
@@ -75,7 +80,7 @@ export declare class ChatHostControls {
|
|
|
75
80
|
private update;
|
|
76
81
|
private request;
|
|
77
82
|
}
|
|
78
|
-
/**
|
|
83
|
+
/** A picked approval mode, never inferred from connection privileges (those only keep a host default at ask). */
|
|
79
84
|
export declare function turnPermission(value: string | undefined): 'ask' | 'autoApprove';
|
|
80
85
|
export declare function harnessDisplayName(harness: string | null | undefined): string;
|
|
81
86
|
export declare function runtimeIdentity(harness: string | null | undefined, model: string | null | undefined): {
|
package/dist/host-controls.js
CHANGED
|
@@ -53,7 +53,7 @@ export class ChatHostControls {
|
|
|
53
53
|
return value;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
56
|
+
/** A picked approval mode, never inferred from connection privileges (those only keep a host default at ask). */
|
|
57
57
|
export function turnPermission(value) {
|
|
58
58
|
if (!value || value === 'default' || value === 'ask')
|
|
59
59
|
return 'ask';
|
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.37",
|
|
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",
|