diff --git a/src/main/runtime/runtime-folder-worktree-create.ts b/src/main/runtime/runtime-folder-worktree-create.ts index efea3c22c72..ef7798c91f9 100644 --- a/src/main/runtime/runtime-folder-worktree-create.ts +++ b/src/main/runtime/runtime-folder-worktree-create.ts @@ -172,7 +172,7 @@ export async function createRuntimeFolderWorktree(args: { undefined, args.startup && !didSpawnStartup ? args.startup : undefined ) - } else if (deps.ptySpawnAvailable && !didSpawnStartup) { + } else if (deps.ptySpawnAvailable && !didSpawnStartup && !args.createdWithAgent) { try { await deps.createTerminal(`id:${worktree.id}`, { surfaceOwner: false }) } catch (error) { diff --git a/src/main/runtime/runtime-local-worktree-terminal-startup.test.ts b/src/main/runtime/runtime-local-worktree-terminal-startup.test.ts new file mode 100644 index 00000000000..1153b173553 --- /dev/null +++ b/src/main/runtime/runtime-local-worktree-terminal-startup.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it, vi } from 'vitest' +import type { Repo } from '../../shared/repo-types' +import type { Worktree } from '../../shared/worktree/types' +import { startRuntimeLocalWorktreeTerminals } from './runtime-local-worktree-terminal-startup' + +const repo: Repo = { + id: 'repo-1', + path: '/repo', + displayName: 'repo', + badgeColor: 'blue', + addedAt: 1 +} + +const worktree: Worktree = { + id: 'worktree-1', + repoId: repo.id, + path: '/worktree', + head: 'abc', + branch: 'feature', + isBare: false, + isMainWorktree: false, + displayName: 'feature', + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 1 +} + +type StartupArgs = Parameters[0] + +function createPorts() { + const createTerminal = vi.fn().mockResolvedValue({ + handle: 'term-1', + worktreeId: worktree.id, + title: null + }) + const ports: StartupArgs['ports'] = { + canSpawn: true, + markTrusted: vi.fn(), + createTerminal, + pasteDraft: vi.fn(), + sendFollowup: vi.fn(), + provision: vi.fn().mockResolvedValue({ setupSpawned: false, setupTerminalHandle: null }), + activate: vi.fn() + } + return { createTerminal, ports } +} + +describe('startRuntimeLocalWorktreeTerminals default shell seeding', () => { + it.each([ + ['Blank Terminal', undefined, 1], + ['an agent', 'codex' as const, 0] + ])('seeds a background shell for %s selection only', async (_label, agent, expectedCalls) => { + const { createTerminal, ports } = createPorts() + + await startRuntimeLocalWorktreeTerminals({ + request: { repoSelector: `id:${repo.id}`, name: worktree.displayName }, + repo, + worktree, + ...(agent ? { createdWithAgent: agent } : {}), + ports + }) + + expect(createTerminal).toHaveBeenCalledTimes(expectedCalls) + if (expectedCalls > 0) { + expect(createTerminal).toHaveBeenCalledWith(`id:${worktree.id}`, { surfaceOwner: false }) + } + }) +}) diff --git a/src/main/runtime/runtime-local-worktree-terminal-startup.ts b/src/main/runtime/runtime-local-worktree-terminal-startup.ts index 35985b53497..7babcd7da7d 100644 --- a/src/main/runtime/runtime-local-worktree-terminal-startup.ts +++ b/src/main/runtime/runtime-local-worktree-terminal-startup.ts @@ -163,7 +163,7 @@ export async function startRuntimeLocalWorktreeTerminals(args: { didSpawnSetup = true } } - } else if (ports.canSpawn) { + } else if (ports.canSpawn && !args.createdWithAgent) { try { await ports.createTerminal(`id:${worktree.id}`, { surfaceOwner: false }) } catch (error) { diff --git a/src/main/runtime/runtime-remote-managed-worktree-create.ts b/src/main/runtime/runtime-remote-managed-worktree-create.ts index 01a49142dc4..83de82b0594 100644 --- a/src/main/runtime/runtime-remote-managed-worktree-create.ts +++ b/src/main/runtime/runtime-remote-managed-worktree-create.ts @@ -222,7 +222,7 @@ export async function createRuntimeRemoteManagedWorktree( didSpawnSetup = true } } - } else if (!shouldActivate && deps.canSpawn()) { + } else if (!shouldActivate && deps.canSpawn() && !args.createdWithAgent) { try { await deps.createTerminal(`path:${result.worktree.path}`, { surfaceOwner: false }) } catch (err) { diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts index a96564b6652..ef27dfcc03b 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts @@ -116,6 +116,7 @@ describe('submitFolderWorkspaceCreate', () => { }) expect(onOpenChange).toHaveBeenCalledWith(false) expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith('folder-workspace-1', { + agent: null, runtimeEnvironmentId: null }) expect(consoleError).toHaveBeenCalledWith( @@ -532,6 +533,7 @@ describe('submitFolderWorkspaceCreate', () => { linkedTask: linkedWorkItem }) expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith('folder-workspace-1', { + agent: null, runtimeEnvironmentId: null }) expect(mocks.ensureAgentStartupInTerminal).not.toHaveBeenCalled() @@ -659,6 +661,7 @@ describe('submitFolderWorkspaceCreate', () => { }) expect(onOpenChange).toHaveBeenCalledWith(false) expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith('folder-workspace-1', { + agent: null, runtimeEnvironmentId: null }) }) diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts index 34eab00e730..77ff1c193b8 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts @@ -207,6 +207,7 @@ export async function submitFolderWorkspaceCreate({ onOpenChange(false) try { let activation = activateAndRevealFolderWorkspace(workspace.id, { + agent: quickAgent, ...(!structuredLaunch && startup ? { startup } : {}), ...(structuredLaunch ? { providesInitialSurface: true } : {}), runtimeEnvironmentId @@ -229,6 +230,7 @@ export async function submitFolderWorkspaceCreate({ connectionId: workspace.connectionId ?? projectGroup.connectionId }) const fallbackActivation = activateAndRevealFolderWorkspace(workspace.id, { + agent: quickAgent, ...(startup ? { startup } : {}), runtimeEnvironmentId }) diff --git a/src/renderer/src/hooks/composer-state/full-creation-execution.ts b/src/renderer/src/hooks/composer-state/full-creation-execution.ts index 28961e355c6..1ad5a92abab 100644 --- a/src/renderer/src/hooks/composer-state/full-creation-execution.ts +++ b/src/renderer/src/hooks/composer-state/full-creation-execution.ts @@ -219,6 +219,7 @@ export function useFullCreationExecution(input: FullCreationExecutionInput) { const initialActivation = activateAndRevealWorktree(worktree.id, { sidebarRevealBehavior: 'auto', + agent: tuiAgent, setup: result.setup, defaultTabs: result.defaultTabs, issueCommand, @@ -229,6 +230,7 @@ export function useFullCreationExecution(input: FullCreationExecutionInput) { const settlement = await settleFullCreationStructuredLaunch({ plan: launchPlan, + agent: tuiAgent, worktreeId: worktree.id, startup, pendingFirstAgentMessageRename, diff --git a/src/renderer/src/hooks/composer-state/full-creation-structured-launch.test.ts b/src/renderer/src/hooks/composer-state/full-creation-structured-launch.test.ts index 4a7b569bf79..d3808357ee8 100644 --- a/src/renderer/src/hooks/composer-state/full-creation-structured-launch.test.ts +++ b/src/renderer/src/hooks/composer-state/full-creation-structured-launch.test.ts @@ -36,6 +36,7 @@ const plan = (overrides: Partial = {}) => const baseArgs = { plan: plan(), + agent: 'codex' as const, worktreeId: 'worktree-1', startup: { command: 'codex' } as never, pendingFirstAgentMessageRename: true, @@ -94,6 +95,7 @@ describe('settleFullCreationStructuredLaunch', () => { }) expect(mocks.activateAndRevealWorktree).toHaveBeenCalledWith('worktree-1', { sidebarRevealBehavior: 'auto', + agent: 'codex', createNewTerminalForStartup: true, startup: baseArgs.startup }) diff --git a/src/renderer/src/hooks/composer-state/full-creation-structured-launch.ts b/src/renderer/src/hooks/composer-state/full-creation-structured-launch.ts index c7e19026ba7..4293e97505a 100644 --- a/src/renderer/src/hooks/composer-state/full-creation-structured-launch.ts +++ b/src/renderer/src/hooks/composer-state/full-creation-structured-launch.ts @@ -3,12 +3,14 @@ import type { WorktreeStartupPayload } from '@/lib/worktree-startup-payload' import { activateAndRevealWorktree } from '@/lib/worktree-activation' import type { StructuredAgentLaunchSettlement } from '@/lib/structured-agent-launch-settlement' import { activateStructuredAgentSessionById } from '@/lib/structured-agent-session-tab-activation' +import type { TuiAgent } from '../../../../shared/tui-agent' /** Full-create dialog: the structured launch plus what this flow did before structured chat * existed. Returns null when the plan's route is not structured. */ export async function settleFullCreationStructuredLaunch(args: { /** Planned before the worktree existed; `worktreeId` names the one that was created. */ plan: AgentSessionLaunchPlan + agent: TuiAgent worktreeId: string startup: WorktreeStartupPayload | undefined pendingFirstAgentMessageRename: boolean @@ -27,6 +29,7 @@ export async function settleFullCreationStructuredLaunch(args: { } const activation = activateAndRevealWorktree(args.worktreeId, { sidebarRevealBehavior: 'auto', + agent: args.agent, createNewTerminalForStartup: true, ...(args.startup ? { startup: args.startup } : {}) }) diff --git a/src/renderer/src/lib/worktree-activation-emptied-workspace-reseed.test.ts b/src/renderer/src/lib/worktree-activation-emptied-workspace-reseed.test.ts index f9ebf55b0d2..ea4f3119bbc 100644 --- a/src/renderer/src/lib/worktree-activation-emptied-workspace-reseed.test.ts +++ b/src/renderer/src/lib/worktree-activation-emptied-workspace-reseed.test.ts @@ -32,6 +32,23 @@ function seedClosedLastTerminal(worktreeId: string): void { } describe('activating a workspace whose last terminal was closed', () => { + it.each([ + ['Blank Terminal', null, 1], + ['an agent', 'codex' as const, 0] + ])('seeds a default shell for %s selection only', (_label, agent, expectedTabCount) => { + const worktree = makeWorktree() + seedEmptyActivatableWorktree(worktree) + + const result = activateAndRevealWorktree(worktree.id, { + agent, + notifyHostRuntime: false + }) + + expect(result).not.toBe(false) + expect(result === false ? null : result.primaryTabId === null).toBe(expectedTabCount === 0) + expect(useAppStore.getState().tabsByWorktree[worktree.id] ?? []).toHaveLength(expectedTabCount) + }) + it.each([true, false])( 'forwards providesInitialSurface=%s through the async activation gate', async (providesInitialSurface) => { @@ -234,6 +251,22 @@ function seedEmptiedFolderWorkspaceOnTwoHosts(): void { } describe('activating a folder workspace whose last terminal was closed', () => { + it.each([ + ['Blank Terminal', null, 1], + ['an agent', 'codex' as const, 0] + ])('seeds a default shell for %s selection only', (_label, agent, expectedTabCount) => { + seedEmptiedFolderWorkspaceOnTwoHosts() + + const result = activateAndRevealFolderWorkspace(FOLDER_ID, { + agent, + executionHostId: 'local' + }) + + expect(result).not.toBe(false) + expect(useAppStore.getState().activeWorktreeId).toBe(FOLDER_KEY) + expect(useAppStore.getState().tabsByWorktree[FOLDER_KEY]).toHaveLength(expectedTabCount) + }) + it.each([true, false])( 'forwards providesInitialSurface=%s through the async activation gate', async (providesInitialSurface) => { diff --git a/src/renderer/src/lib/worktree-activation-surface-caller-wiring.test.ts b/src/renderer/src/lib/worktree-activation-surface-caller-wiring.test.ts index ff2d5c802c1..4e7e02336ca 100644 --- a/src/renderer/src/lib/worktree-activation-surface-caller-wiring.test.ts +++ b/src/renderer/src/lib/worktree-activation-surface-caller-wiring.test.ts @@ -23,7 +23,10 @@ const SURFACE_PROVIDING_CALLERS = [ ] // The activation seam itself: declares the option and forwards it into the tombstone gate. -const SEAM_FILES = ['src/renderer/src/lib/worktree-activation.ts'] +const SEAM_FILES = [ + 'src/renderer/src/lib/worktree-activation-surface-selection.ts', + 'src/renderer/src/lib/worktree-activation.ts' +] function listSourceFiles(dir: string): string[] { return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { diff --git a/src/renderer/src/lib/worktree-activation-surface-selection.ts b/src/renderer/src/lib/worktree-activation-surface-selection.ts new file mode 100644 index 00000000000..3214b1c56dc --- /dev/null +++ b/src/renderer/src/lib/worktree-activation-surface-selection.ts @@ -0,0 +1,14 @@ +import type { TuiAgent } from '../../../shared/tui-agent' + +export type WorktreeActivationSurfaceSelection = { + /** The create picker's selection; null means Blank Terminal. */ + agent?: TuiAgent | null + /** A navigation caller is about to open its own editor, diff, or other non-terminal surface. */ + providesInitialSurface?: boolean +} + +export function activationProvidesInitialSurface( + selection?: WorktreeActivationSurfaceSelection +): boolean { + return selection?.providesInitialSurface === true || selection?.agent != null +} diff --git a/src/renderer/src/lib/worktree-activation-web-runtime.test.ts b/src/renderer/src/lib/worktree-activation-web-runtime.test.ts index 98a7679ae6d..55592d155c7 100644 --- a/src/renderer/src/lib/worktree-activation-web-runtime.test.ts +++ b/src/renderer/src/lib/worktree-activation-web-runtime.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { activateAndRevealWorktree } from './worktree-activation' import { ensureWorktreeHasInitialTerminal } from './worktree-initial-terminal-seeding' import type { AppStoreState } from './worktree-activation-test-harness' import { @@ -6,9 +7,54 @@ import { registerWorktreeActivationReset } from './worktree-activation-test-harness' import { useAppStore } from '@/store' +import { + makeCreatedAgentWorktree, + seedEmptyActivatableWorktree +} from './worktree-activation-created-agent-test-state' +import { resetWebRuntimeWakeTerminalRespawnForTests } from '@/runtime/web-runtime-wake-terminal-respawn' registerWorktreeActivationReset() +afterEach(() => { + vi.unstubAllGlobals() + resetWebRuntimeWakeTerminalRespawnForTests() +}) + +describe('activateAndRevealWorktree', () => { + it('does not ask a paired host to seed a shell for an agent selection', async () => { + const worktree = { + ...makeCreatedAgentWorktree(), + hostId: 'local' as const, + runtimeOwnerEnvironmentId: 'web-runtime-1' + } + const callRuntimeEnvironment = vi.fn().mockResolvedValue({ + ok: false, + error: { code: 'test', message: 'stop after recording the request' } + }) + ;(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ = true + vi.stubGlobal('window', { + api: { runtimeEnvironments: { call: callRuntimeEnvironment } } + }) + seedEmptyActivatableWorktree(worktree) + useAppStore.setState((state) => ({ + settings: state.settings + ? { ...state.settings, activeRuntimeEnvironmentId: 'web-runtime-1' } + : ({ activeRuntimeEnvironmentId: 'web-runtime-1' } as unknown as typeof state.settings) + })) + + activateAndRevealWorktree(worktree.id, { agent: 'codex' }) + await vi.waitFor(() => + expect(callRuntimeEnvironment).toHaveBeenCalledWith( + expect.objectContaining({ method: 'worktree.activate' }) + ) + ) + + expect(callRuntimeEnvironment).not.toHaveBeenCalledWith( + expect.objectContaining({ method: 'session.tabs.createTerminal' }) + ) + }) +}) + describe('ensureWorktreeHasInitialTerminal', () => { it('does not create a local fallback tab in the paired web runtime client', () => { ;(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ = true diff --git a/src/renderer/src/lib/worktree-activation.ts b/src/renderer/src/lib/worktree-activation.ts index d2304c28b9d..0bc6b15b481 100644 --- a/src/renderer/src/lib/worktree-activation.ts +++ b/src/renderer/src/lib/worktree-activation.ts @@ -36,6 +36,10 @@ import { } from '@/lib/worktree-initial-terminal-seeding' import { ensureWebRuntimeWorktreeTerminalAfterWake } from '@/lib/web-runtime-worktree-terminal-after-wake' import { applyWorktreeNavViewEntry } from '@/lib/worktree-nav-view-history-replay' +import { + activationProvidesInitialSurface, + type WorktreeActivationSurfaceSelection +} from './worktree-activation-surface-selection' /** * Shared activation sequence used by the worktree palette and add-repo/worktree dialogs. @@ -80,14 +84,12 @@ function canInspectAgentActivationInventory(): boolean { export function activateAndRevealFolderWorkspace( folderWorkspaceId: string, - opts?: { + opts?: WorktreeActivationSurfaceSelection & { sidebarRevealBehavior?: PendingSidebarWorktreeReveal['behavior'] revealInSidebar?: boolean startup?: WorktreeStartupPayload runtimeEnvironmentId?: string | null executionHostId?: ExecutionHostId - /** See activateAndRevealWorktree — same contract for folder workspaces. */ - providesInitialSurface?: boolean } ): ActivateAndRevealResult | false { const state = useAppStore.getState() @@ -133,6 +135,7 @@ export function activateAndRevealFolderWorkspace( state.setActiveFolderWorkspace(folderWorkspaceId, opts?.executionHostId) const workspaceKey = folderWorkspaceKey(folderWorkspaceId) + const providesInitialSurface = activationProvidesInitialSurface(opts) state.markWorktreeVisited(workspaceKey) if (!state.isNavigatingHistory) { state.recordWorktreeVisit(workspaceKey) @@ -151,17 +154,13 @@ export function activateAndRevealFolderWorkspace( if (shouldGateAgentActivation) { void gateWorktreeAgentActivation(workspaceKey).then((outcome) => { if (outcome === 'empty') { - reseedGatedEmptyWorkspace(workspaceKey, opts?.providesInitialSurface) + reseedGatedEmptyWorkspace(workspaceKey, providesInitialSurface) } }) } const primaryTabId = shouldGateAgentActivation ? null - : ensureFolderWorkspaceInitialTerminal( - folderWorkspace, - opts?.startup, - opts?.providesInitialSurface - ) + : ensureFolderWorkspaceInitialTerminal(folderWorkspace, opts?.startup, providesInitialSurface) if (opts?.revealInSidebar !== false) { state.revealWorktreeInSidebar( @@ -175,7 +174,7 @@ export function activateAndRevealFolderWorkspace( export function activateAndRevealWorktree( worktreeId: string, - opts?: { + opts?: WorktreeActivationSurfaceSelection & { startup?: WorktreeStartupPayload initialCwd?: string setup?: WorktreeSetupLaunch @@ -188,12 +187,6 @@ export function activateAndRevealWorktree( backendStartupTerminalSpawned?: boolean /** Install a preserved fallback startup beside setup/default terminals already seeded. */ createNewTerminalForStartup?: boolean - /** Set by callers that navigate here only to open their own non-terminal surface - * (an editor file, a diff). Activation then leaves a closed-last-terminal workspace - * empty instead of adding a shell the user never asked for. Caveat: on a - * runtime-owned workspace with a live web session the host owns terminal creation, - * so ensureWebRuntimeWorktreeTerminalAfterWake may still seed one (matches main). */ - providesInitialSurface?: boolean /** Keep sidebar filters intact when navigating to a hidden target. */ clearSidebarFilters?: boolean } @@ -206,6 +199,7 @@ export function activateAndRevealWorktree( const hasActivationWork = Boolean( opts?.startup || opts?.setup || opts?.defaultTabs || opts?.issueCommand ) + const providesInitialSurface = activationProvidesInitialSurface(opts) // Why: a plain reselect should still reveal the sidebar row but must not restamp focus recency or wake persistence. const isPlainAlreadyActiveTerminal = !hasActivationWork && @@ -266,7 +260,7 @@ export function activateAndRevealWorktree( if (shouldGateAgentActivation) { void gateWorktreeAgentActivation(worktreeId).then((outcome) => { if (outcome === 'empty') { - reseedGatedEmptyWorkspace(worktreeId, opts?.providesInitialSurface) + reseedGatedEmptyWorkspace(worktreeId, providesInitialSurface) } }) } @@ -274,7 +268,7 @@ export function activateAndRevealWorktree( // 4. Ensure a focusable surface exists for externally-created worktrees const primaryTabId = shouldGateAgentActivation ? null - : opts?.providesInitialSurface === true && !hasActivationWork + : providesInitialSurface && !hasActivationWork ? null : ensureWorktreeHasInitialTerminal( useAppStore.getState(), @@ -286,8 +280,8 @@ export function activateAndRevealWorktree( { ...(opts?.backendStartupTerminalSpawned ? { backendStartupTerminalSpawned: true } : {}), ...(opts?.createNewTerminalForStartup ? { createNewTerminalForStartup: true } : {}), - ...(opts?.providesInitialSurface === true ? { callerProvidesSurface: true } : {}), - reseedEmptiedWorkspace: opts?.providesInitialSurface !== true + ...(providesInitialSurface ? { callerProvidesSurface: true } : {}), + reseedEmptiedWorkspace: !providesInitialSurface } ) if (primaryTabId && opts?.initialCwd) { @@ -325,7 +319,11 @@ export function activateAndRevealWorktree( } } - if (opts?.notifyHostRuntime !== false && !opts?.backendStartupTerminalSpawned) { + if ( + opts?.notifyHostRuntime !== false && + !opts?.backendStartupTerminalSpawned && + opts?.agent == null + ) { ensureWebRuntimeWorktreeTerminalAfterWake(worktreeId) } @@ -340,9 +338,8 @@ export function activateAndRevealWorktree( */ export function activateAndRevealWorkspace( workspaceId: string, - opts?: { + opts?: WorktreeActivationSurfaceSelection & { executionHostId?: ExecutionHostId - providesInitialSurface?: boolean revealInSidebar?: boolean /** Worktree-only: folder workspaces are never filter-hidden. */ clearSidebarFilters?: boolean diff --git a/src/renderer/src/lib/worktree-creation-agent-seeding.test.ts b/src/renderer/src/lib/worktree-creation-agent-seeding.test.ts new file mode 100644 index 00000000000..be4878d3b50 --- /dev/null +++ b/src/renderer/src/lib/worktree-creation-agent-seeding.test.ts @@ -0,0 +1,77 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { WorktreeCreationRequest } from './pending-worktree-creation' + +const mocks = vi.hoisted(() => ({ + activateAndRevealWorktree: vi.fn(), + completeWorktreeCreation: vi.fn(), + ensureWorktreeHasInitialTerminal: vi.fn() +})) + +const store = { + activePendingCreationId: null as string | null, + activeView: 'tasks' as 'tasks' | 'terminal', + createWorktree: vi.fn(), + pendingWorktreeCreations: {} as Record, + repos: [] +} + +vi.mock('@/store', () => ({ useAppStore: { getState: () => store } })) +vi.mock('@/lib/worktree-activation', () => ({ + activateAndRevealWorktree: mocks.activateAndRevealWorktree +})) +vi.mock('@/lib/worktree-initial-terminal-seeding', () => ({ + ensureWorktreeHasInitialTerminal: mocks.ensureWorktreeHasInitialTerminal +})) +vi.mock('@/lib/worktree-creation-completion', () => ({ + completeWorktreeCreation: mocks.completeWorktreeCreation +})) + +import { executeWorktreeCreation } from './worktree-creation-flow-execute' + +const request: WorktreeCreationRequest = { + repoId: 'repo-1', + name: 'feature', + setupDecision: 'inherit', + agent: 'codex', + agentLaunchRoute: 'terminal-tui', + pendingFirstAgentMessageRename: false, + note: '', + startupPlan: null, + quickPrompt: '', + quickTelemetry: null +} + +describe('executeWorktreeCreation agent seeding', () => { + beforeEach(() => { + vi.clearAllMocks() + store.activePendingCreationId = null + store.activeView = 'tasks' + store.pendingWorktreeCreations = { 'creation-1': { creationId: 'creation-1' } } + store.createWorktree.mockResolvedValue({ + worktree: { id: 'worktree-1', repoId: request.repoId } + }) + }) + + it('does not seed a background shell when the request carries an agent selection', async () => { + await executeWorktreeCreation('creation-1', request) + + expect(mocks.activateAndRevealWorktree).not.toHaveBeenCalled() + expect(mocks.ensureWorktreeHasInitialTerminal).not.toHaveBeenCalled() + expect(mocks.completeWorktreeCreation).toHaveBeenCalledWith( + expect.objectContaining({ primaryTabId: null }) + ) + }) + + it('passes the agent selection through an active reveal', async () => { + store.activePendingCreationId = 'creation-1' + store.activeView = 'terminal' + mocks.activateAndRevealWorktree.mockReturnValue({ primaryTabId: null }) + + await executeWorktreeCreation('creation-1', request) + + expect(mocks.activateAndRevealWorktree).toHaveBeenCalledWith( + 'worktree-1', + expect.objectContaining({ agent: 'codex' }) + ) + }) +}) diff --git a/src/renderer/src/lib/worktree-creation-flow-execute.ts b/src/renderer/src/lib/worktree-creation-flow-execute.ts index 273b3ece1a8..586f5e7f8bd 100644 --- a/src/renderer/src/lib/worktree-creation-flow-execute.ts +++ b/src/renderer/src/lib/worktree-creation-flow-execute.ts @@ -168,6 +168,7 @@ export async function executeWorktreeCreation( if (shouldActivateOnCompletion && !structuredLaunch) { activation = activateAndRevealWorktree(worktree.id, { sidebarRevealBehavior: 'auto', + ...(preparedRequest.agent !== null ? { agent: preparedRequest.agent } : {}), ...(result.setup ? { setup: result.setup } : {}), ...(result.defaultTabs ? { defaultTabs: result.defaultTabs } : {}), ...(startupOpt ? { startup: startupOpt } : {}), @@ -181,7 +182,7 @@ export async function executeWorktreeCreation( startupOpt || result.setup || preparedRequest.issueCommand || result.defaultTabs ) primaryTabId = - structuredLaunch && !hasExplicitTerminalWork + preparedRequest.agent !== null && !hasExplicitTerminalWork ? null : ensureWorktreeHasInitialTerminal( useAppStore.getState(), @@ -192,7 +193,7 @@ export async function executeWorktreeCreation( result.defaultTabs, { activateCreatedTabs: false, - ...(structuredLaunch ? { callerProvidesSurface: true } : {}), + ...(preparedRequest.agent !== null ? { callerProvidesSurface: true } : {}), ...(backendSpawned ? { backendStartupTerminalSpawned: true } : {}) } ) diff --git a/src/renderer/src/lib/worktree-creation-flow.test.ts b/src/renderer/src/lib/worktree-creation-flow.test.ts index 84ac3a61b32..a599a265513 100644 --- a/src/renderer/src/lib/worktree-creation-flow.test.ts +++ b/src/renderer/src/lib/worktree-creation-flow.test.ts @@ -606,7 +606,7 @@ describe('staged background worktree creation', () => { delete store.pendingWorktreeCreations['creation-1'] store.activePendingCreationId = null resolveTrust() - await vi.waitFor(() => expect(ensureWorktreeHasInitialTerminal).toHaveBeenCalledTimes(1)) + await vi.waitFor(() => expect(store.removePendingWorktreeCreation).toHaveBeenCalled()) expect(activateAndRevealWorktree).not.toHaveBeenCalled() })