Default agent launches to yolo permissions mode (#5145)

* Default agent launches to yolo mode

* test: update launch default validations
This commit is contained in:
Neil
2026-06-11 12:51:55 -07:00
committed by Jinwoo-H
parent 470d46ecea
commit e4050eccbd
50 changed files with 869 additions and 193 deletions
+4 -1
View File
@@ -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
+41
View File
@@ -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'),
+66
View File
@@ -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<GlobalSettings, 'agentDefaultArgs' | 'agentDefaultEnv' | 'agentYoloDefaultsMigrated'> {
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
+10 -10
View File
@@ -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
})
+23 -1
View File
@@ -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
})
+12
View File
@@ -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
@@ -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
})
@@ -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',
@@ -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<string, string>
isDetected: boolean
isEnabled: boolean
isDefault: boolean
cmdOverride: string | undefined
argsOverride: string
envOverride: Record<string, string>
onSetDefault: () => void
onSetEnabled: (enabled: boolean) => void
onSaveOverride: (value: string) => void
onSaveArgs: (value: string) => void
onSaveEnv: (value: Record<string, string>) => 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<string, string>
envOverride: Record<string, string>
onSaveEnv: (value: Record<string, string>) => 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 (
<div className="flex items-center gap-2">
<span className="shrink-0 text-xs text-muted-foreground">
{translate('auto.components.settings.AgentsPane.cfb3f35775', 'Arguments')}
</span>
<Input
value={argsDraft}
onChange={(e) => 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 && (
<Button
type="button"
variant="ghost"
size="xs"
onClick={() => {
onSaveArgs(defaultArgs)
setArgsDraft(defaultArgs)
}}
className="h-7 shrink-0 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AgentsPane.5200dac9da', 'Reset')}
</Button>
)}
</div>
)
}
function stringifyAgentEnv(env: Record<string, string>): string {
return Object.entries(env)
.map(([name, value]) => `${name}=${value}`)
.join(' ')
}
function parseAgentEnvDraft(value: string): Record<string, string> {
const env: Record<string, string> = {}
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 (
<div className="flex items-center gap-2">
<span className="shrink-0 text-xs text-muted-foreground">
{translate('auto.components.settings.AgentsPane.8fbe1f37c1', 'Environment')}
</span>
<Input
value={envDraft}
onChange={(e) => 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 && (
<Button
type="button"
variant="ghost"
size="xs"
onClick={() => {
onSaveEnv(defaultEnv)
setEnvDraft(defaultEnvText)
}}
className="h-7 shrink-0 text-xs text-muted-foreground hover:text-foreground"
>
{translate('auto.components.settings.AgentsPane.5200dac9da', 'Reset')}
</Button>
)}
</div>
)
}
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 (
<div className={cn('py-3', !isDetected && 'opacity-70')}>
@@ -260,6 +429,8 @@ function AgentRow({
) : (
defaultCmd
)}
{argsOverride && <span className="ml-1.5 text-foreground/70">{argsOverride}</span>}
{envSummary && <span className="ml-1.5 text-foreground/60">{envSummary}</span>}
</div>
</div>
@@ -366,10 +537,28 @@ function AgentRow({
cmdOverride={cmdOverride}
onSaveOverride={onSaveOverride}
/>
<div className="mt-2">
<AgentDefaultArgsInput
key={`${agentId}:${argsOverride}`}
defaultArgs={defaultArgs}
argsOverride={argsOverride}
onSaveArgs={onSaveArgs}
/>
</div>
{(defaultEnvSummary || envSummary) && (
<div className="mt-2">
<AgentDefaultEnvInput
key={`${agentId}:${envSummary}`}
defaultEnv={defaultEnv}
envOverride={envOverride}
onSaveEnv={onSaveEnv}
/>
</div>
)}
<p className="mt-1.5 text-[11px] text-muted-foreground">
{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.'
)}
</p>
</div>
@@ -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<string, string>): 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)}
/>
))}
</div>
@@ -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)}
/>
))}
</div>
@@ -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 (
<ContextMenuContent>
<ContextMenuItem onSelect={onHide}>
<EyeOff className="size-3.5" />
{translate('auto.components.sidebar.SidebarNav.d599269755', 'Hide from sidebar')}
</ContextMenuItem>
</ContextMenuContent>
)
}
const SidebarNav = React.memo(function SidebarNav() {
const worktreePaletteShortcut = useShortcutLabel('worktree.palette')
const openAutomationsPage = useAppStore((s) => s.openAutomationsPage)
@@ -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 (
<ContextMenuContent>
<ContextMenuItem onSelect={onHide}>
<EyeOff className="size-3.5" />
{translate('auto.components.sidebar.SidebarNav.d599269755', 'Hide from sidebar')}
</ContextMenuItem>
</ContextMenuContent>
)
}
export function TaskProviderShortcut({
canBrowseTasks,
label,
onOpen,
children
}: {
canBrowseTasks: boolean
label: string
onOpen: () => void
children: React.ReactNode
}): React.JSX.Element {
return (
<span
role={canBrowseTasks ? 'button' : undefined}
tabIndex={-1}
onClick={(e) => {
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}
</span>
)
}
@@ -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 () => {
@@ -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) {
@@ -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,
+6 -2
View File
@@ -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",
+6 -2
View File
@@ -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",
+6 -2
View File
@@ -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": "次へのアイコン",
+6 -2
View File
@@ -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": "다음 아이콘",
+6 -2
View File
@@ -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": "下一个图标",
@@ -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',
@@ -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
})
@@ -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(
@@ -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
})
}
@@ -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
}
@@ -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
})
@@ -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
@@ -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
}
@@ -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
})
@@ -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) {
@@ -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',
@@ -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
})
@@ -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',
+12
View File
@@ -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<AppState, [], [], SettingsSlice>
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)
}
+8
View File
@@ -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
+17
View File
@@ -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', () => {
+4
View File
@@ -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,
-21
View File
@@ -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<TuiAgent, TuiAgentConfig> = {
claude: {
detectCmd: 'claude',
@@ -91,11 +85,6 @@ export const TUI_AGENT_CONFIG: Record<TuiAgent, TuiAgentConfig> = {
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<TuiAgent, TuiAgentConfig> = {
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',
+103
View File
@@ -0,0 +1,103 @@
import { isTuiAgent } from './tui-agent-config'
import type { TuiAgent } from './types'
export const DEFAULT_TUI_AGENT_ARGS: Partial<Record<TuiAgent, string>> = {
claude: '--dangerously-skip-permissions',
'claude-agent-teams': '--dangerously-skip-permissions',
openclaude: '--dangerously-skip-permissions',
codex: '--dangerously-bypass-approvals-and-sandbox',
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<Record<TuiAgent, Record<string, string>>> = {
goose: { GOOSE_MODE: 'auto' }
}
export function normalizeTuiAgentArgsRecord(value: unknown): Partial<Record<TuiAgent, string>> {
const normalized: Partial<Record<TuiAgent, string>> = {}
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<Record<TuiAgent, Record<string, string>>> {
const normalized: Partial<Record<TuiAgent, Record<string, string>>> = {}
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<string, string> = {}
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<string, string> {
return { ...DEFAULT_TUI_AGENT_ENV[agent] }
}
export function resolveTuiAgentLaunchArgs(
agent: TuiAgent,
configuredArgs: Partial<Record<TuiAgent, string>> | 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<Record<TuiAgent, Record<string, string>>> | null | undefined
): Record<string, string> {
if (configuredEnv && Object.prototype.hasOwnProperty.call(configuredEnv, agent)) {
return { ...configuredEnv[agent] }
}
return getTuiAgentDefaultEnv(agent)
}
+14
View File
@@ -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({
+23 -10
View File
@@ -106,6 +106,7 @@ export function buildAgentStartupPlan(args: {
shell?: AgentStartupShell
allowEmptyPromptLaunch?: boolean
agentArgs?: string | null
agentEnv?: Record<string, string> | 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<Record<TuiAgent, string>>
platform: NodeJS.Platform
shell?: AgentStartupShell
agentArgs?: string | null
agentEnv?: Record<string, string> | 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<string, string> | 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
+6
View File
@@ -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<Record<TuiAgent, string>>
/** Per-agent default CLI arguments appended after the binary/path and before prompts. */
agentDefaultArgs?: Partial<Record<TuiAgent, string>>
/** Per-agent launch environment defaults used when yolo mode is exposed as env. */
agentDefaultEnv?: Partial<Record<TuiAgent, Record<string, string>>>
/** 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
@@ -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()
@@ -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)
})
})
@@ -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')
}
+2 -2
View File
@@ -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 })
+3 -4
View File
@@ -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
@@ -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)
@@ -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', {
+15 -3
View File
@@ -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)
}
+9 -10
View File
@@ -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(() => {