mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
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:
@@ -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
@@ -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}}",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 履歴のインポートについて",
|
||||
|
||||
@@ -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 기록 가져오기 정보",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 歷史記錄",
|
||||
|
||||
@@ -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 历史记录",
|
||||
|
||||
Reference in New Issue
Block a user