diff --git a/src/main/persistence/loading-store/normalize-loaded-global-settings.ts b/src/main/persistence/loading-store/normalize-loaded-global-settings.ts index f2bb14f81771..526788f078fa 100644 --- a/src/main/persistence/loading-store/normalize-loaded-global-settings.ts +++ b/src/main/persistence/loading-store/normalize-loaded-global-settings.ts @@ -137,7 +137,11 @@ export function normalizeLoadedGlobalSettings( // Why: the next settings save stores the seed; until then a reload derives the same one. nativeChatPermissionMode: loadedChatPermissionSetting( parsed.settings?.nativeChatPermissionMode, - migratedAgentYoloDefaults + { + ...migratedAgentYoloDefaults, + terminalWindowsShell: parsed.settings?.terminalWindowsShell + }, + process.platform ), defaultTaskSource: taskProviderSettings.defaultTaskSource, visibleTaskProviders: taskProviderSettings.visibleTaskProviders, diff --git a/src/shared/agent-chat-permission-mode.test.ts b/src/shared/agent-chat-permission-mode.test.ts index be949d7a5305..6f0fb29f268f 100644 --- a/src/shared/agent-chat-permission-mode.test.ts +++ b/src/shared/agent-chat-permission-mode.test.ts @@ -79,40 +79,71 @@ describe('the mode a chat runs', () => { }) describe('the new-chat setting as loaded', () => { + const seed = (terminal: Parameters[1]) => + loadedChatPermissionSetting(undefined, terminal, 'darwin') + const yoloWithArgs = (args: { claude?: string; codex?: string; grok?: string }) => { + const terminal = applyAgentPermissionMode({ mode: 'yolo' }) + Object.assign(terminal.agentDefaultArgs, args) + return terminal + } + it.each([ ['yolo', 'full-access'], ['manual', 'ask'] ] as const)('seeds %s Agent Permissions as %s', (mode, expected) => { - expect(loadedChatPermissionSetting(undefined, applyAgentPermissionMode({ mode }))).toBe( - expected - ) + expect(seed(applyAgentPermissionMode({ mode }))).toBe(expected) }) - it('seeds mixed Agent Permissions as Ask', () => { - const terminal = applyAgentPermissionMode({ mode: 'yolo' }) - terminal.agentDefaultArgs.claude = '' - expect(loadedChatPermissionSetting(undefined, terminal)).toBe('ask') + it('seeds Full access when Claude and Codex carry their bypass flags among other arguments', () => { + expect( + seed( + yoloWithArgs({ + claude: '--dangerously-skip-permissions --model opus', + codex: '--model gpt-5 --dangerously-bypass-approvals-and-sandbox' + }) + ) + ).toBe('full-access') + }) + + it('reads only Claude and Codex, not the other agents', () => { + expect(seed(yoloWithArgs({ grok: '' }))).toBe('full-access') + const othersManual = applyAgentPermissionMode({ mode: 'manual' }) + Object.assign(othersManual.agentDefaultArgs, { + claude: '--dangerously-skip-permissions', + codex: '--dangerously-bypass-approvals-and-sandbox' + }) + expect(seed(othersManual)).toBe('full-access') + }) + + it('seeds Ask when Claude or Codex launches without its bypass flag', () => { + expect(seed(yoloWithArgs({ claude: '' }))).toBe('ask') + expect(seed(yoloWithArgs({ codex: '--model gpt-5' }))).toBe('ask') + }) + + // Manual leaves custom arguments alone, and the all-agent summary still shows Yolo for them. + it('seeds Ask after Manual even when another agent keeps custom arguments', () => { + const custom = yoloWithArgs({ codex: '--model gpt-5', grok: '--model grok-4' }) + expect(seed(applyAgentPermissionMode({ mode: 'manual', ...custom }))).toBe('ask') }) it('seeds a fresh profile from the shipped Yolo defaults', () => { - expect(loadedChatPermissionSetting(undefined, getDefaultSettings('/tmp'))).toBe('full-access') + expect(seed(getDefaultSettings('/tmp'))).toBe('full-access') expect(getDefaultSettings('/tmp').nativeChatPermissionMode).toBe('full-access') }) it.each(AGENT_CHAT_PERMISSION_MODES)( 'keeps a saved %s whatever Agent Permissions say', (mode) => { - expect(loadedChatPermissionSetting(mode, applyAgentPermissionMode({ mode: 'manual' }))).toBe( - mode - ) - expect(loadedChatPermissionSetting(mode, applyAgentPermissionMode({ mode: 'yolo' }))).toBe( - mode - ) + for (const terminal of ['manual', 'yolo'] as const) { + expect( + loadedChatPermissionSetting(mode, applyAgentPermissionMode({ mode: terminal }), 'darwin') + ).toBe(mode) + } } ) it.each(['future', null, 42, 'bypass'])('reads an unknown saved %s as Ask', (value) => { - expect(loadedChatPermissionSetting(value, getDefaultSettings('/tmp'))).toBe('ask') + expect(loadedChatPermissionSetting(value, getDefaultSettings('/tmp'), 'darwin')).toBe('ask') }) // The catalog shape `claude` 2.1.280 lists: the default row is folded into the row it resolves to. diff --git a/src/shared/agent-chat-permission-mode.ts b/src/shared/agent-chat-permission-mode.ts index 478b3019c886..fcf63a8b5e42 100644 --- a/src/shared/agent-chat-permission-mode.ts +++ b/src/shared/agent-chat-permission-mode.ts @@ -1,5 +1,5 @@ import type { GlobalSettings } from './global-settings-types' -import { resolveAgentPermissionModeSummary } from './tui-agent-permissions' +import { resolvedTuiAgentArgsBypassPermissions } from './tui-agent-launch-defaults' /** A chat's own permission choice. Saved in chat records and settings: never rename a value. */ export type AgentChatPermissionMode = 'ask' | 'accept-edits' | 'auto' | 'full-access' @@ -104,13 +104,18 @@ export function newChatPermissionMode( return support ? savedChatPermissionMode(support, setting) : null } -/** Seeded once from Agent Permissions when absent, then stored; an unknown value asks. */ +/** Seeded once when absent, then stored; an unknown value asks. */ export function loadedChatPermissionSetting( saved: unknown, - terminal: Partial> + terminal: Partial>, + platform: NodeJS.Platform ): AgentChatPermissionMode { if (saved !== undefined) { return isAgentChatPermissionMode(saved) ? saved : 'ask' } - return resolveAgentPermissionModeSummary(terminal) === 'yolo' ? 'full-access' : 'ask' + // What chats ran before they had modes: Claude's and Codex's own arguments, not the all-agent + // Yolo/Manual summary, which shows custom arguments on any agent as Yolo even after Manual. + const bypasses = (agent: 'claude' | 'codex'): boolean => + resolvedTuiAgentArgsBypassPermissions(agent, terminal, platform) + return bypasses('claude') && bypasses('codex') ? 'full-access' : 'ask' } diff --git a/src/shared/default-global-settings.ts b/src/shared/default-global-settings.ts index 178ca2e19d5e..e644e997cc04 100644 --- a/src/shared/default-global-settings.ts +++ b/src/shared/default-global-settings.ts @@ -9,7 +9,6 @@ import { DEFAULT_APP_ICON_ID } from './app-icon' import { DEFAULT_OPEN_IN_APPLICATIONS } from './open-in-applications' import { DEFAULT_DISABLED_TUI_AGENTS } from './tui-agent-selection' import { DEFAULT_TUI_AGENT_ARGS, DEFAULT_TUI_AGENT_ENV } from './tui-agent-launch-defaults' -import { loadedChatPermissionSetting } from './agent-chat-permission-mode' import { UI_LANGUAGE_SYSTEM } from './ui-language' import { DEFAULT_LEFT_SIDEBAR_TINT_COLOR, @@ -140,10 +139,8 @@ export function buildDefaultSettings(args: { nativeChatInheritShellEnvironment: true, nativeChatShellEnvironmentVariables: [], nativeChatSessionOptions: {}, - nativeChatPermissionMode: loadedChatPermissionSetting(undefined, { - agentDefaultArgs: DEFAULT_TUI_AGENT_ARGS, - agentDefaultEnv: DEFAULT_TUI_AGENT_ENV - }), + // The shipped Claude and Codex arguments carry their bypass flags. + nativeChatPermissionMode: 'full-access', openInApplications: [...DEFAULT_OPEN_IN_APPLICATIONS], rightSidebarOpenByDefault: true, showGitIgnoredFiles: true,