Keep the permission pill inside Arguments and share the Permissions menu (#32971)

* Keep the permission pill inside the Arguments box

* Share the Settings permission menu and commit Arguments on field exit

* Honor changed permission choices and acknowledged host picks in reset notices

* Show acknowledged permission resets with scoped Undo

* Consume removed permission recipes in the Arguments draft

* Keep reset notices separate from the inline permission field

* Use Textarea variant for composite arguments field

* Fix permission test query typing and sync English runtime catalog

* Flow arguments beneath the permission chip

* Keep composite argument typography in Textarea variant

* Share native scrolling between permission chip and arguments

* Place all-agents permission menu beside its label

* Preserve permission menu width while settings load
This commit is contained in:
Brennan Benson
2026-10-25 14:30:15 -07:00
committed by GitHub
parent 65e513b193
commit 27e7f8fd6d
14 changed files with 408 additions and 148 deletions
@@ -26,6 +26,7 @@ vi.mock('@/components/ui/tooltip', () => ({
vi.mock('@/components/ui/dropdown-menu', async () => {
const React = await import('react')
const Selection = React.createContext<string | undefined>(undefined)
return {
DropdownMenu: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DropdownMenuTrigger: ({
@@ -45,48 +46,38 @@ vi.mock('@/components/ui/dropdown-menu', async () => {
DropdownMenuRadioGroup: ({
children,
value,
onValueChange,
'aria-label': ariaLabel
}: {
children: React.ReactNode
value?: string
onValueChange?: (value: string) => void
'aria-label'?: string
}) => (
<div role="radiogroup" aria-label={ariaLabel}>
{React.Children.map(children, (child) => {
if (
!React.isValidElement<{
value?: string
disabled?: boolean
children?: React.ReactNode
onSelect?: () => void
}>(child)
) {
return child
}
const props = child.props
return (
<button
role="radio"
aria-checked={props.value === value}
disabled={props.disabled}
data-value={props.value}
onClick={() => {
props.onSelect?.()
if (props.value !== undefined) {
onValueChange?.(props.value)
}
}}
>
{props.children}
</button>
)
})}
</div>
<Selection.Provider value={value}>
<div role="radiogroup" aria-label={ariaLabel}>
{children}
</div>
</Selection.Provider>
),
DropdownMenuRadioItem: ({ children }: { children: React.ReactNode; value: string }) => (
<span>{children}</span>
DropdownMenuRadioItem: ({
children,
value,
disabled,
onSelect
}: {
children: React.ReactNode
value: string
disabled?: boolean
onSelect?: () => void
}) => (
<button
role="radio"
aria-checked={React.useContext(Selection) === value}
disabled={disabled}
data-value={value}
onClick={onSelect}
>
{children}
</button>
)
}
})
@@ -27,7 +27,10 @@ import {
} from './native-chat-permission-mode-labels'
import { runSessionOptionSurfaceCall } from './native-chat-session-option-surface-call'
function ModeName(props: { mode: AgentChatPermissionMode; truncate?: boolean }): React.JSX.Element {
export function NativeChatPermissionModeName(props: {
mode: AgentChatPermissionMode
truncate?: boolean
}): React.JSX.Element {
const Icon = NATIVE_CHAT_PERMISSION_MODE_ICONS[props.mode]
const label = nativeChatPermissionModeLabel(props.mode)
const body = (
@@ -43,6 +46,36 @@ function ModeName(props: { mode: AgentChatPermissionMode; truncate?: boolean }):
)
}
export function NativeChatPermissionModeItems({
modes,
agent,
disabled,
onSelect
}: Pick<
NativeChatPermissionModePickerProps,
'modes' | 'agent' | 'disabled' | 'onSelect'
>): React.JSX.Element {
return (
<>
{modes.map((mode) => (
<DropdownMenuRadioItem
key={mode}
value={mode}
disabled={disabled}
onSelect={() => onSelect(mode)}
>
<div className="min-w-0 py-0.5">
<NativeChatPermissionModeName mode={mode} />
<div className="text-xs font-normal text-muted-foreground">
{nativeChatPermissionModeDescription(mode, agent)}
</div>
</div>
</DropdownMenuRadioItem>
))}
</>
)
}
export type NativeChatPermissionModePickerProps = {
current: AgentChatPermissionMode
modes: readonly AgentChatPermissionMode[]
@@ -77,7 +110,7 @@ function NativeChatPermissionModePickerInner({
)}
className="max-w-40 min-w-0 shrink"
>
<ModeName mode={current} truncate />
<NativeChatPermissionModeName mode={current} truncate />
<ChevronDown className="size-3" />
</Button>
</DropdownMenuTrigger>
@@ -88,21 +121,12 @@ function NativeChatPermissionModePickerInner({
</Tooltip>
<DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-72">
<DropdownMenuRadioGroup aria-label={title} value={current}>
{modes.map((mode) => (
<DropdownMenuRadioItem
key={mode}
value={mode}
disabled={disabled}
onSelect={() => onSelect(mode)}
>
<div className="min-w-0 py-0.5">
<ModeName mode={mode} />
<div className="text-xs font-normal text-muted-foreground">
{nativeChatPermissionModeDescription(mode, agent)}
</div>
</div>
</DropdownMenuRadioItem>
))}
<NativeChatPermissionModeItems
modes={modes}
agent={agent}
disabled={disabled}
onSelect={onSelect}
/>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
@@ -10,6 +10,7 @@ import type { AgentSessionSourceHomeControl } from './codex-session-source-home-
import { AgentSessionSourceHomeInput } from './codex-session-source-home-control'
import { resolveTuiAgentPermissionMode } from '../../../../shared/tui-agent-permissions'
import { stringifyAgentDefaultEnvDraft } from './agent-default-env-draft'
import { liftAgentPermissionArgs } from '../../../../shared/tui-agent-permission-lift'
import {
AgentPermissionExceptionBadge,
AgentRowPermissionControl,
@@ -21,7 +22,6 @@ import {
AgentDefaultEnvInput
} from './AgentLaunchDefaultsEditor'
import {
AgentPermissionArgsChip,
agentPermissionChoiceLabel,
AgentPermissionMenu,
type AgentPermissionChoiceRow
@@ -299,13 +299,17 @@ export function AgentCatalogRow({
argsOverride={argsOverride}
onSaveArgs={onSaveArgs}
permissionChip={
permissionChoice?.flag && !permissionChoice.argsSetPermission ? (
<AgentPermissionArgsChip
label={label}
flag={permissionChoice.flag}
onRemove={() => permissionChoice.onSelect('own')}
/>
) : undefined
permissionChoice?.flag && !permissionChoice.argsSetPermission
? {
label,
flag: permissionChoice.flag,
removeRecipe: (text) => {
const lifted = liftAgentPermissionArgs(agentId, text)
return lifted.choice !== null ? lifted.args : null
},
onRemove: () => permissionChoice.onSelect('own')
}
: undefined
}
note={
permissionChoice?.argsSetPermission
@@ -1,6 +1,7 @@
import { useId, useState } from 'react'
import { useId, useLayoutEffect, useRef, useState } from 'react'
import { Button } from '../ui/button'
import { Input } from '../ui/input'
import { Textarea } from '../ui/textarea'
import { translate } from '@/i18n/i18n'
import {
parseAgentDefaultEnvDraft,
@@ -8,6 +9,7 @@ import {
type AgentDefaultEnvDraftInvalidReason
} from './agent-default-env-draft'
import { useAgentLaunchFieldWrite } from './use-agent-launch-field-write'
import { AgentPermissionArgsChip } from './AgentPermissionChoiceControls'
export function AgentCommandOverrideInput({
defaultCmd,
@@ -91,13 +93,38 @@ export function AgentDefaultArgsInput({
defaultArgs: string
argsOverride: string
onSaveArgs: (value: string) => void | Promise<void>
/** The permission flag terminals add ahead of the text, drawn just before the box. */
permissionChip?: React.ReactNode
permissionChip?: {
label: string
flag: string
removeRecipe: (text: string) => string | null
onRemove: () => void
}
note?: string
}): React.JSX.Element {
const [draft, setDraft] = useState<string | null>(null)
const inputRef = useRef<HTMLTextAreaElement>(null)
const chipRef = useRef<HTMLSpanElement>(null)
const chipButtonRef = useRef<HTMLButtonElement>(null)
const argsDraft = draft ?? argsOverride
const { pending, write } = useAgentLaunchFieldWrite()
useLayoutEffect(() => {
const input = inputRef.current
const chip = chipRef.current
if (!input) {
return
}
input.style.textIndent = ''
if (!chip) {
return
}
const measure = (): void => {
input.style.textIndent = `calc(${chip.getBoundingClientRect().width}px + var(--spacing) * 2)`
}
measure()
const observer = new ResizeObserver(measure)
observer.observe(chip)
return () => observer.disconnect()
}, [permissionChip?.flag])
const commitArgs = (): void => {
if (pending || draft === null) {
return
@@ -120,28 +147,72 @@ export function AgentDefaultArgsInput({
{translate('auto.components.settings.AgentsPane.cfb3f35775', 'Arguments')}
</span>
<div className="flex items-center gap-2">
{permissionChip}
<Input
value={argsDraft}
readOnly={pending}
onChange={(event) => setDraft(event.target.value)}
onBlur={commitArgs}
onKeyDown={(event) => {
if (event.key === 'Enter') {
<div
className="scrollbar-sleek relative max-h-[calc(var(--spacing)*41+2px)] min-h-7 min-w-0 flex-1 overflow-x-hidden overflow-y-auto rounded-md border border-input px-3 py-0.5 shadow-xs transition-[color,box-shadow] focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 dark:bg-input/30"
onBlur={(event) => {
if (!event.currentTarget.contains(event.relatedTarget)) {
commitArgs()
event.currentTarget.blur()
}
if (event.key === 'Escape') {
setDraft(null)
}
}}
placeholder={
defaultArgs ||
translate('auto.components.settings.AgentsPane.6f99bf5dd0', 'No default arguments')
}
spellCheck={false}
className="h-7 flex-1 font-mono text-xs"
/>
onClick={(event) => {
if (event.target === event.currentTarget) {
inputRef.current?.focus()
}
}}
>
{permissionChip && (
<AgentPermissionArgsChip
{...permissionChip}
chipRef={chipRef}
buttonRef={chipButtonRef}
disabled={pending}
onRemove={() => {
if (draft !== null) {
setDraft(permissionChip.removeRecipe(draft) ?? draft)
}
inputRef.current?.focus()
permissionChip.onRemove()
}}
/>
)}
<Textarea
variant="composite"
ref={inputRef}
rows={1}
value={argsDraft}
readOnly={pending}
onChange={(event) => setDraft(event.target.value.replace(/[\r\n]/g, ''))}
onKeyDown={(event) => {
if (
permissionChip &&
event.key === 'Backspace' &&
!event.repeat &&
!event.altKey &&
!event.ctrlKey &&
!event.metaKey &&
event.currentTarget.selectionStart === 0 &&
event.currentTarget.selectionEnd === 0
) {
event.preventDefault()
chipButtonRef.current?.focus()
}
if (event.key === 'Enter') {
event.preventDefault()
commitArgs()
event.currentTarget.blur()
}
if (event.key === 'Escape') {
setDraft(null)
}
}}
placeholder={
defaultArgs ||
translate('auto.components.settings.AgentsPane.6f99bf5dd0', 'No default arguments')
}
spellCheck={false}
className="block min-h-5 [field-sizing:content]"
/>
</div>
{(argsOverride !== defaultArgs || argsDraft !== defaultArgs) && (
<Button
type="button"
@@ -263,7 +263,6 @@ export function AgentLaunchSettingsCatalog({
(agent) => !agentPermissionChoices(agent).includes('auto')
).map((agent) => resolveAgentPermission(agent, permissionSettings).choice)
)}
ownChoiceCount={Object.keys(permissionSettings.agentPermissionOverrides ?? {}).length}
onChange={(choice) => save({ type: 'permission', choice })}
onChangeWithoutAuto={(choice) => save({ type: 'permission', choice, withoutAuto: true })}
/>
@@ -304,3 +303,27 @@ export function AgentLaunchSettingsCatalog({
function sharedChoice(choices: readonly AgentPermissionChoice[]): AgentPermissionChoice | null {
return choices.length > 0 && choices.every((choice) => choice === choices[0]) ? choices[0] : null
}
export function AgentLaunchSettingsLoading(): React.JSX.Element {
return (
<fieldset disabled className="min-w-0 space-y-8">
<AgentDefaultSetting
defaultAgent="blank"
detectedIds={null}
enabledDetectedAgents={[]}
catalog={[]}
description={translate(
'settings.agents.hostDefaults',
'Defaults for new agents on the selected host.'
)}
onSetDefault={() => {}}
/>
<AllAgentsPermissionSetting
choice={null}
withoutAutoChoice={null}
onChange={() => {}}
onChangeWithoutAuto={() => {}}
/>
</fieldset>
)
}
@@ -29,6 +29,7 @@ vi.mock('./HostAgentLaunchSettings', async () => {
}
})
vi.mock('./AgentLaunchSettingsCatalog', () => ({
AgentLaunchSettingsLoading: () => <button disabled>Permissions</button>,
AgentLaunchSettingsCatalog: (props: unknown) => {
state.local(props)
return <p>Local launch settings</p>
@@ -93,7 +94,7 @@ describe('launch settings host', () => {
it('waits for the server list instead of displaying desktop defaults', () => {
state.runtimeEnvironments = []
render(section(SSH_HOST))
expect(screen.getByText('Loading agent settings…')).toBeTruthy()
expect(screen.getByRole('button', { name: 'Permissions' })).toHaveProperty('disabled', true)
expect(state.local).not.toHaveBeenCalled()
expect(state.remote).not.toHaveBeenCalled()
})
@@ -12,6 +12,7 @@ import {
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import {
AgentLaunchSettingsCatalog,
AgentLaunchSettingsLoading,
type AgentPermissionsRenderer
} from './AgentLaunchSettingsCatalog'
import { HostAgentLaunchSettings } from './HostAgentLaunchSettings'
@@ -55,9 +56,7 @@ export function AgentLaunchSettingsSection({
) : !hostScope.available ? (
<SettingsHostScopeUnavailableNotice />
) : (
<p className="text-sm text-muted-foreground">
{translate('settings.agents.loadingHost', 'Loading agent settings…')}
</p>
<AgentLaunchSettingsLoading />
)}
</section>
)
@@ -1,4 +1,5 @@
import { ChevronDown, X } from 'lucide-react'
import { ChevronDown, Terminal, 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'
import { translate } from '@/i18n/i18n'
@@ -17,6 +18,10 @@ import {
nativeChatPermissionModeDescription,
nativeChatPermissionModeLabel
} from '../native-chat/native-chat-permission-mode-labels'
import {
NativeChatPermissionModeItems,
NativeChatPermissionModeName
} from '../native-chat/NativeChatPermissionModePicker'
import { SettingsSegmentedControl } from './SettingsFormControls'
/** Use chat-mode wording; own names the agent when available. */
@@ -45,57 +50,81 @@ function ownDescription(): string {
export function AllAgentsPermissionSetting({
choice,
withoutAutoChoice,
ownChoiceCount,
onChange,
onChangeWithoutAuto
}: {
choice: AgentPermissionChoice
choice: AgentPermissionChoice | null
/** What the agents without Approve for me share; null when they differ. */
withoutAutoChoice: AgentPermissionChoice | null
/** Agents set separately, which choosing a mode here resets. */
ownChoiceCount: number
onChange: (choice: AgentPermissionChoice) => void
onChangeWithoutAuto: (choice: AgentPermissionChoice) => void
}): React.JSX.Element {
const title = translate(
'auto.components.settings.AgentsPane.agentPermissions',
'Agent Permissions'
)
const title = translate('settings.agents.permissionsTitle', 'Permissions')
const ownLabel = translate('settings.agents.permissionEachOwn', "Each agent's own settings")
return (
<section className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1 space-y-1">
<section className="space-y-1">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
<h3 className="text-sm font-semibold">{title}</h3>
<p className="text-xs text-muted-foreground">
{choice === 'own'
? ownDescription()
: translate(
'settings.agents.permissionChoiceDescription',
'Applies where each agent supports this mode. Change one agent below.'
<DropdownMenu>
<DropdownMenuTrigger asChild disabled={choice === null}>
<Button
type="button"
variant="outline"
size="xs"
aria-label={title}
className="w-48 max-w-full justify-between"
>
{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 />
)}
</p>
{ownChoiceCount > 0 ? (
<p className="text-xs text-muted-foreground">
{translate(
'settings.agents.permissionResetsOwnChoices',
'Choosing a mode here resets the agents marked as set separately.'
)}
</p>
) : null}
<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']}
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">
{translate(
'settings.agents.permissionEachOwnDescription',
'Orca adds nothing; each CLI uses its own config'
)}
</div>
</div>
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex shrink-0 flex-col items-end gap-1.5">
<SettingsSegmentedControl<AgentPermissionChoice>
value={choice}
onChange={onChange}
ariaLabel={title}
size="sm"
options={(['own', 'ask', 'auto', 'full-access'] as const).map((value) => ({
value,
label:
value === 'own'
? translate('settings.agents.permissionOwnShort', 'Own settings')
: nativeChatPermissionModeLabel(value)
}))}
/>
<p className="text-xs text-muted-foreground">
{translate(
'settings.agents.permissionsChoiceExplanation',
'What every agent may do without asking. You can set one agent differently below.'
)}
</p>
<p className="text-xs text-muted-foreground">
{translate(
'settings.agents.permissionsResetExplanation',
'Picking a mode here resets agents you set separately.'
)}
</p>
<div className="min-h-6">
{choice === 'auto' ? (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
@@ -223,26 +252,45 @@ export function AgentPermissionMenu({
export function AgentPermissionArgsChip({
label,
flag,
onRemove
onRemove,
disabled,
chipRef,
buttonRef
}: {
label: string
flag: string
onRemove: () => void
disabled?: boolean
chipRef?: Ref<HTMLSpanElement>
buttonRef?: Ref<HTMLButtonElement>
}): React.JSX.Element {
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex h-5 max-w-[60%] shrink-0 items-center gap-1 rounded bg-muted px-1.5 font-mono text-[11px] text-foreground/80">
<span
ref={chipRef}
className="relative z-10 -mb-5 flex h-5 w-fit max-w-[60%] items-center gap-1 rounded bg-muted px-1.5 font-mono text-xs leading-5 text-foreground/80 focus-within:ring-2 focus-within:ring-ring"
>
<span className="truncate">{flag}</span>
<button
ref={buttonRef}
type="button"
disabled={disabled}
onClick={onRemove}
onKeyDown={(event) => {
if (event.key === 'Backspace') {
event.preventDefault()
if (!event.repeat && !event.altKey && !event.ctrlKey && !event.metaKey) {
onRemove()
}
}
}}
aria-label={translate(
'settings.agents.permissionChipRemove',
"Remove {{flag}} and use {{agent}}'s own settings",
{ flag, agent: label }
)}
className="rounded-sm text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
className="rounded-sm text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50"
>
<X className="size-3" />
</button>
@@ -209,8 +209,9 @@ describe('AgentsPane', () => {
})
expect(detectedAgentsMock.lastTarget).toBeUndefined()
expect(markup).toContain('Loading agent settings…')
expect(markup).not.toContain('Default Agent')
expect(markup).toContain('<fieldset disabled=""')
expect(markup).toContain('aria-label="Permissions"')
expect(markup).not.toContain('Installed')
} finally {
initialState.runtimeEnvironments = priorRuntimeEnvironments
}
@@ -4,7 +4,10 @@ import type {
AgentLaunchSettingsMutation
} from '../../../../shared/agent-launch-settings'
import type { AgentPermissionsRenderer } from './AgentLaunchSettingsCatalog'
import { AgentLaunchSettingsCatalog } from './AgentLaunchSettingsCatalog'
import {
AgentLaunchSettingsCatalog,
AgentLaunchSettingsLoading
} from './AgentLaunchSettingsCatalog'
import {
mutateHostAgentLaunchSettings,
readHostAgentLaunchSettings,
@@ -105,21 +108,22 @@ export function HostAgentLaunchSettings({
mutationTail.current = write
return write
}
if (read.kind === 'loading') {
return <AgentLaunchSettingsLoading />
}
if (read.kind !== 'ready') {
return (
<div className="space-y-2 text-sm text-muted-foreground">
<p>
{read.kind === 'loading'
? translate('settings.agents.loadingHost', 'Loading agent settings…')
: read.kind === 'unsupported'
? translate(
'settings.agents.updateHost',
'Update this server to edit its agent launch settings.'
)
: translate(
'settings.agents.readFailed',
'Couldn’t read agent settings. Check the host connection and try again.'
)}
{read.kind === 'unsupported'
? translate(
'settings.agents.updateHost',
'Update this server to edit its agent launch settings.'
)
: translate(
'settings.agents.readFailed',
'Couldn’t read agent settings. Check the host connection and try again.'
)}
</p>
{read.kind === 'failed' ? (
<Button type="button" variant="ghost" size="xs" onClick={() => void refresh()}>
@@ -94,7 +94,7 @@ function AgentsHarness() {
)
}
function argumentsInput(label: string): HTMLInputElement {
function argumentsInput(label: string): HTMLTextAreaElement {
const row = screen.getByText(label, { selector: 'span' }).closest('div.py-3')
if (!(row instanceof HTMLElement)) {
throw new Error('Missing agent row')
@@ -104,7 +104,7 @@ function argumentsInput(label: string): HTMLInputElement {
fireEvent.click(expand)
}
const input = within(row).getByPlaceholderText('No default arguments')
if (!(input instanceof HTMLInputElement)) {
if (!(input instanceof HTMLTextAreaElement)) {
throw new Error('Missing Arguments input')
}
return input
@@ -169,6 +169,85 @@ it.each(
expect(persist).toHaveBeenCalledTimes(3)
})
it.each(['none', '×', 'Backspace'] as const)(
'keeps dirty Arguments and explicit %s permission removal in order',
async (removal) => {
savedArguments('--model sonnet', 'full-access')
const own = removal === 'Backspace' ? deferredSave() : null
render(<AgentsHarness />)
const user = userEvent.setup()
const input = argumentsInput('Claude')
const chip = screen.getByRole('button', { name: /Remove .* and use Claude's own settings/ })
await user.click(input)
await user.keyboard('{Home}{Shift>}{ArrowRight}{/Shift}{Backspace}')
expect(document.activeElement).toBe(input)
fireEvent.change(input, { target: { value: '--permission-mode auto --model opus' } })
input.setSelectionRange(0, 0)
for (const modifiers of [
{ ctrlKey: true },
{ altKey: true },
{ metaKey: true },
{ repeat: true }
]) {
fireEvent.keyDown(input, { key: 'Backspace', ...modifiers })
expect(document.activeElement).toBe(input)
}
await user.keyboard('{Home}{Backspace}')
expect(document.activeElement).toBe(chip)
fireEvent.keyDown(chip, { key: 'Backspace', repeat: true })
expect(persist).not.toHaveBeenCalled()
expect(useAppStore.getState().settings?.agentPermissionDefault).toBe('full-access')
await user.tab()
expect(document.activeElement).toBe(input)
await user.tab({ shift: true })
expect(document.activeElement).toBe(chip)
expect(persist).not.toHaveBeenCalled()
if (removal === 'none') {
await user.tab({ shift: true })
await waitFor(() => expect(input.value).toBe('--model opus'))
expect(useAppStore.getState().settings?.agentPermissionOverrides?.claude).toBe('auto')
expect(toast.success).toHaveBeenCalledTimes(1)
return
}
await (removal === '×' ? user.click(chip) : user.keyboard('{Backspace}'))
expect(document.activeElement).toBe(input)
expect(input.value).toBe('--model opus')
await waitFor(() => expect(persist).toHaveBeenCalledTimes(1))
const raw = own ? deferredSave() : null
const reset = own ? deferredSave() : null
await user.tab()
if (own && raw && reset) {
expect(input.readOnly).toBe(true)
expect(chip).toHaveProperty('disabled', true)
const row = input.closest('div.py-3')
if (!(row instanceof HTMLElement)) {
throw new Error('Missing row')
}
await user.click(within(row).getByRole('button', { name: 'Reset' }))
await act(async () => own.resolve())
await waitFor(() => expect(persist).toHaveBeenCalledTimes(2))
expect(input.value).toBe('--model opus')
await act(async () => raw.resolve())
await waitFor(() => expect(persist).toHaveBeenCalledTimes(3))
await act(async () => reset.reject(new Error('unknown acknowledgement')))
await waitFor(() => expect(input.readOnly).toBe(false))
expect(input.value).toBe('--model opus')
expect(toast.success).not.toHaveBeenCalled()
expect(toast.error).toHaveBeenCalledTimes(1)
await user.click(within(row).getByRole('button', { name: 'Reset' }))
await waitFor(() => expect(input.value).toBe(''))
} else {
await waitFor(() => expect(persist).toHaveBeenCalledTimes(2))
await waitFor(() => expect(input.readOnly).toBe(false))
expect(useAppStore.getState().settings?.agentDefaultArgs?.claude).toBe('--model opus')
}
const settings = useAppStore.getState().settings
expect(settings?.agentPermissionOverrides?.claude).toBe('own')
expect(resolveTuiAgentLaunchArgs('claude', settings ?? {})).toBe(own ? '' : '--model opus')
expect(toast.success).not.toHaveBeenCalled()
}
)
it('retains failed conversion through the ordinary callback and announces only the saved retry', async () => {
savedArguments('--model opus')
const save = deferredSave()
@@ -459,13 +538,16 @@ it.each([20, 200])('keeps Goose Environment editable after Reset/reopen (%i)', a
it('reports one direct local permission action failure without changing saved intent', async () => {
persist.mockRejectedValueOnce(new Error('private error'))
render(<AgentsHarness />)
fireEvent.click(screen.getByRole('radio', { name: 'Full access' }))
const user = userEvent.setup()
await user.click(screen.getByRole('button', { name: 'Permissions' }))
await user.click(await screen.findByRole('menuitemradio', { name: /Full access/ }))
await waitFor(() =>
expect(toast.error).toHaveBeenCalledExactlyOnceWith(
'Agent settings could not be saved. Try again.'
)
)
expect(useAppStore.getState().settings?.agentPermissionDefault).toBe('ask')
expect(toast.success).not.toHaveBeenCalled()
})
const uiWork = { entries: 0, bytes: 0 }
+3 -1
View File
@@ -9,7 +9,9 @@ const textareaVariants = cva('', {
variant: {
default: '',
code: 'font-mono',
cell: 'h-full min-h-0 resize-none rounded-none border-0 px-2 py-0 font-mono text-xs shadow-none md:text-xs focus-visible:ring-1 focus-visible:ring-inset'
cell: 'h-full min-h-0 resize-none rounded-none border-0 px-2 py-0 font-mono text-xs shadow-none md:text-xs focus-visible:ring-1 focus-visible:ring-inset',
composite:
'resize-none rounded-none border-0 p-0 font-mono text-xs leading-5 shadow-none md:text-xs focus-visible:ring-0 dark:bg-transparent'
}
},
defaultVariants: { variant: 'default' }
+5
View File
@@ -3458,6 +3458,11 @@
}
},
"settings": {
"agents": {
"loadingHost": "Loading agent settings…",
"permissionChoiceDescription": "Applies where each agent supports this mode. Change one agent below.",
"permissionResetsOwnChoices": "Choosing a mode here resets the agents marked as set separately."
},
"browser": {
"clientHostedRemote": {
"description": "Render remote workspace pages on this desktop; network traffic still goes through the remote host. Applies to new pages only.",
+5
View File
@@ -461,6 +461,11 @@
}
},
"agents": {
"permissionsTitle": "Permissions",
"permissionsResetExplanation": "Picking a mode here resets agents you set separately.",
"permissionsChoiceExplanation": "What every agent may do without asking. You can set one agent differently below.",
"permissionEachOwn": "Each agent's own settings",
"permissionEachOwnDescription": "Orca adds nothing; each CLI uses its own config",
"codexTerminalServerIsolation": {
"title": "Run each Codex terminal on its own server",
"description": "Keeps Orca's status and closing tabs working correctly. Turn off to use Codex's shared server and its agents overview. Applies to new terminals."