@volter/supercode-frontend-vscode 0.1.39 → 0.1.41
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 +4 -2
- package/dist/extension.js +95 -52
- package/dist/host-controls.d.ts +5 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -152,8 +152,10 @@ owns no processes.
|
|
|
152
152
|
The permission group offers **Ask for approval** and, on an answerable interactive attachment,
|
|
153
153
|
**Auto approve**. Policies are scoped by conversation and read throughout the turn: switching to
|
|
154
154
|
**Auto approve** while a turn runs answers the approvals already waiting and every later one.
|
|
155
|
-
A lifecycle host may
|
|
156
|
-
|
|
155
|
+
A lifecycle host may start the conversations it creates in **Auto approve**: it records
|
|
156
|
+
`defaultPermission: "autoApprove"` on each such conversation and declares `newChatPermission` for the new
|
|
157
|
+
chat's picker (Model Editor does). A conversation without its own recorded default asks, so one that existed
|
|
158
|
+
before a host began doing this keeps its behaviour. It applies to Claude Code and Codex where the runtime answers approvals
|
|
157
159
|
through Chat, every other harness keeps its own prompts, and a person's explicit pick always
|
|
158
160
|
wins. Only a pick that differs from the default (or replaces an earlier pick) is saved, so the
|
|
159
161
|
default is never stored as a choice (a pick equal to the default follows the default if it later
|
package/dist/extension.js
CHANGED
|
@@ -13,6 +13,8 @@ import { acceptedDecisions, opaqueText, projectEvent, projectPendingRequests, pr
|
|
|
13
13
|
const SESSION_SCHEME = 'supercode';
|
|
14
14
|
/** The harnesses whose model and reasoning effort a new chat can set (the host passes them to the CLI). */
|
|
15
15
|
const CONFIGURABLE_HARNESSES = ['claude-code', 'codex'];
|
|
16
|
+
/** How long an automatic approval may go unanswered before its card is shown for the person to answer. */
|
|
17
|
+
const AUTO_ANSWER_SETTLE_MS = 15_000;
|
|
16
18
|
const PARTICIPANT_ID = 'supercode.chat';
|
|
17
19
|
const DEFAULT_PARTICIPANT_ID = 'supercode.chat.default';
|
|
18
20
|
/** `package.json` gates the `isDefault` participant on this key. */
|
|
@@ -372,18 +374,22 @@ export async function activate(context) {
|
|
|
372
374
|
// The approval sweeps of each conversation's running turns, so a picker change reaches a turn already running.
|
|
373
375
|
const runningTurns = new Map();
|
|
374
376
|
const answeredRequests = new Map();
|
|
375
|
-
// The
|
|
376
|
-
|
|
377
|
+
// The mode a NEW chat's picker shows (`HostState.newChatPermission`; Model Editor declares Auto approve). It is
|
|
378
|
+
// never applied to a conversation that exists: that reads its own recorded default below.
|
|
379
|
+
const newChatDefault = () => controls?.state.newChatPermission === 'autoApprove' ? 'autoApprove' : 'ask';
|
|
377
380
|
const canAnswer = (connection) => Boolean(connection.session.descriptor?.actions.respond && connection.session.permissions.has('approve'));
|
|
378
|
-
// The
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
381
|
+
// The default of one conversation: the one the host recorded for THAT conversation when it created it
|
|
382
|
+
// (`ManagedChatSession.defaultPermission`), so a conversation that existed before a host began starting chats
|
|
383
|
+
// in Auto approve keeps asking. Auto approve only for a conversation the host manages and only where this Chat
|
|
384
|
+
// can answer the runtime's approvals, read from its live connection, or, while it is not connected, from its
|
|
385
|
+
// harness (hosted Claude Code and Codex answer through Chat). A runtime attached by address and every other
|
|
386
|
+
// harness keep their own prompts.
|
|
382
387
|
const defaultFor = (id) => {
|
|
383
|
-
|
|
388
|
+
const session = controls?.state.sessions.find(entry => entry.id === id);
|
|
389
|
+
if (session?.defaultPermission !== 'autoApprove' || attachedIds.has(id))
|
|
384
390
|
return 'ask';
|
|
385
391
|
const connection = frontends.get(id);
|
|
386
|
-
const harness =
|
|
392
|
+
const harness = session.selection.harness;
|
|
387
393
|
// Only the harnesses whose approvals this default was decided for; a runtime that answers through another
|
|
388
394
|
// protocol (an ACP agent) keeps asking until a host names it.
|
|
389
395
|
return ['claude-code', 'codex'].includes(harness) && (!connection || canAnswer(connection)) ? 'autoApprove' : 'ask';
|
|
@@ -429,6 +435,16 @@ export async function activate(context) {
|
|
|
429
435
|
// reports them, and never taken for the conversation the host manages.
|
|
430
436
|
const attachedIds = new Set();
|
|
431
437
|
const managed = () => frontends.all.find((connection) => !attachedIds.has(connection.id));
|
|
438
|
+
// The host runs one conversation at a time, so a running or waiting turn refuses a new or another chat. This is
|
|
439
|
+
// the sentence that refusal says to the person; each caller picks the one place it is shown (see its uses).
|
|
440
|
+
const busyRefusal = (active, action) => {
|
|
441
|
+
if (!active)
|
|
442
|
+
return;
|
|
443
|
+
if (active.session.pendingRequests.size)
|
|
444
|
+
return `The current chat is waiting for your answer. Answer it or cancel the turn before ${action}.`;
|
|
445
|
+
if (active.session.descriptor?.turn_state === 'busy')
|
|
446
|
+
return `A turn is still running in the current chat. Let it finish or cancel it before ${action}.`;
|
|
447
|
+
};
|
|
432
448
|
// The attached runtime the person is looking at: the Chat names its harness, not the host's.
|
|
433
449
|
let focusedAttached;
|
|
434
450
|
let approvalAvailable = false;
|
|
@@ -491,7 +507,7 @@ export async function activate(context) {
|
|
|
491
507
|
}
|
|
492
508
|
const currentOptions = (resource) => {
|
|
493
509
|
const id = resource ? boundId(resource) : controls?.state.activeSession;
|
|
494
|
-
return { permissions: id ? approvalFor(id) : permission ??
|
|
510
|
+
return { permissions: id ? approvalFor(id) : permission ?? newChatDefault() };
|
|
495
511
|
};
|
|
496
512
|
const savedConnections = new Map();
|
|
497
513
|
const saveManagedHistory = async (connection) => {
|
|
@@ -670,9 +686,11 @@ export async function activate(context) {
|
|
|
670
686
|
return existing.pending;
|
|
671
687
|
const pending = applying.then(async () => {
|
|
672
688
|
const active = managed();
|
|
673
|
-
if (active && active.id !== id && (active.session.descriptor?.turn_state === 'busy' || active.session.pendingRequests.size))
|
|
674
|
-
throw new Error('Finish or cancel the active turn before opening another conversation.');
|
|
675
689
|
if (active && active.id !== id) {
|
|
690
|
+
// Thrown as is: the workbench shows a focus command's error, and a send writes it into the chat.
|
|
691
|
+
const refusal = busyRefusal(active, 'opening another conversation');
|
|
692
|
+
if (refusal)
|
|
693
|
+
throw new Error(refusal);
|
|
676
694
|
// Capture the exact saved transcript while its runtime still belongs
|
|
677
695
|
// to the host. Subsequent background content reads need no activation.
|
|
678
696
|
await saveManagedHistory(active);
|
|
@@ -710,8 +728,12 @@ export async function activate(context) {
|
|
|
710
728
|
if (!controls)
|
|
711
729
|
throw new Error('This host does not expose harness or model configuration.');
|
|
712
730
|
const active = managed();
|
|
713
|
-
|
|
714
|
-
|
|
731
|
+
// A send from a blank chat lands here, and the workbench does not show its error: say it, then refuse.
|
|
732
|
+
const refusal = busyRefusal(active, 'starting a new chat');
|
|
733
|
+
if (refusal) {
|
|
734
|
+
void vscode.window.showWarningMessage(refusal);
|
|
735
|
+
throw new Error(refusal);
|
|
736
|
+
}
|
|
715
737
|
if (active)
|
|
716
738
|
await saveManagedHistory(active);
|
|
717
739
|
const state = await controls.select(selection);
|
|
@@ -722,9 +744,13 @@ export async function activate(context) {
|
|
|
722
744
|
const openNewChat = async () => {
|
|
723
745
|
if (!controls || !granted.has('chatSessionsProvider'))
|
|
724
746
|
throw new Error('This host does not expose native conversation creation.');
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
747
|
+
// Its callers (the new-chat pickers) swallow the cancellation, so the warning is the one message shown. An error
|
|
748
|
+
// would reach the person as a bare "Canceled" or not at all through the contributed-command boundary.
|
|
749
|
+
const refusal = busyRefusal(managed(), 'starting a new chat');
|
|
750
|
+
if (refusal) {
|
|
751
|
+
void vscode.window.showWarningMessage(refusal);
|
|
752
|
+
throw new vscode.CancellationError();
|
|
753
|
+
}
|
|
728
754
|
// Code-OSS materializes this untitled session on first send, awaiting our
|
|
729
755
|
// newChatSessionItemHandler before accepting/clearing the draft. Never boot
|
|
730
756
|
// beside the old widget and reveal another resource after it accepts input.
|
|
@@ -785,20 +811,23 @@ export async function activate(context) {
|
|
|
785
811
|
const chooseNewChat = async (pick) => {
|
|
786
812
|
if (pendingDraft?.choosing)
|
|
787
813
|
throw new Error('Finish choosing an agent before starting another chat.');
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
const draft = await openNewChat();
|
|
791
|
-
const picking = (async () => {
|
|
792
|
-
const choice = await pick();
|
|
793
|
-
if (activeDraft() !== draft)
|
|
794
|
-
throw new vscode.CancellationError();
|
|
795
|
-
draft.choice = choice;
|
|
796
|
-
controlsChanged.fire();
|
|
797
|
-
if (choice.kind !== 'agent')
|
|
798
|
-
throw new vscode.CancellationError();
|
|
799
|
-
})();
|
|
800
|
-
draft.choosing = picking;
|
|
814
|
+
let draft;
|
|
815
|
+
let picking;
|
|
801
816
|
try {
|
|
817
|
+
// Open before the picker: a send can beat its result across the extension-
|
|
818
|
+
// host boundary. Materialization awaits this exact draft's choice too.
|
|
819
|
+
// Inside the try: a busy refusal is a cancellation already explained.
|
|
820
|
+
const opened = draft = await openNewChat();
|
|
821
|
+
picking = (async () => {
|
|
822
|
+
const choice = await pick();
|
|
823
|
+
if (activeDraft() !== opened)
|
|
824
|
+
throw new vscode.CancellationError();
|
|
825
|
+
opened.choice = choice;
|
|
826
|
+
controlsChanged.fire();
|
|
827
|
+
if (choice.kind !== 'agent')
|
|
828
|
+
throw new vscode.CancellationError();
|
|
829
|
+
})();
|
|
830
|
+
opened.choosing = picking;
|
|
802
831
|
await picking;
|
|
803
832
|
}
|
|
804
833
|
catch (error) {
|
|
@@ -806,9 +835,9 @@ export async function activate(context) {
|
|
|
806
835
|
throw error;
|
|
807
836
|
}
|
|
808
837
|
finally {
|
|
809
|
-
if (draft.choosing === picking)
|
|
838
|
+
if (draft && draft.choosing === picking)
|
|
810
839
|
draft.choosing = undefined;
|
|
811
|
-
if (draft
|
|
840
|
+
if (draft?.choice?.kind === 'signIn') {
|
|
812
841
|
void refreshSetup().catch(error => log.warn(`Could not refresh sign-in: ${String(error)}`)).finally(pollSetup);
|
|
813
842
|
}
|
|
814
843
|
}
|
|
@@ -873,7 +902,7 @@ export async function activate(context) {
|
|
|
873
902
|
const connection = await pending;
|
|
874
903
|
// The picker shows the host's default until someone changes it; only a change is the person's choice.
|
|
875
904
|
const picked = turnPermission(inputState.groups.find(group => group.id === 'permissions')?.selected?.id);
|
|
876
|
-
choosePermission(connection.id, picked, permission !== undefined ? null :
|
|
905
|
+
choosePermission(connection.id, picked, permission !== undefined ? null : newChatDefault());
|
|
877
906
|
permission = undefined;
|
|
878
907
|
if (pendingDraft === draft) {
|
|
879
908
|
pendingDraft = undefined;
|
|
@@ -961,17 +990,20 @@ export async function activate(context) {
|
|
|
961
990
|
return { kind: 'agent', selection: { harness: harness.id, model: '', effort: '' } };
|
|
962
991
|
});
|
|
963
992
|
}));
|
|
964
|
-
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.configureModel', async (resource,
|
|
993
|
+
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.configureModel', async (resource, _sessionId) => {
|
|
965
994
|
if (!controls || focusedAttached)
|
|
966
995
|
throw new Error('Model settings require a host-managed conversation.');
|
|
967
996
|
await controls.refresh();
|
|
968
|
-
// The native session-options picker prepends its
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
997
|
+
// The native session-options picker prepends its widget's resource to command arguments, so the resource is the
|
|
998
|
+
// conversation the person clicked from. `sessionId` is not used: the workbench keeps one option-group list per
|
|
999
|
+
// session type, last written by whichever input state updated, so any chat's dropdown can carry another
|
|
1000
|
+
// conversation's id. A resource with no binding (a blank draft) is no conversation; only the command palette,
|
|
1001
|
+
// which passes no resource, falls back to the active one. Either way the choice opens a new chat.
|
|
1002
|
+
const bound = resource ? boundId(resource) : controls.state.activeSession ?? undefined;
|
|
972
1003
|
// The conversation it was opened from sets the agent and the current values; with none open (a fresh project,
|
|
973
|
-
// the command palette) the person picks the agent first.
|
|
974
|
-
|
|
1004
|
+
// the command palette) the person picks the agent first. A running turn refuses the new chat before any picker
|
|
1005
|
+
// opens, and says so (`openNewChat`).
|
|
1006
|
+
const selected = controls.state.sessions.find(session => session.id === bound);
|
|
975
1007
|
// An agent that still needs sign-in is set up from the agent picker first, as there.
|
|
976
1008
|
const needsSignIn = new Set((controls.state.setup?.actions ?? []).filter(action => action.kind === 'login').map(action => action.harness));
|
|
977
1009
|
const configurable = controls.state.harnesses.filter(harness => CONFIGURABLE_HARNESSES.includes(harness.id) && !needsSignIn.has(harness.id));
|
|
@@ -1034,25 +1066,36 @@ export async function activate(context) {
|
|
|
1034
1066
|
catch {
|
|
1035
1067
|
log.info(text.trim());
|
|
1036
1068
|
} };
|
|
1069
|
+
const label = opaqueText(named ?? `request #${pending.id}`, 200);
|
|
1070
|
+
// A card this watch hid is shown when its automatic answer fails or does not settle: a typed reply cannot
|
|
1071
|
+
// reach a running turn, its buttons can. With the response closed, the person is told an approval waits.
|
|
1072
|
+
const showCard = () => {
|
|
1073
|
+
if (!unshown.delete(pending.id) || !connection.session.pendingRequests.has(pending.id))
|
|
1074
|
+
return;
|
|
1075
|
+
try {
|
|
1076
|
+
writeProjected(stream, projectPendingRequests([pending], {
|
|
1077
|
+
connectionId: connection.id,
|
|
1078
|
+
canRespond: connection.session.descriptor?.actions.respond !== false,
|
|
1079
|
+
}), granted, { toolInputs: new Map(), toolNames: new Map() });
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
void vscode.window.showWarningMessage(`${connection.label}: ${label} is waiting for approval. Open its chat to answer it.`);
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
const unsettled = setTimeout(() => {
|
|
1086
|
+
note(`\nAuto approve has had no answer for ${label} for ${AUTO_ANSWER_SETTLE_MS / 1000} s. Use the approval buttons to answer.\n`);
|
|
1087
|
+
showCard();
|
|
1088
|
+
}, AUTO_ANSWER_SETTLE_MS);
|
|
1037
1089
|
void connection.session.respond({ kind: 'approval', request_id: pending.id, decision: 'allow' }).then(() => {
|
|
1090
|
+
clearTimeout(unsettled);
|
|
1038
1091
|
// A card that was never shown needs no note: its resolution line says it was allowed.
|
|
1039
1092
|
if (unshown.has(pending.id))
|
|
1040
1093
|
return;
|
|
1041
|
-
note(`\n_auto-approved: ${
|
|
1094
|
+
note(`\n_auto-approved: ${label}_\n`);
|
|
1042
1095
|
}, error => {
|
|
1096
|
+
clearTimeout(unsettled);
|
|
1043
1097
|
note(`\nApproval failed: ${String(error.message || error)}. Use the approval buttons to answer.\n`);
|
|
1044
|
-
|
|
1045
|
-
if (unshown.delete(pending.id) && connection.session.pendingRequests.has(pending.id)) {
|
|
1046
|
-
try {
|
|
1047
|
-
writeProjected(stream, projectPendingRequests([pending], {
|
|
1048
|
-
connectionId: connection.id,
|
|
1049
|
-
canRespond: connection.session.descriptor?.actions.respond !== false,
|
|
1050
|
-
}), granted, { toolInputs: new Map(), toolNames: new Map() });
|
|
1051
|
-
}
|
|
1052
|
-
catch {
|
|
1053
|
-
log.info(`Approval card for request #${pending.id} could not be shown: the response has closed`);
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1098
|
+
showCard();
|
|
1056
1099
|
});
|
|
1057
1100
|
}
|
|
1058
1101
|
};
|
package/dist/host-controls.d.ts
CHANGED
|
@@ -13,12 +13,14 @@ export interface ChatChoice {
|
|
|
13
13
|
autoStart?: boolean;
|
|
14
14
|
efforts?: string[];
|
|
15
15
|
}
|
|
16
|
+
/** `defaultPermission`: the approval mode the host started this conversation in when no one picked one; absent, it asks. */
|
|
16
17
|
export interface ManagedChatSession {
|
|
17
18
|
id: string;
|
|
18
19
|
selection: ChatSelection;
|
|
19
20
|
identity: string | null;
|
|
20
21
|
title: string;
|
|
21
22
|
created: number;
|
|
23
|
+
defaultPermission?: 'ask' | 'autoApprove';
|
|
22
24
|
}
|
|
23
25
|
/** Host-owned operations. The host revalidates commands before native terminal execution. */
|
|
24
26
|
export interface ChatSetupAction {
|
|
@@ -51,9 +53,9 @@ export interface HostState {
|
|
|
51
53
|
actualModel?: string | null;
|
|
52
54
|
configurable: boolean;
|
|
53
55
|
setup?: ChatSetupState;
|
|
54
|
-
/** The
|
|
55
|
-
* where the runtime can answer approvals
|
|
56
|
-
|
|
56
|
+
/** The mode a new chat's picker shows. A conversation's own default is its `ManagedChatSession.defaultPermission`;
|
|
57
|
+
* this is never applied to one that exists. `autoApprove` takes effect only where the runtime can answer approvals. */
|
|
58
|
+
newChatPermission?: 'ask' | 'autoApprove';
|
|
57
59
|
}
|
|
58
60
|
export interface HostSelectionResult extends HostState {
|
|
59
61
|
connection: Record<string, string>;
|
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.41",
|
|
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",
|