Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
61 changes: 46 additions & 15 deletions src/shared/agent-chat-permission-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,40 +79,71 @@ describe('the mode a chat runs', () => {
})

describe('the new-chat setting as loaded', () => {
const seed = (terminal: Parameters<typeof loadedChatPermissionSetting>[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.
Expand Down
13 changes: 9 additions & 4 deletions src/shared/agent-chat-permission-mode.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<Pick<GlobalSettings, 'agentDefaultArgs' | 'agentDefaultEnv'>>
terminal: Partial<Pick<GlobalSettings, 'agentDefaultArgs' | 'terminalWindowsShell'>>,
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'
}
7 changes: 2 additions & 5 deletions src/shared/default-global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading