From 019318b8dfd4b970fdfefa3222f7d3ce6fd5270f Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:37:16 -0700 Subject: [PATCH] fix(settings): permission menus match the chat permission picker (#36104) * fix(settings): permission menus match the chat permission picker The per-agent permission menu (Settings > Agents and Source Control actions) and the 'Agents without Approve for me' toggle rendered their own plain-text items. They now reuse the chat picker's items: the same icons, Full access warning colour, per-agent descriptions and width, with the agent's own settings after a separator. * Unify legacy permission menus and verify selection behavior * Fix permission selection test typing and remove obsolete locale keys --- .../AgentPermissionChoiceControls.tsx | 157 +++++------ .../settings/AgentPermissionChoiceItems.tsx | 84 ++++++ .../settings/AgentPermissionModeMenu.tsx | 71 +++++ .../settings/AgentPermissionsSetting.tsx | 39 +-- .../settings/AgentRowPermissions.tsx | 24 +- .../components/settings/AgentsPane.test.tsx | 18 +- .../settings/HostAgentLaunchSettings.test.tsx | 260 +++++++++++++++--- .../settings/agent-permission-mode-label.ts | 12 +- .../src/components/settings/agents-search.ts | 12 +- .../src/i18n/en-runtime-required.json | 1 - src/renderer/src/i18n/locales/de.json | 3 - src/renderer/src/i18n/locales/en.json | 8 +- src/renderer/src/i18n/locales/es.json | 3 - src/renderer/src/i18n/locales/fr.json | 3 - src/renderer/src/i18n/locales/ja.json | 3 - src/renderer/src/i18n/locales/ko.json | 3 - src/renderer/src/i18n/locales/pt.json | 3 - src/renderer/src/i18n/locales/ru.json | 3 - src/renderer/src/i18n/locales/tr.json | 3 - src/renderer/src/i18n/locales/zh-Hant.json | 3 - src/renderer/src/i18n/locales/zh.json | 3 - 21 files changed, 497 insertions(+), 219 deletions(-) create mode 100644 src/renderer/src/components/settings/AgentPermissionChoiceItems.tsx create mode 100644 src/renderer/src/components/settings/AgentPermissionModeMenu.tsx diff --git a/src/renderer/src/components/settings/AgentPermissionChoiceControls.tsx b/src/renderer/src/components/settings/AgentPermissionChoiceControls.tsx index fb1f8d53943..88bfc3ec789 100644 --- a/src/renderer/src/components/settings/AgentPermissionChoiceControls.tsx +++ b/src/renderer/src/components/settings/AgentPermissionChoiceControls.tsx @@ -1,4 +1,4 @@ -import { ChevronDown, Terminal, X } from 'lucide-react' +import { ChevronDown, X } from 'lucide-react' import type { Ref } from 'react' import type { AgentChatPermissionMode } from '../../../../shared/agent-chat-permission-mode' import type { AgentPermissionChoice } from '../../../../shared/tui-agent-permission-choice' @@ -15,15 +15,8 @@ import { DropdownMenuTrigger } from '../ui/dropdown-menu' import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip' -import { - nativeChatPermissionModeDescription, - nativeChatPermissionModeLabel -} from '../native-chat/native-chat-permission-mode-labels' -import { - NativeChatPermissionModeItems, - NativeChatPermissionModeName -} from '../native-chat/NativeChatPermissionModePicker' -import { SettingsSegmentedControl } from './SettingsFormControls' +import { nativeChatPermissionModeLabel } from '../native-chat/native-chat-permission-mode-labels' +import { AgentPermissionChoiceItems, AgentPermissionChoiceName } from './AgentPermissionChoiceItems' /** Use chat-mode wording; own names the agent when available. */ export function agentPermissionChoiceLabel( @@ -40,19 +33,6 @@ export function agentPermissionChoiceLabel( : translate('settings.agents.permissionOwnAll', "Agents' own settings") } -function ownDescription(chatOwnSupported: boolean): string { - if (!chatOwnSupported) { - return translate( - 'settings.agents.permissionOwnLegacyDescription', - 'Orca adds no permission flag in terminals; chats use Settings → Chat.' - ) - } - return translate( - 'settings.agents.permissionOwnDescription', - 'Terminals and chats follow the agent’s own permission settings.' - ) -} - export function AllAgentsPermissionSetting({ chatOwnSupported = false, choice, @@ -69,6 +49,10 @@ export function AllAgentsPermissionSetting({ }): React.JSX.Element { const title = translate('settings.agents.permissionsTitle', 'Permissions') const ownLabel = translate('settings.agents.permissionEachOwn', "Each agent's own settings") + const withoutAutoTitle = translate( + 'settings.agents.otherAgentsPermissionsLabel', + 'Permissions for other agents' + ) return (
@@ -84,35 +68,20 @@ export function AllAgentsPermissionSetting({ > {choice === null ? ( - ) : choice === 'own' ? ( - - - {ownLabel} - ) : ( - + )} - - - onChange('own')}> -
- - - {ownLabel} - -
- {ownDescription(chatOwnSupported)} -
-
-
@@ -129,26 +98,43 @@ export function AllAgentsPermissionSetting({ {translate('settings.agents.permissionWithoutAuto', 'Agents without Approve for me')} - - value={withoutAutoChoice ?? 'mixed'} - onChange={(next) => { - if (next !== 'mixed') { - onChangeWithoutAuto(next) - } - }} - ariaLabel={translate( - 'settings.agents.otherAgentsPermissionsLabel', - 'Permissions for other agents' - )} - size="sm" - options={(['own', 'full-access'] as const).map((value) => ({ - value, - label: - value === 'own' - ? translate('settings.agents.permissionOwnShort', 'Own settings') - : nativeChatPermissionModeLabel(value) - }))} - /> + + + + + + + + + +
) : null} @@ -191,7 +177,7 @@ export function AgentPermissionMenu({ }): React.JSX.Element { const followAll = inheritLabel ?? translate('settings.agents.permissionSameAsAll', 'Same as all agents') - const current = agentPermissionChoiceLabel(permission.choice, label) + const ownLabel = agentPermissionChoiceLabel('own', label) return (
- - {permission.source === 'all' ? `${followAll} · ${current}` : current} + + {permission.source === 'all' ? {followAll} · : null} + - + @@ -236,30 +223,24 @@ export function AgentPermissionMenu({ disabled={disabled} onSelect={() => permission.onSelect('default')} > - - {followAll} · {agentPermissionChoiceLabel(permission.defaultChoice, label)} + + {followAll} · + - {permission.choices.map((choice) => ( - permission.onSelect(choice)} - > -
-
- {agentPermissionChoiceLabel(choice, label)} -
-
- {choice === 'own' - ? ownDescription(permission.chatOwnSupported === true) - : nativeChatPermissionModeDescription(choice, agentId)} -
-
-
- ))} +
diff --git a/src/renderer/src/components/settings/AgentPermissionChoiceItems.tsx b/src/renderer/src/components/settings/AgentPermissionChoiceItems.tsx new file mode 100644 index 00000000000..425dce246a9 --- /dev/null +++ b/src/renderer/src/components/settings/AgentPermissionChoiceItems.tsx @@ -0,0 +1,84 @@ +import { Terminal } from 'lucide-react' +import type { AgentPermissionChoice } from '../../../../shared/tui-agent-permission-choice' +import { translate } from '@/i18n/i18n' +import { DropdownMenuRadioItem, DropdownMenuSeparator } from '../ui/dropdown-menu' +import { + NativeChatPermissionModeItems, + NativeChatPermissionModeName +} from '../native-chat/NativeChatPermissionModePicker' + +function ownDescription(chatOwnSupported: boolean): string { + if (!chatOwnSupported) { + return translate( + 'settings.agents.permissionOwnLegacyDescription', + 'Orca adds no permission flag in terminals; chats use Settings → Chat.' + ) + } + return translate( + 'settings.agents.permissionOwnDescription', + 'Terminals and chats follow the agent’s own permission settings.' + ) +} + +/** A choice's name as the chat picker shows it; own gets a terminal icon. */ +export function AgentPermissionChoiceName({ + choice, + ownLabel, + truncate +}: { + choice: AgentPermissionChoice + ownLabel: string + truncate?: boolean +}): React.JSX.Element { + if (choice !== 'own') { + return + } + return ( + + + {truncate ? {ownLabel} : {ownLabel}} + + ) +} + +/** The chat picker's items, with the agent's own settings after a separator. */ +export function AgentPermissionChoiceItems({ + choices, + agent, + ownLabel, + chatOwnSupported, + disabled, + onSelect +}: { + choices: readonly AgentPermissionChoice[] + agent?: string + ownLabel: string + chatOwnSupported: boolean + disabled?: boolean + onSelect: (choice: AgentPermissionChoice) => void +}): React.JSX.Element { + const modes = choices.filter((choice) => choice !== 'own') + return ( + <> + + {choices.includes('own') ? ( + <> + {modes.length > 0 ? : null} + onSelect('own')}> +
+ +
+ {ownDescription(chatOwnSupported)} +
+
+
+ + ) : null} + + ) +} diff --git a/src/renderer/src/components/settings/AgentPermissionModeMenu.tsx b/src/renderer/src/components/settings/AgentPermissionModeMenu.tsx new file mode 100644 index 00000000000..b3c3233591a --- /dev/null +++ b/src/renderer/src/components/settings/AgentPermissionModeMenu.tsx @@ -0,0 +1,71 @@ +import { ChevronDown } from 'lucide-react' +import type { AgentPermissionMode } from '../../../../shared/tui-agent-permissions' +import { translate } from '@/i18n/i18n' +import { Button } from '../ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuTrigger +} from '../ui/dropdown-menu' +import { AgentPermissionChoiceItems, AgentPermissionChoiceName } from './AgentPermissionChoiceItems' +import { agentPermissionModeChoice } from './agent-permission-mode-label' + +/** Legacy hosts keep their launch modes; only the menu uses the shared choice vocabulary. */ +export function AgentPermissionModeMenu({ + mode, + label, + autoAvailable, + autoDisabled, + onChange +}: { + mode: AgentPermissionMode + label: string + autoAvailable: boolean + autoDisabled?: boolean + onChange: (mode: Exclude) => void +}): React.JSX.Element { + const ownLabel = translate('settings.agents.permissionEachOwn', "Each agent's own settings") + const choice = mode === 'mixed' ? '' : agentPermissionModeChoice(mode) + return ( + + + + + + + {autoAvailable ? ( + onChange('auto')} + /> + ) : null} + onChange(next === 'own' ? 'manual' : 'yolo')} + /> + + + + ) +} diff --git a/src/renderer/src/components/settings/AgentPermissionsSetting.tsx b/src/renderer/src/components/settings/AgentPermissionsSetting.tsx index e02e58b80fb..baa9413655a 100644 --- a/src/renderer/src/components/settings/AgentPermissionsSetting.tsx +++ b/src/renderer/src/components/settings/AgentPermissionsSetting.tsx @@ -3,7 +3,7 @@ import type { AgentPermissionMode } from '../../../../shared/tui-agent-permissio import { getIntlLocale, i18n, translate } from '@/i18n/i18n' import { cn } from '@/lib/utils' import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip' -import { SettingsSegmentedControl } from './SettingsFormControls' +import { AgentPermissionModeMenu } from './AgentPermissionModeMenu' import { agentPermissionModeLabel } from './agent-permission-mode-label' type PresetMode = Exclude @@ -96,25 +96,20 @@ export function AgentPermissionsSetting(props: AgentPermissionsSettingProps): Re

{!autoSupported ? (

- {translate('settings.agents.autoUnsupported', 'Auto needs a newer Orca on this host.')} + {translate( + 'settings.agents.autoUnsupported', + 'Approve for me needs a newer Orca on this host.' + )}

) : null}
- - value={mode} - onChange={(next) => { - if (next !== 'mixed') { - onChange(next) - } - }} - ariaLabel={title} - size="sm" - options={[ - { value: 'manual', label: agentPermissionModeLabel('manual') }, - { value: 'auto', label: agentPermissionModeLabel('auto'), disabled: !autoSupported }, - { value: 'yolo', label: agentPermissionModeLabel('yolo') } - ]} + {/* Keep mounted for animation; inert excludes hidden controls from focus and accessibility. */}
{translate('settings.agents.otherAgentsPermissions', 'Other agents')} - - value={nonAutoMode ?? 'mixed'} + { if ((next === 'manual' || next === 'yolo') && onChangeNonAuto) { onChangeNonAuto(next) } }} - ariaLabel={translate( + label={translate( 'settings.agents.otherAgentsPermissionsLabel', 'Permissions for other agents' )} - size="sm" - options={[ - { value: 'manual', label: agentPermissionModeLabel('manual') }, - { value: 'yolo', label: agentPermissionModeLabel('yolo') } - ]} + autoAvailable={false} />
diff --git a/src/renderer/src/components/settings/AgentRowPermissions.tsx b/src/renderer/src/components/settings/AgentRowPermissions.tsx index f03ca9924c7..3bfd5ef0a2d 100644 --- a/src/renderer/src/components/settings/AgentRowPermissions.tsx +++ b/src/renderer/src/components/settings/AgentRowPermissions.tsx @@ -2,7 +2,8 @@ import type { AgentPermissionMode } from '../../../../shared/tui-agent-permissio import { translate } from '@/i18n/i18n' import { Button } from '../ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip' -import { SettingsBadge, SettingsSegmentedControl } from './SettingsFormControls' +import { SettingsBadge } from './SettingsFormControls' +import { AgentPermissionModeMenu } from './AgentPermissionModeMenu' import { agentPermissionModeLabel } from './agent-permission-mode-label' export type AgentRowPermission = { @@ -86,24 +87,13 @@ export function AgentRowPermissionControl({ ) : null} ) : ( - - value={permission.mode} - onChange={(next) => { - if (next !== 'mixed') { - permission.onChange(next) - } - }} - ariaLabel={translate('settings.agents.rowPermissionsLabel', '{{agent}} permissions', { + )} diff --git a/src/renderer/src/components/settings/AgentsPane.test.tsx b/src/renderer/src/components/settings/AgentsPane.test.tsx index 8271da4c02c..0bc3dfeaeae 100644 --- a/src/renderer/src/components/settings/AgentsPane.test.tsx +++ b/src/renderer/src/components/settings/AgentsPane.test.tsx @@ -518,11 +518,15 @@ describe('AgentsPane', () => { }) it.each([ - ['manual', undefined, 'How much agents can do without asking you.', 1], - ['auto', 'yolo', 'Supported agents approve safe actions on their own.', 2], - ['auto', 'mixed', 'Supported agents approve safe actions on their own.', 1], - ['mixed', undefined, 'Most agents use Yolo. Choosing a mode resets the marked exceptions', 0] - ] as const)('describes %s permissions with others %s', (mode, nonAutoMode, copy, checked) => { + ['manual', undefined, 'How much agents can do without asking you.'], + ['auto', 'yolo', 'Supported agents approve safe actions on their own.'], + ['auto', 'mixed', 'Supported agents approve safe actions on their own.'], + [ + 'mixed', + undefined, + 'Most agents use Full access. Choosing a mode resets the marked exceptions' + ] + ] as const)('describes %s permissions with others %s', (mode, nonAutoMode, copy) => { const markup = renderToStaticMarkup( { ) expect(markup).toContain(copy) expect(markup.includes('inert=""')).toBe(mode !== 'auto') - expect(markup.match(/aria-checked="true"/g)?.length ?? 0).toBe(checked) }) it('explains inline when the host cannot run Auto', () => { @@ -546,8 +549,7 @@ describe('AgentsPane', () => { ) - expect(markup).toContain('Auto needs a newer Orca on this host.') - expect(markup).toMatch(/aria-disabled="true"[^>]*>Auto { diff --git a/src/renderer/src/components/settings/HostAgentLaunchSettings.test.tsx b/src/renderer/src/components/settings/HostAgentLaunchSettings.test.tsx index 670ee8d335a..9ed19d07a8d 100644 --- a/src/renderer/src/components/settings/HostAgentLaunchSettings.test.tsx +++ b/src/renderer/src/components/settings/HostAgentLaunchSettings.test.tsx @@ -6,10 +6,15 @@ import userEvent from '@testing-library/user-event' import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { + agentLaunchSettingsMutationUpdates, projectAgentLaunchSettings, - type AgentLaunchSettings + type AgentLaunchSettings, + type AgentLaunchSettingsMutation } from '../../../../shared/agent-launch-settings' import { HostAgentLaunchSettings } from './HostAgentLaunchSettings' +import * as permissionControls from './AgentPermissionChoiceControls' +import { SettingsSegmentedControl } from './SettingsFormControls' +import type { AgentPermissionChoice } from '../../../../shared/tui-agent-permission-choice' import { TooltipProvider } from '../ui/tooltip' import type { AgentPermissionsRenderer } from './AgentLaunchSettingsCatalog' import { AgentPermissionsSetting, AgentsPane } from './AgentsPane' @@ -36,15 +41,20 @@ vi.mock('./agent-launch-settings-transport', () => ({ })) vi.mock('@/hooks/useDetectedAgents', () => ({ useDetectedAgents: (target: unknown) => { - transport.detection(target) return { - detectedIds: ['claude'], + detectedIds: transport.detection(target) ?? ['claude'], detectionFailed: false, isRefreshing: false, refresh: vi.fn() } } })) +async function selectLegacyPermission(label: string, name: string) { + const user = userEvent.setup() + await user.click(screen.getByRole('button', { name: label })) + await user.click(await screen.findByRole('menuitemradio', { name: new RegExp(name) })) +} + const host = () => projectAgentLaunchSettings({ agentDefaultArgs: { claude: '--host-marker' }, @@ -70,7 +80,7 @@ beforeEach(() => { vi.mocked(toast.success).mockClear() transport.read.mockReset().mockResolvedValue(host()) transport.mutate.mockReset().mockResolvedValue(host()) - transport.detection.mockClear() + transport.detection.mockReset() transport.auto.mockReset().mockResolvedValue(true) transport.native.mockReset().mockResolvedValue(true) transport.own.mockReset().mockResolvedValue(false) @@ -122,7 +132,8 @@ describe('host launch settings form', () => { ) await screen.findByDisplayValue('--host-marker') - const auto = screen.getByRole('radio', { name: 'Auto' }) + await userEvent.click(screen.getByRole('button', { name: 'Agent Permissions' })) + const auto = await screen.findByRole('menuitemradio', { name: /Approve for me/ }) expect(auto.getAttribute('aria-disabled') === 'true').toBe(!supported) fireEvent.click(auto) if (supported) { @@ -154,7 +165,7 @@ describe('host launch settings form', () => { ) await screen.findByDisplayValue('--host-marker') - fireEvent.click(screen.getByRole('radio', { name: 'Manual' })) + await selectLegacyPermission('Agent Permissions', "Each agent's own settings") await waitFor(() => expect(transport.mutate).toHaveBeenCalledWith( { environmentId: 'ssh-host', pairingRevision: 17 }, @@ -180,30 +191,161 @@ describe('host launch settings form', () => { ) - it('changes only the agents Auto does not cover from the Other agents control', async () => { - const automatic = projectAgentLaunchSettings( - applyAgentPermissionMode({ mode: 'auto', ...applyAgentPermissionMode({ mode: 'yolo' }) }) - ) - transport.read.mockResolvedValue(automatic) - transport.mutate.mockResolvedValue(automatic) - render(fullPane()) - const others = await screen.findByRole('radiogroup', { name: 'Permissions for other agents' }) - fireEvent.focus(screen.getByRole('button', { name: 'Supported agents' })) - expect((await screen.findByRole('tooltip')).textContent).toContain( - 'Claude, Claude Agent Teams, and Codex' - ) - expect(within(others).getByRole('radio', { name: 'Yolo' }).getAttribute('aria-checked')).toBe( - 'true' - ) - fireEvent.click(within(others).getByRole('radio', { name: 'Manual' })) - await waitFor(() => - expect(transport.mutate).toHaveBeenCalledWith( - { environmentId: 'ssh-host', pairingRevision: 17 }, - { type: 'permissions', mode: 'manual', exceptAuto: true, syncNativeChat: true }, - expect.any(AbortSignal) + it.each([false, true])( + 'changes only agents without Auto from the legacy secondary picker (mixed=%s)', + async (mixed) => { + const source = applyAgentPermissionMode({ + mode: 'auto', + ...applyAgentPermissionMode({ mode: 'yolo' }) + }) + const automatic = projectAgentLaunchSettings({ + ...source, + agentDefaultArgs: { ...source.agentDefaultArgs, ...(mixed ? { gemini: '' } : {}) } + }) + transport.read.mockResolvedValue(automatic) + transport.mutate.mockResolvedValue(automatic) + render(fullPane()) + const trigger = await screen.findByRole('button', { name: 'Permissions for other agents' }) + expect(trigger.textContent).toContain(mixed ? 'Mixed' : 'Full access') + fireEvent.focus(screen.getByRole('button', { name: 'Supported agents' })) + expect((await screen.findByRole('tooltip')).textContent).toContain( + 'Claude, Claude Agent Teams, and Codex' ) - ) - }) + await userEvent.click(screen.getByRole('button', { name: 'Permissions for other agents' })) + expect( + screen.getByRole('menuitemradio', { name: /Full access/ }).getAttribute('aria-checked') + ).toBe(String(!mixed)) + expect(screen.queryByRole('menuitemradio', { name: /Approve for me/ })).toBeNull() + await userEvent.click( + screen.getByRole('menuitemradio', { name: /Each agent's own settings/ }) + ) + await waitFor(() => + expect(transport.mutate).toHaveBeenCalledWith( + { environmentId: 'ssh-host', pairingRevision: 17 }, + { type: 'permissions', mode: 'manual', exceptAuto: true, syncNativeChat: true }, + expect.any(AbortSignal) + ) + ) + } + ) + + it.each(['own', 'full-access'] as const)( + 'preserves the former secondary path inputs and rejection outcomes for %s', + async (choice) => { + const saved = { + agentPermissionDefault: 'auto', + agentPermissionOverrides: { claude: 'ask', gemini: 'own', goose: 'full-access' } + } as const + const initial = projectAgentLaunchSettings(saved) + const mutation = { type: 'permission', choice, withoutAuto: true } as const + const acknowledged = projectAgentLaunchSettings({ + ...saved, + ...agentLaunchSettingsMutationUpdates(saved, mutation, 'darwin') + }) + expect(acknowledged.agentPermission?.default).toBe('auto') + expect(acknowledged.agentPermission?.overrides).toMatchObject({ + claude: 'ask', + gemini: choice, + goose: choice + }) + const requests: AgentLaunchSettingsMutation[][] = [] + for (const presentation of ['segments', 'menu']) { + // Former secondary view, exercising the same production caller and failed-save path. + const previous = + presentation === 'segments' + ? vi + .spyOn(permissionControls, 'AllAgentsPermissionSetting') + .mockImplementation((props) => ( + + value={props.withoutAutoChoice ?? 'mixed'} + onChange={(next) => { + if (next !== 'mixed') { + props.onChangeWithoutAuto(next) + } + }} + ariaLabel="Permissions for other agents" + size="sm" + options={[ + { value: 'own', label: 'Own settings' }, + { value: 'full-access', label: 'Full access' } + ]} + /> + )) + : undefined + transport.read.mockResolvedValue(initial) + transport.mutate + .mockReset() + .mockRejectedValueOnce(new Error('offline')) + .mockResolvedValueOnce(acknowledged) + const view = render(fullPane()) + try { + const user = userEvent.setup() + const trigger = + presentation === 'menu' + ? await screen.findByRole('button', { name: 'Permissions for other agents' }) + : null + if (trigger) { + expect(trigger.textContent).toContain('Mixed') + } else { + await screen.findByRole('radiogroup', { name: 'Permissions for other agents' }) + } + const select = async () => { + if (trigger) { + await user.click(trigger) + } + const role = trigger ? 'menuitemradio' : 'radio' + const group = screen.getByRole(trigger ? 'group' : 'radiogroup', { + name: 'Permissions for other agents' + }) + const items = within(group).getAllByRole(role) + expect(items).toHaveLength(2) + expect(items.every((item) => item.getAttribute('aria-checked') === 'false')).toBe(true) + await user.click( + within(group).getByRole(role, { + name: + choice === 'own' ? /(?:Each agent's own settings|Own settings)/ : /Full access/ + }) + ) + } + await select() + const alert = await screen.findByRole('alert') + expect(alert.textContent).toBe( + 'Couldn’t save agent settings. Check the host connection and try again.' + ) + expect(transport.mutate).toHaveBeenCalledExactlyOnceWith( + { environmentId: 'ssh-host', pairingRevision: 17 }, + mutation, + expect.any(AbortSignal) + ) + if (trigger) { + expect(trigger.textContent).toContain('Mixed') + } + await select() + await waitFor(() => expect(screen.queryByRole('alert')).toBeNull()) + requests.push(transport.mutate.mock.calls.map((call) => call[1])) + const selected = trigger + ? trigger.textContent + : screen + .getByRole('radio', { name: choice === 'own' ? 'Own settings' : 'Full access' }) + .getAttribute('aria-checked') + if (trigger) { + expect(selected).toContain( + choice === 'own' ? "Each agent's own settings" : 'Full access' + ) + } else { + expect(selected).toBe('true') + } + } finally { + view.unmount() + previous?.mockRestore() + } + } + expect(requests).toEqual([ + [mutation, mutation], + [mutation, mutation] + ]) + } + ) it('marks an exception and opens its own permission control from the badge', async () => { const yolo = applyAgentPermissionMode({ mode: 'yolo' }) @@ -214,12 +356,14 @@ describe('host launch settings form', () => { transport.read.mockResolvedValue(settings) transport.mutate.mockResolvedValue(settings) render(fullPane()) - fireEvent.click(await screen.findByRole('button', { name: 'Claude permissions: Manual' })) - const control = await screen.findByRole('radiogroup', { name: 'Claude permissions' }) - await waitFor(() => - expect(document.activeElement).toBe(within(control).getByRole('radio', { name: 'Manual' })) + const user = userEvent.setup() + await user.click( + await screen.findByRole('button', { name: "Claude permissions: Agents' own settings" }) ) - fireEvent.click(within(control).getByRole('radio', { name: 'Auto' })) + const control = await screen.findByRole('button', { name: 'Claude permissions' }) + await waitFor(() => expect(document.activeElement).toBe(control)) + await user.keyboard('{Enter}') + await user.click(await screen.findByRole('menuitemradio', { name: /Approve for me/ })) await waitFor(() => expect(transport.mutate).toHaveBeenCalledWith( { environmentId: 'ssh-host', pairingRevision: 17 }, @@ -229,10 +373,52 @@ describe('host launch settings form', () => { ) }) + it('keeps legacy mixed global state unselected and omits unsupported row Auto', async () => { + transport.detection.mockReturnValue(['claude', 'gemini']) + transport.read.mockResolvedValue( + projectAgentLaunchSettings({ + ...applyAgentPermissionMode({ mode: 'yolo' }), + agentDefaultArgs: { + ...applyAgentPermissionMode({ mode: 'yolo' }).agentDefaultArgs, + claude: '' + } + }) + ) + render(fullPane()) + const global = await screen.findByRole('button', { name: 'Agent Permissions' }) + expect(global.textContent).toContain('Mixed') + const user = userEvent.setup() + await user.click(global) + expect( + screen + .getAllByRole('menuitemradio') + .every((item) => item.getAttribute('aria-checked') === 'false') + ).toBe(true) + await user.keyboard('{Escape}') + const gemini = screen.getByText('Gemini', { selector: 'span' }).closest('div.py-3') + if (!(gemini instanceof HTMLElement)) { + throw new Error('Missing Gemini row') + } + const expand = within(gemini).queryByRole('button', { name: 'Expand agent settings' }) + if (expand) { + await user.click(expand) + } + await user.click(within(gemini).getByRole('button', { name: 'Gemini permissions' })) + expect(screen.queryByRole('menuitemradio', { name: /Approve for me/ })).toBeNull() + await user.click(screen.getByRole('menuitemradio', { name: /Full access/ })) + await waitFor(() => + expect(transport.mutate).toHaveBeenCalledWith( + { environmentId: 'ssh-host', pairingRevision: 17 }, + { type: 'permissions', agent: 'gemini', mode: 'yolo', syncNativeChat: true }, + expect.any(AbortSignal) + ) + ) + }) + it('replaces custom arguments with the expected mode from the row', async () => { render(fullPane()) await screen.findByDisplayValue('--host-marker') - fireEvent.click(screen.getByRole('button', { name: 'Replace with Manual' })) + fireEvent.click(screen.getByRole('button', { name: "Replace with Agents' own settings" })) await waitFor(() => expect(transport.mutate).toHaveBeenCalledWith( { environmentId: 'ssh-host', pairingRevision: 17 }, @@ -251,7 +437,7 @@ describe('host launch settings form', () => { transport.read.mockResolvedValue(settings) transport.mutate.mockResolvedValue(settings) render(fullPane()) - fireEvent.click(await screen.findByRole('button', { name: 'Replace with Yolo' })) + fireEvent.click(await screen.findByRole('button', { name: 'Replace with Full access' })) await waitFor(() => expect(transport.mutate).toHaveBeenCalledWith( { environmentId: 'ssh-host', pairingRevision: 17 }, @@ -369,7 +555,7 @@ describe('host launch settings form', () => { ) expect(updateDesktop).not.toHaveBeenCalled() expect(desktop.agentDefaultEnv?.claude?.DESKTOP_KEY).toBe('desktop-secret') - fireEvent.click(screen.getByRole('radio', { name: 'Manual' })) + await selectLegacyPermission('Agent Permissions', "Each agent's own settings") await waitFor(() => expect(transport.mutate).toHaveBeenCalledWith( { environmentId: 'ssh-host', pairingRevision: 17 }, diff --git a/src/renderer/src/components/settings/agent-permission-mode-label.ts b/src/renderer/src/components/settings/agent-permission-mode-label.ts index 965969d4fa6..04db7c10b1f 100644 --- a/src/renderer/src/components/settings/agent-permission-mode-label.ts +++ b/src/renderer/src/components/settings/agent-permission-mode-label.ts @@ -1,10 +1,10 @@ import type { AgentPermissionMode } from '../../../../shared/tui-agent-permissions' -import { translate } from '@/i18n/i18n' +import { agentPermissionChoiceLabel } from './AgentPermissionChoiceControls' + +export function agentPermissionModeChoice(mode: Exclude) { + return mode === 'manual' ? 'own' : mode === 'yolo' ? 'full-access' : 'auto' +} export function agentPermissionModeLabel(mode: Exclude): string { - return mode === 'auto' - ? translate('settings.agents.autoPermissions', 'Auto') - : mode === 'yolo' - ? translate('auto.components.settings.AgentsPane.agentPermissionsYolo', 'Yolo') - : translate('auto.components.settings.AgentsPane.agentPermissionsManual', 'Manual') + return agentPermissionChoiceLabel(agentPermissionModeChoice(mode), null) } diff --git a/src/renderer/src/components/settings/agents-search.ts b/src/renderer/src/components/settings/agents-search.ts index d24aee58c12..ce37be9a355 100644 --- a/src/renderer/src/components/settings/agents-search.ts +++ b/src/renderer/src/components/settings/agents-search.ts @@ -55,7 +55,8 @@ function buildAgentSettingsKeywords(): string[] { { key: 'auto.components.settings.agents.search.permissions', fallback: 'permissions' }, { key: 'auto.components.settings.agents.search.yolo', fallback: 'yolo', englishOnly: true }, { key: 'auto.components.settings.agents.search.manual', fallback: 'manual' }, - { key: 'settings.agents.autoPermissions', fallback: 'Auto' }, + { key: 'settings.agents.permissionEachOwn', fallback: "Each agent's own settings" }, + { key: 'components.native-chat.composer.permissionFullAccess', fallback: 'Full access' }, { key: 'settings.agents.approveForMe', fallback: 'approve for me' }, { key: 'auto.components.settings.agents.search.e2b7c0dcd7', @@ -175,7 +176,7 @@ const getAllAgentsPaneSearchEntries = createLocalizedCatalog(() => [ ), description: translate( 'settings.agents.permissionsSearchDescription', - 'Launch agents with Manual, Auto, or Yolo permissions.' + 'Choose Approve for me, Full access, or each agent’s own permission settings.' ), keywords: [ ...translateSearchKeyword('auto.components.settings.agents.search.permission', 'permission'), @@ -185,7 +186,12 @@ const getAllAgentsPaneSearchEntries = createLocalizedCatalog(() => [ ), ...translateSearchKeyword('auto.components.settings.agents.search.yolo', 'yolo'), ...translateSearchKeyword('auto.components.settings.agents.search.manual', 'manual'), - ...translateSearchKeyword('settings.agents.autoPermissions', 'Auto'), + ...translateSearchKeyword('settings.agents.permissionEachOwn', "Each agent's own settings"), + ...translateSearchKeyword('settings.agents.approveForMe', 'approve for me'), + ...translateSearchKeyword( + 'components.native-chat.composer.permissionFullAccess', + 'Full access' + ), ...translateSearchKeyword('auto.components.settings.agents.search.skip', 'skip'), ...translateSearchKeyword('auto.components.settings.agents.search.checks', 'checks') ] diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index 25e22e2f836..5c5798fcc8d 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -2114,7 +2114,6 @@ }, "settings": { "agents": { - "approveForMe": "approve for me", "permissionOverrideMore_one": "and {{count}} more", "permissionOverrideMore_other": "and {{count}} more", "permissionOverridesFollow_one": "{{count}} agent now follows {{mode}}", diff --git a/src/renderer/src/i18n/locales/de.json b/src/renderer/src/i18n/locales/de.json index 53744fc17e1..020f233429c 100644 --- a/src/renderer/src/i18n/locales/de.json +++ b/src/renderer/src/i18n/locales/de.json @@ -760,7 +760,6 @@ "defaultOpensAsScope": "Gilt für neue {{value0}}-Sitzungen, die von diesem Computer aus gestartet werden, auch auf gekoppelten Hosts. Entspricht dem zugehörigen Schalter unter „Anbieter“.", "save": "Speichern", "clearDefault": "Standardagent-Auswahl aufheben", - "autoPermissions": "Automatisch", "autoUnsupported": "„Automatisch“ erfordert eine neuere Orca-Version auf diesem Host.", "permissionsDescription": "Legt fest, wie viel Agenten ohne Rückfrage tun dürfen. Agenten mit benutzerdefinierten Startargumenten behalten diese bei.", "permissionsAutoDescription": "Unterstützte Agenten genehmigen sichere Aktionen selbstständig.", @@ -7506,8 +7505,6 @@ "8fbe1f37c1": "Umgebung", "2d133152fa": "Keine Standardumgebung", "agentPermissions": "Agentenberechtigungen", - "agentPermissionsYolo": "Yolo", - "agentPermissionsManual": "Manuell", "3f1bdf3cb4": "Der Umgebungstext ist zu groß, um sicher analysiert zu werden.", "codexSessionSource": "Codex-Home-Verzeichnis für den Import", "codexSessionSourceInfo": "Informationen zum Import des Codex-Verlaufs", diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 10abfb8869c..5bd991aa00c 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -800,13 +800,12 @@ "defaultOpensAsScope": "Applies to new {{value0}} sessions launched from this computer, including on paired hosts. Matches its Providers switch.", "save": "Save", "clearDefault": "Clear default agent", - "autoPermissions": "Auto", - "autoUnsupported": "Auto needs a newer Orca on this host.", + "autoUnsupported": "Approve for me needs a newer Orca on this host.", "permissionsDescription": "How much agents can do without asking you. Agents with custom launch arguments keep them.", "permissionsAutoDescription": "Supported agents approve safe actions on their own.", "permissionsMixed": "Most agents use {{mode}}. Choosing a mode resets the marked exceptions, except custom arguments.", "permissionsMixedNoMajority": "Agents use different settings. Choosing a mode applies to all.", - "permissionsSearchDescription": "Launch agents with Manual, Auto, or Yolo permissions.", + "permissionsSearchDescription": "Choose Approve for me, Full access, or each agent’s own permission settings.", "otherAgentsPermissions": "Other agents", "otherAgentsPermissionsLabel": "Permissions for other agents", "customArgsBadge": "Custom", @@ -829,6 +828,7 @@ "permissionOwnDescription": "Terminals and chats follow the agent’s own permission settings.", "permissionOwnShort": "Own settings", "permissionWithoutAuto": "Agents without Approve for me", + "permissionMixed": "Mixed", "permissionSurfaceDifference": "Terminals use the agent’s own settings; new chats use {{mode}}.", "permissionSameAsAll": "Same as all agents", "choiceChipRemove": "Remove {{flag}}", @@ -7584,8 +7584,6 @@ "8fbe1f37c1": "Environment", "2d133152fa": "No default environment", "agentPermissions": "Agent Permissions", - "agentPermissionsYolo": "Yolo", - "agentPermissionsManual": "Manual", "3f1bdf3cb4": "Environment text is too large to parse safely.", "codexSessionSource": "Codex home to import from", "codexSessionSourceInfo": "About importing Codex history", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index ebc7d838e0e..63f0474b71f 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -331,7 +331,6 @@ "clearDefault": "Borrar agente predeterminado", "customArgsBadge": "Personalizado", "rowPermissions": "Permisos", - "autoPermissions": "Automático", "otherAgentsPermissions": "Otros agentes", "permissionsAutoDescription": "Los agentes compatibles aprueban las acciones seguras por su cuenta.", "permissionsSearchDescription": "Inicia agentes con permisos Manual, Automático o Yolo.", @@ -7159,8 +7158,6 @@ "8fbe1f37c1": "Entorno", "2d133152fa": "Sin entorno predeterminado", "agentPermissions": "Permisos de Agents", - "agentPermissionsYolo": "Yolo", - "agentPermissionsManual": "Manual", "3f1bdf3cb4": "El texto de entorno es demasiado grande para analizarlo de forma segura.", "codexSessionSource": "Directorio Codex a importar", "codexSessionSourceInfo": "Sobre la importación del historial de Codex", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 02b78222ec0..ddd62db773e 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -551,7 +551,6 @@ "clearDefault": "Effacer l'agent par défaut", "customArgsBadge": "Personnalisé", "rowPermissions": "Autorisations", - "autoPermissions": "Auto", "autoUnsupported": "Le mode automatique nécessite une version plus récente d’Orca sur cet hôte.", "otherAgentsPermissions": "Autres agents", "permissionsMixed": "La plupart des agents utilisent {{mode}}. Le choix d'un mode réinitialise les exceptions marquées, à l'exception des arguments personnalisés.", @@ -7387,8 +7386,6 @@ "8fbe1f37c1": "Environnement", "2d133152fa": "Aucun environnement par défaut", "agentPermissions": "Permissions des agents", - "agentPermissionsYolo": "Yolo", - "agentPermissionsManual": "Manuel", "3f1bdf3cb4": "Le texte d'environnement est trop volumineux pour être analysé sans risque.", "codexSessionSource": "Home Codex d'où importer", "codexSessionSourceInfo": "À propos de l'import de l'historique Codex", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index c93a448916f..f185b72b763 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -329,7 +329,6 @@ "defaultOpensAsScope": "このコンピューターから起動する新しい{{value0}}セッション(ペアリングしたホスト上も含む)に適用されます。プロバイダーのスイッチと連動します。", "save": "保存", "clearDefault": "デフォルトの Agent をクリア", - "autoPermissions": "自動", "customArgsBadge": "カスタム", "rowPermissions": "権限", "otherAgentsPermissionsLabel": "他の Agent の権限", @@ -6868,8 +6867,6 @@ "8fbe1f37c1": "環境", "2d133152fa": "デフォルト環境なし", "agentPermissions": "Agent の権限", - "agentPermissionsYolo": "Yolo", - "agentPermissionsManual": "手動", "3f1bdf3cb4": "環境テキストが大きすぎるため安全に解析できません。", "codexSessionSource": "インポートする Codex ホーム", "codexSessionSourceInfo": "Codex 履歴のインポートについて", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index eec2bb910a5..dc8fccf3a55 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -329,7 +329,6 @@ "defaultOpensAsScope": "이 컴퓨터에서 시작하는 새 {{value0}} 세션(페어링된 호스트 포함)에 적용됩니다. 공급자의 스위치와 같습니다.", "save": "저장", "clearDefault": "기본 에이전트 지우기", - "autoPermissions": "자동", "customArgsBadge": "사용자 정의", "rowPermissions": "권한", "permissionsAutoDescription": "지원되는 에이전트는 안전한 작업을 스스로 승인합니다.", @@ -6868,8 +6867,6 @@ "8fbe1f37c1": "환경", "2d133152fa": "기본 환경 없음", "agentPermissions": "에이전트 권한", - "agentPermissionsYolo": "Yolo", - "agentPermissionsManual": "수동", "3f1bdf3cb4": "환경 텍스트가 너무 커서 안전하게 파싱할 수 없습니다.", "codexSessionSource": "가져올 Codex 홈", "codexSessionSourceInfo": "Codex 기록 가져오기 정보", diff --git a/src/renderer/src/i18n/locales/pt.json b/src/renderer/src/i18n/locales/pt.json index 9489230ce43..f485de595e5 100644 --- a/src/renderer/src/i18n/locales/pt.json +++ b/src/renderer/src/i18n/locales/pt.json @@ -1491,7 +1491,6 @@ "hostDefaults": "Padrões para novos agentes no host selecionado.", "save": "Salvar", "clearDefault": "Limpar agente padrão", - "autoPermissions": "Automático", "autoUnsupported": "O modo Automático requer uma versão mais recente do Orca neste host.", "permissionsDescription": "O que os agentes podem fazer sem pedir sua aprovação. Agentes com argumentos de inicialização personalizados os mantêm.", "permissionsAutoDescription": "Agentes compatíveis aprovam ações seguras por conta própria.", @@ -5570,8 +5569,6 @@ "8fbe1f37c1": "Ambiente", "2d133152fa": "Nenhum ambiente padrão", "agentPermissions": "Permissões do Agente", - "agentPermissionsYolo": "Yolo", - "agentPermissionsManual": "Manual", "3f1bdf3cb4": "O texto do ambiente é muito grande para interpretar com segurança.", "codexSessionSource": "Diretório do Codex de onde importar", "codexSessionSourceInfo": "Sobre importar o histórico do Codex", diff --git a/src/renderer/src/i18n/locales/ru.json b/src/renderer/src/i18n/locales/ru.json index 2287d026be6..4e744eebd20 100644 --- a/src/renderer/src/i18n/locales/ru.json +++ b/src/renderer/src/i18n/locales/ru.json @@ -806,7 +806,6 @@ "defaultOpensAsScope": "Для новых сессий {{value0}}, запускаемых с этого компьютера, в том числе на подключённых серверах. Соответствует переключателю в разделе «Провайдеры».", "save": "Сохранить", "clearDefault": "Сбросить агента по умолчанию", - "autoPermissions": "Автоматически", "autoUnsupported": "Для автоматического режима нужна более новая версия Orca на этом сервере.", "permissionsDescription": "Что агенты могут делать без вашего разрешения. Агенты с собственными аргументами запуска сохраняют их.", "permissionsAutoDescription": "Поддерживаемые агенты самостоятельно одобряют безопасные действия.", @@ -7848,8 +7847,6 @@ "8fbe1f37c1": "Переменные окружения", "2d133152fa": "Нет переменных окружения по умолчанию", "agentPermissions": "Разрешения агентов", - "agentPermissionsYolo": "Yolo", - "agentPermissionsManual": "Вручную", "3f1bdf3cb4": "Текст переменных окружения слишком велик для безопасного разбора.", "codexSessionSource": "Домашняя папка Codex для импорта", "codexSessionSourceInfo": "Об импорте истории Codex", diff --git a/src/renderer/src/i18n/locales/tr.json b/src/renderer/src/i18n/locales/tr.json index 882f5a333b0..b92a6785e7a 100644 --- a/src/renderer/src/i18n/locales/tr.json +++ b/src/renderer/src/i18n/locales/tr.json @@ -752,7 +752,6 @@ "defaultOpensAsScope": "Eşleştirilmiş sunuculardakiler dahil, bu bilgisayardan başlatılan yeni {{value0}} oturumları için geçerlidir. Sağlayıcılar bölümündeki seçimiyle eşleşir.", "save": "Kaydet", "clearDefault": "Varsayılan ajanı temizle", - "autoPermissions": "Otomatik", "autoUnsupported": "Otomatik modu için bu ana makinede daha yeni bir Orca sürümü gerekir.", "permissionsDescription": "Ajanların size sormadan ne kadar işlem yapabileceği. Özel başlatma argümanları olan ajanlar bunları korur.", "permissionsAutoDescription": "Desteklenen ajanlar güvenli işlemleri kendileri onaylar.", @@ -7492,8 +7491,6 @@ "8fbe1f37c1": "Ortam", "2d133152fa": "Varsayılan ortam yok", "agentPermissions": "Ajan İzinleri", - "agentPermissionsYolo": "Yolo", - "agentPermissionsManual": "Manuel", "3f1bdf3cb4": "Ortam metni güvenli bir şekilde ayrıştırılamayacak kadar büyük.", "codexSessionSource": "İçe aktarılacak Codex ana dizini", "codexSessionSourceInfo": "Codex geçmişini içe aktarma hakkında", diff --git a/src/renderer/src/i18n/locales/zh-Hant.json b/src/renderer/src/i18n/locales/zh-Hant.json index 6767771e275..71533b5dd56 100644 --- a/src/renderer/src/i18n/locales/zh-Hant.json +++ b/src/renderer/src/i18n/locales/zh-Hant.json @@ -725,7 +725,6 @@ "defaultOpensAsScope": "適用於從這台電腦啟動的新 {{value0}} 工作階段,包括在已配對主機上啟動的工作階段。與「提供者」中對應的開關一致。", "save": "儲存", "clearDefault": "清除預設代理程式", - "autoPermissions": "自動", "autoUnsupported": "此主機上的 Orca 需更新至較新版本才能使用自動模式。", "permissionsDescription": "設定代理程式無須詢問即可執行的操作範圍。已自訂啟動參數的代理程式會保留這些參數。", "permissionsAutoDescription": "支援的代理程式會自行核准安全的操作。", @@ -7329,8 +7328,6 @@ "8fbe1f37c1": "環境", "2d133152fa": "無預設環境", "agentPermissions": "代理程式權限", - "agentPermissionsYolo": "Yolo", - "agentPermissionsManual": "手動", "3f1bdf3cb4": "環境文字過長,無法安全解析。", "codexSessionSource": "要匯入的 Codex 主目錄", "codexSessionSourceInfo": "關於匯入 Codex 歷史記錄", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 0f35e418777..84a277db214 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -332,7 +332,6 @@ "defaultOpensAsScope": "适用于从这台电脑启动的新 {{value0}} 会话,包括在已配对主机上启动的会话。与提供方中的开关一致。", "save": "保存", "clearDefault": "清除默认智能体", - "autoPermissions": "自动", "customArgsBadge": "自定义", "rowPermissions": "权限", "permissionsDescription": "设置智能体在无需询问您的情况下可以执行的操作范围。已设置自定义启动参数的智能体会保留这些参数。", @@ -6868,8 +6867,6 @@ "8fbe1f37c1": "环境", "2d133152fa": "无默认环境", "agentPermissions": "智能体权限", - "agentPermissionsYolo": "Yolo", - "agentPermissionsManual": "手动", "3f1bdf3cb4": "环境文本过长,无法安全解析。", "codexSessionSource": "要导入的 Codex 主目录", "codexSessionSourceInfo": "关于导入 Codex 历史记录",