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
This commit is contained in:
Brennan Benson
2026-10-29 15:37:16 -07:00
committed by GitHub
parent a51164379e
commit 019318b8df
21 changed files with 497 additions and 219 deletions
@@ -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 (
<section className="space-y-1">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
@@ -84,35 +68,20 @@ export function AllAgentsPermissionSetting({
>
{choice === null ? (
<span className="h-4" />
) : choice === 'own' ? (
<span className="flex min-w-0 items-center gap-1.5">
<Terminal className="size-3.5 shrink-0" />
{ownLabel}
</span>
) : (
<NativeChatPermissionModeName mode={choice} truncate />
<AgentPermissionChoiceName choice={choice} ownLabel={ownLabel} truncate />
)}
<ChevronDown className="size-3 shrink-0" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" collisionPadding={8} className="w-72">
<DropdownMenuRadioGroup aria-label={title} value={choice ?? ''}>
<NativeChatPermissionModeItems
modes={['ask', 'auto', 'full-access']}
<AgentPermissionChoiceItems
choices={['ask', 'auto', 'full-access', 'own']}
ownLabel={ownLabel}
chatOwnSupported={chatOwnSupported}
onSelect={onChange}
/>
<DropdownMenuSeparator />
<DropdownMenuRadioItem value="own" onSelect={() => onChange('own')}>
<div className="min-w-0 py-0.5">
<span className="flex items-center gap-1.5">
<Terminal className="size-3.5 shrink-0" />
{ownLabel}
</span>
<div className="text-xs font-normal text-muted-foreground">
{ownDescription(chatOwnSupported)}
</div>
</div>
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
@@ -129,26 +98,43 @@ export function AllAgentsPermissionSetting({
<span className="text-xs text-muted-foreground">
{translate('settings.agents.permissionWithoutAuto', 'Agents without Approve for me')}
</span>
<SettingsSegmentedControl<AgentPermissionChoice | 'mixed'>
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)
}))}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="xs"
aria-label={withoutAutoTitle}
className="min-w-0 max-w-full"
>
{withoutAutoChoice === null ? (
<span className="truncate">
{translate('settings.agents.permissionMixed', 'Mixed')}
</span>
) : (
<AgentPermissionChoiceName
choice={withoutAutoChoice}
ownLabel={ownLabel}
truncate
/>
)}
<ChevronDown className="size-3 shrink-0" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" collisionPadding={8} className="w-72">
<DropdownMenuRadioGroup
aria-label={withoutAutoTitle}
value={withoutAutoChoice ?? ''}
>
<AgentPermissionChoiceItems
choices={['full-access', 'own']}
ownLabel={ownLabel}
chatOwnSupported={chatOwnSupported}
onSelect={onChangeWithoutAuto}
/>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
) : null}
</div>
@@ -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 (
<div
className={cn(
@@ -221,13 +207,14 @@ export function AgentPermissionMenu({
})}
className={cn('min-w-0 max-w-full', layout === 'stacked' && 'w-full justify-between')}
>
<span className="truncate text-xs">
{permission.source === 'all' ? `${followAll} · ${current}` : current}
<span className="flex min-w-0 items-center gap-1.5 text-xs">
{permission.source === 'all' ? <span className="shrink-0">{followAll} ·</span> : null}
<AgentPermissionChoiceName choice={permission.choice} ownLabel={ownLabel} truncate />
</span>
<ChevronDown className="size-3 shrink-0" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" collisionPadding={8} className="w-80">
<DropdownMenuContent align="start" collisionPadding={8} className="w-72">
<DropdownMenuRadioGroup
value={permission.source === 'all' ? 'default' : permission.choice}
>
@@ -236,30 +223,24 @@ export function AgentPermissionMenu({
disabled={disabled}
onSelect={() => permission.onSelect('default')}
>
<span className="truncate">
{followAll} · {agentPermissionChoiceLabel(permission.defaultChoice, label)}
<span className="flex min-w-0 items-center gap-1.5 py-0.5">
<span className="shrink-0">{followAll} ·</span>
<AgentPermissionChoiceName
choice={permission.defaultChoice}
ownLabel={ownLabel}
truncate
/>
</span>
</DropdownMenuRadioItem>
<DropdownMenuSeparator />
{permission.choices.map((choice) => (
<DropdownMenuRadioItem
key={choice}
value={choice}
disabled={disabled}
onSelect={() => permission.onSelect(choice)}
>
<div className="min-w-0 py-0.5">
<div className={cn(choice === 'full-access' && 'text-status-warning')}>
{agentPermissionChoiceLabel(choice, label)}
</div>
<div className="text-xs font-normal text-muted-foreground">
{choice === 'own'
? ownDescription(permission.chatOwnSupported === true)
: nativeChatPermissionModeDescription(choice, agentId)}
</div>
</div>
</DropdownMenuRadioItem>
))}
<AgentPermissionChoiceItems
choices={permission.choices}
agent={agentId}
ownLabel={ownLabel}
chatOwnSupported={permission.chatOwnSupported === true}
disabled={disabled}
onSelect={permission.onSelect}
/>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
@@ -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 <NativeChatPermissionModeName mode={choice} truncate={truncate} />
}
return (
<span className="flex min-w-0 items-center gap-1.5">
<Terminal className="size-3.5 shrink-0" aria-hidden />
{truncate ? <span className="truncate">{ownLabel}</span> : <span>{ownLabel}</span>}
</span>
)
}
/** 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 (
<>
<NativeChatPermissionModeItems
modes={modes}
agent={agent}
disabled={disabled}
onSelect={onSelect}
/>
{choices.includes('own') ? (
<>
{modes.length > 0 ? <DropdownMenuSeparator /> : null}
<DropdownMenuRadioItem value="own" disabled={disabled} onSelect={() => onSelect('own')}>
<div className="min-w-0 py-0.5">
<AgentPermissionChoiceName choice="own" ownLabel={ownLabel} />
<div className="text-xs font-normal text-muted-foreground">
{ownDescription(chatOwnSupported)}
</div>
</div>
</DropdownMenuRadioItem>
</>
) : null}
</>
)
}
@@ -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<AgentPermissionMode, 'mixed'>) => void
}): React.JSX.Element {
const ownLabel = translate('settings.agents.permissionEachOwn', "Each agent's own settings")
const choice = mode === 'mixed' ? '' : agentPermissionModeChoice(mode)
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="xs"
aria-label={label}
className="min-w-0 max-w-full"
>
{choice === '' ? (
<span className="truncate">
{translate('settings.agents.permissionMixed', 'Mixed')}
</span>
) : (
<AgentPermissionChoiceName choice={choice} ownLabel={ownLabel} truncate />
)}
<ChevronDown className="size-3 shrink-0" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" collisionPadding={8} className="w-72">
<DropdownMenuRadioGroup aria-label={label} value={choice}>
{autoAvailable ? (
<AgentPermissionChoiceItems
choices={['auto']}
ownLabel={ownLabel}
chatOwnSupported={false}
disabled={autoDisabled}
onSelect={() => onChange('auto')}
/>
) : null}
<AgentPermissionChoiceItems
choices={['full-access', 'own']}
ownLabel={ownLabel}
chatOwnSupported={false}
onSelect={(next) => onChange(next === 'own' ? 'manual' : 'yolo')}
/>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -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<AgentPermissionMode, 'mixed'>
@@ -96,25 +96,20 @@ export function AgentPermissionsSetting(props: AgentPermissionsSettingProps): Re
</p>
{!autoSupported ? (
<p className="text-xs text-muted-foreground">
{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.'
)}
</p>
) : null}
</div>
<div className="flex shrink-0 flex-col items-end">
<SettingsSegmentedControl<AgentPermissionMode>
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') }
]}
<AgentPermissionModeMenu
mode={mode}
onChange={onChange}
label={title}
autoAvailable
autoDisabled={!autoSupported}
/>
{/* Keep mounted for animation; inert excludes hidden controls from focus and accessibility. */}
<div
@@ -138,22 +133,18 @@ export function AgentPermissionsSetting(props: AgentPermissionsSettingProps): Re
<span className="text-xs text-muted-foreground">
{translate('settings.agents.otherAgentsPermissions', 'Other agents')}
</span>
<SettingsSegmentedControl<AgentPermissionMode>
value={nonAutoMode ?? 'mixed'}
<AgentPermissionModeMenu
mode={nonAutoMode ?? 'mixed'}
onChange={(next) => {
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}
/>
</div>
</div>
@@ -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}
</span>
) : (
<SettingsSegmentedControl<AgentPermissionMode>
value={permission.mode}
onChange={(next) => {
if (next !== 'mixed') {
permission.onChange(next)
}
}}
ariaLabel={translate('settings.agents.rowPermissionsLabel', '{{agent}} permissions', {
<AgentPermissionModeMenu
mode={permission.mode}
onChange={permission.onChange}
label={translate('settings.agents.rowPermissionsLabel', '{{agent}} permissions', {
agent: label
})}
size="sm"
options={[
{ value: 'manual', label: agentPermissionModeLabel('manual') },
...(permission.autoAvailable
? [{ value: 'auto' as const, label: agentPermissionModeLabel('auto') }]
: []),
{ value: 'yolo', label: agentPermissionModeLabel('yolo') }
]}
autoAvailable={permission.autoAvailable}
/>
)}
</div>
@@ -518,11 +518,15 @@ describe('AgentsPane', () => {
})
it.each([
['manual', undefined, 'How much agents can do without asking you.', 1],
['auto', 'yolo', 'Supported agents</button> approve safe actions on their own.', 2],
['auto', 'mixed', 'Supported agents</button> 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</button> approve safe actions on their own.'],
['auto', 'mixed', 'Supported agents</button> 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(
<TooltipProvider>
<AgentPermissionsSetting
@@ -537,7 +541,6 @@ describe('AgentsPane', () => {
)
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', () => {
<AgentPermissionsSetting mode="yolo" autoSupported={false} onChange={vi.fn()} />
</TooltipProvider>
)
expect(markup).toContain('Auto needs a newer Orca on this host.')
expect(markup).toMatch(/aria-disabled="true"[^>]*>Auto</)
expect(markup).toContain('Approve for me needs a newer Orca on this host.')
})
it('keeps catalog agent ids, labels, and commands discoverable in settings search', () => {
@@ -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', () => {
</TooltipProvider>
)
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', () => {
</TooltipProvider>
)
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', () => {
</TooltipProvider>
)
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) => (
<SettingsSegmentedControl<AgentPermissionChoice | 'mixed'>
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 },
@@ -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<AgentPermissionMode, 'mixed'>) {
return mode === 'manual' ? 'own' : mode === 'yolo' ? 'full-access' : 'auto'
}
export function agentPermissionModeLabel(mode: Exclude<AgentPermissionMode, 'mixed'>): 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)
}
@@ -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')
]
-1
View File
@@ -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}}",
-3
View File
@@ -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": "<agents>Unterstützte Agenten</agents> 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",
+3 -5
View File
@@ -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": "<agents>Supported agents</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",
-3
View File
@@ -331,7 +331,6 @@
"clearDefault": "Borrar agente predeterminado",
"customArgsBadge": "Personalizado",
"rowPermissions": "Permisos",
"autoPermissions": "Automático",
"otherAgentsPermissions": "Otros agentes",
"permissionsAutoDescription": "Los <agents>agentes compatibles</agents> 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",
-3
View File
@@ -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",
-3
View File
@@ -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 履歴のインポートについて",
-3
View File
@@ -329,7 +329,6 @@
"defaultOpensAsScope": "이 컴퓨터에서 시작하는 새 {{value0}} 세션(페어링된 호스트 포함)에 적용됩니다. 공급자의 스위치와 같습니다.",
"save": "저장",
"clearDefault": "기본 에이전트 지우기",
"autoPermissions": "자동",
"customArgsBadge": "사용자 정의",
"rowPermissions": "권한",
"permissionsAutoDescription": "<agents>지원되는 에이전트</agents>는 안전한 작업을 스스로 승인합니다.",
@@ -6868,8 +6867,6 @@
"8fbe1f37c1": "환경",
"2d133152fa": "기본 환경 없음",
"agentPermissions": "에이전트 권한",
"agentPermissionsYolo": "Yolo",
"agentPermissionsManual": "수동",
"3f1bdf3cb4": "환경 텍스트가 너무 커서 안전하게 파싱할 수 없습니다.",
"codexSessionSource": "가져올 Codex 홈",
"codexSessionSourceInfo": "Codex 기록 가져오기 정보",
-3
View File
@@ -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": "<agents>Agentes compatíveis</agents> 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",
-3
View File
@@ -806,7 +806,6 @@
"defaultOpensAsScope": "Для новых сессий {{value0}}, запускаемых с этого компьютера, в том числе на подключённых серверах. Соответствует переключателю в разделе «Провайдеры».",
"save": "Сохранить",
"clearDefault": "Сбросить агента по умолчанию",
"autoPermissions": "Автоматически",
"autoUnsupported": "Для автоматического режима нужна более новая версия Orca на этом сервере.",
"permissionsDescription": "Что агенты могут делать без вашего разрешения. Агенты с собственными аргументами запуска сохраняют их.",
"permissionsAutoDescription": "<agents>Поддерживаемые агенты</agents> самостоятельно одобряют безопасные действия.",
@@ -7848,8 +7847,6 @@
"8fbe1f37c1": "Переменные окружения",
"2d133152fa": "Нет переменных окружения по умолчанию",
"agentPermissions": "Разрешения агентов",
"agentPermissionsYolo": "Yolo",
"agentPermissionsManual": "Вручную",
"3f1bdf3cb4": "Текст переменных окружения слишком велик для безопасного разбора.",
"codexSessionSource": "Домашняя папка Codex для импорта",
"codexSessionSourceInfo": "Об импорте истории Codex",
-3
View File
@@ -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": "<agents>Desteklenen ajanlar</agents> 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",
@@ -725,7 +725,6 @@
"defaultOpensAsScope": "適用於從這台電腦啟動的新 {{value0}} 工作階段,包括在已配對主機上啟動的工作階段。與「提供者」中對應的開關一致。",
"save": "儲存",
"clearDefault": "清除預設代理程式",
"autoPermissions": "自動",
"autoUnsupported": "此主機上的 Orca 需更新至較新版本才能使用自動模式。",
"permissionsDescription": "設定代理程式無須詢問即可執行的操作範圍。已自訂啟動參數的代理程式會保留這些參數。",
"permissionsAutoDescription": "<agents>支援的代理程式</agents>會自行核准安全的操作。",
@@ -7329,8 +7328,6 @@
"8fbe1f37c1": "環境",
"2d133152fa": "無預設環境",
"agentPermissions": "代理程式權限",
"agentPermissionsYolo": "Yolo",
"agentPermissionsManual": "手動",
"3f1bdf3cb4": "環境文字過長,無法安全解析。",
"codexSessionSource": "要匯入的 Codex 主目錄",
"codexSessionSourceInfo": "關於匯入 Codex 歷史記錄",
-3
View File
@@ -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 历史记录",