@volter/supercode-frontend-vscode 0.1.35 → 0.1.36
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 +11 -2
- package/dist/extension.js +75 -22
- package/dist/host-controls.d.ts +4 -1
- package/dist/host-controls.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,8 +142,17 @@ the same cached conversation. Managed resources then notify
|
|
|
142
142
|
owns no processes.
|
|
143
143
|
|
|
144
144
|
The permission group offers **Ask for approval** and, on an answerable interactive attachment,
|
|
145
|
-
**Auto approve**. Policies are scoped by conversation and
|
|
146
|
-
|
|
145
|
+
**Auto approve**. Policies are scoped by conversation and read throughout the turn: switching to
|
|
146
|
+
**Auto approve** while a turn runs answers the approvals already waiting and every later one.
|
|
147
|
+
A lifecycle host may declare `defaultPermission: "autoApprove"` for conversations no one has
|
|
148
|
+
picked a mode for (Model Editor does); it applies to Claude Code and Codex where the runtime answers approvals
|
|
149
|
+
through Chat, every other harness keeps its own prompts, and a person's explicit pick always
|
|
150
|
+
wins. Only a pick that differs from the default (or replaces an earlier pick) is saved, so the
|
|
151
|
+
default is never stored as a choice (a pick equal to the default follows the default if it later
|
|
152
|
+
changes), and an Auto approve is never stored for a conversation that cannot answer. Conversations opened before 0.1.36 saved their picker's
|
|
153
|
+
**Ask** when opened and keep it. Auto approve approves the requests the harness would ask you about, including requests to run
|
|
154
|
+
outside its sandbox (Codex's approval requests are its sandbox escalations); the harness's deny
|
|
155
|
+
rules still apply. Each request it answers is noted in the turn ("auto-approved: …"). Unsupported modes are refused. Unanswerable
|
|
147
156
|
harnesses show **Harness permissions**. Previously answered requests cannot affect another
|
|
148
157
|
connection. Native command-button fallback limitations are documented below.
|
|
149
158
|
|
package/dist/extension.js
CHANGED
|
@@ -356,11 +356,45 @@ export async function activate(context) {
|
|
|
356
356
|
}), vscode.commands.registerCommand(CHATGPT_SIGN_IN_STATUS, (operationId) => operationId === undefined || operationId === chatGPTSignInOperation?.id ? signInSnapshot() : null));
|
|
357
357
|
const controlsChanged = new vscode.EventEmitter();
|
|
358
358
|
context.subscriptions.push(controlsChanged);
|
|
359
|
-
|
|
359
|
+
// A person's explicit picker choice: per conversation once it is bound, and for the new chat before then.
|
|
360
|
+
let permission;
|
|
360
361
|
const permissions = new Map(context.workspaceState.get('supercode.sessionPermissions', []));
|
|
361
362
|
const savePermission = (id, value) => {
|
|
362
363
|
permissions.set(id, value);
|
|
363
364
|
void Promise.resolve(context.workspaceState.update('supercode.sessionPermissions', [...permissions])).catch(error => log.error(`Could not persist conversation permissions: ${String(error)}`));
|
|
365
|
+
for (const sweep of runningTurns.get(id) ?? [])
|
|
366
|
+
sweep();
|
|
367
|
+
};
|
|
368
|
+
// The approval sweeps of each conversation's running turns, so a picker change reaches a turn already running.
|
|
369
|
+
const runningTurns = new Map();
|
|
370
|
+
// The host's default for a conversation no one has chosen for (Model Editor declares Auto approve).
|
|
371
|
+
const hostDefault = () => controls?.state.defaultPermission === 'autoApprove' ? 'autoApprove' : 'ask';
|
|
372
|
+
const canAnswer = (connection) => Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve'));
|
|
373
|
+
// The host's default for one conversation: Auto approve only for a conversation the host manages and only
|
|
374
|
+
// where this Chat can answer the runtime's approvals, read from its live connection, or, while it is not
|
|
375
|
+
// connected, from the harness the host records for it (hosted Claude Code and Codex answer through Chat).
|
|
376
|
+
// A runtime attached by address and every other harness keep their own prompts.
|
|
377
|
+
const defaultFor = (id) => {
|
|
378
|
+
if (hostDefault() !== 'autoApprove' || attachedIds.has(id))
|
|
379
|
+
return 'ask';
|
|
380
|
+
const connection = frontends.get(id);
|
|
381
|
+
const harness = controls?.state.sessions.find(session => session.id === id)?.selection.harness ?? '';
|
|
382
|
+
// Only the harnesses whose approvals this default was decided for; a runtime that answers through another
|
|
383
|
+
// protocol (an ACP agent) keeps asking until a host names it.
|
|
384
|
+
return ['claude-code', 'codex'].includes(harness) && (!connection || canAnswer(connection)) ? 'autoApprove' : 'ask';
|
|
385
|
+
};
|
|
386
|
+
// What governs a conversation's approvals: the person's choice, else the host's default.
|
|
387
|
+
const approvalFor = (id) => permissions.get(id) ?? defaultFor(id);
|
|
388
|
+
// A picker value becomes the person's choice only when it is one: it differs from the default the picker showed
|
|
389
|
+
// (`shown`; null when the person already picked it), or replaces an earlier choice. Echoes of the displayed default
|
|
390
|
+
// (the picker re-applied when a conversation opens) are not saved, and neither is an Auto approve the conversation
|
|
391
|
+
// cannot answer: the picker offers it only where it can, so there it is a default shown before the harness was known.
|
|
392
|
+
const choosePermission = (id, value, shown = defaultFor(id)) => {
|
|
393
|
+
const connection = frontends.get(id);
|
|
394
|
+
if (value === 'autoApprove' && connection && !canAnswer(connection))
|
|
395
|
+
return;
|
|
396
|
+
if (permissions.has(id) || value !== shown)
|
|
397
|
+
savePermission(id, value);
|
|
364
398
|
};
|
|
365
399
|
const resourceBindings = new Map(context.workspaceState.get('supercode.sessionBindings', []));
|
|
366
400
|
const boundId = (resource) => resolveSessionBinding(resource, controls?.state.sessions ?? [], resourceBindings);
|
|
@@ -452,7 +486,7 @@ export async function activate(context) {
|
|
|
452
486
|
}
|
|
453
487
|
const currentOptions = (resource) => {
|
|
454
488
|
const id = resource ? boundId(resource) : controls?.state.activeSession;
|
|
455
|
-
return { permissions: id ?
|
|
489
|
+
return { permissions: id ? approvalFor(id) : permission ?? hostDefault() };
|
|
456
490
|
};
|
|
457
491
|
const savedConnections = new Map();
|
|
458
492
|
const saveManagedHistory = async (connection) => {
|
|
@@ -749,8 +783,8 @@ export async function activate(context) {
|
|
|
749
783
|
const active = resource ? (id ? frontends.get(id) : undefined) : managed();
|
|
750
784
|
const canApprove = active?.session.descriptor?.actions.respond && active.session.permissions.has('approve');
|
|
751
785
|
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 ===
|
|
786
|
+
...((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.' }] : [])];
|
|
787
|
+
const groups = [{ id: 'permissions', name: 'Permissions', kind: 'permissions', items: permissionItems, selected: permissionItems.find(i => i.id === currentOptions(resource).permissions) }];
|
|
754
788
|
const selected = controls?.state.sessions.find(session => session.id === id);
|
|
755
789
|
if (selected && ['claude-code', 'codex'].includes(selected.selection.harness)) {
|
|
756
790
|
// Native session-option commands remain reachable for nondelegating providers,
|
|
@@ -768,7 +802,7 @@ export async function activate(context) {
|
|
|
768
802
|
if (controls && !id)
|
|
769
803
|
throw new Error('This conversation has no session binding.');
|
|
770
804
|
if (id)
|
|
771
|
-
|
|
805
|
+
choosePermission(id, turnPermission(value));
|
|
772
806
|
else
|
|
773
807
|
permission = turnPermission(value);
|
|
774
808
|
},
|
|
@@ -794,7 +828,10 @@ export async function activate(context) {
|
|
|
794
828
|
const pending = applying.then(() => applySelection(selection));
|
|
795
829
|
applying = pending.then(() => undefined, () => undefined);
|
|
796
830
|
const connection = await pending;
|
|
797
|
-
|
|
831
|
+
// The picker shows the host's default until someone changes it; only a change is the person's choice.
|
|
832
|
+
const picked = turnPermission(inputState.groups.find(group => group.id === 'permissions')?.selected?.id);
|
|
833
|
+
choosePermission(connection.id, picked, permission !== undefined ? null : hostDefault());
|
|
834
|
+
permission = undefined;
|
|
798
835
|
if (pendingDraft === draft) {
|
|
799
836
|
pendingDraft = undefined;
|
|
800
837
|
controlsChanged.fire();
|
|
@@ -825,7 +862,7 @@ export async function activate(context) {
|
|
|
825
862
|
if (supplied?.permissions) {
|
|
826
863
|
const id = controls?.state.activeSession;
|
|
827
864
|
if (id)
|
|
828
|
-
|
|
865
|
+
choosePermission(id, turnPermission(supplied.permissions));
|
|
829
866
|
else
|
|
830
867
|
permission = turnPermission(supplied.permissions);
|
|
831
868
|
controlsChanged.fire();
|
|
@@ -909,7 +946,7 @@ export async function activate(context) {
|
|
|
909
946
|
}));
|
|
910
947
|
context.subscriptions.push({ dispose: () => frontends.dispose() });
|
|
911
948
|
const runTurnInto = async (connection, prompt, stream, token, approval) => {
|
|
912
|
-
if (approval === 'autoApprove' && (
|
|
949
|
+
if (approval() === 'autoApprove' && !canAnswer(connection)) {
|
|
913
950
|
throw new Error('This harness does not support automatic approval through Chat. Select Harness permissions.');
|
|
914
951
|
}
|
|
915
952
|
const answered = new Set();
|
|
@@ -925,25 +962,40 @@ export async function activate(context) {
|
|
|
925
962
|
// this view reports.
|
|
926
963
|
});
|
|
927
964
|
});
|
|
965
|
+
// Read at every event and on every picker change, so switching to Auto approve mid-turn
|
|
966
|
+
// answers the approvals already waiting and the ones after them.
|
|
967
|
+
const sweep = () => {
|
|
968
|
+
if (approval() !== 'autoApprove' || token.isCancellationRequested || !canAnswer(connection))
|
|
969
|
+
return;
|
|
970
|
+
for (const pending of connection.session.pendingRequests.values()) {
|
|
971
|
+
if (pending.kind !== 'approval' || answered.has(pending.id))
|
|
972
|
+
continue;
|
|
973
|
+
const decisions = acceptedDecisions(pending);
|
|
974
|
+
if (decisions && !decisions.includes('allow'))
|
|
975
|
+
continue;
|
|
976
|
+
answered.add(pending.id);
|
|
977
|
+
// Its card is already in the stream (the event that raised it was written first), so say it was answered.
|
|
978
|
+
const payload = pending.payload && typeof pending.payload === 'object' && !Array.isArray(pending.payload) ? pending.payload : {};
|
|
979
|
+
const named = [payload.title, payload.tool, payload.tool_name].find((value) => typeof value === 'string');
|
|
980
|
+
void connection.session.respond({ kind: 'approval', request_id: pending.id, decision: 'allow' }).then(() => {
|
|
981
|
+
stream.markdown(`\n_auto-approved: ${opaqueText(named ?? `request #${pending.id}`, 200)}_\n`);
|
|
982
|
+
}, error => {
|
|
983
|
+
stream.markdown(`\nApproval failed: ${String(error.message || error)}. Use the approval buttons to answer.\n`);
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
const sweeps = runningTurns.get(connection.id) ?? new Set();
|
|
988
|
+
runningTurns.set(connection.id, sweeps.add(sweep));
|
|
928
989
|
try {
|
|
929
990
|
await connection.session.runTurn(prompt, (event) => {
|
|
930
991
|
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
|
-
}
|
|
992
|
+
sweep();
|
|
944
993
|
});
|
|
945
994
|
}
|
|
946
995
|
finally {
|
|
996
|
+
sweeps.delete(sweep);
|
|
997
|
+
if (!sweeps.size && runningTurns.get(connection.id) === sweeps)
|
|
998
|
+
runningTurns.delete(connection.id);
|
|
947
999
|
cancellation.dispose();
|
|
948
1000
|
if (controls && !attachedIds.has(connection.id)) {
|
|
949
1001
|
if (connection.session.descriptor?.session_id)
|
|
@@ -993,7 +1045,8 @@ export async function activate(context) {
|
|
|
993
1045
|
stream.markdown('_nothing to submit_');
|
|
994
1046
|
return {};
|
|
995
1047
|
}
|
|
996
|
-
const
|
|
1048
|
+
const native = controls ? undefined : turnPermission(request.permissionLevel);
|
|
1049
|
+
const approval = () => native ?? approvalFor(connection.id);
|
|
997
1050
|
const attached = [];
|
|
998
1051
|
for (const reference of request.references) {
|
|
999
1052
|
const value = reference.value;
|
package/dist/host-controls.d.ts
CHANGED
|
@@ -49,6 +49,9 @@ export interface HostState {
|
|
|
49
49
|
actualModel?: string | null;
|
|
50
50
|
configurable: boolean;
|
|
51
51
|
setup?: ChatSetupState;
|
|
52
|
+
/** The approval mode of a conversation no one has picked one for. `autoApprove` takes effect only
|
|
53
|
+
* where the runtime can answer approvals through Chat; absent, every conversation asks. */
|
|
54
|
+
defaultPermission?: 'ask' | 'autoApprove';
|
|
52
55
|
}
|
|
53
56
|
export interface HostSelectionResult extends HostState {
|
|
54
57
|
connection: Record<string, string>;
|
|
@@ -75,7 +78,7 @@ export declare class ChatHostControls {
|
|
|
75
78
|
private update;
|
|
76
79
|
private request;
|
|
77
80
|
}
|
|
78
|
-
/**
|
|
81
|
+
/** A picked approval mode, never inferred from connection privileges (those only keep a host default at ask). */
|
|
79
82
|
export declare function turnPermission(value: string | undefined): 'ask' | 'autoApprove';
|
|
80
83
|
export declare function harnessDisplayName(harness: string | null | undefined): string;
|
|
81
84
|
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.36",
|
|
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",
|