mirror of
https://github.com/stablyai/orca.git
synced 2026-10-15 16:02:49 +00:00
Default agent launches to yolo permissions mode (#5145)
* Default agent launches to yolo mode * test: update launch default validations
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "次へのアイコン",
|
||||
|
||||
@@ -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": "다음 아이콘",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user