diff --git a/src/renderer/src/components/onboarding/AgentStep.test.tsx b/src/renderer/src/components/onboarding/AgentStep.test.tsx index 5bb6bc57d33..654a339a234 100644 --- a/src/renderer/src/components/onboarding/AgentStep.test.tsx +++ b/src/renderer/src/components/onboarding/AgentStep.test.tsx @@ -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( - + + + ) 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( - + + + ) expect(html).toContain('Hide agents') diff --git a/src/renderer/src/components/onboarding/AgentStep.tsx b/src/renderer/src/components/onboarding/AgentStep.tsx index 4ef3e8cafbd..96894f357dd 100644 --- a/src/renderer/src/components/onboarding/AgentStep.tsx +++ b/src/renderer/src/components/onboarding/AgentStep.tsx @@ -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 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 }: )} +
)} {currentStep.id === 'theme' && ( diff --git a/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.ts b/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.ts index 1c0b226fd14..d671057bab7 100644 --- a/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.ts +++ b/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.ts @@ -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) => Promise | 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 ]) } diff --git a/src/renderer/src/components/onboarding/use-onboarding-flow.ts b/src/renderer/src/components/onboarding/use-onboarding-flow.ts index 88d02e23af2..f278187a24b 100644 --- a/src/renderer/src/components/onboarding/use-onboarding-flow.ts +++ b/src/renderer/src/components/onboarding/use-onboarding-flow.ts @@ -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(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, diff --git a/src/renderer/src/components/settings/AgentsPane.test.tsx b/src/renderer/src/components/settings/AgentsPane.test.tsx index 4a65bc68e7c..82fd2e87d4c 100644 --- a/src/renderer/src/components/settings/AgentsPane.test.tsx +++ b/src/renderer/src/components/settings/AgentsPane.test.tsx @@ -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> = {} ): 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) diff --git a/src/renderer/src/components/settings/AgentsPane.tsx b/src/renderer/src/components/settings/AgentsPane.tsx index 08121326730..93be35b36bb 100644 --- a/src/renderer/src/components/settings/AgentsPane.tsx +++ b/src/renderer/src/components/settings/AgentsPane.tsx @@ -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) => void +} + export function buildAgentAvailabilitySettingsUpdate( settings: Pick, id: TuiAgent, @@ -177,6 +188,76 @@ export function AgentAvailabilityControl({ ) } +export function AgentPermissionsSetting({ + mode, + onChange +}: AgentPermissionsSettingProps): React.JSX.Element { + const visibleMode: Exclude = mode === 'manual' ? 'manual' : 'yolo' + return ( +
+ + {translate('auto.components.settings.AgentsPane.agentPermissions', 'Agent Permissions')} + + + + + + {translate( + 'auto.components.settings.AgentsPane.agentPermissionsTooltip', + "Doesn't apply to agents where you've overridden launch arguments." + )} + + + + } + description={translate( + 'auto.components.settings.AgentsPane.agentPermissionsDescription', + 'Choose whether Orca launches agents with fewer permission prompts or with manual checks.' + )} + action={ + + 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' + ) + } + ]} + /> + } + /> +
+ ) +} + 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): 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({ + + {detectedAgents.length > 0 && (
[ 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') + ] } ]) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 1723d20e6ff..b06bd4cb719 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -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.", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 564d25c78af..48c434e7e40 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -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.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 2722bc433ed..5c08ecb8e6e 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -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 は相互にメッセージを送信し、タスクを実行し、引き継ぎを調整できます。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 5f7e9ce59ce..d8c34a2551e 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -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는 서로 메시지를 보내고, 작업을 수행하고, 핸드오프를 조정할 수 있습니다.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index bccdbc88fad..8ed920816b0 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -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 可互相通信、领取任务并协调交接。", diff --git a/src/shared/tui-agent-launch-defaults.ts b/src/shared/tui-agent-launch-defaults.ts index deb757143b0..94ca6e103a1 100644 --- a/src/shared/tui-agent-launch-defaults.ts +++ b/src/shared/tui-agent-launch-defaults.ts @@ -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> = { @@ -6,35 +7,10 @@ const UNSUPPORTED_TUI_AGENT_ARGS: Partial> = kilo: ['--dangerously-skip-permissions'] } -export const DEFAULT_TUI_AGENT_ARGS: Partial> = { - 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> = YOLO_TUI_AGENT_ARGS -export const DEFAULT_TUI_AGENT_ENV: Partial>> = { - goose: { GOOSE_MODE: 'auto' } -} +export const DEFAULT_TUI_AGENT_ENV: Partial>> = + YOLO_TUI_AGENT_ENV function argPattern(arg: string): RegExp { return new RegExp(`(^|\\s)${arg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=\\s|$)`, 'g') diff --git a/src/shared/tui-agent-permissions.test.ts b/src/shared/tui-agent-permissions.test.ts new file mode 100644 index 00000000000..7ed1ff440c3 --- /dev/null +++ b/src/shared/tui-agent-permissions.test.ts @@ -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') + }) +}) diff --git a/src/shared/tui-agent-permissions.ts b/src/shared/tui-agent-permissions.ts new file mode 100644 index 00000000000..e5716e962a8 --- /dev/null +++ b/src/shared/tui-agent-permissions.ts @@ -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> = { + 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>> = { + 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 | null | undefined, + right: Record | 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 | null | undefined, + yoloEnv: Record | undefined +): AgentPermissionMode { + if (sameEnv(env, {})) { + return 'manual' + } + return sameEnv(env, yoloEnv) ? 'yolo' : 'mixed' +} + +export function resolveAgentPermissionModeSummary(args: { + agentDefaultArgs?: Partial> | null + agentDefaultEnv?: Partial>> | 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 + agentDefaultArgs?: Partial> | null + agentDefaultEnv?: Partial>> | null +}): { + agentDefaultArgs: Partial> + agentDefaultEnv: Partial>> +} { + 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 } +}