Add agent permission mode controls (#5440)

This commit is contained in:
Neil
2026-06-15 18:38:56 -07:00
committed by GitHub
parent c94c20c30c
commit 13f7163365
16 changed files with 537 additions and 64 deletions
@@ -2,16 +2,21 @@ import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { AGENT_CATALOG } from '@/lib/agent-catalog'
import { AgentStep } from './AgentStep'
import { TooltipProvider } from '@/components/ui/tooltip'
describe('AgentStep', () => {
it('shows the collapsed fallback agents summary', () => {
const html = renderToStaticMarkup(
<AgentStep
selectedAgent={null}
onSelect={vi.fn()}
detectedSet={new Set([AGENT_CATALOG[0].id])}
isDetecting={false}
/>
<TooltipProvider>
<AgentStep
selectedAgent={null}
onSelect={vi.fn()}
detectedSet={new Set([AGENT_CATALOG[0].id])}
isDetecting={false}
yoloPermissions
onYoloPermissionsChange={vi.fn()}
/>
</TooltipProvider>
)
expect(html).toContain(`Show ${AGENT_CATALOG.length - 1} more agents→`)
@@ -19,12 +24,16 @@ describe('AgentStep', () => {
it('labels the fallback agents summary as hide when expanded', () => {
const html = renderToStaticMarkup(
<AgentStep
selectedAgent={AGENT_CATALOG[1].id}
onSelect={vi.fn()}
detectedSet={new Set([AGENT_CATALOG[0].id])}
isDetecting={false}
/>
<TooltipProvider>
<AgentStep
selectedAgent={AGENT_CATALOG[1].id}
onSelect={vi.fn()}
detectedSet={new Set([AGENT_CATALOG[0].id])}
isDetecting={false}
yoloPermissions
onYoloPermissionsChange={vi.fn()}
/>
</TooltipProvider>
)
expect(html).toContain('Hide agents')
@@ -1,8 +1,10 @@
import { useState } from 'react'
import { Check, ExternalLink } from 'lucide-react'
import { Check, ExternalLink, Info } from 'lucide-react'
import { getAgentCatalog, AgentIcon, type AgentCatalogEntry } from '@/lib/agent-catalog'
import { cn } from '@/lib/utils'
import { Checkbox } from '@/components/ui/checkbox'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import type { TuiAgent } from '../../../../shared/types'
import { translate } from '@/i18n/i18n'
@@ -14,9 +16,18 @@ type AgentStepProps = {
onSelect: (agent: TuiAgent, fromCollapsedSection: boolean) => void
detectedSet: Set<TuiAgent>
isDetecting: boolean
yoloPermissions?: boolean
onYoloPermissionsChange?: (enabled: boolean) => void
}
export function AgentStep({ selectedAgent, onSelect, detectedSet, isDetecting }: AgentStepProps) {
export function AgentStep({
selectedAgent,
onSelect,
detectedSet,
isDetecting,
yoloPermissions = true,
onYoloPermissionsChange
}: AgentStepProps) {
const agentCatalog = getAgentCatalog()
const detected = agentCatalog.filter((agent) => detectedSet.has(agent.id))
const rest = agentCatalog.filter((agent) => !detectedSet.has(agent.id))
@@ -82,6 +93,44 @@ export function AgentStep({ selectedAgent, onSelect, detectedSet, isDetecting }:
</button>
</div>
)}
<label className="flex cursor-pointer items-center justify-between gap-4 rounded-lg border border-border bg-background/50 px-4 py-3 transition-colors hover:bg-accent/50">
<span className="flex min-w-0 items-center gap-3">
<Checkbox
checked={yoloPermissions}
onCheckedChange={(checked) => onYoloPermissionsChange?.(checked === true)}
aria-label={translate(
'auto.components.onboarding.AgentStep.yoloPermissionsLabel',
'Yolo / Dangerously skip permissions'
)}
/>
<span className="min-w-0 text-sm font-medium text-foreground">
{translate(
'auto.components.onboarding.AgentStep.yoloPermissionsLabel',
'Yolo / Dangerously skip permissions'
)}
</span>
</span>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={translate(
'auto.components.onboarding.AgentStep.yoloPermissionsInfo',
'Agent permission info'
)}
className="grid size-6 shrink-0 place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<Info className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6} style={{ zIndex: 120 }}>
{translate(
'auto.components.onboarding.AgentStep.yoloPermissionsTooltip',
'Skip permission checks for agents for less interruptions'
)}
</TooltipContent>
</Tooltip>
</label>
<section className="space-y-3">
<SectionHeader
label={
@@ -274,6 +274,8 @@ export default function OnboardingFlow({
onSelect={flow.setSelectedAgent}
detectedSet={flow.detectedSet}
isDetecting={flow.isDetectingAgents}
yoloPermissions={flow.yoloPermissions}
onYoloPermissionsChange={flow.setYoloPermissions}
/>
)}
{currentStep.id === 'theme' && (
@@ -4,6 +4,7 @@ import { useAppStore } from '@/store'
import { ONBOARDING_FINAL_STEP, ONBOARDING_FLOW_VERSION } from '../../../../shared/constants'
import type { EventProps } from '../../../../shared/telemetry-events'
import type { GlobalSettings, OnboardingState, TuiAgent } from '../../../../shared/types'
import { applyAgentPermissionMode } from '../../../../shared/tui-agent-permissions'
import type { StepId, StepNumber } from './use-onboarding-flow-types'
export async function persistStep(
@@ -126,6 +127,7 @@ export function useCloseWith({
type PersistCurrentStepDeps = {
currentStepId: StepId
selectedAgent: TuiAgent | null
yoloPermissions: boolean
theme: GlobalSettings['theme']
settings: GlobalSettings | null
updateSettings: (updates: Partial<GlobalSettings>) => Promise<void> | void
@@ -141,6 +143,7 @@ export type PersistCurrentStepResult = {
export function usePersistCurrentStep({
currentStepId,
selectedAgent,
yoloPermissions,
theme,
settings,
updateSettings,
@@ -155,7 +158,14 @@ export function usePersistCurrentStep({
try {
if (currentStepId === 'agent') {
const defaultTuiAgent = selectedAgentOrBlank(selectedAgent)
await updateSettings({ defaultTuiAgent })
await updateSettings({
defaultTuiAgent,
...applyAgentPermissionMode({
mode: yoloPermissions ? 'yolo' : 'manual',
agentDefaultArgs: settings.agentDefaultArgs,
agentDefaultEnv: settings.agentDefaultEnv
})
})
const choseAgent = defaultTuiAgent !== 'blank'
const wasAlreadyChosen = onboardingChecklist.choseAgent
onOnboardingChange(
@@ -210,6 +220,7 @@ export function usePersistCurrentStep({
settings,
theme,
updateSettings,
yoloPermissions,
setError
])
}
@@ -33,6 +33,7 @@ import { buildOnboardingFolderAgentStartup } from '@/lib/onboarding-folder-agent
import { resolveOnboardingSettingsHydration } from './onboarding-settings-hydration'
import { openProjectDefaultCheckout } from '../sidebar/project-added-default-checkout'
import { translate } from '@/i18n/i18n'
import { resolveAgentPermissionModeSummary } from '../../../../shared/tui-agent-permissions'
export { STEPS } from './use-onboarding-flow-types'
export type { StepId, StepNumber } from './use-onboarding-flow-types'
@@ -240,6 +241,12 @@ export function useOnboardingFlow(
? settings.defaultTuiAgent
: null
)
const [yoloPermissions, setYoloPermissions] = useState(
resolveAgentPermissionModeSummary({
agentDefaultArgs: settings?.agentDefaultArgs,
agentDefaultEnv: settings?.agentDefaultEnv
}) !== 'manual'
)
// Why: hydrate theme from saved settings instead of hardcoding 'dark' so users
// who already configured a theme see their choice preselected.
const [theme, setTheme] = useState<GlobalSettings['theme']>(settings?.theme ?? 'dark')
@@ -263,6 +270,7 @@ export function useOnboardingFlow(
// fallback defaults, unless the user already interacted with that field.
const themeInteractedRef = useRef(false)
const agentInteractedRef = useRef(false)
const yoloPermissionsInteractedRef = useRef(false)
const [settingsHydrated, setSettingsHydrated] = useState(settings != null)
const settingsHydration = resolveOnboardingSettingsHydration({
settings,
@@ -281,6 +289,16 @@ export function useOnboardingFlow(
setSelectedAgent(settingsHydration.selectedAgent)
}
}
if (settings && !yoloPermissionsInteractedRef.current) {
const nextYoloPermissions =
resolveAgentPermissionModeSummary({
agentDefaultArgs: settings.agentDefaultArgs,
agentDefaultEnv: settings.agentDefaultEnv
}) !== 'manual'
if (nextYoloPermissions !== yoloPermissions) {
setYoloPermissions(nextYoloPermissions)
}
}
// Why: track user interaction so async settings hydration above doesn't
// overwrite a value the user explicitly chose.
@@ -335,6 +353,10 @@ export function useOnboardingFlow(
},
[]
)
const setYoloPermissionsInteractive = useCallback((enabled: boolean) => {
yoloPermissionsInteractedRef.current = true
setYoloPermissions(enabled)
}, [])
const detectedSet = useMemo(() => new Set(detectedAgentIds ?? []), [detectedAgentIds])
const currentStep = STEPS[stepIndex]
@@ -573,6 +595,7 @@ export function useOnboardingFlow(
const persistCurrentStep = usePersistCurrentStep({
currentStepId: currentStep.id,
selectedAgent,
yoloPermissions,
theme,
settings,
updateSettings,
@@ -1211,6 +1234,8 @@ export function useOnboardingFlow(
currentStep,
selectedAgent,
setSelectedAgent: setSelectedAgentInteractive,
yoloPermissions,
setYoloPermissions: setYoloPermissionsInteractive,
theme,
setTheme: setThemeInteractive,
cloneUrl,
@@ -13,6 +13,7 @@ import { getAgentAwakeDescription, getAgentAwakeTitle } from './agent-awake-copy
import { AgentAwakeSetting } from './AgentAwakeSetting'
import {
AgentAvailabilityControl,
AgentPermissionsSetting,
AgentGeneratedTabTitlesSetting,
AgentStatusHooksSetting,
AgentsPane,
@@ -21,6 +22,7 @@ import {
createAgentAvailabilityUpdateQueue
} from './AgentsPane'
import { matchesSettingsSearch } from './settings-search'
import { TooltipProvider } from '../ui/tooltip'
const detectedAgentsMock = vi.hoisted(() => ({
detectedIds: ['claude'] as TuiAgent[] | null,
@@ -64,11 +66,15 @@ function renderPane(
props: Partial<React.ComponentProps<typeof AgentsPane>> = {}
): string {
return renderToStaticMarkup(
React.createElement(AgentsPane, {
settings,
updateSettings: vi.fn(),
...props
})
React.createElement(
TooltipProvider,
null,
React.createElement(AgentsPane, {
settings,
updateSettings: vi.fn(),
...props
})
)
)
}
@@ -259,6 +265,30 @@ describe('AgentsPane', () => {
expect(matchesSettingsSearch('hide', getAgentsPaneSearchEntries())).toBe(true)
})
it('includes agent permission search metadata', () => {
expect(matchesSettingsSearch('permission', getAgentsPaneSearchEntries())).toBe(true)
expect(matchesSettingsSearch('yolo', getAgentsPaneSearchEntries())).toBe(true)
expect(matchesSettingsSearch('manual', getAgentsPaneSearchEntries())).toBe(true)
})
it('applies the selected agent permission mode from settings without a mixed segment', () => {
const onChange = vi.fn()
const element = AgentPermissionsSetting({ mode: 'mixed', onChange })
const props = element.props.children.props.action.props as {
value: 'yolo'
onChange: (value: 'yolo' | 'manual' | 'mixed') => void
options: { value: string }[]
}
expect(props.value).toBe('yolo')
expect(props.options.map((option) => option.value)).toEqual(['yolo', 'manual'])
props.onChange('mixed')
expect(onChange).not.toHaveBeenCalled()
props.onChange('manual')
expect(onChange).toHaveBeenCalledWith('manual')
})
it('keeps catalog agent ids, labels, and commands discoverable in settings search', () => {
for (const agent of AGENT_CATALOG) {
expect(matchesSettingsSearch(agent.id, getAgentsPaneSearchEntries())).toBe(true)
@@ -2,7 +2,7 @@
selection, per-agent controls, and runtime location together so settings
reconciliation stays visible in one file. */
import { useMemo, useState } from 'react'
import { Check, ChevronDown, ExternalLink, RefreshCw, Terminal } from 'lucide-react'
import { Check, ChevronDown, ExternalLink, Info, RefreshCw, Terminal } from 'lucide-react'
import type { GlobalSettings, TuiAgent } from '../../../../shared/types'
import { getAgentCatalog, AgentIcon } from '@/lib/agent-catalog'
import { useDetectedAgents } from '@/hooks/useDetectedAgents'
@@ -33,8 +33,14 @@ import {
resolveTuiAgentLaunchArgs,
resolveTuiAgentLaunchEnv
} from '../../../../shared/tui-agent-launch-defaults'
import {
applyAgentPermissionMode,
resolveAgentPermissionModeSummary,
type AgentPermissionMode
} from '../../../../shared/tui-agent-permissions'
import { getSettingOwnershipSummary } from './setting-ownership'
import { translate } from '@/i18n/i18n'
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'
export { getAgentsPaneSearchEntries } from './agents-search'
@@ -103,6 +109,11 @@ type AgentAvailabilityControlProps = {
onSetEnabled: (enabled: boolean) => void
}
type AgentPermissionsSettingProps = {
mode: AgentPermissionMode
onChange: (mode: Exclude<AgentPermissionMode, 'mixed'>) => void
}
export function buildAgentAvailabilitySettingsUpdate(
settings: Pick<GlobalSettings, 'defaultTuiAgent' | 'disabledTuiAgents'>,
id: TuiAgent,
@@ -177,6 +188,76 @@ export function AgentAvailabilityControl({
)
}
export function AgentPermissionsSetting({
mode,
onChange
}: AgentPermissionsSettingProps): React.JSX.Element {
const visibleMode: Exclude<AgentPermissionMode, 'mixed'> = mode === 'manual' ? 'manual' : 'yolo'
return (
<section className="space-y-3">
<SettingsSubsectionHeader
title={
<span className="flex items-center gap-2">
{translate('auto.components.settings.AgentsPane.agentPermissions', 'Agent Permissions')}
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={translate(
'auto.components.settings.AgentsPane.agentPermissionsInfo',
'Agent permissions info'
)}
className="grid size-5 place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<Info className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6}>
{translate(
'auto.components.settings.AgentsPane.agentPermissionsTooltip',
"Doesn't apply to agents where you've overridden launch arguments."
)}
</TooltipContent>
</Tooltip>
</span>
}
description={translate(
'auto.components.settings.AgentsPane.agentPermissionsDescription',
'Choose whether Orca launches agents with fewer permission prompts or with manual checks.'
)}
action={
<SettingsSegmentedControl<AgentPermissionMode>
value={visibleMode}
onChange={(nextMode) => {
if (nextMode !== 'mixed') {
onChange(nextMode)
}
}}
ariaLabel={translate(
'auto.components.settings.AgentsPane.agentPermissions',
'Agent Permissions'
)}
size="sm"
options={[
{
value: 'yolo',
label: translate('auto.components.settings.AgentsPane.agentPermissionsYolo', 'Yolo')
},
{
value: 'manual',
label: translate(
'auto.components.settings.AgentsPane.agentPermissionsManual',
'Manual'
)
}
]}
/>
}
/>
</section>
)
}
function AgentCommandOverrideInput({
defaultCmd,
cmdOverride,
@@ -617,6 +698,10 @@ export function AgentsPane({
const cmdOverrides = settings.agentCmdOverrides ?? {}
const agentDefaultArgs = settings.agentDefaultArgs ?? {}
const agentDefaultEnv = settings.agentDefaultEnv ?? {}
const agentPermissionMode = resolveAgentPermissionModeSummary({
agentDefaultArgs,
agentDefaultEnv
})
const disabledAgents = normalizeDisabledTuiAgents(settings.disabledTuiAgents)
const setDefault = (id: TuiAgent | 'blank' | null): void => {
@@ -661,6 +746,16 @@ export function AgentsPane({
})
}
const saveAgentPermissionMode = (mode: Exclude<AgentPermissionMode, 'mixed'>): void => {
updateSettings(
applyAgentPermissionMode({
mode,
agentDefaultArgs,
agentDefaultEnv
})
)
}
// Why: null means detection is in flight, not "all agents are installed".
// Showing the full catalog here makes the default-agent picker flash invalid
// options while switching between Windows and WSL detection contexts.
@@ -742,6 +837,8 @@ export function AgentsPane({
<AgentAwakeSetting settings={settings} updateSettings={updateSettings} />
<AgentPermissionsSetting mode={agentPermissionMode} onChange={saveAgentPermissionMode} />
{detectedAgents.length > 0 && (
<section className="space-y-3">
<SettingsSubsectionHeader
@@ -30,6 +30,10 @@ function buildAgentSettingsKeywords(): string[] {
{ key: 'auto.components.settings.agents.search.60393e1b17', fallback: 'disable' },
{ key: 'auto.components.settings.agents.search.2e188c771c', fallback: 'hide' },
{ key: 'auto.components.settings.agents.search.87fffe6c20', fallback: 'show' },
{ key: 'auto.components.settings.agents.search.permission', fallback: 'permission' },
{ 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: 'auto.components.settings.agents.search.e2b7c0dcd7',
fallback: 'github',
@@ -94,5 +98,26 @@ export const getAgentsPaneSearchEntries = createLocalizedCatalog(() => [
title: getAgentAwakeTitle(),
description: getAgentAwakeDescription(),
keywords: getAgentAwakeSearchKeywords()
},
{
title: translate(
'auto.components.settings.agents.search.agentPermissions',
'Agent Permissions'
),
description: translate(
'auto.components.settings.agents.search.agentPermissionsDescription',
'Switch agent permission defaults between Yolo and Manual.'
),
keywords: [
...translateSearchKeyword('auto.components.settings.agents.search.permission', 'permission'),
...translateSearchKeyword(
'auto.components.settings.agents.search.permissions',
'permissions'
),
...translateSearchKeyword('auto.components.settings.agents.search.yolo', 'yolo'),
...translateSearchKeyword('auto.components.settings.agents.search.manual', 'manual'),
...translateSearchKeyword('auto.components.settings.agents.search.skip', 'skip'),
...translateSearchKeyword('auto.components.settings.agents.search.checks', 'checks')
]
}
])
+14 -3
View File
@@ -4280,7 +4280,13 @@
"cfb3f35775": "Arguments",
"6f99bf5dd0": "No default arguments",
"8fbe1f37c1": "Environment",
"2d133152fa": "No default environment"
"2d133152fa": "No default environment",
"agentPermissions": "Agent Permissions",
"agentPermissionsInfo": "Agent permissions info",
"agentPermissionsTooltip": "Doesn't apply to agents where you've overridden launch arguments.",
"agentPermissionsDescription": "Choose whether Orca launches agents with fewer permission prompts or with manual checks.",
"agentPermissionsYolo": "Yolo",
"agentPermissionsManual": "Manual"
},
"AppIconSelector": {
"d5a112dc9b": "Next icon",
@@ -6351,7 +6357,9 @@
"5784ae8c43": "rename",
"8a17fd6026": "stable",
"a79d266f71": "session",
"afbf35be68": "stable session"
"afbf35be68": "stable session",
"agentPermissions": "Agent Permissions",
"agentPermissionsDescription": "Switch agent permission defaults between Yolo and Manual."
}
},
"appearance": {
@@ -8811,7 +8819,10 @@
"69af7e9c1c": "isn't on your PATH yet. Orca will set it as your default and you can install it any time.",
"1eee1c7bd8": "No agents detected on your PATH. Pick one to install later, or continue with a blank terminal.",
"hideAgents": "Hide agents",
"showMoreAgents": "Show {{value0}} more agents→"
"showMoreAgents": "Show {{value0}} more agents→",
"yoloPermissionsLabel": "Yolo / Dangerously skip permissions",
"yoloPermissionsInfo": "Agent permission info",
"yoloPermissionsTooltip": "Skip permission checks for agents for less interruptions"
},
"FeatureSetupChecklist": {
"77f74946f5": "Agents can message each other, take tasks, and coordinate handoffs.",
+14 -3
View File
@@ -4280,7 +4280,13 @@
"cfb3f35775": "Arguments",
"6f99bf5dd0": "No default arguments",
"8fbe1f37c1": "Environment",
"2d133152fa": "No default environment"
"2d133152fa": "No default environment",
"agentPermissions": "Agent Permissions",
"agentPermissionsInfo": "Agent permissions info",
"agentPermissionsTooltip": "Custom agent arguments stay unchanged when switching modes.",
"agentPermissionsDescription": "Choose whether Orca launches agents with fewer permission prompts or with manual checks.",
"agentPermissionsYolo": "Yolo",
"agentPermissionsManual": "Manual"
},
"AppIconSelector": {
"d5a112dc9b": "Icono siguiente",
@@ -6314,7 +6320,9 @@
"5784ae8c43": "rebautizar",
"8a17fd6026": "estable",
"a79d266f71": "sesión",
"afbf35be68": "sesión estable"
"afbf35be68": "sesión estable",
"agentPermissions": "Agent Permissions",
"agentPermissionsDescription": "Switch agent permission defaults between Yolo and Manual."
}
},
"appearance": {
@@ -8811,7 +8819,10 @@
"69af7e9c1c": "aún no está en tu RUTA. Orca lo configurará como predeterminado y podrás instalarlo en cualquier momento.",
"1eee1c7bd8": "No se detectaron agents en su RUTA. Elija uno para instalarlo más tarde o continúe con un terminal en blanco.",
"hideAgents": "Ocultar agents",
"showMoreAgents": "Mostrar {{value0}} más agents→"
"showMoreAgents": "Mostrar {{value0}} más agents→",
"yoloPermissionsLabel": "Yolo / Dangerously skip permissions",
"yoloPermissionsInfo": "Agent permission info",
"yoloPermissionsTooltip": "Skip permission checks for agents for less interruptions"
},
"FeatureSetupChecklist": {
"77f74946f5": "Los Agents pueden enviarse mensajes entre sí, realizar tareas y coordinar traspasos.",
+14 -3
View File
@@ -4265,7 +4265,13 @@
"cfb3f35775": "Arguments",
"6f99bf5dd0": "No default arguments",
"8fbe1f37c1": "Environment",
"2d133152fa": "No default environment"
"2d133152fa": "No default environment",
"agentPermissions": "Agent Permissions",
"agentPermissionsInfo": "Agent permissions info",
"agentPermissionsTooltip": "Custom agent arguments stay unchanged when switching modes.",
"agentPermissionsDescription": "Choose whether Orca launches agents with fewer permission prompts or with manual checks.",
"agentPermissionsYolo": "Yolo",
"agentPermissionsManual": "Manual"
},
"AppIconSelector": {
"d5a112dc9b": "次へのアイコン",
@@ -6336,7 +6342,9 @@
"5784ae8c43": "名前変更",
"8a17fd6026": "安定",
"a79d266f71": "セッション",
"afbf35be68": "安定したセッション"
"afbf35be68": "安定したセッション",
"agentPermissions": "Agent Permissions",
"agentPermissionsDescription": "Switch agent permission defaults between Yolo and Manual."
}
},
"appearance": {
@@ -8811,7 +8819,10 @@
"69af7e9c1c": "はまだ PATH 上にありません。 Orca はこれをデフォルトとして設定し、いつでもインストールできます。",
"1eee1c7bd8": "PATH 上に agents が検出されませんでした。後でインストールするものを選択するか、空の terminal を使用して続行します。",
"hideAgents": "agents を非表示にする",
"showMoreAgents": "{{value0}} 件の agents をさらに表示→"
"showMoreAgents": "{{value0}} 件の agents をさらに表示→",
"yoloPermissionsLabel": "Yolo / Dangerously skip permissions",
"yoloPermissionsInfo": "Agent permission info",
"yoloPermissionsTooltip": "Skip permission checks for agents for less interruptions"
},
"FeatureSetupChecklist": {
"77f74946f5": "Agents は相互にメッセージを送信し、タスクを実行し、引き継ぎを調整できます。",
+14 -3
View File
@@ -4265,7 +4265,13 @@
"cfb3f35775": "Arguments",
"6f99bf5dd0": "No default arguments",
"8fbe1f37c1": "Environment",
"2d133152fa": "No default environment"
"2d133152fa": "No default environment",
"agentPermissions": "Agent Permissions",
"agentPermissionsInfo": "Agent permissions info",
"agentPermissionsTooltip": "Custom agent arguments stay unchanged when switching modes.",
"agentPermissionsDescription": "Choose whether Orca launches agents with fewer permission prompts or with manual checks.",
"agentPermissionsYolo": "Yolo",
"agentPermissionsManual": "Manual"
},
"AppIconSelector": {
"d5a112dc9b": "다음 아이콘",
@@ -6299,7 +6305,9 @@
"5784ae8c43": "이름 변경",
"8a17fd6026": "안정적",
"a79d266f71": "세션",
"afbf35be68": "안정적인 세션"
"afbf35be68": "안정적인 세션",
"agentPermissions": "Agent Permissions",
"agentPermissionsDescription": "Switch agent permission defaults between Yolo and Manual."
}
},
"appearance": {
@@ -8811,7 +8819,10 @@
"69af7e9c1c": "아직 PATH에 없습니다. Orca는 이를 기본값으로 설정하며 언제든지 설치할 수 있습니다.",
"1eee1c7bd8": "PATH에서 agents가 감지되지 않습니다. 나중에 설치할 항목을 선택하거나 빈 terminal을 계속 사용하세요.",
"hideAgents": "agents 숨기기",
"showMoreAgents": "{{value0}}개 더 많은 agents 표시→"
"showMoreAgents": "{{value0}}개 더 많은 agents 표시→",
"yoloPermissionsLabel": "Yolo / Dangerously skip permissions",
"yoloPermissionsInfo": "Agent permission info",
"yoloPermissionsTooltip": "Skip permission checks for agents for less interruptions"
},
"FeatureSetupChecklist": {
"77f74946f5": "Agents는 서로 메시지를 보내고, 작업을 수행하고, 핸드오프를 조정할 수 있습니다.",
+14 -3
View File
@@ -4265,7 +4265,13 @@
"cfb3f35775": "Arguments",
"6f99bf5dd0": "No default arguments",
"8fbe1f37c1": "Environment",
"2d133152fa": "No default environment"
"2d133152fa": "No default environment",
"agentPermissions": "Agent Permissions",
"agentPermissionsInfo": "Agent permissions info",
"agentPermissionsTooltip": "Custom agent arguments stay unchanged when switching modes.",
"agentPermissionsDescription": "Choose whether Orca launches agents with fewer permission prompts or with manual checks.",
"agentPermissionsYolo": "Yolo",
"agentPermissionsManual": "Manual"
},
"AppIconSelector": {
"d5a112dc9b": "下一个图标",
@@ -6299,7 +6305,9 @@
"5784ae8c43": "重命名",
"8a17fd6026": "稳定",
"a79d266f71": "会话",
"afbf35be68": "稳定会话"
"afbf35be68": "稳定会话",
"agentPermissions": "Agent Permissions",
"agentPermissionsDescription": "Switch agent permission defaults between Yolo and Manual."
}
},
"appearance": {
@@ -8811,7 +8819,10 @@
"69af7e9c1c": "尚未加入 PATH。Orca 会将其设为默认,你可随时安装。",
"1eee1c7bd8": "PATH 中未检测到 agents。可选择一个稍后安装,或使用空白 terminal 继续。",
"hideAgents": "隐藏 Agent",
"showMoreAgents": "显示另外 {{value0}} 个 Agent→"
"showMoreAgents": "显示另外 {{value0}} 个 Agent→",
"yoloPermissionsLabel": "Yolo / Dangerously skip permissions",
"yoloPermissionsInfo": "Agent permission info",
"yoloPermissionsTooltip": "Skip permission checks for agents for less interruptions"
},
"FeatureSetupChecklist": {
"77f74946f5": "Agent 可互相通信、领取任务并协调交接。",
+4 -28
View File
@@ -1,4 +1,5 @@
import { isTuiAgent } from './tui-agent-config'
import { YOLO_TUI_AGENT_ARGS, YOLO_TUI_AGENT_ENV } from './tui-agent-permissions'
import type { TuiAgent } from './types'
const UNSUPPORTED_TUI_AGENT_ARGS: Partial<Record<TuiAgent, readonly string[]>> = {
@@ -6,35 +7,10 @@ const UNSUPPORTED_TUI_AGENT_ARGS: Partial<Record<TuiAgent, readonly string[]>> =
kilo: ['--dangerously-skip-permissions']
}
export const DEFAULT_TUI_AGENT_ARGS: Partial<Record<TuiAgent, string>> = {
claude: '--dangerously-skip-permissions',
'claude-agent-teams': '--dangerously-skip-permissions',
openclaude: '--dangerously-skip-permissions',
codex: '--dangerously-bypass-approvals-and-sandbox',
gemini: '--yolo',
antigravity: '--dangerously-skip-permissions',
aider: '--yes-always',
amp: '--dangerously-allow-all',
kiro: '--trust-all-tools',
crush: '--yolo',
autohand: '--unrestricted',
cline: '--auto-approve true',
'command-code': '--yolo',
continue: '--allow "*"',
cursor: '--yolo',
kimi: '--yolo',
'mistral-vibe': '--agent auto-approve',
'qwen-code': '--approval-mode yolo',
rovo: '--yolo',
hermes: '--yolo',
copilot: '--yolo',
grok: '--permission-mode bypassPermissions',
devin: '--permission-mode bypass'
}
export const DEFAULT_TUI_AGENT_ARGS: Partial<Record<TuiAgent, string>> = YOLO_TUI_AGENT_ARGS
export const DEFAULT_TUI_AGENT_ENV: Partial<Record<TuiAgent, Record<string, string>>> = {
goose: { GOOSE_MODE: 'auto' }
}
export const DEFAULT_TUI_AGENT_ENV: Partial<Record<TuiAgent, Record<string, string>>> =
YOLO_TUI_AGENT_ENV
function argPattern(arg: string): RegExp {
return new RegExp(`(^|\\s)${arg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=\\s|$)`, 'g')
+51
View File
@@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest'
import {
applyAgentPermissionMode,
resolveAgentPermissionModeSummary,
YOLO_TUI_AGENT_ARGS,
YOLO_TUI_AGENT_ENV
} from './tui-agent-permissions'
describe('tui agent permissions', () => {
it('recognizes the current default profile as yolo', () => {
expect(
resolveAgentPermissionModeSummary({
agentDefaultArgs: YOLO_TUI_AGENT_ARGS,
agentDefaultEnv: YOLO_TUI_AGENT_ENV
})
).toBe('yolo')
})
it('recognizes an empty profile as manual', () => {
expect(resolveAgentPermissionModeSummary({ agentDefaultArgs: {}, agentDefaultEnv: {} })).toBe(
'manual'
)
})
it('preserves custom agent arguments when applying manual mode', () => {
const result = applyAgentPermissionMode({
mode: 'manual',
agentDefaultArgs: {
claude: '--dangerously-skip-permissions',
codex: '--model gpt-5'
},
agentDefaultEnv: YOLO_TUI_AGENT_ENV
})
expect(result.agentDefaultArgs.claude).toBe('')
expect(result.agentDefaultArgs.codex).toBe('--model gpt-5')
expect(result.agentDefaultEnv.goose).toEqual({})
})
it('reports mixed when custom arguments are present', () => {
expect(
resolveAgentPermissionModeSummary({
agentDefaultArgs: {
...YOLO_TUI_AGENT_ARGS,
codex: '--model gpt-5'
},
agentDefaultEnv: YOLO_TUI_AGENT_ENV
})
).toBe('mixed')
})
})
+143
View File
@@ -0,0 +1,143 @@
import { TUI_AGENT_CONFIG } from './tui-agent-config'
import type { TuiAgent } from './types'
export type AgentPermissionMode = 'yolo' | 'manual' | 'mixed'
export const YOLO_TUI_AGENT_ARGS: Partial<Record<TuiAgent, string>> = {
claude: '--dangerously-skip-permissions',
'claude-agent-teams': '--dangerously-skip-permissions',
openclaude: '--dangerously-skip-permissions',
codex: '--dangerously-bypass-approvals-and-sandbox',
gemini: '--yolo',
antigravity: '--dangerously-skip-permissions',
aider: '--yes-always',
amp: '--dangerously-allow-all',
kiro: '--trust-all-tools',
crush: '--yolo',
autohand: '--unrestricted',
cline: '--auto-approve true',
'command-code': '--yolo',
continue: '--allow "*"',
cursor: '--yolo',
kimi: '--yolo',
'mistral-vibe': '--agent auto-approve',
'qwen-code': '--approval-mode yolo',
rovo: '--yolo',
hermes: '--yolo',
copilot: '--yolo',
grok: '--permission-mode bypassPermissions',
devin: '--permission-mode bypass'
}
export const YOLO_TUI_AGENT_ENV: Partial<Record<TuiAgent, Record<string, string>>> = {
goose: { GOOSE_MODE: 'auto' }
}
const PERMISSION_AGENT_IDS = Object.keys(TUI_AGENT_CONFIG).filter(
(agent): agent is TuiAgent => agent in YOLO_TUI_AGENT_ARGS || agent in YOLO_TUI_AGENT_ENV
)
function normalizeArgs(value: string | null | undefined): string {
return value?.trim() ?? ''
}
function sameEnv(
left: Record<string, string> | null | undefined,
right: Record<string, string> | null | undefined
): boolean {
const leftEntries = Object.entries(left ?? {})
const rightEntries = Object.entries(right ?? {})
if (leftEntries.length !== rightEntries.length) {
return false
}
return leftEntries.every(([name, value]) => right?.[name] === value)
}
function resolveAgentPermissionMode(args: string, yoloArgs: string): AgentPermissionMode {
if (!args) {
return 'manual'
}
return args === yoloArgs ? 'yolo' : 'mixed'
}
function resolveAgentEnvPermissionMode(
env: Record<string, string> | null | undefined,
yoloEnv: Record<string, string> | undefined
): AgentPermissionMode {
if (sameEnv(env, {})) {
return 'manual'
}
return sameEnv(env, yoloEnv) ? 'yolo' : 'mixed'
}
export function resolveAgentPermissionModeSummary(args: {
agentDefaultArgs?: Partial<Record<TuiAgent, string>> | null
agentDefaultEnv?: Partial<Record<TuiAgent, Record<string, string>>> | null
}): AgentPermissionMode {
let sawYolo = false
let sawManual = false
let sawMixed = false
for (const agent of PERMISSION_AGENT_IDS) {
const modes: AgentPermissionMode[] = []
if (agent in YOLO_TUI_AGENT_ARGS) {
modes.push(
resolveAgentPermissionMode(
normalizeArgs(args.agentDefaultArgs?.[agent]),
YOLO_TUI_AGENT_ARGS[agent] ?? ''
)
)
}
if (agent in YOLO_TUI_AGENT_ENV) {
modes.push(
resolveAgentEnvPermissionMode(args.agentDefaultEnv?.[agent], YOLO_TUI_AGENT_ENV[agent])
)
}
for (const mode of modes) {
if (mode === 'yolo') {
sawYolo = true
} else if (mode === 'manual') {
sawManual = true
} else {
sawMixed = true
}
}
}
if (sawMixed || (sawYolo && sawManual)) {
return 'mixed'
}
return sawYolo ? 'yolo' : 'manual'
}
export function applyAgentPermissionMode(args: {
mode: Exclude<AgentPermissionMode, 'mixed'>
agentDefaultArgs?: Partial<Record<TuiAgent, string>> | null
agentDefaultEnv?: Partial<Record<TuiAgent, Record<string, string>>> | null
}): {
agentDefaultArgs: Partial<Record<TuiAgent, string>>
agentDefaultEnv: Partial<Record<TuiAgent, Record<string, string>>>
} {
const nextArgs = { ...args.agentDefaultArgs }
const nextEnv = { ...args.agentDefaultEnv }
for (const agent of PERMISSION_AGENT_IDS) {
if (agent in YOLO_TUI_AGENT_ARGS) {
const yoloArgs = YOLO_TUI_AGENT_ARGS[agent] ?? ''
const currentArgs = normalizeArgs(nextArgs[agent])
if (!currentArgs || currentArgs === yoloArgs) {
nextArgs[agent] = args.mode === 'yolo' ? yoloArgs : ''
}
}
if (agent in YOLO_TUI_AGENT_ENV) {
const yoloEnv = YOLO_TUI_AGENT_ENV[agent]
const currentEnv = nextEnv[agent]
if (sameEnv(currentEnv, {}) || sameEnv(currentEnv, yoloEnv)) {
nextEnv[agent] = args.mode === 'yolo' ? { ...yoloEnv } : {}
}
}
}
return { agentDefaultArgs: nextArgs, agentDefaultEnv: nextEnv }
}