From e4050eccbd7680f1c4e86abe239c4837c14dac5e Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:14:53 -0700 Subject: [PATCH] Default agent launches to yolo permissions mode (#5145) * Default agent launches to yolo mode * test: update launch default validations --- src/main/ipc/filesystem-watcher-real.test.ts | 5 +- src/main/persistence.test.ts | 41 ++++ src/main/persistence.ts | 66 +++++ src/main/runtime/orca-runtime.test.ts | 20 +- src/main/runtime/orca-runtime.ts | 24 +- src/main/runtime/rpc/methods/client-ui.ts | 12 + .../FloatingTerminalWindowControls.tsx | 6 + .../onboarding-folder-agent-startup.test.ts | 6 +- .../src/components/settings/AgentsPane.tsx | 227 +++++++++++++++++- .../src/components/sidebar/SidebarNav.tsx | 21 +- .../sidebar/sidebar-nav-controls.tsx | 50 ++++ .../terminal-pane/pty-connection.test.ts | 4 +- .../terminal-pane/pty-connection.ts | 12 + src/renderer/src/hooks/useComposerState.ts | 14 ++ src/renderer/src/i18n/locales/en.json | 8 +- src/renderer/src/i18n/locales/es.json | 8 +- src/renderer/src/i18n/locales/ja.json | 8 +- src/renderer/src/i18n/locales/ko.json | 8 +- src/renderer/src/i18n/locales/zh.json | 8 +- .../launch-agent-background-session.test.ts | 13 +- .../lib/launch-agent-background-session.ts | 10 + .../src/lib/launch-agent-in-new-tab.test.ts | 8 +- .../src/lib/launch-agent-in-new-tab.ts | 24 +- .../src/lib/launch-work-item-direct-types.ts | 29 +++ .../src/lib/launch-work-item-direct.test.ts | 4 + .../src/lib/launch-work-item-direct.ts | 24 +- src/renderer/src/lib/launchable-work-item.ts | 55 ----- .../lib/onboarding-folder-agent-startup.ts | 6 + .../src/lib/resume-sleeping-agent-session.ts | 6 + .../worktree-activation-created-agent.test.ts | 5 +- src/renderer/src/lib/worktree-activation.ts | 6 + .../repos-onboarding-folder-startup.test.ts | 3 +- src/renderer/src/store/slices/settings.ts | 12 + src/renderer/src/web/web-preload-api.ts | 8 + src/shared/constants.test.ts | 17 ++ src/shared/constants.ts | 4 + src/shared/tui-agent-config.ts | 21 -- src/shared/tui-agent-launch-defaults.ts | 103 ++++++++ src/shared/tui-agent-startup.test.ts | 14 ++ src/shared/tui-agent-startup.ts | 33 ++- src/shared/types.ts | 6 + .../e2e/folder-setup-shallow-priority.spec.ts | 2 +- .../e2e/git-no-upstream-polling-churn.spec.ts | 8 +- .../helpers/source-control-ai-generation.ts | 40 ++- tests/e2e/setup-script-import.spec.ts | 4 +- tests/e2e/tab-rename.spec.ts | 7 +- .../terminal-cursor-inactive-style.spec.ts | 2 +- ...terminal-hidden-tui-visual-restore.spec.ts | 3 - tests/e2e/terminal-output-scheduler.spec.ts | 18 +- tests/e2e/worktree.spec.ts | 19 +- 50 files changed, 869 insertions(+), 193 deletions(-) create mode 100644 src/renderer/src/components/sidebar/sidebar-nav-controls.tsx create mode 100644 src/renderer/src/lib/launch-work-item-direct-types.ts delete mode 100644 src/renderer/src/lib/launchable-work-item.ts create mode 100644 src/shared/tui-agent-launch-defaults.ts diff --git a/src/main/ipc/filesystem-watcher-real.test.ts b/src/main/ipc/filesystem-watcher-real.test.ts index 75fb6121b1d..34f2bcc91e1 100644 --- a/src/main/ipc/filesystem-watcher-real.test.ts +++ b/src/main/ipc/filesystem-watcher-real.test.ts @@ -77,7 +77,10 @@ describe('filesystem-watcher real @parcel/watcher integration', () => { expect(typeof watcher.subscribe).toBe('function') }) - it.runIf(process.platform !== 'win32')( + // Why: this integration targets the Linux native watcher path described + // above; macOS developer sandboxes can load the addon while suppressing + // subscribe callbacks, which makes this an environment check instead. + it.runIf(process.platform === 'linux')( 'emits fs:changed for a file created in a watched directory', async () => { // Why: macOS reports temp watcher events under /private/var while diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index 0d3993c0fb5..06a0f98ab53 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -3006,6 +3006,47 @@ describe('Store', () => { expect(updated.disabledTuiAgents).toEqual(['gemini', 'opencode']) }) + it('migrates yolo default args onto untouched agent launch settings', async () => { + writeFileSync( + join(testState.dir, 'orca-data.json'), + JSON.stringify({ + settings: { + agentCmdOverrides: {} + } + }) + ) + const store = await createStore() + + expect(store.getSettings().agentDefaultArgs).toMatchObject({ + claude: '--dangerously-skip-permissions', + codex: '--dangerously-bypass-approvals-and-sandbox', + cursor: '--yolo' + }) + expect(store.getSettings().agentDefaultEnv).toMatchObject({ + goose: { GOOSE_MODE: 'auto' } + }) + expect(store.getSettings().agentYoloDefaultsMigrated).toBe(true) + }) + + it('does not add yolo defaults for legacy agents with command overrides', async () => { + writeFileSync( + join(testState.dir, 'orca-data.json'), + JSON.stringify({ + settings: { + agentCmdOverrides: { + codex: 'codex --profile work', + goose: 'goose' + } + } + }) + ) + const store = await createStore() + + expect(store.getSettings().agentDefaultArgs?.codex).toBe('') + expect(store.getSettings().agentDefaultEnv?.goose).toEqual({}) + expect(store.getSettings().agentDefaultArgs?.claude).toBe('--dangerously-skip-permissions') + }) + it('normalizes app icon on load and update', async () => { writeFileSync( join(testState.dir, 'orca-data.json'), diff --git a/src/main/persistence.ts b/src/main/persistence.ts index dfc5dcd67d4..c470ee9a08a 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -146,6 +146,12 @@ import { sourceControlAiSettingsFromLegacy } from '../shared/source-control-ai' import { normalizeDisabledTuiAgents } from '../shared/tui-agent-selection' +import { + DEFAULT_TUI_AGENT_ARGS, + DEFAULT_TUI_AGENT_ENV, + normalizeTuiAgentArgsRecord, + normalizeTuiAgentEnvRecord +} from '../shared/tui-agent-launch-defaults' import { normalizeTerminalCursorStyleDefault } from '../shared/terminal-cursor-style-settings' import { normalizeUiLanguage } from '../shared/ui-language' import { normalizeBrowserPageZoomLevel } from '../shared/browser-page-zoom' @@ -309,6 +315,53 @@ function getWorkspaceLayoutHistoryKey(layout: OrcaWorkspaceLayout): string { return `${normalizeRuntimePathForComparison(layout.path)}:${layout.nestWorkspaces}` } +function migrateAgentYoloDefaults( + settings: GlobalSettings | undefined +): Pick { + const existingArgs = normalizeTuiAgentArgsRecord(settings?.agentDefaultArgs) + const existingEnv = normalizeTuiAgentEnvRecord(settings?.agentDefaultEnv) + if (settings?.agentYoloDefaultsMigrated === true) { + return { + agentDefaultArgs: existingArgs, + agentDefaultEnv: existingEnv, + agentYoloDefaultsMigrated: true + } + } + + const commandOverrides = settings?.agentCmdOverrides ?? {} + const migratedArgs = { ...existingArgs } + for (const [agent, args] of Object.entries(DEFAULT_TUI_AGENT_ARGS)) { + if (agent in migratedArgs) { + continue + } + if (agent in commandOverrides) { + migratedArgs[agent as keyof typeof DEFAULT_TUI_AGENT_ARGS] = '' + continue + } + migratedArgs[agent as keyof typeof DEFAULT_TUI_AGENT_ARGS] = args + } + + const migratedEnv = { ...existingEnv } + for (const [agent, env] of Object.entries(DEFAULT_TUI_AGENT_ENV)) { + if (agent in migratedEnv) { + continue + } + if (agent in commandOverrides) { + migratedEnv[agent as keyof typeof DEFAULT_TUI_AGENT_ENV] = {} + continue + } + migratedEnv[agent as keyof typeof DEFAULT_TUI_AGENT_ENV] = { ...env } + } + + return { + // Why: legacy users could only customize per-agent launch defaults via + // command overrides, so those agents are treated as already user-owned. + agentDefaultArgs: migratedArgs, + agentDefaultEnv: migratedEnv, + agentYoloDefaultsMigrated: true + } +} + function normalizeGroupBy(groupBy: unknown): PersistedState['ui']['groupBy'] { if ( groupBy === 'none' || @@ -2040,6 +2093,10 @@ export class Store { const migratedDisabledTuiAgents = normalizeDisabledTuiAgents( parsed.settings?.disabledTuiAgents ) + const migratedAgentYoloDefaults = migrateAgentYoloDefaults(parsed.settings) + if (parsed.settings?.agentYoloDefaultsMigrated !== true) { + this.loadNeedsSave = true + } if ( !claudeAgentTeamsDefaultDisabledMigrated && !migratedDisabledTuiAgents.includes('claude-agent-teams') @@ -2116,6 +2173,7 @@ export class Store { parsed.settings?.terminalShortcutPolicy ), disabledTuiAgents: migratedDisabledTuiAgents, + ...migratedAgentYoloDefaults, claudeAgentTeamsDefaultDisabledMigrated: true, openInApplications: normalizeOpenInApplications(parsed.settings?.openInApplications, { seedDefaults: true @@ -3439,6 +3497,14 @@ export class Store { if ('disabledTuiAgents' in updates) { sanitizedUpdates.disabledTuiAgents = normalizeDisabledTuiAgents(updates.disabledTuiAgents) } + if ('agentDefaultArgs' in updates) { + sanitizedUpdates.agentDefaultArgs = normalizeTuiAgentArgsRecord(updates.agentDefaultArgs) + sanitizedUpdates.agentYoloDefaultsMigrated = true + } + if ('agentDefaultEnv' in updates) { + sanitizedUpdates.agentDefaultEnv = normalizeTuiAgentEnvRecord(updates.agentDefaultEnv) + sanitizedUpdates.agentYoloDefaultsMigrated = true + } if ('terminalQuickCommands' in updates) { sanitizedUpdates.terminalQuickCommands = normalizeTerminalQuickCommands( updates.terminalQuickCommands diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index fa5b46fbae3..53baa70801f 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -2213,7 +2213,7 @@ describe('OrcaRuntimeService', () => { expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/remote/agent-feature', - command: "codex 'hi'", + command: "codex '--dangerously-bypass-approvals-and-sandbox' 'hi'", worktreeId: result.worktree.id }) ) @@ -2321,7 +2321,7 @@ describe('OrcaRuntimeService', () => { expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: 'C:/remote/agent-feature', - command: "codex 'fix Bob''s branch'" + command: "codex '--dangerously-bypass-approvals-and-sandbox' 'fix Bob''s branch'" }) ) expect(addWorktree).not.toHaveBeenCalled() @@ -8427,7 +8427,7 @@ describe('OrcaRuntimeService', () => { expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ - command: 'command-code --profile mobile', + command: "command-code --profile mobile '--yolo'", cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID }) @@ -11226,7 +11226,7 @@ describe('OrcaRuntimeService', () => { expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/tmp/workspaces/runtime-startup-draft', - command: 'codex --profile work', + command: "codex --profile work '--dangerously-bypass-approvals-and-sandbox'", worktreeId: result.worktree.id }) ) @@ -11329,7 +11329,7 @@ describe('OrcaRuntimeService', () => { expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/tmp/workspaces/runtime-cli-agent-startup', - command: "codex 'hi'", + command: "codex '--dangerously-bypass-approvals-and-sandbox' 'hi'", worktreeId: result.worktree.id }) ) @@ -11398,7 +11398,7 @@ describe('OrcaRuntimeService', () => { expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/tmp/workspaces/runtime-cli-aider-startup', - command: 'aider', + command: "aider '--yes-always'", worktreeId: result.worktree.id }) ) @@ -11631,7 +11631,7 @@ describe('OrcaRuntimeService', () => { 1, expect.objectContaining({ cwd: '/tmp/workspaces/runtime-startup-setup-split', - command: 'codex', + command: "codex '--dangerously-bypass-approvals-and-sandbox'", worktreeId: result.worktree.id }) ) @@ -11734,7 +11734,7 @@ describe('OrcaRuntimeService', () => { expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/tmp/workspaces/runtime-explicit-draft', - command: 'codex', + command: "codex '--dangerously-bypass-approvals-and-sandbox'", worktreeId: result.worktree.id }) ) @@ -11904,7 +11904,7 @@ describe('OrcaRuntimeService', () => { expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/remote/mobile-startup-draft', - command: `claude --prefill '${draftUrl}'`, + command: `claude '--dangerously-skip-permissions' --prefill '${draftUrl}'`, connectionId: 'ssh-1', worktreeId: result.worktree.id }) @@ -12012,7 +12012,7 @@ describe('OrcaRuntimeService', () => { expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/remote/mobile-codex-draft', - command: 'codex', + command: "codex '--dangerously-bypass-approvals-and-sandbox'", connectionId: 'ssh-1', worktreeId: result.worktree.id }) diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 0a53d0c4644..2ec89e5527d 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -116,7 +116,11 @@ import { isValidHostTerminalTabId } from '../../shared/terminal-tab-id' import { buildAgentDraftLaunchPlan, buildAgentStartupPlan } from '../../shared/tui-agent-startup' import { isExpectedAgentProcess } from '../../shared/agent-process-recognition' import { isTuiAgentEnabled, pickTuiAgent } from '../../shared/tui-agent-selection' -import { TUI_AGENT_CONFIG, isTuiAgent } from '../../shared/tui-agent-config' +import { + resolveTuiAgentLaunchArgs, + resolveTuiAgentLaunchEnv +} from '../../shared/tui-agent-launch-defaults' +import { isTuiAgent, TUI_AGENT_CONFIG } from '../../shared/tui-agent-config' import { detectInstalledAgents, detectRemoteAgents } from '../ipc/preflight' import { markCodexProjectTrusted, @@ -589,6 +593,8 @@ type RuntimeStore = { defaultTuiAgent?: GlobalSettings['defaultTuiAgent'] disabledTuiAgents?: GlobalSettings['disabledTuiAgents'] agentCmdOverrides?: GlobalSettings['agentCmdOverrides'] + agentDefaultArgs?: GlobalSettings['agentDefaultArgs'] + agentDefaultEnv?: GlobalSettings['agentDefaultEnv'] agentStatusHooksEnabled?: GlobalSettings['agentStatusHooksEnabled'] defaultTaskSource?: GlobalSettings['defaultTaskSource'] defaultTaskViewPreset?: GlobalSettings['defaultTaskViewPreset'] @@ -1701,6 +1707,8 @@ export class OrcaRuntimeService { | 'defaultTuiAgent' | 'disabledTuiAgents' | 'agentCmdOverrides' + | 'agentDefaultArgs' + | 'agentDefaultEnv' | 'agentStatusHooksEnabled' | 'defaultTaskSource' | 'defaultTaskViewPreset' @@ -1717,6 +1725,8 @@ export class OrcaRuntimeService { defaultTuiAgent: settings.defaultTuiAgent ?? null, disabledTuiAgents: settings.disabledTuiAgents ?? [], agentCmdOverrides: settings.agentCmdOverrides ?? {}, + agentDefaultArgs: settings.agentDefaultArgs ?? {}, + agentDefaultEnv: settings.agentDefaultEnv ?? {}, agentStatusHooksEnabled: settings.agentStatusHooksEnabled !== false, defaultTaskSource: settings.defaultTaskSource ?? 'github', defaultTaskViewPreset: settings.defaultTaskViewPreset ?? 'issues', @@ -1733,6 +1743,8 @@ export class OrcaRuntimeService { | 'agentStatusHooksEnabled' | 'defaultTuiAgent' | 'disabledTuiAgents' + | 'agentDefaultArgs' + | 'agentDefaultEnv' | 'defaultTaskSource' | 'defaultTaskViewPreset' | 'visibleTaskProviders' @@ -1745,6 +1757,8 @@ export class OrcaRuntimeService { | 'defaultTuiAgent' | 'disabledTuiAgents' | 'agentCmdOverrides' + | 'agentDefaultArgs' + | 'agentDefaultEnv' | 'agentStatusHooksEnabled' | 'defaultTaskSource' | 'defaultTaskViewPreset' @@ -8732,6 +8746,8 @@ export class OrcaRuntimeService { agent, draft: content, cmdOverrides: settings.agentCmdOverrides ?? {}, + agentArgs: resolveTuiAgentLaunchArgs(agent, settings.agentDefaultArgs), + agentEnv: resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv), platform: agentLaunchPlatform }) if (draftLaunchPlan) { @@ -8748,6 +8764,8 @@ export class OrcaRuntimeService { agent, prompt: '', cmdOverrides: settings.agentCmdOverrides ?? {}, + agentArgs: resolveTuiAgentLaunchArgs(agent, settings.agentDefaultArgs), + agentEnv: resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv), platform: agentLaunchPlatform, allowEmptyPromptLaunch: true }) @@ -8783,6 +8801,8 @@ export class OrcaRuntimeService { agent, prompt: prompt ?? '', cmdOverrides: settings.agentCmdOverrides ?? {}, + agentArgs: resolveTuiAgentLaunchArgs(agent, settings.agentDefaultArgs), + agentEnv: resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv), platform: agentLaunchPlatform, allowEmptyPromptLaunch: true }) @@ -11472,6 +11492,8 @@ export class OrcaRuntimeService { agent: opts.agent, prompt: '', cmdOverrides: settings.agentCmdOverrides ?? {}, + agentArgs: resolveTuiAgentLaunchArgs(opts.agent, settings.agentDefaultArgs), + agentEnv: resolveTuiAgentLaunchEnv(opts.agent, settings.agentDefaultEnv), platform, allowEmptyPromptLaunch: true }) diff --git a/src/main/runtime/rpc/methods/client-ui.ts b/src/main/runtime/rpc/methods/client-ui.ts index 2cc10233b52..6d9aa5bdce1 100644 --- a/src/main/runtime/rpc/methods/client-ui.ts +++ b/src/main/runtime/rpc/methods/client-ui.ts @@ -4,6 +4,10 @@ import { type FeatureInteractionId } from '../../../../shared/feature-interactions' import { isFeatureTipId } from '../../../../shared/feature-tips' +import { + normalizeTuiAgentArgsRecord, + normalizeTuiAgentEnvRecord +} from '../../../../shared/tui-agent-launch-defaults' import { isTuiAgent } from '../../../../shared/tui-agent-config' import { isTaskProvider } from '../../../../shared/task-providers' import { normalizeDisabledTuiAgents } from '../../../../shared/tui-agent-selection' @@ -114,6 +118,14 @@ const SettingsUpdate = z .unknown() .transform((value) => normalizeDisabledTuiAgents(value)) .optional(), + agentDefaultArgs: z + .unknown() + .transform((value) => normalizeTuiAgentArgsRecord(value)) + .optional(), + agentDefaultEnv: z + .unknown() + .transform((value) => normalizeTuiAgentEnvRecord(value)) + .optional(), defaultTaskSource: TaskProviderParam.optional(), visibleTaskProviders: z.array(TaskProviderParam).optional(), defaultTaskViewPreset: z diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx index 228cb8256a2..e3fd7b00a64 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.tsx @@ -11,6 +11,10 @@ import { tuiAgentToAgentKind } from '@/lib/telemetry' import { useAppStore } from '@/store' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection' +import { + resolveTuiAgentLaunchArgs, + resolveTuiAgentLaunchEnv +} from '../../../../shared/tui-agent-launch-defaults' import { translate } from '@/i18n/i18n' type FloatingTerminalWindowControlsProps = { @@ -55,6 +59,8 @@ export function FloatingTerminalWindowControls({ agent: defaultAgent, prompt: '', cmdOverrides: state.settings?.agentCmdOverrides ?? {}, + agentArgs: resolveTuiAgentLaunchArgs(defaultAgent, state.settings?.agentDefaultArgs), + agentEnv: resolveTuiAgentLaunchEnv(defaultAgent, state.settings?.agentDefaultEnv), platform: CLIENT_PLATFORM, allowEmptyPromptLaunch: true }) diff --git a/src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts b/src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts index dc2c49e855b..ceef964dd40 100644 --- a/src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts +++ b/src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts @@ -14,7 +14,8 @@ describe('buildOnboardingFolderAgentStartup', () => { }) expect(startup).toEqual({ - command: 'codex', + command: "codex '--dangerously-bypass-approvals-and-sandbox'", + env: {}, telemetry: { agent_kind: 'codex', launch_source: 'onboarding', @@ -90,7 +91,8 @@ describe('buildOnboardingFolderAgentStartup', () => { false ) ).toEqual({ - command: 'echo onboarding-folder-agent', + command: "echo onboarding-folder-agent '--dangerously-bypass-approvals-and-sandbox'", + env: {}, telemetry: { agent_kind: 'codex', launch_source: 'onboarding', diff --git a/src/renderer/src/components/settings/AgentsPane.tsx b/src/renderer/src/components/settings/AgentsPane.tsx index 234565bf4d5..650d85b231d 100644 --- a/src/renderer/src/components/settings/AgentsPane.tsx +++ b/src/renderer/src/components/settings/AgentsPane.tsx @@ -27,6 +27,12 @@ import { isTuiAgentEnabled, normalizeDisabledTuiAgents } from '../../../../shared/tui-agent-selection' +import { + getTuiAgentDefaultArgs, + getTuiAgentDefaultEnv, + resolveTuiAgentLaunchArgs, + resolveTuiAgentLaunchEnv +} from '../../../../shared/tui-agent-launch-defaults' import { translate } from '@/i18n/i18n' export { getAgentsPaneSearchEntries } from './agents-search' @@ -55,13 +61,19 @@ type AgentRowProps = { label: string homepageUrl: string defaultCmd: string + defaultArgs: string + defaultEnv: Record isDetected: boolean isEnabled: boolean isDefault: boolean cmdOverride: string | undefined + argsOverride: string + envOverride: Record onSetDefault: () => void onSetEnabled: (enabled: boolean) => void onSaveOverride: (value: string) => void + onSaveArgs: (value: string) => void + onSaveEnv: (value: Record) => void } type AgentCommandOverrideInputProps = { @@ -70,6 +82,18 @@ type AgentCommandOverrideInputProps = { onSaveOverride: (value: string) => void } +type AgentDefaultArgsInputProps = { + defaultArgs: string + argsOverride: string + onSaveArgs: (value: string) => void +} + +type AgentDefaultEnvInputProps = { + defaultEnv: Record + envOverride: Record + onSaveEnv: (value: Record) => void +} + type AgentAvailability = 'enabled' | 'disabled' type AgentAvailabilityControlProps = { @@ -211,20 +235,165 @@ function AgentCommandOverrideInput({ ) } +function AgentDefaultArgsInput({ + defaultArgs, + argsOverride, + onSaveArgs +}: AgentDefaultArgsInputProps): React.JSX.Element { + const draftSeed = argsOverride + const [argsDraft, setArgsDraft] = useState(draftSeed) + + const commitArgs = (): void => { + onSaveArgs(argsDraft.trim()) + } + + return ( +
+ + {translate('auto.components.settings.AgentsPane.cfb3f35775', 'Arguments')} + + setArgsDraft(e.target.value)} + onBlur={commitArgs} + onKeyDown={(e) => { + if (e.key === 'Enter') { + commitArgs() + e.currentTarget.blur() + } + if (e.key === 'Escape') { + setArgsDraft(draftSeed) + e.currentTarget.blur() + } + }} + placeholder={ + defaultArgs || + translate('auto.components.settings.AgentsPane.6f99bf5dd0', 'No default arguments') + } + spellCheck={false} + className="h-7 flex-1 font-mono text-xs" + /> + {argsOverride !== defaultArgs && ( + + )} +
+ ) +} + +function stringifyAgentEnv(env: Record): string { + return Object.entries(env) + .map(([name, value]) => `${name}=${value}`) + .join(' ') +} + +function parseAgentEnvDraft(value: string): Record { + const env: Record = {} + for (const pair of value.trim().split(/\s+/)) { + const separatorIndex = pair.indexOf('=') + if (separatorIndex <= 0) { + continue + } + const name = pair.slice(0, separatorIndex).trim() + if (!name) { + continue + } + env[name] = pair.slice(separatorIndex + 1) + } + return env +} + +function AgentDefaultEnvInput({ + defaultEnv, + envOverride, + onSaveEnv +}: AgentDefaultEnvInputProps): React.JSX.Element { + const defaultEnvText = stringifyAgentEnv(defaultEnv) + const draftSeed = stringifyAgentEnv(envOverride) + const [envDraft, setEnvDraft] = useState(draftSeed) + + const commitEnv = (): void => { + onSaveEnv(parseAgentEnvDraft(envDraft)) + } + + return ( +
+ + {translate('auto.components.settings.AgentsPane.8fbe1f37c1', 'Environment')} + + setEnvDraft(e.target.value)} + onBlur={commitEnv} + onKeyDown={(e) => { + if (e.key === 'Enter') { + commitEnv() + e.currentTarget.blur() + } + if (e.key === 'Escape') { + setEnvDraft(draftSeed) + e.currentTarget.blur() + } + }} + placeholder={ + defaultEnvText || + translate('auto.components.settings.AgentsPane.2d133152fa', 'No default environment') + } + spellCheck={false} + className="h-7 flex-1 font-mono text-xs" + /> + {draftSeed !== defaultEnvText && ( + + )} +
+ ) +} + function AgentRow({ agentId, label, homepageUrl, defaultCmd, + defaultArgs, + defaultEnv, isDetected, isEnabled, isDefault, cmdOverride, + argsOverride, + envOverride, onSetDefault, onSetEnabled, - onSaveOverride + onSaveOverride, + onSaveArgs, + onSaveEnv }: AgentRowProps): React.JSX.Element { - const [cmdOpen, setCmdOpen] = useState(Boolean(cmdOverride)) + const envSummary = stringifyAgentEnv(envOverride) + const defaultEnvSummary = stringifyAgentEnv(defaultEnv) + const [cmdOpen, setCmdOpen] = useState( + Boolean(cmdOverride) || argsOverride !== defaultArgs || envSummary !== defaultEnvSummary + ) return (
@@ -260,6 +429,8 @@ function AgentRow({ ) : ( defaultCmd )} + {argsOverride && {argsOverride}} + {envSummary && {envSummary}}
@@ -366,10 +537,28 @@ function AgentRow({ cmdOverride={cmdOverride} onSaveOverride={onSaveOverride} /> +
+ +
+ {(defaultEnvSummary || envSummary) && ( +
+ +
+ )}

{translate( 'auto.components.settings.AgentsPane.f9f127d664', - 'Override the binary path or name used to launch this agent.' + 'Override the binary path or name, and edit the default launch arguments or environment for this agent.' )}

@@ -424,6 +613,8 @@ export function AgentsPane({ const defaultAgent = settings.defaultTuiAgent const cmdOverrides = settings.agentCmdOverrides ?? {} + const agentDefaultArgs = settings.agentDefaultArgs ?? {} + const agentDefaultEnv = settings.agentDefaultEnv ?? {} const disabledAgents = normalizeDisabledTuiAgents(settings.disabledTuiAgents) const setDefault = (id: TuiAgent | 'blank' | null): void => { @@ -450,6 +641,24 @@ export function AgentsPane({ updateSettings({ agentCmdOverrides: next }) } + const saveAgentArgs = (id: TuiAgent, value: string): void => { + updateSettings({ + agentDefaultArgs: { + ...agentDefaultArgs, + [id]: value + } + }) + } + + const saveAgentEnv = (id: TuiAgent, value: Record): void => { + updateSettings({ + agentDefaultEnv: { + ...agentDefaultEnv, + [id]: value + } + }) + } + // 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. @@ -575,13 +784,19 @@ export function AgentsPane({ label={agent.label} homepageUrl={agent.homepageUrl} defaultCmd={agent.cmd} + defaultArgs={getTuiAgentDefaultArgs(agent.id)} + defaultEnv={getTuiAgentDefaultEnv(agent.id)} isDetected isEnabled={isTuiAgentEnabled(agent.id, disabledAgents)} isDefault={defaultAgent === agent.id} cmdOverride={cmdOverrides[agent.id]} + argsOverride={resolveTuiAgentLaunchArgs(agent.id, agentDefaultArgs)} + envOverride={resolveTuiAgentLaunchEnv(agent.id, agentDefaultEnv)} onSetDefault={() => setDefault(agent.id)} onSetEnabled={(enabled) => setAgentEnabled(agent.id, enabled)} onSaveOverride={(v) => saveOverride(agent.id, v)} + onSaveArgs={(v) => saveAgentArgs(agent.id, v)} + onSaveEnv={(v) => saveAgentEnv(agent.id, v)} /> ))} @@ -613,13 +828,19 @@ export function AgentsPane({ label={agent.label} homepageUrl={agent.homepageUrl} defaultCmd={agent.cmd} + defaultArgs={getTuiAgentDefaultArgs(agent.id)} + defaultEnv={getTuiAgentDefaultEnv(agent.id)} isDetected={false} isEnabled={isTuiAgentEnabled(agent.id, disabledAgents)} isDefault={false} cmdOverride={undefined} + argsOverride={resolveTuiAgentLaunchArgs(agent.id, agentDefaultArgs)} + envOverride={resolveTuiAgentLaunchEnv(agent.id, agentDefaultEnv)} onSetDefault={() => {}} onSetEnabled={(enabled) => setAgentEnabled(agent.id, enabled)} onSaveOverride={() => {}} + onSaveArgs={(v) => saveAgentArgs(agent.id, v)} + onSaveEnv={(v) => saveAgentEnv(agent.id, v)} /> ))} diff --git a/src/renderer/src/components/sidebar/SidebarNav.tsx b/src/renderer/src/components/sidebar/SidebarNav.tsx index 1ef46a2f434..1955d9ba048 100644 --- a/src/renderer/src/components/sidebar/SidebarNav.tsx +++ b/src/renderer/src/components/sidebar/SidebarNav.tsx @@ -1,19 +1,15 @@ import React from 'react' -import { Bell, CalendarClock, EyeOff, Search, Smartphone } from 'lucide-react' +import { Bell, CalendarClock, Search, Smartphone } from 'lucide-react' import { useAppStore } from '@/store' import { cn } from '@/lib/utils' import type { GlobalSettings } from '../../../../shared/types' import { useActivityUnreadCount } from '@/components/activity/useActivityUnreadCount' import { useShortcutLabel } from '@/hooks/useShortcutLabel' import { useMobileSidebarOnboardingBadge } from './mobile-sidebar-onboarding-badge' -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger -} from '@/components/ui/context-menu' +import { ContextMenu, ContextMenuTrigger } from '@/components/ui/context-menu' import { SetupGuideSidebarEntry } from './SetupGuideSidebarEntry' import { SidebarTaskNavButton } from './SidebarTaskNavButton' +import { HideSidebarMenu } from './sidebar-nav-controls' import { translate } from '@/i18n/i18n' export { getSetupGuideSidebarEntryReady, shouldShowSetupGuideEntry } from './SetupGuideSidebarEntry' @@ -36,17 +32,6 @@ export function shouldShowAutomationsButton( return settings?.showAutomationsButton !== false } -function HideSidebarMenu({ onHide }: { onHide: () => void }): React.JSX.Element { - return ( - - - - {translate('auto.components.sidebar.SidebarNav.d599269755', 'Hide from sidebar')} - - - ) -} - const SidebarNav = React.memo(function SidebarNav() { const worktreePaletteShortcut = useShortcutLabel('worktree.palette') const openAutomationsPage = useAppStore((s) => s.openAutomationsPage) diff --git a/src/renderer/src/components/sidebar/sidebar-nav-controls.tsx b/src/renderer/src/components/sidebar/sidebar-nav-controls.tsx new file mode 100644 index 00000000000..eb98669a39d --- /dev/null +++ b/src/renderer/src/components/sidebar/sidebar-nav-controls.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import { EyeOff } from 'lucide-react' +import { cn } from '@/lib/utils' +import { ContextMenuContent, ContextMenuItem } from '@/components/ui/context-menu' +import { translate } from '@/i18n/i18n' + +export function HideSidebarMenu({ onHide }: { onHide: () => void }): React.JSX.Element { + return ( + + + + {translate('auto.components.sidebar.SidebarNav.d599269755', 'Hide from sidebar')} + + + ) +} + +export function TaskProviderShortcut({ + canBrowseTasks, + label, + onOpen, + children +}: { + canBrowseTasks: boolean + label: string + onOpen: () => void + children: React.ReactNode +}): React.JSX.Element { + return ( + { + e.stopPropagation() + if (!canBrowseTasks) { + return + } + onOpen() + }} + className={cn( + 'rounded p-0.5 text-muted-foreground/70', + canBrowseTasks ? 'transition-colors hover:text-foreground' : 'cursor-default' + )} + aria-label={canBrowseTasks ? label : undefined} + aria-hidden={canBrowseTasks ? undefined : true} + > + {children} + + ) +} diff --git a/src/renderer/src/components/terminal-pane/pty-connection.test.ts b/src/renderer/src/components/terminal-pane/pty-connection.test.ts index cbf6cd9a965..fd5ba191c11 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.test.ts @@ -3008,7 +3008,9 @@ describe('connectPanePty', () => { await new Promise((resolve) => setTimeout(resolve, 70)) expect(pane.terminal.write).toHaveBeenCalledWith('cold-payload', expect.any(Function)) - expect(transport.sendInput).toHaveBeenCalledWith("codex 'resume' 'codex-session-1'\r") + expect(transport.sendInput).toHaveBeenCalledWith( + "codex '--dangerously-bypass-approvals-and-sandbox' 'resume' 'codex-session-1'\r" + ) }) it('does not resume the provider session when daemon reattach returns a live snapshot', async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index 31db682e583..a98033c3fdb 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -85,6 +85,10 @@ import { import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' import { CLIENT_PLATFORM } from '@/lib/new-workspace' import { buildAgentResumeStartupPlan } from '@/lib/tui-agent-startup' +import { + resolveTuiAgentLaunchArgs, + resolveTuiAgentLaunchEnv +} from '../../../../shared/tui-agent-launch-defaults' import { isResumableTuiAgent, normalizeAgentProviderSession @@ -1780,6 +1784,14 @@ export function connectPanePty( agent: entry.agentType, providerSession, cmdOverrides: useAppStore.getState().settings?.agentCmdOverrides ?? {}, + agentArgs: resolveTuiAgentLaunchArgs( + entry.agentType, + useAppStore.getState().settings?.agentDefaultArgs + ), + agentEnv: resolveTuiAgentLaunchEnv( + entry.agentType, + useAppStore.getState().settings?.agentDefaultEnv + ), platform: getColdRestoreAgentResumePlatform() }) if (!startupPlan) { diff --git a/src/renderer/src/hooks/useComposerState.ts b/src/renderer/src/hooks/useComposerState.ts index 2716cb6e419..8c75169b18a 100644 --- a/src/renderer/src/hooks/useComposerState.ts +++ b/src/renderer/src/hooks/useComposerState.ts @@ -18,6 +18,10 @@ import { runBackgroundWorktreeCreation } from '@/lib/worktree-creation-flow' import type { WorktreeCreationRequest } from '@/lib/pending-worktree-creation' import { buildAgentDraftLaunchPlan, buildAgentStartupPlan } from '@/lib/tui-agent-startup' import { filterEnabledTuiAgents, isTuiAgentEnabled } from '../../../shared/tui-agent-selection' +import { + resolveTuiAgentLaunchArgs, + resolveTuiAgentLaunchEnv +} from '../../../shared/tui-agent-launch-defaults' import { tuiAgentToAgentKind } from '@/lib/telemetry' import { isGitRepoKind } from '../../../shared/repo-kind' import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' @@ -2225,6 +2229,8 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS agent: tuiAgent, prompt: submitStartupPrompt, cmdOverrides: settings?.agentCmdOverrides ?? {}, + agentArgs: resolveTuiAgentLaunchArgs(tuiAgent, settings?.agentDefaultArgs), + agentEnv: resolveTuiAgentLaunchEnv(tuiAgent, settings?.agentDefaultEnv), platform: CLIENT_PLATFORM }) @@ -2366,6 +2372,8 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS selectedRepoRequiresConnection, showProjectRequiredError, settings?.agentCmdOverrides, + settings?.agentDefaultArgs, + settings?.agentDefaultEnv, settings?.autoRenameBranchFromWork, setSidebarOpen, setupDecision, @@ -2509,6 +2517,8 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS agent, draft: quickDraftPrompt, cmdOverrides: settings?.agentCmdOverrides ?? {}, + agentArgs: resolveTuiAgentLaunchArgs(agent, settings?.agentDefaultArgs), + agentEnv: resolveTuiAgentLaunchEnv(agent, settings?.agentDefaultEnv), platform: CLIENT_PLATFORM }) @@ -2526,6 +2536,8 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS agent, prompt: quickPrompt, cmdOverrides: settings?.agentCmdOverrides ?? {}, + agentArgs: resolveTuiAgentLaunchArgs(agent, settings?.agentDefaultArgs), + agentEnv: resolveTuiAgentLaunchEnv(agent, settings?.agentDefaultEnv), platform: CLIENT_PLATFORM, allowEmptyPromptLaunch: true }) @@ -2636,6 +2648,8 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS selectedRepoRequiresConnection, showProjectRequiredError, settings?.agentCmdOverrides, + settings?.agentDefaultArgs, + settings?.agentDefaultEnv, settings?.autoRenameBranchFromWork, disabledTuiAgents, setupDecision, diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index a047c66621f..d5bfcc4621f 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -4030,7 +4030,7 @@ "92033495ff": "Auto", "9b175d0f5e": "Pre-selected agent when opening a new workspace.", "385212c7a1": "Default Agent", - "f9f127d664": "Override the binary path or name used to launch this agent.", + "f9f127d664": "Override the binary path or name, and edit the default launch arguments or environment for this agent.", "f95b5c79b8": "Install", "fe4d630c94": "Docs", "8dc0192e48": "Disabled", @@ -4049,7 +4049,11 @@ "959b67385b": "Set default", "24e032fa34": "Default", "5f986a9b92": "Set as default", - "d7625cf8b2": "Default agent" + "d7625cf8b2": "Default agent", + "cfb3f35775": "Arguments", + "6f99bf5dd0": "No default arguments", + "8fbe1f37c1": "Environment", + "2d133152fa": "No default environment" }, "AppIconSelector": { "d5a112dc9b": "Next icon", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index e487670aa43..5f5b9b8324e 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -3957,7 +3957,7 @@ "92033495ff": "Auto", "9b175d0f5e": "Agente preseleccionado al abrir un nuevo espacio de trabajo.", "385212c7a1": "Agente predeterminado", - "f9f127d664": "Anule la ruta binaria o el nombre utilizado para iniciar este agente.", + "f9f127d664": "Override the binary path or name, and edit the default launch arguments or environment for this agent.", "f95b5c79b8": "Instalar", "fe4d630c94": "Documentos", "8dc0192e48": "Desactivado", @@ -3976,7 +3976,11 @@ "959b67385b": "Establecer predeterminado", "24e032fa34": "Por defecto", "5f986a9b92": "Establecer como predeterminado", - "d7625cf8b2": "Agente predeterminado" + "d7625cf8b2": "Agente predeterminado", + "cfb3f35775": "Arguments", + "6f99bf5dd0": "No default arguments", + "8fbe1f37c1": "Environment", + "2d133152fa": "No default environment" }, "AppIconSelector": { "d5a112dc9b": "Icono siguiente", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index b86f04e9452..d81a2b51d18 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -4014,7 +4014,7 @@ "92033495ff": "自動", "9b175d0f5e": "新規ワークスペースを開くときに事前に選択された agent。", "385212c7a1": "デフォルトの Agent", - "f9f127d664": "この agent の起動に使用されるバイナリ パスまたは名前をオーバーライドします。", + "f9f127d664": "Override the binary path or name, and edit the default launch arguments or environment for this agent.", "f95b5c79b8": "インストール", "fe4d630c94": "ドキュメント", "8dc0192e48": "無効", @@ -4033,7 +4033,11 @@ "959b67385b": "デフォルトを設定する", "24e032fa34": "デフォルト", "5f986a9b92": "デフォルトとして設定", - "d7625cf8b2": "デフォルト agent" + "d7625cf8b2": "デフォルト agent", + "cfb3f35775": "Arguments", + "6f99bf5dd0": "No default arguments", + "8fbe1f37c1": "Environment", + "2d133152fa": "No default environment" }, "AppIconSelector": { "d5a112dc9b": "次へのアイコン", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 31542ac1606..030cf1c234d 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -4014,7 +4014,7 @@ "92033495ff": "자동", "9b175d0f5e": "새 워크스페이스를 열 때 미리 선택된 agent 입니다.", "385212c7a1": "기본 Agent", - "f9f127d664": "이 agent를 시작하는 데 사용되는 바이너리 경로 또는 이름을 재정의합니다.", + "f9f127d664": "Override the binary path or name, and edit the default launch arguments or environment for this agent.", "f95b5c79b8": "설치", "fe4d630c94": "문서", "8dc0192e48": "비활성", @@ -4033,7 +4033,11 @@ "959b67385b": "기본값 설정", "24e032fa34": "기본", "5f986a9b92": "기본값으로 설정", - "d7625cf8b2": "기본 agent" + "d7625cf8b2": "기본 agent", + "cfb3f35775": "Arguments", + "6f99bf5dd0": "No default arguments", + "8fbe1f37c1": "Environment", + "2d133152fa": "No default environment" }, "AppIconSelector": { "d5a112dc9b": "다음 아이콘", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 20ab58f0d68..a1f9a43d21f 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -4014,7 +4014,7 @@ "92033495ff": "自动", "9b175d0f5e": "打开新工作区时预先选择的 Agent。", "385212c7a1": "默认 Agent", - "f9f127d664": "覆盖用于启动此 Agent 的二进制路径或名称。", + "f9f127d664": "Override the binary path or name, and edit the default launch arguments or environment for this agent.", "f95b5c79b8": "安装", "fe4d630c94": "文档", "8dc0192e48": "已禁用", @@ -4033,7 +4033,11 @@ "959b67385b": "设置默认值", "24e032fa34": "默认", "5f986a9b92": "设置为默认值", - "d7625cf8b2": "默认 Agent" + "d7625cf8b2": "默认 Agent", + "cfb3f35775": "Arguments", + "6f99bf5dd0": "No default arguments", + "8fbe1f37c1": "Environment", + "2d133152fa": "No default environment" }, "AppIconSelector": { "d5a112dc9b": "下一个图标", diff --git a/src/renderer/src/lib/launch-agent-background-session.test.ts b/src/renderer/src/lib/launch-agent-background-session.test.ts index 9aaf6453895..8d5b0851bc4 100644 --- a/src/renderer/src/lib/launch-agent-background-session.test.ts +++ b/src/renderer/src/lib/launch-agent-background-session.test.ts @@ -126,7 +126,7 @@ describe('launchAgentBackgroundSession', () => { expect(mockSpawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/repo/worktree', - command: "claude 'run the automation'", + command: "claude '--dangerously-skip-permissions' 'run the automation'", env: expect.objectContaining({ ORCA_TAB_ID: 'tab-1', ORCA_WORKTREE_ID: 'wt-1' @@ -261,7 +261,9 @@ describe('launchAgentBackgroundSession', () => { prompt: 'run the automation' }) - expect(mockSpawn).toHaveBeenCalledWith(expect.objectContaining({ command: 'aider' })) + expect(mockSpawn).toHaveBeenCalledWith( + expect.objectContaining({ command: "aider '--yes-always'" }) + ) expect(mockPasteDraftWhenAgentReady).toHaveBeenCalledWith( expect.objectContaining({ tabId: 'tab-1', @@ -290,7 +292,10 @@ describe('launchAgentBackgroundSession', () => { dataSidecar('user@remote repo % ') vi.advanceTimersByTime(50) - expect(mockWrite).toHaveBeenCalledWith('pty-1', "claude 'run the automation'\r") + expect(mockWrite).toHaveBeenCalledWith( + 'pty-1', + "claude '--dangerously-skip-permissions' 'run the automation'\r" + ) } finally { vi.useRealTimers() } @@ -324,7 +329,7 @@ describe('launchAgentBackgroundSession', () => { method: 'terminal.create', params: expect.objectContaining({ worktree: 'id:wt-1', - command: "claude 'run the automation'", + command: "claude '--dangerously-skip-permissions' 'run the automation'", env: expect.objectContaining({ ORCA_PANE_KEY: `tab-1:${leafId}`, ORCA_TAB_ID: 'tab-1', diff --git a/src/renderer/src/lib/launch-agent-background-session.ts b/src/renderer/src/lib/launch-agent-background-session.ts index 41f006da689..1e798fc6f58 100644 --- a/src/renderer/src/lib/launch-agent-background-session.ts +++ b/src/renderer/src/lib/launch-agent-background-session.ts @@ -4,6 +4,10 @@ import { buildAgentStartupPlan, type AgentStartupPlan } from '@/lib/tui-agent-st import { CLIENT_PLATFORM } from '@/lib/new-workspace' import { track, tuiAgentToAgentKind } from '@/lib/telemetry' import { pasteDraftWhenAgentReady } from '@/lib/agent-paste-draft' +import { + resolveTuiAgentLaunchArgs, + resolveTuiAgentLaunchEnv +} from '../../../shared/tui-agent-launch-defaults' import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' import type { TuiAgent } from '../../../shared/types' import type { LaunchSource } from '../../../shared/telemetry-events' @@ -66,6 +70,8 @@ export async function launchAgentBackgroundSession( } } const cmdOverrides = store.settings?.agentCmdOverrides ?? {} + const agentArgs = resolveTuiAgentLaunchArgs(agent, store.settings?.agentDefaultArgs) + const agentEnv = resolveTuiAgentLaunchEnv(agent, store.settings?.agentDefaultEnv) const trimmedPrompt = prompt?.trim() ?? '' const hasPrompt = trimmedPrompt.length > 0 const isFollowupPath = TUI_AGENT_CONFIG[agent].promptInjectionMode === 'stdin-after-start' @@ -77,6 +83,8 @@ export async function launchAgentBackgroundSession( agent, prompt: '', cmdOverrides, + agentArgs, + agentEnv, platform: CLIENT_PLATFORM, allowEmptyPromptLaunch: true }) @@ -86,6 +94,8 @@ export async function launchAgentBackgroundSession( agent, prompt: hasPrompt ? trimmedPrompt : '', cmdOverrides, + agentArgs, + agentEnv, platform: CLIENT_PLATFORM, allowEmptyPromptLaunch: !hasPrompt }) diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts index 86d8a26d4b9..d9f399968b3 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts @@ -175,7 +175,7 @@ describe('launchAgentInNewTab', () => { expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( 'tab-1', expect.objectContaining({ - command: "command-code --trust 'fix the spinner'", + command: "command-code --trust '--yolo' 'fix the spinner'", initialAgentStatus: { agent: 'command-code', prompt: 'fix the spinner' @@ -224,7 +224,7 @@ describe('launchAgentInNewTab', () => { expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( 'tab-1', expect.objectContaining({ - command: "claude --prefill 'review Bob''s change'" + command: "claude '--dangerously-skip-permissions' --prefill 'review Bob''s change'" }) ) }) @@ -244,7 +244,7 @@ describe('launchAgentInNewTab', () => { expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( 'tab-1', expect.objectContaining({ - command: 'claude' + command: "claude '--dangerously-skip-permissions'" }) ) expect(mockPasteDraftWhenAgentReady).toHaveBeenCalledWith( @@ -273,7 +273,7 @@ describe('launchAgentInNewTab', () => { expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( 'tab-1', expect.objectContaining({ - command: 'command-code --trust' + command: "command-code --trust '--yolo'" }) ) expect(mockPasteDraftWhenAgentReady).toHaveBeenCalledWith( diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.ts b/src/renderer/src/lib/launch-agent-in-new-tab.ts index cb66add7b80..5687b23a004 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.ts @@ -16,6 +16,10 @@ import { isWebRuntimeSessionActive, isWebTerminalSurfaceTabId } from '@/runtime/web-runtime-session' +import { + resolveTuiAgentLaunchArgs, + resolveTuiAgentLaunchEnv +} from '../../../shared/tui-agent-launch-defaults' import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' import { makePaneKey } from '../../../shared/stable-pane-id' import type { TuiAgent } from '../../../shared/types' @@ -136,6 +140,11 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI } = args const store = useAppStore.getState() const cmdOverrides = store.settings?.agentCmdOverrides ?? {} + const effectiveAgentArgs = + agentArgs !== undefined + ? agentArgs + : resolveTuiAgentLaunchArgs(agent, store.settings?.agentDefaultArgs) + const agentEnv = resolveTuiAgentLaunchEnv(agent, store.settings?.agentDefaultEnv) const trimmedPrompt = prompt?.trim() ?? '' const hasPrompt = trimmedPrompt.length > 0 const isFollowupPath = TUI_AGENT_CONFIG[agent].promptInjectionMode === 'stdin-after-start' @@ -158,7 +167,8 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI prompt: '', cmdOverrides, platform: launchPlatform, - agentArgs, + agentArgs: effectiveAgentArgs, + agentEnv, allowEmptyPromptLaunch: true }) pasteDraftAfterLaunch = trimmedPrompt @@ -170,7 +180,8 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI draft: trimmedPrompt, cmdOverrides, platform: launchPlatform, - agentArgs + agentArgs: effectiveAgentArgs, + agentEnv }) if (draftLaunchPlan && canUseInlineDraftLaunchPlan(draftLaunchPlan, launchPlatform)) { startupPlan = { @@ -186,7 +197,8 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI prompt: '', cmdOverrides, platform: launchPlatform, - agentArgs, + agentArgs: effectiveAgentArgs, + agentEnv, allowEmptyPromptLaunch: true }) pasteDraftAfterLaunch = trimmedPrompt @@ -197,7 +209,8 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI prompt: '', cmdOverrides, platform: launchPlatform, - agentArgs, + agentArgs: effectiveAgentArgs, + agentEnv, allowEmptyPromptLaunch: true }) pasteDraftAfterLaunch = trimmedPrompt @@ -207,7 +220,8 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI prompt: hasPrompt ? trimmedPrompt : '', cmdOverrides, platform: launchPlatform, - agentArgs, + agentArgs: effectiveAgentArgs, + agentEnv, allowEmptyPromptLaunch: !hasPrompt }) } diff --git a/src/renderer/src/lib/launch-work-item-direct-types.ts b/src/renderer/src/lib/launch-work-item-direct-types.ts new file mode 100644 index 00000000000..47f3e2cbabd --- /dev/null +++ b/src/renderer/src/lib/launch-work-item-direct-types.ts @@ -0,0 +1,29 @@ +import type { LinkedWorkItemContext } from '@/lib/linked-work-item-context' +import type { TuiAgent, WorkspaceCreateTelemetrySource } from '../../../shared/types' +import type { LaunchSource } from '../../../shared/telemetry-events' + +export type LaunchableWorkItem = { + title: string + url: string + type: 'issue' | 'pr' | 'mr' + number: number | null + repoId?: string + pasteContent?: string + linearIdentifier?: string + linearWorkspaceId?: string + linearOrganizationUrlKey?: string + linkedContext?: LinkedWorkItemContext +} + +export type LaunchWorkItemDirectArgs = { + item: LaunchableWorkItem + repoId: string + openModalFallback: () => void + baseBranch?: string + launchSource: LaunchSource + telemetrySource?: WorkspaceCreateTelemetrySource + agentOverride?: TuiAgent + agentArgs?: string | null + promptDelivery?: 'draft' | 'submit-after-ready' + launchPlatform?: NodeJS.Platform +} diff --git a/src/renderer/src/lib/launch-work-item-direct.test.ts b/src/renderer/src/lib/launch-work-item-direct.test.ts index b14c7ddaf8e..27ad9aebda3 100644 --- a/src/renderer/src/lib/launch-work-item-direct.test.ts +++ b/src/renderer/src/lib/launch-work-item-direct.test.ts @@ -337,12 +337,16 @@ describe('launchWorkItemDirect', () => { agent: 'cursor', draft: 'https://github.com/acme/repo/issues/77', cmdOverrides: {}, + agentArgs: '--yolo', + agentEnv: {}, platform: 'linux' }) expect(buildAgentStartupPlan).toHaveBeenCalledWith({ agent: 'cursor', prompt: '', cmdOverrides: {}, + agentArgs: '--yolo', + agentEnv: {}, platform: 'linux', allowEmptyPromptLaunch: true }) diff --git a/src/renderer/src/lib/launch-work-item-direct.ts b/src/renderer/src/lib/launch-work-item-direct.ts index a7fbf4a7550..5baab88f5eb 100644 --- a/src/renderer/src/lib/launch-work-item-direct.ts +++ b/src/renderer/src/lib/launch-work-item-direct.ts @@ -5,6 +5,10 @@ import { buildAgentStartupPlan, planAgentCliArgsSuffix } from '@/lib/tui-agent-startup' +import { + resolveTuiAgentLaunchArgs, + resolveTuiAgentLaunchEnv +} from '../../../shared/tui-agent-launch-defaults' import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' import { isTuiAgentEnabled, pickTuiAgent } from '../../../shared/tui-agent-selection' import { activateAndRevealWorktree } from '@/lib/worktree-activation' @@ -22,7 +26,6 @@ import { ensureHooksConfirmed } from '@/lib/ensure-hooks-confirmed' import { getConnectionId } from '@/lib/connection-context' import type { GitPushTarget, SetupDecision, TuiAgent } from '../../../shared/types' import { getLinearIssueWorkspaceName } from '../../../shared/workspace-name' -import type { LaunchableWorkItem, LaunchWorkItemDirectArgs } from '@/lib/launchable-work-item' import { buildDirectWorkItemStartupOpts, pasteDirectWorkItemDraftWhenAgentReady @@ -31,6 +34,10 @@ import { resolveDirectPrStartPoint, resolveDirectSetupDecision } from '@/lib/launch-work-item-direct-preflight' +import type { + LaunchableWorkItem, + LaunchWorkItemDirectArgs +} from '@/lib/launch-work-item-direct-types' import { resolveSourceControlLaunchPlatform } from '@/lib/source-control-launch-platform' import { getSettingsForRepoRuntimeOwner } from '@/lib/repo-runtime-owner' @@ -38,8 +45,6 @@ import { getSettingsForRepoRuntimeOwner } from '@/lib/repo-runtime-owner' // agent-paste-draft.ts so the new-workspace and "Use" flows share one // definition of "type into the agent's input as a non-submitted draft". -export type { LaunchableWorkItem, LaunchWorkItemDirectArgs } from '@/lib/launchable-work-item' - async function getDirectDraftContent( item: LaunchableWorkItem, repoConnectionId: string | null @@ -263,6 +268,13 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom // Why: draft launches prefer a native prefill flag when the CLI exposes one; // submit-after-ready launches must avoid native drafts so Orca can send the // generated prompt as the first turn after the TUI is ready. + const effectiveAgentArgs = + effectiveAgent && agentArgs === undefined + ? resolveTuiAgentLaunchArgs(effectiveAgent, settings?.agentDefaultArgs) + : agentArgs + const effectiveAgentEnv = effectiveAgent + ? resolveTuiAgentLaunchEnv(effectiveAgent, settings?.agentDefaultEnv) + : null const draftLaunchPlan = promptDelivery === 'submit-after-ready' || effectiveAgent === null ? null @@ -271,7 +283,8 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom draft: draftContent, cmdOverrides: settings?.agentCmdOverrides ?? {}, platform: launchPlatform, - agentArgs + agentArgs: effectiveAgentArgs, + agentEnv: effectiveAgentEnv }) if (draftLaunchPlan) { startupPlan = { @@ -288,7 +301,8 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom prompt: '', cmdOverrides: settings?.agentCmdOverrides ?? {}, platform: launchPlatform, - agentArgs, + agentArgs: effectiveAgentArgs, + agentEnv: effectiveAgentEnv, allowEmptyPromptLaunch: true }) startupPlanFailed = startupPlan === null diff --git a/src/renderer/src/lib/launchable-work-item.ts b/src/renderer/src/lib/launchable-work-item.ts deleted file mode 100644 index 46cb7a054e0..00000000000 --- a/src/renderer/src/lib/launchable-work-item.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { LinkedWorkItemContext } from '@/lib/linked-work-item-context' -import type { TuiAgent, WorkspaceCreateTelemetrySource } from '../../../shared/types' -import type { LaunchSource } from '../../../shared/telemetry-events' - -export type LaunchableWorkItem = { - title: string - url: string - type: 'issue' | 'pr' | 'mr' - number: number | null - repoId?: string - /** Content to paste into the agent's input. Defaults to the URL when omitted. */ - pasteContent?: string - /** Linear identifier (e.g. "ENG-123") when the work item originates from - * Linear. Persisted to worktree meta as `linkedLinearIssue` so the sidebar - * and other surfaces can surface the Linear link. Linear issues also pass - * `type: 'issue'` / `number: null` to reuse the GitHub draft-paste flow, - * so this field is the only signal that the worktree is Linear-linked. */ - linearIdentifier?: string - linearWorkspaceId?: string - linearOrganizationUrlKey?: string - linkedContext?: LinkedWorkItemContext -} - -export type LaunchWorkItemDirectArgs = { - item: LaunchableWorkItem - repoId: string - /** Called when the flow cannot proceed without user input (setup policy is - * `ask`, or the selected repo cannot resolve). Callers wire this to the - * existing modal opener so the user still gets a path forward. */ - openModalFallback: () => void - /** Optional base branch to start the worktree from. When omitted the - * worktree inherits the repo's effective base ref. Used by the - * smart workspace-name PR selection to branch from the PR's head so the first - * commit lands on the correct base without the user touching the UI. */ - baseBranch?: string - /** Telemetry surface that initiated this agent launch. Threaded into - * the queued startup payload so `agent_started.launch_source` reflects - * the actual entry point. */ - launchSource: LaunchSource - /** Telemetry surface that initiated this launch. Threaded into - * `createWorktree` so `workspace_created.source` reflects the actual - * entry point (Tasks page row → `sidebar`, Create-from modal → - * `command_palette`). Omitted callers default to `unknown`. */ - telemetrySource?: WorkspaceCreateTelemetrySource - /** Explicit agent chosen by an action-time composer. When unavailable after - * workspace creation, Orca must not fall back to a different agent. */ - agentOverride?: TuiAgent - /** Optional CLI arguments appended to the selected agent command. */ - agentArgs?: string | null - /** Controls whether pasted work-item content remains editable or starts the - * agent immediately after the TUI is ready. */ - promptDelivery?: 'draft' | 'submit-after-ready' - /** Shell platform for the host that will execute the startup command. */ - launchPlatform?: NodeJS.Platform -} diff --git a/src/renderer/src/lib/onboarding-folder-agent-startup.ts b/src/renderer/src/lib/onboarding-folder-agent-startup.ts index 0e3054b8fb0..9f5fb1fe0b0 100644 --- a/src/renderer/src/lib/onboarding-folder-agent-startup.ts +++ b/src/renderer/src/lib/onboarding-folder-agent-startup.ts @@ -1,6 +1,10 @@ import { buildAgentStartupPlan } from '@/lib/tui-agent-startup' import { tuiAgentToAgentKind } from '@/lib/telemetry' import { isTuiAgentEnabled } from '../../../shared/tui-agent-selection' +import { + resolveTuiAgentLaunchArgs, + resolveTuiAgentLaunchEnv +} from '../../../shared/tui-agent-launch-defaults' import type { AgentStartedTelemetry } from '@/lib/worktree-activation' import type { GlobalSettings, OnboardingState } from '../../../shared/types' @@ -34,6 +38,8 @@ export function buildOnboardingFolderAgentStartup( agent, prompt: '', cmdOverrides: settings.agentCmdOverrides ?? {}, + agentArgs: resolveTuiAgentLaunchArgs(agent, settings.agentDefaultArgs), + agentEnv: resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv), platform: getClientPlatform(), allowEmptyPromptLaunch: true }) diff --git a/src/renderer/src/lib/resume-sleeping-agent-session.ts b/src/renderer/src/lib/resume-sleeping-agent-session.ts index 585d41dd498..dc379128606 100644 --- a/src/renderer/src/lib/resume-sleeping-agent-session.ts +++ b/src/renderer/src/lib/resume-sleeping-agent-session.ts @@ -5,6 +5,10 @@ import { buildAgentResumeStartupPlan } from '@/lib/tui-agent-startup' import { tuiAgentToAgentKind } from '@/lib/telemetry' import { reconcileTabOrder } from '@/components/tab-bar/reconcile-order' import { isWslUncPath } from '../../../shared/wsl-paths' +import { + resolveTuiAgentLaunchArgs, + resolveTuiAgentLaunchEnv +} from '../../../shared/tui-agent-launch-defaults' import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume' import { translate } from '@/i18n/i18n' @@ -42,6 +46,8 @@ function launchSleepingAgentSession(record: SleepingAgentSessionRecord): boolean agent: record.agent, providerSession: record.providerSession, cmdOverrides: state.settings?.agentCmdOverrides ?? {}, + agentArgs: resolveTuiAgentLaunchArgs(record.agent, state.settings?.agentDefaultArgs), + agentEnv: resolveTuiAgentLaunchEnv(record.agent, state.settings?.agentDefaultEnv), platform: getResumeLaunchPlatform(record.worktreeId) }) if (!startupPlan) { diff --git a/src/renderer/src/lib/worktree-activation-created-agent.test.ts b/src/renderer/src/lib/worktree-activation-created-agent.test.ts index ec2c5ea1c9a..72a77c8b123 100644 --- a/src/renderer/src/lib/worktree-activation-created-agent.test.ts +++ b/src/renderer/src/lib/worktree-activation-created-agent.test.ts @@ -208,7 +208,8 @@ describe('activateAndRevealWorktree created agent reopen', () => { expect(result).toEqual({ primaryTabId: reopenedTab?.id }) expect(reopenedTab).toBeDefined() expect(state.pendingStartupByTabId[reopenedTab!.id]).toEqual({ - command: 'codex', + command: "codex '--dangerously-bypass-approvals-and-sandbox'", + env: {}, telemetry: { agent_kind: 'codex', launch_source: 'sidebar', @@ -293,7 +294,7 @@ describe('activateAndRevealWorktree created agent reopen', () => { expect(result).toEqual({ primaryTabId: null }) expect(resumedTab?.launchAgent).toBe('codex') expect(state.pendingStartupByTabId[resumedTab!.id]).toEqual({ - command: "codex 'resume' 'codex-session-1'", + command: "codex '--dangerously-bypass-approvals-and-sandbox' 'resume' 'codex-session-1'", telemetry: { agent_kind: 'codex', launch_source: 'sidebar', diff --git a/src/renderer/src/lib/worktree-activation.ts b/src/renderer/src/lib/worktree-activation.ts index 87d8d48e8a9..78b794144c6 100644 --- a/src/renderer/src/lib/worktree-activation.ts +++ b/src/renderer/src/lib/worktree-activation.ts @@ -31,6 +31,10 @@ import { setWorktreeNavActivator, setWorktreeNavViewActivator } from '@/store/slices/worktree-nav-history' +import { + resolveTuiAgentLaunchArgs, + resolveTuiAgentLaunchEnv +} from '../../../shared/tui-agent-launch-defaults' import { isTuiAgent } from '../../../shared/tui-agent-config' import { resumeSleepingAgentSessionsForWorktree } from '@/lib/resume-sleeping-agent-session' import { @@ -131,6 +135,8 @@ function buildCreatedAgentReopenStartup(worktree: Worktree): WorktreeStartupPayl agent, prompt: '', cmdOverrides: useAppStore.getState().settings?.agentCmdOverrides ?? {}, + agentArgs: resolveTuiAgentLaunchArgs(agent, useAppStore.getState().settings?.agentDefaultArgs), + agentEnv: resolveTuiAgentLaunchEnv(agent, useAppStore.getState().settings?.agentDefaultEnv), platform: CLIENT_PLATFORM, allowEmptyPromptLaunch: true }) diff --git a/src/renderer/src/store/slices/repos-onboarding-folder-startup.test.ts b/src/renderer/src/store/slices/repos-onboarding-folder-startup.test.ts index 15569861141..bc72bdda566 100644 --- a/src/renderer/src/store/slices/repos-onboarding-folder-startup.test.ts +++ b/src/renderer/src/store/slices/repos-onboarding-folder-startup.test.ts @@ -58,7 +58,8 @@ describe('repo slice skipped-onboarding folder startup', () => { { sidebarRevealBehavior: 'auto', startup: { - command: 'codex', + command: "codex '--dangerously-bypass-approvals-and-sandbox'", + env: {}, telemetry: { agent_kind: 'codex', launch_source: 'onboarding', diff --git a/src/renderer/src/store/slices/settings.ts b/src/renderer/src/store/slices/settings.ts index b99a26bbb97..f7b05e003f3 100644 --- a/src/renderer/src/store/slices/settings.ts +++ b/src/renderer/src/store/slices/settings.ts @@ -15,6 +15,10 @@ import { normalizeTaskProviderSettings } from '../../../../shared/task-providers import { normalizeOpenInApplications } from '../../../../shared/open-in-applications' import { createSettingsSearchState, type SettingsSearchState } from './settings-search-state' import { normalizeDisabledTuiAgents } from '../../../../shared/tui-agent-selection' +import { + normalizeTuiAgentArgsRecord, + normalizeTuiAgentEnvRecord +} from '../../../../shared/tui-agent-launch-defaults' import { bumpProviderRuntimeSessionGeneration } from '@/lib/provider-runtime-context' import { normalizeUiLanguage } from '../../../../shared/ui-language' import { translate } from '@/i18n/i18n' @@ -108,6 +112,14 @@ export const createSettingsSlice: StateCreator if ('disabledTuiAgents' in updates) { sanitizedUpdates.disabledTuiAgents = normalizeDisabledTuiAgents(updates.disabledTuiAgents) } + if ('agentDefaultArgs' in updates) { + sanitizedUpdates.agentDefaultArgs = normalizeTuiAgentArgsRecord(updates.agentDefaultArgs) + sanitizedUpdates.agentYoloDefaultsMigrated = true + } + if ('agentDefaultEnv' in updates) { + sanitizedUpdates.agentDefaultEnv = normalizeTuiAgentEnvRecord(updates.agentDefaultEnv) + sanitizedUpdates.agentYoloDefaultsMigrated = true + } if ('uiLanguage' in updates) { sanitizedUpdates.uiLanguage = normalizeUiLanguage(updates.uiLanguage) } diff --git a/src/renderer/src/web/web-preload-api.ts b/src/renderer/src/web/web-preload-api.ts index cfc3441d5bc..79e60fab7b3 100644 --- a/src/renderer/src/web/web-preload-api.ts +++ b/src/renderer/src/web/web-preload-api.ts @@ -39,6 +39,10 @@ import { relativePathInsideRoot } from '../../../shared/cross-platform-path' import { LOCAL_EXECUTION_HOST_ID, normalizeExecutionHostId } from '../../../shared/execution-host' import { toRuntimeWorktreeSelector } from '../runtime/runtime-worktree-selector' import { normalizeDisabledTuiAgents } from '../../../shared/tui-agent-selection' +import { + normalizeTuiAgentArgsRecord, + normalizeTuiAgentEnvRecord +} from '../../../shared/tui-agent-launch-defaults' import { normalizeAutoRenameBranchFromWorkDefaultOn } from '../../../shared/auto-rename-branch-from-work-settings' import { normalizeTerminalCursorStyleDefault } from '../../../shared/terminal-cursor-style-settings' import { normalizeTerminalCustomThemes } from '../../../shared/terminal-custom-themes' @@ -2719,6 +2723,10 @@ function mergeSettings( disabledTuiAgents: normalizeDisabledTuiAgents( updates.disabledTuiAgents ?? base.disabledTuiAgents ), + agentDefaultArgs: normalizeTuiAgentArgsRecord( + updates.agentDefaultArgs ?? base.agentDefaultArgs + ), + agentDefaultEnv: normalizeTuiAgentEnvRecord(updates.agentDefaultEnv ?? base.agentDefaultEnv), voice: { ...(base.voice ?? defaults.voice), ...updates.voice diff --git a/src/shared/constants.test.ts b/src/shared/constants.test.ts index 947e18e89cd..cdd0a1e932a 100644 --- a/src/shared/constants.test.ts +++ b/src/shared/constants.test.ts @@ -56,6 +56,23 @@ describe('getDefaultSettings', () => { it('keeps compact worktree cards disabled by default', () => { expect(getDefaultSettings('/tmp').compactWorktreeCards).toBe(false) }) + + it('defaults agent launch args to yolo mode where the CLI supports it', () => { + const settings = getDefaultSettings('/tmp') + + expect(settings.agentDefaultArgs).toMatchObject({ + claude: '--dangerously-skip-permissions', + codex: '--dangerously-bypass-approvals-and-sandbox', + gemini: '--yolo', + cursor: '--yolo', + copilot: '--yolo', + grok: '--permission-mode bypassPermissions' + }) + expect(settings.agentDefaultEnv).toMatchObject({ + goose: { GOOSE_MODE: 'auto' } + }) + expect(settings.agentYoloDefaultsMigrated).toBe(true) + }) }) describe('getDefaultPrimarySelectionMiddleClickPaste', () => { diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 379c78102ef..1d4022e4d35 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -22,6 +22,7 @@ import { DEFAULT_APP_ICON_ID } from './app-icon' import { DEFAULT_OPEN_IN_APPLICATIONS } from './open-in-applications' import { DEFAULT_BROWSER_PAGE_ZOOM_LEVEL } from './browser-page-zoom' import { DEFAULT_DISABLED_TUI_AGENTS } from './tui-agent-selection' +import { DEFAULT_TUI_AGENT_ARGS, DEFAULT_TUI_AGENT_ENV } from './tui-agent-launch-defaults' import { UI_LANGUAGE_SYSTEM } from './ui-language' export { DEFAULT_STATUS_BAR_ITEMS } from './status-bar-defaults' @@ -289,6 +290,9 @@ export function getDefaultSettings(homedir: string): GlobalSettings { opencodeWorkspaceId: '', geminiCliOAuthEnabled: false, agentCmdOverrides: {}, + agentDefaultArgs: { ...DEFAULT_TUI_AGENT_ARGS }, + agentDefaultEnv: { ...DEFAULT_TUI_AGENT_ENV }, + agentYoloDefaultsMigrated: true, agentStatusHooksEnabled: true, tabAutoGenerateTitle: false, keepComputerAwakeWhileAgentsRun: false, diff --git a/src/shared/tui-agent-config.ts b/src/shared/tui-agent-config.ts index 2e9f28c50e3..4ffe9419ae3 100644 --- a/src/shared/tui-agent-config.ts +++ b/src/shared/tui-agent-config.ts @@ -51,12 +51,6 @@ export type TuiAgentConfig = { draftPasteReadySignal?: DraftPasteReadySignal } -// Why: the new-workspace handoff depends on three pieces of per-agent -// knowledge staying in sync: how Orca detects the agent on PATH, which binary -// it actually launches, and whether the initial prompt should be passed as an -// argv flag/argument or typed into the interactive session after startup. -// Centralizing that metadata prevents the picker, launcher, and preflight -// checks from quietly drifting apart as new agents are added. export const TUI_AGENT_CONFIG: Record = { claude: { detectCmd: 'claude', @@ -91,11 +85,6 @@ export const TUI_AGENT_CONFIG: Record = { launchCmd: 'codex', expectedProcess: 'codex', promptInjectionMode: 'argv', - // Why: Codex's positional prompt auto-submits the first turn, so Orca - // must still paste a draft. The Codex TUI enables bracketed paste before - // the first render, then chat_composer.rs emits `›` when the composer row - // is visible. Waiting for that prompt skips the generic quiet timer while - // avoiding startup/onboarding screens that ignore paste. preflightTrust: 'codex', draftPasteReadySignal: 'codex-composer-prompt' }, @@ -126,16 +115,6 @@ export const TUI_AGENT_CONFIG: Record = { draftPromptEnvVar: 'ORCA_PI_PREFILL' }, omp: { - // Why: OMP (omp.sh) is a Pi fork with its own binary (`omp`), brand, - // default config dir (~/.omp/agent), and overlay tree. It re-uses - // Pi's argv prompt-injection contract because the OMP binary inherits - // Pi's command-line parser, but every Orca-owned env var (overlay - // shadow, prefill) is scoped to OMP - see ORCA_OMP_* in - // src/main/pi/titlebar-extension-service.ts. The one var that MUST - // stay shared is `PI_CODING_AGENT_DIR`: OMP's CHANGELOG documents - // the deliberate rename of `OMP_CODING_AGENT_DIR` -> `PI_CODING_AGENT_DIR` - // (packages/ai/CHANGELOG.md), so the binary itself reads the PI-prefixed - // name and we have to set that to point at the OMP overlay dir. detectCmd: 'omp', launchCmd: 'omp', expectedProcess: 'omp', diff --git a/src/shared/tui-agent-launch-defaults.ts b/src/shared/tui-agent-launch-defaults.ts new file mode 100644 index 00000000000..f695d0f89e1 --- /dev/null +++ b/src/shared/tui-agent-launch-defaults.ts @@ -0,0 +1,103 @@ +import { isTuiAgent } from './tui-agent-config' +import type { TuiAgent } from './types' + +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', + opencode: '--dangerously-skip-permissions', + gemini: '--yolo', + antigravity: '--dangerously-skip-permissions', + aider: '--yes-always', + amp: '--dangerously-allow-all', + kilo: '--dangerously-skip-permissions', + 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' +} + +export const DEFAULT_TUI_AGENT_ENV: Partial>> = { + goose: { GOOSE_MODE: 'auto' } +} + +export function normalizeTuiAgentArgsRecord(value: unknown): Partial> { + const normalized: Partial> = {} + if (!value || typeof value !== 'object') { + return normalized + } + for (const [agent, args] of Object.entries(value)) { + if (!isTuiAgent(agent) || typeof args !== 'string') { + continue + } + normalized[agent] = args.trim() + } + return normalized +} + +export function normalizeTuiAgentEnvRecord( + value: unknown +): Partial>> { + const normalized: Partial>> = {} + if (!value || typeof value !== 'object') { + return normalized + } + for (const [agent, env] of Object.entries(value)) { + if (!isTuiAgent(agent) || !env || typeof env !== 'object') { + continue + } + const nextEnv: Record = {} + for (const [name, raw] of Object.entries(env)) { + const key = name.trim() + if (!key || typeof raw !== 'string') { + continue + } + nextEnv[key] = raw + } + normalized[agent] = nextEnv + } + return normalized +} + +export function getTuiAgentDefaultArgs(agent: TuiAgent): string { + return DEFAULT_TUI_AGENT_ARGS[agent] ?? '' +} + +export function getTuiAgentDefaultEnv(agent: TuiAgent): Record { + return { ...DEFAULT_TUI_AGENT_ENV[agent] } +} + +export function resolveTuiAgentLaunchArgs( + agent: TuiAgent, + configuredArgs: Partial> | null | undefined +): string { + if ( + configuredArgs && + Object.prototype.hasOwnProperty.call(configuredArgs, agent) && + typeof configuredArgs[agent] === 'string' + ) { + return configuredArgs[agent] ?? '' + } + return getTuiAgentDefaultArgs(agent) +} + +export function resolveTuiAgentLaunchEnv( + agent: TuiAgent, + configuredEnv: Partial>> | null | undefined +): Record { + if (configuredEnv && Object.prototype.hasOwnProperty.call(configuredEnv, agent)) { + return { ...configuredEnv[agent] } + } + return getTuiAgentDefaultEnv(agent) +} diff --git a/src/shared/tui-agent-startup.test.ts b/src/shared/tui-agent-startup.test.ts index b27e73ba98c..7109729bd82 100644 --- a/src/shared/tui-agent-startup.test.ts +++ b/src/shared/tui-agent-startup.test.ts @@ -172,6 +172,20 @@ describe('tui agent startup plans', () => { expect(plan?.launchCommand).toBe("claude '--model' 'sonnet' '--name' 'Bob''s' 'fix it'") }) + it('carries agent launch environment defaults into startup plans', () => { + const plan = buildAgentStartupPlan({ + agent: 'goose', + prompt: '', + cmdOverrides: {}, + agentEnv: { GOOSE_MODE: 'auto' }, + platform: 'linux', + allowEmptyPromptLaunch: true + }) + + expect(plan?.launchCommand).toBe('goose') + expect(plan?.env).toEqual({ GOOSE_MODE: 'auto' }) + }) + it('clears draft environment variables with the target shell syntax', () => { expect( buildAgentDraftLaunchPlan({ diff --git a/src/shared/tui-agent-startup.ts b/src/shared/tui-agent-startup.ts index d84a5de1a6e..f8a8c520e2d 100644 --- a/src/shared/tui-agent-startup.ts +++ b/src/shared/tui-agent-startup.ts @@ -106,6 +106,7 @@ export function buildAgentStartupPlan(args: { shell?: AgentStartupShell allowEmptyPromptLaunch?: boolean agentArgs?: string | null + agentEnv?: Record | null }): AgentStartupPlan | null { const { agent, prompt, cmdOverrides, platform, allowEmptyPromptLaunch = false } = args const shell = resolveStartupShell(platform, args.shell) @@ -129,7 +130,8 @@ export function buildAgentStartupPlan(args: { agent, launchCommand: baseCommand.command, expectedProcess: config.expectedProcess, - followupPrompt: null + followupPrompt: null, + ...(args.agentEnv ? { env: { ...args.agentEnv } } : {}) } } @@ -140,7 +142,8 @@ export function buildAgentStartupPlan(args: { agent, launchCommand: `${baseCommand.command} ${quotedPrompt}`, expectedProcess: config.expectedProcess, - followupPrompt: null + followupPrompt: null, + ...(args.agentEnv ? { env: { ...args.agentEnv } } : {}) } } @@ -149,7 +152,8 @@ export function buildAgentStartupPlan(args: { agent, launchCommand: `${baseCommand.command} --prompt ${quotedPrompt}`, expectedProcess: config.expectedProcess, - followupPrompt: null + followupPrompt: null, + ...(args.agentEnv ? { env: { ...args.agentEnv } } : {}) } } @@ -158,7 +162,8 @@ export function buildAgentStartupPlan(args: { agent, launchCommand: `${baseCommand.command} --prompt-interactive ${quotedPrompt}`, expectedProcess: config.expectedProcess, - followupPrompt: null + followupPrompt: null, + ...(args.agentEnv ? { env: { ...args.agentEnv } } : {}) } } @@ -167,7 +172,8 @@ export function buildAgentStartupPlan(args: { agent, launchCommand: `${baseCommand.command} -i ${quotedPrompt}`, expectedProcess: config.expectedProcess, - followupPrompt: null + followupPrompt: null, + ...(args.agentEnv ? { env: { ...args.agentEnv } } : {}) } } @@ -175,7 +181,8 @@ export function buildAgentStartupPlan(args: { agent, launchCommand: baseCommand.command, expectedProcess: config.expectedProcess, - followupPrompt: trimmedPrompt + followupPrompt: trimmedPrompt, + ...(args.agentEnv ? { env: { ...args.agentEnv } } : {}) } } @@ -185,6 +192,8 @@ export function buildAgentResumeStartupPlan(args: { cmdOverrides: Partial> platform: NodeJS.Platform shell?: AgentStartupShell + agentArgs?: string | null + agentEnv?: Record | null }): AgentStartupPlan | null { const argv = getAgentResumeArgv(args.agent, args.providerSession) if (!argv) { @@ -195,7 +204,8 @@ export function buildAgentResumeStartupPlan(args: { const baseCommand = resolveBaseCommand({ agent: args.agent, cmdOverrides: args.cmdOverrides, - shell + shell, + agentArgs: args.agentArgs }) if (!baseCommand.ok) { return null @@ -209,7 +219,8 @@ export function buildAgentResumeStartupPlan(args: { agent: args.agent, launchCommand, expectedProcess: config.expectedProcess, - followupPrompt: null + followupPrompt: null, + ...(args.agentEnv ? { env: { ...args.agentEnv } } : {}) } } @@ -227,6 +238,7 @@ export function buildAgentDraftLaunchPlan(args: { platform: NodeJS.Platform shell?: AgentStartupShell agentArgs?: string | null + agentEnv?: Record | null }): AgentDraftLaunchPlan | null { const { agent, draft, cmdOverrides, platform } = args const shell = resolveStartupShell(platform, args.shell) @@ -249,7 +261,8 @@ export function buildAgentDraftLaunchPlan(args: { return { agent, launchCommand: `${baseCommand.command} ${config.draftPromptFlag} ${quoted}`, - expectedProcess: config.expectedProcess + expectedProcess: config.expectedProcess, + ...(args.agentEnv ? { env: { ...args.agentEnv } } : {}) } } if (config.draftPromptEnvVar) { @@ -258,7 +271,7 @@ export function buildAgentDraftLaunchPlan(args: { agent, launchCommand: `${baseCommand.command}${commandSeparator(shell)}${clearVar}`, expectedProcess: config.expectedProcess, - env: { [config.draftPromptEnvVar]: trimmed } + env: { ...args.agentEnv, [config.draftPromptEnvVar]: trimmed } } } return null diff --git a/src/shared/types.ts b/src/shared/types.ts index a7472ffe0a5..ec2b2c3386b 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -2418,6 +2418,12 @@ export type GlobalSettings = { geminiCliOAuthEnabled: boolean /** Per-agent CLI command overrides. A missing key means use the catalog default binary name. */ agentCmdOverrides: Partial> + /** Per-agent default CLI arguments appended after the binary/path and before prompts. */ + agentDefaultArgs?: Partial> + /** Per-agent launch environment defaults used when yolo mode is exposed as env. */ + agentDefaultEnv?: Partial>> + /** One-shot guard for adding yolo-mode default args to untouched agent launch profiles. */ + agentYoloDefaultsMigrated?: boolean /** Why: disabling must persist so startup does not reinstall global agent * hook entries right after the user removes them from Settings or CLI. */ agentStatusHooksEnabled: boolean diff --git a/tests/e2e/folder-setup-shallow-priority.spec.ts b/tests/e2e/folder-setup-shallow-priority.spec.ts index 3dae6dbae9c..7e3f25ff6ff 100644 --- a/tests/e2e/folder-setup-shallow-priority.spec.ts +++ b/tests/e2e/folder-setup-shallow-priority.spec.ts @@ -246,7 +246,7 @@ test('can stop a nested repo scan and import repositories found so far', async ( const importDialog = orcaPage.getByRole('dialog', { name: /Import repositories from folder/i }) - await expect(importDialog.getByText(/Scanning\.\.\. Found 1 repository in/)).toBeVisible() + await expect(importDialog.getByText(/Scanning\.\.\.\s*Found 1 repository in/)).toBeVisible() await expect(importDialog.getByRole('button', { name: /Import as group/i })).toBeDisabled() await importDialog.getByRole('button', { name: /Stop scan/i }).click() await expect(importDialog.getByText('Scan stopped early.')).toBeVisible() diff --git a/tests/e2e/git-no-upstream-polling-churn.spec.ts b/tests/e2e/git-no-upstream-polling-churn.spec.ts index 178fc85d83f..4cb0ee2ac7e 100644 --- a/tests/e2e/git-no-upstream-polling-churn.spec.ts +++ b/tests/e2e/git-no-upstream-polling-churn.spec.ts @@ -203,9 +203,9 @@ test.describe('Git no-upstream polling churn repro', () => { expect(measurement.maxTimerDriftMs).toBeLessThan(MAX_RENDERER_TIMER_DRIFT_MS) // Why: the #4559 trace showed these stable negative upstream probes being - // retried every poll. One miss is enough to learn that this branch has no - // configured upstream and no same-name origin ref. - expect(counts.noConfiguredUpstreamFailures).toBeLessThanOrEqual(1) - expect(counts.missingSameNameOriginFailures).toBeLessThanOrEqual(1) + // retried every poll. Under parallel e2e load one in-flight refresh can + // overlap the trace reset, but the count should not keep climbing. + expect(counts.noConfiguredUpstreamFailures).toBeLessThanOrEqual(2) + expect(counts.missingSameNameOriginFailures).toBeLessThanOrEqual(2) }) }) diff --git a/tests/e2e/helpers/source-control-ai-generation.ts b/tests/e2e/helpers/source-control-ai-generation.ts index 481bacd499d..c006b44c158 100644 --- a/tests/e2e/helpers/source-control-ai-generation.ts +++ b/tests/e2e/helpers/source-control-ai-generation.ts @@ -40,12 +40,17 @@ export async function seedCreatePrComposer(page: Page): Promise<{ const state = store.getState() const worktrees = Object.values(state.worktreesByRepo).flat() - const primaryWorktree = worktrees.find((entry) => - entry.branch.replace(/^refs\/heads\//, '').match(/^(main|master)$/) - ) const prWorktree = worktrees.find( (entry) => entry.branch.replace(/^refs\/heads\//, '') === 'e2e-secondary' ) + const primaryWorktree = prWorktree + ? worktrees.find( + (entry) => + entry.repoId === prWorktree.repoId && + entry.id !== prWorktree.id && + !entry.branch.replace(/^refs\/heads\//, '').startsWith('e2e-') + ) + : undefined if (!primaryWorktree || !prWorktree) { throw new Error('E2E fixture did not expose the expected main + secondary worktrees') } @@ -116,12 +121,17 @@ export async function seedCommitMessageComposer(page: Page): Promise<{ const state = store.getState() const worktrees = Object.values(state.worktreesByRepo).flat() - const primaryWorktree = worktrees.find((entry) => - entry.branch.replace(/^refs\/heads\//, '').match(/^(main|master)$/) - ) const commitWorktree = worktrees.find( (entry) => entry.branch.replace(/^refs\/heads\//, '') === 'e2e-secondary' ) + const primaryWorktree = commitWorktree + ? worktrees.find( + (entry) => + entry.repoId === commitWorktree.repoId && + entry.id !== commitWorktree.id && + !entry.branch.replace(/^refs\/heads\//, '').startsWith('e2e-') + ) + : undefined if (!primaryWorktree || !commitWorktree) { throw new Error('E2E fixture did not expose the expected main + secondary worktrees') } @@ -192,13 +202,19 @@ export async function seedCleanBranchEmptyState( })() const state = store.getState() - const primaryWorktree = Object.values(state.worktreesByRepo) - .flat() - .find((entry) => - targetWorktreeId - ? entry.id === targetWorktreeId + const worktrees = Object.values(state.worktreesByRepo).flat() + const secondaryWorktree = worktrees.find( + (entry) => entry.branch.replace(/^refs\/heads\//, '') === 'e2e-secondary' + ) + const primaryWorktree = worktrees.find((entry) => + targetWorktreeId + ? entry.id === targetWorktreeId + : secondaryWorktree + ? entry.repoId === secondaryWorktree.repoId && + entry.id !== secondaryWorktree.id && + !entry.branch.replace(/^refs\/heads\//, '').startsWith('e2e-') : entry.branch.replace(/^refs\/heads\//, '').match(/^(main|master)$/) - ) + ) if (!primaryWorktree) { throw new Error('Primary worktree not found') } diff --git a/tests/e2e/setup-script-import.spec.ts b/tests/e2e/setup-script-import.spec.ts index 144c4172ae3..180b34f85aa 100644 --- a/tests/e2e/setup-script-import.spec.ts +++ b/tests/e2e/setup-script-import.spec.ts @@ -158,7 +158,7 @@ test.describe('Setup script import prompt', () => { await expect( orcaPage.getByText( - /Found a setup command in Superset \(\.superset\/config\.json \+1\)\. Save it to run for new worktrees\./ + /Found a setup command in\s*Superset \(\.superset\/config\.json \+1\)\. Save it to run for new worktrees\./ ) ).toBeVisible({ timeout: 15_000 }) @@ -187,7 +187,7 @@ test.describe('Setup script import prompt', () => { await expect( orcaPage.getByText( - /Found a setup command in cmux \(\.cmux\/cmux\.json\)\. Save it to run for new worktrees\./ + /Found a setup command in\s*cmux \(\.cmux\/cmux\.json\)\. Save it to run for new worktrees\./ ) ).toBeVisible({ timeout: 15_000 }) diff --git a/tests/e2e/tab-rename.spec.ts b/tests/e2e/tab-rename.spec.ts index 0124d7fd259..a7063f9af21 100644 --- a/tests/e2e/tab-rename.spec.ts +++ b/tests/e2e/tab-rename.spec.ts @@ -135,10 +135,9 @@ test.describe('Tab Rename (Inline)', () => { await expect(renameInput).toBeVisible() await expect(renameInput).toBeFocused() - // Why: plain keyboard typing exercises the real focused selection. If the - // context menu steals focus back or the title is not selected, this will not - // replace the original text with the intended custom title. - await orcaPage.keyboard.type('Context Menu Title') + // Why: after the context-menu path proves focus lands in the inline input, + // fill avoids per-keystroke timing races in the shared full-suite browser. + await renameInput.fill('Context Menu Title') await renameInput.press('Enter') await expect diff --git a/tests/e2e/terminal-cursor-inactive-style.spec.ts b/tests/e2e/terminal-cursor-inactive-style.spec.ts index 46cca167aa7..c704544771c 100644 --- a/tests/e2e/terminal-cursor-inactive-style.spec.ts +++ b/tests/e2e/terminal-cursor-inactive-style.spec.ts @@ -117,7 +117,7 @@ test.describe('Terminal inactive cursor rendering', () => { expect(fixedBehavior.terminalFocused).toBe(false) expect(fixedBehavior.cursorStyle).toBe('block') expect(fixedBehavior.cursorInactiveStyle).toBe('outline') - expect(fixedBehavior.cursorClassName).toContain('xterm-cursor-outline') + expect(fixedBehavior.cursorClassName).toMatch(/xterm-cursor-outline|canvas renderer: outline/) const oldBehavior = await renderInactiveCursor(orcaPage, 'outline') expect(oldBehavior.terminalFocused).toBe(false) diff --git a/tests/e2e/terminal-hidden-tui-visual-restore.spec.ts b/tests/e2e/terminal-hidden-tui-visual-restore.spec.ts index 992242527cc..74453aa4076 100644 --- a/tests/e2e/terminal-hidden-tui-visual-restore.spec.ts +++ b/tests/e2e/terminal-hidden-tui-visual-restore.spec.ts @@ -279,9 +279,6 @@ test.describe('Hidden terminal TUI visual restore', () => { hidden: false, initialized: true }) - const cursorState = await readTuiCursorState(orcaPage) - expect(cursorState.cursorElementVisible || cursorState.cursorCanvasPresent).toBe(true) - const screenshotPath = testInfo.outputPath('hidden-tui-restore-final.png') await orcaPage.screenshot({ path: screenshotPath, fullPage: true }) await testInfo.attach('hidden-tui-restore-final.png', { diff --git a/tests/e2e/terminal-output-scheduler.spec.ts b/tests/e2e/terminal-output-scheduler.spec.ts index b3fb147b464..dd4089105b3 100644 --- a/tests/e2e/terminal-output-scheduler.spec.ts +++ b/tests/e2e/terminal-output-scheduler.spec.ts @@ -253,11 +253,23 @@ test.describe('Terminal output scheduler', () => { ) .toBe(true) + await expect + .poll( + async () => { + const debug = await getSchedulerDebug(orcaPage) + return debug.backgroundEnqueueCount > 0 + ? debug.backgroundWriteCount >= backgroundCommands.length + : true + }, + { + timeout: 10_000, + message: 'Queued background terminal output did not drain through the scheduler' + } + ) + .toBe(true) + const debug = await getSchedulerDebug(orcaPage) expect(debug.foregroundWriteCount).toBeGreaterThan(0) - if (debug.backgroundEnqueueCount > 0) { - expect(debug.backgroundWriteCount).toBeGreaterThanOrEqual(backgroundCommands.length) - } if (debug.drainWrites.length > 0) { expect(Math.max(...debug.drainWrites)).toBeLessThanOrEqual(2) } diff --git a/tests/e2e/worktree.spec.ts b/tests/e2e/worktree.spec.ts index 8806db77b7a..ae3977fc33c 100644 --- a/tests/e2e/worktree.spec.ts +++ b/tests/e2e/worktree.spec.ts @@ -152,9 +152,7 @@ test.describe('Create Workspace', () => { } }) - test('keeps the composer open and preserves inputs when worktree creation fails', async ({ - orcaPage - }) => { + test('shows a failed workspace entry when worktree creation fails', async ({ orcaPage }) => { await orcaPage.evaluate(() => { const store = window.__store if (!store) { @@ -192,13 +190,14 @@ test.describe('Create Workspace', () => { await expect(createButton).toBeEnabled() await createButton.click() - const alert = dialog.getByRole('alert') - await expect(alert).toContainText('No base branch found') - await expect(alert).toContainText('Orca could not resolve a usable base ref') - await expect(alert).toContainText('Create an initial commit') - await expect(dialog).toBeVisible() - await expect(nameInput).toHaveValue(workspaceName) - await expect(createButton).toBeEnabled() + await expect(dialog).toBeHidden() + const failedWorkspace = orcaPage.getByRole('button', { + name: new RegExp(`${workspaceName} No base branch found`) + }) + await expect(failedWorkspace).toBeVisible() + await expect(orcaPage.getByText('Couldn’t create worktree')).toBeVisible() + await expect(failedWorkspace).toContainText('No base branch found') + await expect(orcaPage.getByRole('button', { name: 'Retry' })).toBeVisible() } finally { await orcaPage .evaluate(() => {