Skip to content

Commit e9ff3cc

Browse files
committed
fix(agent): a change to the approvals mode in Settings or another window takes effect at once
The mode was read only when the chat webview said ready, and when the chat's own menu changed it. Switching from Autopilot to Default Approvals in Settings, or in another window, left this window's next commands on Autopilot until the chat was reloaded — the pill said one thing, the gate did another. onConfigChanged now re-reads levelcode.ai.agent.approvals (and the older agent.autopilot) when either changes, and posts the effective mode to the pill. The chat menu's own change comes back through the same path. Tested on the real onConfigChanged, lifted into sessionExpiredHost: Settings → Autopilot, back to Default Approvals, the old boolean alone, and an unrelated change.
1 parent d24d0b5 commit e9ff3cc

2 files changed

Lines changed: 34 additions & 3 deletions

File tree

‎extensions/levelcode-ai/extension.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3711,6 +3711,13 @@ async function openWorkspaceFile(rel) {
37113711
*/
37123712
function onConfigChanged(e) {
37133713
if (e.affectsConfiguration('levelcode.ai')) { sendConfigToWebview(); }
3714+
// The approvals mode, changed in Settings or by another window: the very next command uses it (agent.js
3715+
// reads it per command), and the pill says so. The chat menu's own change comes back through here too.
3716+
if (e.affectsConfiguration('levelcode.ai.agent.approvals') || e.affectsConfiguration('levelcode.ai.agent.autopilot')) {
3717+
approvalMode = readApprovalMode();
3718+
dbg('approvals.reloaded', { mode: approvalMode });
3719+
post({ type: 'approvals', mode: approvalMode });
3720+
}
37143721
if (e.affectsConfiguration('levelcode.ai.providerMode') || e.affectsConfiguration('levelcode.cloud')) {
37153722
postAccount();
37163723
replaySessionExpired();

‎extensions/levelcode-ai/test/sessionExpiredHost.test.js‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ const FUNCTIONS = [
9797
'refreshCloudToken', 'renewSession', 'storedToken', 'catchUpWithStoredSession', 'withSessionLock', 'sameSession', 'sessionExpired', 'postSessionExpired',
9898
'sessionExpiredPending', 'isEndedSessionError', 'clearSessionExpired', 'replaySessionExpired', 'checkCloudSession',
9999
'refreshGatewayToken', 'prepProviderRequest', 'handleSend', 'currentAccount', 'postAccount', 'accountSignOut',
100-
'storeSession', 'newChat', 'resumeSession', 'restoreCheckpoint', 'onConfigChanged'
100+
'storeSession', 'newChat', 'resumeSession', 'restoreCheckpoint', 'onConfigChanged', 'readApprovalMode'
101101
];
102102
// eslint-disable-next-line no-new-func
103103
const makeHost = new Function('env', [
@@ -112,6 +112,8 @@ const makeHost = new Function('env', [
112112
'let agentMessages = [], currentCheckpoint = null, lastAgentGoal = null;',
113113
// resumeSession starts the Ask When Risky reviewer's record of the person's goals over (agentReview.js).
114114
'let humanGoals = [], humanAnswers = [];',
115+
// onConfigChanged re-reads the approvals mode (Settings, another window).
116+
"let approvalMode = 'ask';", decl('APPROVAL_MODES'),
115117
// resumeSession also hands a session's stored diagrams back to the diagram service
116118
// (docs/RICH-DIAGRAMS.md). These sessions have none; test/diagramHost.test.js runs it with the real one.
117119
'let diagramsStubbed = false; const diagrams = { load: () => {}, stubsFor: () => [] };',
@@ -121,7 +123,7 @@ const makeHost = new Function('env', [
121123
'return { ' + FUNCTIONS.join(', ') + ',',
122124
' KEY: { token: ACCOUNT_TOKEN_KEY, refresh: ACCOUNT_REFRESH_KEY, profile: ACCOUNT_PROFILE_KEY, expired: ACCOUNT_EXPIRED_KEY },',
123125
' signedIn: () => cloudSignedIn, conversation: () => conversation, abort: () => abort,',
124-
' openWebview: (w) => { activeWebview = w; }, setAgentMessages: (m) => { agentMessages = m; } };'
126+
' openWebview: (w) => { activeWebview = w; }, setAgentMessages: (m) => { agentMessages = m; }, approvalMode: () => approvalMode };'
125127
].join('\n'));
126128

127129
// The defaults a fresh install runs with come from package.json, not from this file: the whole
@@ -185,7 +187,8 @@ function boot(over) {
185187

186188
const secrets = new Map(), state = new Map(), posted = [], ops = [];
187189
const calls = { refresh: [], stream: [], keyPrompts: 0, flows: [] };
188-
const vscode = { workspace: { getConfiguration: (section) => ({ get: (k, d) => (settings[section + '.' + k] === undefined ? d : settings[section + '.' + k]) }) } };
190+
// inspect() as the user scope sees it: these settings are the user's (a folder cannot set them).
191+
const vscode = { workspace: { getConfiguration: (section) => ({ get: (k, d) => (settings[section + '.' + k] === undefined ? d : settings[section + '.' + k]), inspect: (k) => ({ globalValue: settings[section + '.' + k] }) }) } };
189192
const webview = { postMessage: (m) => { posted.push(m); } };
190193
const t = {
191194
posted, calls, ops, secrets, state, webview, settings,
@@ -1099,6 +1102,27 @@ async function test(name, fn) { await fn(); n++; console.log(' ok - ' + name);
10991102
assert.match(src, /case 'continueAgent':[^\n]*await agentFlow\('Continue from where you left off[^\n]*'\);/);
11001103
});
11011104

1105+
await test('SETTINGS: the approvals mode changed in Settings or another window takes effect at once, and the pill follows', async () => {
1106+
const t = boot({ settings: { 'levelcode.ai.agent.approvals': 'autopilot' } });
1107+
assert.strictEqual(t.host.approvalMode(), 'ask', 'the window started before the change');
1108+
t.host.onConfigChanged(settingChanged('levelcode.ai', 'levelcode.ai.agent.approvals'));
1109+
assert.strictEqual(t.host.approvalMode(), 'autopilot');
1110+
assert.deepStrictEqual(t.posted.filter((m) => m.type === 'approvals'), [{ type: 'approvals', mode: 'autopilot' }]);
1111+
// Back to Default Approvals the same way — the case that matters: Autopilot must not linger.
1112+
const t2 = boot({ settings: { 'levelcode.ai.agent.approvals': 'ask', 'levelcode.ai.agent.autopilot': true } });
1113+
t2.host.onConfigChanged(settingChanged('levelcode.ai', 'levelcode.ai.agent.approvals'));
1114+
assert.strictEqual(t2.host.approvalMode(), 'ask', 'an explicit approvals value wins over the old boolean');
1115+
// The old boolean alone still means Autopilot, and changing it is picked up too.
1116+
const t3 = boot({ settings: { 'levelcode.ai.agent.autopilot': true } });
1117+
t3.host.onConfigChanged(settingChanged('levelcode.ai', 'levelcode.ai.agent.autopilot'));
1118+
assert.strictEqual(t3.host.approvalMode(), 'autopilot');
1119+
// An unrelated change leaves it alone.
1120+
const t4 = boot({ settings: { 'levelcode.ai.agent.approvals': 'review' } });
1121+
t4.host.onConfigChanged(settingChanged('levelcode.ai', 'levelcode.ai.chat.fontSize'));
1122+
assert.strictEqual(t4.host.approvalMode(), 'ask');
1123+
assert.deepStrictEqual(t4.posted.filter((m) => m.type === 'approvals'), []);
1124+
});
1125+
11021126
await test('`ready` does not say it twice: the handler has just read the same token, and the card is replayed last', async () => {
11031127
const t = boot({ expired: true, profile: { name: 'Ada' } });
11041128
await t.host.checkCloudSession('ready');

0 commit comments

Comments
 (0)