@volter/supercode-frontend-vscode 0.1.40 → 0.1.42
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 +9 -2
- package/dist/extension.js +89 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -155,7 +155,8 @@ The permission group offers **Ask for approval** and, on an answerable interacti
|
|
|
155
155
|
A lifecycle host may start the conversations it creates in **Auto approve**: it records
|
|
156
156
|
`defaultPermission: "autoApprove"` on each such conversation and declares `newChatPermission` for the new
|
|
157
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.
|
|
158
|
+
before a host began doing this keeps its behaviour. A new blank chat does not inherit the previous conversation's permission choice.
|
|
159
|
+
It applies to Claude Code and Codex where the runtime answers approvals
|
|
159
160
|
through Chat, every other harness keeps its own prompts, and a person's explicit pick always
|
|
160
161
|
wins. Only a pick that differs from the default (or replaces an earlier pick) is saved, so the
|
|
161
162
|
default is never stored as a choice (a pick equal to the default follows the default if it later
|
|
@@ -403,7 +404,13 @@ the extension manifest beside those bytes with no build step of its own.
|
|
|
403
404
|
Conversation creation uses `ChatSessionItemController.newChatSessionItemHandler`.
|
|
404
405
|
Permissions use `getChatSessionInputState` and its change/disposal events, with
|
|
405
406
|
per-conversation preferences persisted in workspace state. New untitled chats
|
|
406
|
-
start with
|
|
407
|
+
start with the host's new-chat default and offer modes for the selected draft's harness,
|
|
408
|
+
independently of the previous runtime. The paired native host exposes actual
|
|
409
|
+
`initialSessionOptions` separately from its type-wide previous input state, so an
|
|
410
|
+
explicit draft choice survives materialization. Draft input groups refresh after
|
|
411
|
+
agent selection without overwriting the person's choice. Stock hosts without
|
|
412
|
+
that optional context retain their previous-state contract; old untitled bindings
|
|
413
|
+
are read only for backwards compatibility.
|
|
407
414
|
Restored response cancellation interrupts the runtime and waits for acknowledgement.
|
|
408
415
|
Session-list updates consistently prioritize pending input over busy status.
|
|
409
416
|
|
package/dist/extension.js
CHANGED
|
@@ -435,6 +435,16 @@ export async function activate(context) {
|
|
|
435
435
|
// reports them, and never taken for the conversation the host manages.
|
|
436
436
|
const attachedIds = new Set();
|
|
437
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
|
+
};
|
|
438
448
|
// The attached runtime the person is looking at: the Chat names its harness, not the host's.
|
|
439
449
|
let focusedAttached;
|
|
440
450
|
let approvalAvailable = false;
|
|
@@ -496,7 +506,7 @@ export async function activate(context) {
|
|
|
496
506
|
}));
|
|
497
507
|
}
|
|
498
508
|
const currentOptions = (resource) => {
|
|
499
|
-
const id = resource ? boundId(resource) : controls?.state.activeSession;
|
|
509
|
+
const id = resource ? boundId(resource) : activeDraft() ? undefined : controls?.state.activeSession;
|
|
500
510
|
return { permissions: id ? approvalFor(id) : permission ?? newChatDefault() };
|
|
501
511
|
};
|
|
502
512
|
const savedConnections = new Map();
|
|
@@ -676,9 +686,11 @@ export async function activate(context) {
|
|
|
676
686
|
return existing.pending;
|
|
677
687
|
const pending = applying.then(async () => {
|
|
678
688
|
const active = managed();
|
|
679
|
-
if (active && active.id !== id && (active.session.descriptor?.turn_state === 'busy' || active.session.pendingRequests.size))
|
|
680
|
-
throw new Error('Finish or cancel the active turn before opening another conversation.');
|
|
681
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);
|
|
682
694
|
// Capture the exact saved transcript while its runtime still belongs
|
|
683
695
|
// to the host. Subsequent background content reads need no activation.
|
|
684
696
|
await saveManagedHistory(active);
|
|
@@ -716,8 +728,12 @@ export async function activate(context) {
|
|
|
716
728
|
if (!controls)
|
|
717
729
|
throw new Error('This host does not expose harness or model configuration.');
|
|
718
730
|
const active = managed();
|
|
719
|
-
|
|
720
|
-
|
|
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
|
+
}
|
|
721
737
|
if (active)
|
|
722
738
|
await saveManagedHistory(active);
|
|
723
739
|
const state = await controls.select(selection);
|
|
@@ -728,9 +744,13 @@ export async function activate(context) {
|
|
|
728
744
|
const openNewChat = async () => {
|
|
729
745
|
if (!controls || !granted.has('chatSessionsProvider'))
|
|
730
746
|
throw new Error('This host does not expose native conversation creation.');
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
+
}
|
|
734
754
|
// Code-OSS materializes this untitled session on first send, awaiting our
|
|
735
755
|
// newChatSessionItemHandler before accepting/clearing the draft. Never boot
|
|
736
756
|
// beside the old widget and reveal another resource after it accepts input.
|
|
@@ -791,20 +811,23 @@ export async function activate(context) {
|
|
|
791
811
|
const chooseNewChat = async (pick) => {
|
|
792
812
|
if (pendingDraft?.choosing)
|
|
793
813
|
throw new Error('Finish choosing an agent before starting another chat.');
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const draft = await openNewChat();
|
|
797
|
-
const picking = (async () => {
|
|
798
|
-
const choice = await pick();
|
|
799
|
-
if (activeDraft() !== draft)
|
|
800
|
-
throw new vscode.CancellationError();
|
|
801
|
-
draft.choice = choice;
|
|
802
|
-
controlsChanged.fire();
|
|
803
|
-
if (choice.kind !== 'agent')
|
|
804
|
-
throw new vscode.CancellationError();
|
|
805
|
-
})();
|
|
806
|
-
draft.choosing = picking;
|
|
814
|
+
let draft;
|
|
815
|
+
let picking;
|
|
807
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;
|
|
808
831
|
await picking;
|
|
809
832
|
}
|
|
810
833
|
catch (error) {
|
|
@@ -812,9 +835,9 @@ export async function activate(context) {
|
|
|
812
835
|
throw error;
|
|
813
836
|
}
|
|
814
837
|
finally {
|
|
815
|
-
if (draft.choosing === picking)
|
|
838
|
+
if (draft && draft.choosing === picking)
|
|
816
839
|
draft.choosing = undefined;
|
|
817
|
-
if (draft
|
|
840
|
+
if (draft?.choice?.kind === 'signIn') {
|
|
818
841
|
void refreshSetup().catch(error => log.warn(`Could not refresh sign-in: ${String(error)}`)).finally(pollSetup);
|
|
819
842
|
}
|
|
820
843
|
}
|
|
@@ -829,11 +852,16 @@ export async function activate(context) {
|
|
|
829
852
|
if (controls?.state.setup && !controls.state.setup.ready)
|
|
830
853
|
return [];
|
|
831
854
|
const id = resource ? boundId(resource) : undefined;
|
|
832
|
-
const active =
|
|
833
|
-
|
|
855
|
+
const active = id ? frontends.get(id) : undefined;
|
|
856
|
+
// A new draft's harness, not the previous runtime, owns its offered modes.
|
|
857
|
+
const harness = resource ? controls?.state.sessions.find(session => session.id === id)?.selection.harness : draftSelection()?.harness;
|
|
858
|
+
const canApprove = resource ? (active ? canAnswer(active) : ['claude-code', 'codex'].includes(harness ?? ''))
|
|
859
|
+
: (harness ? ['claude-code', 'codex'].includes(harness) : newChatDefault() === 'autoApprove' || ['claude-code', 'codex'].includes(controls?.state.selection.harness ?? ''));
|
|
860
|
+
// An unbound input is a new chat, not the active conversation left behind it.
|
|
861
|
+
const selectedPermission = resource ? currentOptions(resource).permissions : permission ?? newChatDefault();
|
|
834
862
|
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.' },
|
|
835
863
|
...((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.' }] : [])];
|
|
836
|
-
const groups = [{ id: 'permissions', name: 'Permissions', kind: 'permissions', items: permissionItems, selected: permissionItems.find(i => i.id ===
|
|
864
|
+
const groups = [{ id: 'permissions', name: 'Permissions', kind: 'permissions', items: permissionItems, selected: permissionItems.find(i => i.id === selectedPermission) ?? permissionItems[0] }];
|
|
837
865
|
const selected = controls?.state.sessions.find(session => session.id === id);
|
|
838
866
|
if (selected && ['claude-code', 'codex'].includes(selected.selection.harness)) {
|
|
839
867
|
// Native session-option commands remain reachable for nondelegating providers,
|
|
@@ -878,7 +906,8 @@ export async function activate(context) {
|
|
|
878
906
|
applying = pending.then(() => undefined, () => undefined);
|
|
879
907
|
const connection = await pending;
|
|
880
908
|
// The picker shows the host's default until someone changes it; only a change is the person's choice.
|
|
881
|
-
const
|
|
909
|
+
const selected = inputState.groups.find(group => group.id === 'permissions')?.selected?.id;
|
|
910
|
+
const picked = selected === 'ask' || selected === 'autoApprove' ? selected : permission ?? newChatDefault();
|
|
882
911
|
choosePermission(connection.id, picked, permission !== undefined ? null : newChatDefault());
|
|
883
912
|
permission = undefined;
|
|
884
913
|
if (pendingDraft === draft) {
|
|
@@ -967,17 +996,20 @@ export async function activate(context) {
|
|
|
967
996
|
return { kind: 'agent', selection: { harness: harness.id, model: '', effort: '' } };
|
|
968
997
|
});
|
|
969
998
|
}));
|
|
970
|
-
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.configureModel', async (resource,
|
|
999
|
+
context.subscriptions.push(vscode.commands.registerCommand('supercode.frontend.configureModel', async (resource, _sessionId) => {
|
|
971
1000
|
if (!controls || focusedAttached)
|
|
972
1001
|
throw new Error('Model settings require a host-managed conversation.');
|
|
973
1002
|
await controls.refresh();
|
|
974
|
-
// The native session-options picker prepends its
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1003
|
+
// The native session-options picker prepends its widget's resource to command arguments, so the resource is the
|
|
1004
|
+
// conversation the person clicked from. `sessionId` is not used: the workbench keeps one option-group list per
|
|
1005
|
+
// session type, last written by whichever input state updated, so any chat's dropdown can carry another
|
|
1006
|
+
// conversation's id. A resource with no binding (a blank draft) is no conversation; only the command palette,
|
|
1007
|
+
// which passes no resource, falls back to the active one. Either way the choice opens a new chat.
|
|
1008
|
+
const bound = resource ? boundId(resource) : controls.state.activeSession ?? undefined;
|
|
978
1009
|
// The conversation it was opened from sets the agent and the current values; with none open (a fresh project,
|
|
979
|
-
// the command palette) the person picks the agent first.
|
|
980
|
-
|
|
1010
|
+
// the command palette) the person picks the agent first. A running turn refuses the new chat before any picker
|
|
1011
|
+
// opens, and says so (`openNewChat`).
|
|
1012
|
+
const selected = controls.state.sessions.find(session => session.id === bound);
|
|
981
1013
|
// An agent that still needs sign-in is set up from the agent picker first, as there.
|
|
982
1014
|
const needsSignIn = new Set((controls.state.setup?.actions ?? []).filter(action => action.kind === 'login').map(action => action.harness));
|
|
983
1015
|
const configurable = controls.state.harnesses.filter(harness => CONFIGURABLE_HARNESSES.includes(harness.id) && !needsSignIn.has(harness.id));
|
|
@@ -1650,19 +1682,30 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
1650
1682
|
context.subscriptions.push(controller, frontends.onDidChange(refreshItems));
|
|
1651
1683
|
if (controls) {
|
|
1652
1684
|
const states = new Set();
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
const
|
|
1685
|
+
const draftPermissions = new WeakMap();
|
|
1686
|
+
const applyOptions = (groups, options) => groups.map(group => {
|
|
1687
|
+
const value = options.find(option => option.optionId === group.id)?.value;
|
|
1688
|
+
const selected = group.items.find(item => item.id === value);
|
|
1689
|
+
return selected ? { ...group, selected } : group;
|
|
1690
|
+
});
|
|
1691
|
+
controller.getChatSessionInputState = (resource, inputContext) => {
|
|
1692
|
+
const groups = controls.options(resource);
|
|
1693
|
+
// Native request options belong to this draft. previousInputState may instead
|
|
1694
|
+
// be the type-wide last chat, so the paired host exposes them separately.
|
|
1695
|
+
// Stock hosts without that context keep their existing previous-state contract.
|
|
1696
|
+
const initial = inputContext.initialSessionOptions ?? inputContext.previousInputState?.groups.flatMap(group => group.selected ? [{ optionId: group.id, value: group.selected.id }] : []) ?? [];
|
|
1697
|
+
const state = controller.createChatSessionInputState(resource ? groups : applyOptions(groups, initial));
|
|
1698
|
+
const initialPermission = initial.find(option => option.optionId === 'permissions')?.value;
|
|
1699
|
+
if (!resource && initialPermission)
|
|
1700
|
+
draftPermissions.set(state, initialPermission);
|
|
1661
1701
|
states.add(state);
|
|
1662
1702
|
const changed = state.onDidChange(() => {
|
|
1663
1703
|
const target = state.sessionResource ?? resource;
|
|
1704
|
+
const value = state.groups.find(group => group.id === 'permissions')?.selected?.id;
|
|
1664
1705
|
if (target)
|
|
1665
|
-
controls.setPermission(target,
|
|
1706
|
+
controls.setPermission(target, value);
|
|
1707
|
+
else if (value)
|
|
1708
|
+
draftPermissions.set(state, value);
|
|
1666
1709
|
});
|
|
1667
1710
|
const disposed = state.onDidDispose(() => { states.delete(state); changed.dispose(); disposed.dispose(); });
|
|
1668
1711
|
return state;
|
|
@@ -1675,8 +1718,9 @@ function registerSessions(context, frontends, participant, granted, handler, his
|
|
|
1675
1718
|
context.subscriptions.push(controls.changed(() => {
|
|
1676
1719
|
refreshItems();
|
|
1677
1720
|
for (const state of states) {
|
|
1678
|
-
|
|
1679
|
-
|
|
1721
|
+
const groups = controls.options(state.sessionResource);
|
|
1722
|
+
const value = draftPermissions.get(state);
|
|
1723
|
+
state.groups = state.sessionResource || !value ? groups : applyOptions(groups, [{ optionId: 'permissions', value }]);
|
|
1680
1724
|
}
|
|
1681
1725
|
}));
|
|
1682
1726
|
}
|
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.42",
|
|
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",
|