From c8f080764b1d764e37933c56bc321488b8a2bdf5 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:15:46 -0400 Subject: [PATCH] refactor(terminal): publish new tabs together with their startup command (#28848) Seven call sites created a terminal tab and then queued its startup command in a second store write. Pass the command through createTab's existing pendingStartup option instead, so the tab and its command land in one write. Co-authored-by: Claude Opus 5.5 --- .../ipc-events/terminal-request-ipc-bridge.ts | 39 +++++++++-------- ...pcEvents-terminal-create-surfacing.test.ts | 25 ++++++----- .../lib/agent-launch-caller-placement.test.ts | 16 +++---- .../lib/agent-launch-caller-routing.test.ts | 12 +++--- .../lib/agent-launch-funnel-test-harness.ts | 14 +++---- ...h-agent-in-new-tab-host-resolution.test.ts | 8 ++-- .../launch-agent-in-new-tab-placement.test.ts | 11 +++-- ...h-agent-in-new-tab-windows-quoting.test.ts | 29 ++++++------- .../src/lib/launch-agent-in-new-tab.test.ts | 42 ++++++++++--------- .../src/lib/launch-agent-in-new-tab.ts | 39 +++++++++-------- .../src/lib/launch-ai-vault-session.test.ts | 23 ++++++---- .../src/lib/launch-ai-vault-session.ts | 26 ++++++------ .../lib/run-quick-command-in-new-tab.test.ts | 17 ++++---- .../src/lib/run-quick-command-in-new-tab.ts | 7 +--- .../worktree-activation-agent-startup.test.ts | 19 ++++++--- .../worktree-activation-default-tabs.test.ts | 33 ++++++++------- .../worktree-activation-issue-command.test.ts | 7 ++-- .../lib/worktree-activation-reveal.test.ts | 2 - .../worktree-activation-setup-script.test.ts | 41 ++++++++---------- .../lib/worktree-activation-store-contract.ts | 27 ++++++------ ...activation-structured-chat-surface.test.ts | 5 ++- .../lib/worktree-activation-test-harness.ts | 22 ++++++++-- .../worktree-activation-web-runtime.test.ts | 9 ++-- .../src/lib/worktree-default-terminal-tabs.ts | 27 +++++------- .../lib/worktree-initial-terminal-seeding.ts | 21 ++++------ .../lib/worktree-setup-issue-command-queue.ts | 4 +- 26 files changed, 271 insertions(+), 254 deletions(-) diff --git a/src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge.ts b/src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge.ts index 530846c08bc..64898b7ef13 100644 --- a/src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge.ts +++ b/src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge.ts @@ -62,7 +62,28 @@ export function registerTerminalRequestIpcBridge(unsubs: (() => void)[]): void { recordInteraction: false, ...(data.cwd ? { startupCwd: data.cwd } : {}) } - const tab = store.createTab(worktreeId, data.targetGroupId, data.shellOverride, tabOptions) + const pendingStartup = data.command + ? { + command: data.command, + ...(data.env ? { env: data.env } : {}), + ...(data.envToDelete ? { envToDelete: data.envToDelete } : {}), + ...(data.launchConfig ? { launchConfig: data.launchConfig } : {}), + ...(data.resumeProviderSession + ? { resumeProviderSession: data.resumeProviderSession } + : {}), + ...(data.launchToken ? { launchToken: data.launchToken } : {}), + ...(data.launchAgent ? { launchAgent: data.launchAgent } : {}), + ...(data.startupCommandDelivery + ? { startupCommandDelivery: data.startupCommandDelivery } + : {}) + } + : undefined + const tab = store.createTab( + worktreeId, + data.targetGroupId, + data.shellOverride, + pendingStartup ? { ...tabOptions, pendingStartup } : tabOptions + ) if (!shouldActivate) { // Why: renderer-backed Codex startup must mount its new TerminalPane without switching UI or connecting every saved tab. requestBackgroundTerminalWorktreeMount({ worktreeId, tabIds: [tab.id] }) @@ -89,22 +110,6 @@ export function registerTerminalRequestIpcBridge(unsubs: (() => void)[]): void { if (data.title) { store.setTabCustomTitle(tab.id, data.title, { recordInteraction: false }) } - if (data.command) { - store.queueTabStartupCommand(tab.id, { - command: data.command, - ...(data.env ? { env: data.env } : {}), - ...(data.envToDelete ? { envToDelete: data.envToDelete } : {}), - ...(data.launchConfig ? { launchConfig: data.launchConfig } : {}), - ...(data.resumeProviderSession - ? { resumeProviderSession: data.resumeProviderSession } - : {}), - ...(data.launchToken ? { launchToken: data.launchToken } : {}), - ...(data.launchAgent ? { launchAgent: data.launchAgent } : {}), - ...(data.startupCommandDelivery - ? { startupCommandDelivery: data.startupCommandDelivery } - : {}) - }) - } window.api.ui.replyTerminalCreate({ requestId: data.requestId, tabId: tab.id, diff --git a/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts b/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts index 63c3c9122e5..e489341fe5d 100644 --- a/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts @@ -231,7 +231,15 @@ describe('useIpcEvents updater integration', () => { activate: false, recordInteraction: false, launchAgent: 'codex', - startupCwd: '/repo/packages/app' + startupCwd: '/repo/packages/app', + pendingStartup: { + command: 'codex', + launchConfig: { + agentArgs: '--model gpt-5', + agentEnv: { CODEX_PROFILE: 'request' } + }, + launchAgent: 'codex' + } }) expect(setActiveView).not.toHaveBeenCalled() expect(setActiveWorktree).not.toHaveBeenCalled() @@ -251,14 +259,7 @@ describe('useIpcEvents updater integration', () => { expect(setTabCustomTitle).toHaveBeenCalledWith('tab-new', 'Codex', { recordInteraction: false }) - expect(queueTabStartupCommand).toHaveBeenCalledWith('tab-new', { - command: 'codex', - launchConfig: { - agentArgs: '--model gpt-5', - agentEnv: { CODEX_PROFILE: 'request' } - }, - launchAgent: 'codex' - }) + expect(queueTabStartupCommand).not.toHaveBeenCalled() expect(replyTerminalCreate).toHaveBeenCalledWith({ requestId: 'req-renderer-backed', tabId: 'tab-new', @@ -282,7 +283,8 @@ describe('useIpcEvents updater integration', () => { expect(createTab).toHaveBeenCalledWith('wt-2', 'group-left', undefined, { launchAgent: 'codex', - viewMode: 'terminal' + viewMode: 'terminal', + pendingStartup: { command: 'codex', launchAgent: 'codex' } }) expect(replyTerminalCreate).toHaveBeenCalledWith({ requestId: 'req-runtime-session', @@ -444,7 +446,8 @@ describe('useIpcEvents updater integration', () => { expect(createTab).toHaveBeenCalledWith('wt-2', undefined, undefined, { activate: false, - recordInteraction: false + recordInteraction: false, + pendingStartup: { command: 'codex' } }) expect(setActiveView).not.toHaveBeenCalled() expect(setActiveWorktree).not.toHaveBeenCalled() diff --git a/src/renderer/src/lib/agent-launch-caller-placement.test.ts b/src/renderer/src/lib/agent-launch-caller-placement.test.ts index 3b20851e43c..657f1f9b9ac 100644 --- a/src/renderer/src/lib/agent-launch-caller-placement.test.ts +++ b/src/renderer/src/lib/agent-launch-caller-placement.test.ts @@ -12,6 +12,7 @@ import { createdTabOptions, createLaunchFunnelStore, hostLaunchRequest, + queuedStartupCommand, queuedStartupPayload, resetLaunchFunnelStore } from './agent-launch-funnel-test-harness' @@ -143,10 +144,6 @@ describe('agent launch caller placement and telemetry', () => { 'tab-1', profile.args.initialCwd ) - // Ordering matters: the pane snapshots the queued cwd on first render. - expect(store.queueTabInitialCwd.mock.invocationCallOrder[0]).toBeLessThan( - store.queueTabStartupCommand.mock.invocationCallOrder[0] ?? Number.MAX_SAFE_INTEGER - ) return } expect(store.queueTabInitialCwd).not.toHaveBeenCalled() @@ -173,16 +170,15 @@ describe('agent launch caller placement and telemetry', () => { }) }) - it('creates the tab before queueing its startup command', async () => { + it('publishes the tab together with its startup command', async () => { const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') launchAgentInNewTab({ requestId: 'request-2', agent: 'codex', worktreeId: 'wt-1' }) - // Why: the terminal pane snapshots pending startup in useState on first render, so a startup - // queued after mount is never seen. - expect(store.createTab.mock.invocationCallOrder[0]).toBeLessThan( - store.queueTabStartupCommand.mock.invocationCallOrder[0] ?? 0 - ) + // Why: the terminal pane snapshots pending startup in useState on first render, so the tab + // must never exist without its startup command. + expect(store.createTab).toHaveBeenCalledTimes(1) + expect(queuedStartupCommand(store)).toEqual(expect.any(String)) }) it('seeds working status for a Command Code prompt that rides argv', async () => { diff --git a/src/renderer/src/lib/agent-launch-caller-routing.test.ts b/src/renderer/src/lib/agent-launch-caller-routing.test.ts index 2e9f96e8257..600a3c83c46 100644 --- a/src/renderer/src/lib/agent-launch-caller-routing.test.ts +++ b/src/renderer/src/lib/agent-launch-caller-routing.test.ts @@ -11,7 +11,11 @@ import { callerProfileCases, type AgentLaunchCallerProfile } from './agent-launch-caller-profiles-test-harness' -import { createLaunchFunnelStore, resetLaunchFunnelStore } from './agent-launch-funnel-test-harness' +import { + createLaunchFunnelStore, + queuedStartupCommand, + resetLaunchFunnelStore +} from './agent-launch-funnel-test-harness' import type * as LaunchAdmissionModule from './structured-agent-session-launch-admission' const store = createLaunchFunnelStore() @@ -270,10 +274,7 @@ describe('agent launch caller routing', () => { await expect(result?.structuredSettlement).resolves.toEqual({ kind: 'terminal' }) expect(store.createTab).toHaveBeenCalledOnce() expect(store.createTab.mock.calls[0]?.slice(0, 2)).toEqual(['wt-1', 'group-1']) - expect(store.queueTabStartupCommand).toHaveBeenCalledOnce() - expect(store.queueTabStartupCommand.mock.calls[0]?.[1]?.command).toBe( - "claude '--model' 'sonnet'" - ) + expect(queuedStartupCommand(store)).toBe("claude '--model' 'sonnet'") // The typed prompt goes to that terminal once, and the caller hears it arrived. await expect(result?.promptDeliveryResult).resolves.toEqual({ delivered: true, @@ -339,7 +340,6 @@ describe('agent launch caller routing', () => { expect(result).toBeNull() expect(beforeSurfaceOpen).toHaveBeenCalledExactlyOnceWith({ kind: 'local-terminal' }) expect(store.createTab).not.toHaveBeenCalled() - expect(store.queueTabStartupCommand).not.toHaveBeenCalled() }) it('aborts a host-published open when beforeSurfaceOpen refuses', async () => { diff --git a/src/renderer/src/lib/agent-launch-funnel-test-harness.ts b/src/renderer/src/lib/agent-launch-funnel-test-harness.ts index 8ae8b40595e..ec26a151a58 100644 --- a/src/renderer/src/lib/agent-launch-funnel-test-harness.ts +++ b/src/renderer/src/lib/agent-launch-funnel-test-harness.ts @@ -45,7 +45,6 @@ export function createLaunchFunnelStore(): { tabBarOrderByWorktree: Record createTab: ReturnType queueTabInitialCwd: ReturnType - queueTabStartupCommand: ReturnType setActiveTabType: ReturnType setTabBarOrder: ReturnType setAgentStatus: ReturnType @@ -65,7 +64,6 @@ export function createLaunchFunnelStore(): { tabBarOrderByWorktree: {}, createTab: vi.fn(), queueTabInitialCwd: vi.fn(), - queueTabStartupCommand: vi.fn(), setActiveTabType: vi.fn(), setTabBarOrder: vi.fn(), setAgentStatus: vi.fn(), @@ -117,16 +115,18 @@ export function resetLaunchFunnelStore( store.createTab.mockImplementation(createTabInto(store)) } -/** The startup command `queueTabStartupCommand` received, or undefined when no tab was queued. */ +/** The startup command the window planned for the new tab, or undefined when the host plans it. */ export function queuedStartupCommand(store: LaunchFunnelStore): string | undefined { - return store.queueTabStartupCommand.mock.calls[0]?.[1]?.command + return queuedStartupPayload(store)?.command } -/** The whole startup-command payload, for callers that assert more than the command string. */ +/** The whole window-planned startup payload, for callers that assert more than the command string. */ export function queuedStartupPayload( store: LaunchFunnelStore -): Record | undefined { - return store.queueTabStartupCommand.mock.calls[0]?.[1] +): { command?: string; [key: string]: unknown } | undefined { + const options: { pendingStartup?: { command?: string; agentLaunch?: unknown } } = + store.createTab.mock.calls[0]?.[3] ?? {} + return options.pendingStartup?.agentLaunch ? undefined : options.pendingStartup } /** The tab-creation options object (`createTab`'s 4th argument). */ diff --git a/src/renderer/src/lib/launch-agent-in-new-tab-host-resolution.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab-host-resolution.test.ts index bcdc67faeb3..ca7cb21c191 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab-host-resolution.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab-host-resolution.test.ts @@ -4,7 +4,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mockCreateTab = vi.fn() -const mockQueueTabStartupCommand = vi.fn() +/** Startup commands published with the created tabs, in creation order. */ +function queuedStartups() { + return mockCreateTab.mock.calls.flatMap((call) => call[3]?.pendingStartup ?? []) +} type StoreRepo = { id: string @@ -57,7 +60,6 @@ const store = { ptyIdsByTabId: {} as Record, createTab: mockCreateTab, closeTab: vi.fn(), - queueTabStartupCommand: mockQueueTabStartupCommand, setActiveTabType: vi.fn(), setTabBarOrder: vi.fn(), setAgentStatus: vi.fn(), @@ -112,7 +114,7 @@ async function launchOnLinux(): Promise { } function queuedCommand(): string { - return mockQueueTabStartupCommand.mock.calls[0]?.[1]?.command + return queuedStartups()[0]?.command } describe('launchAgentInNewTab execution host resolution', () => { diff --git a/src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts index 29db1273f1a..54aa9903bef 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts @@ -6,7 +6,10 @@ import { setLocalRuntimeCapabilitiesForTests } from '@/runtime/local-runtime-cap import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' const mockCreateTab = vi.fn() -const mockQueueTabStartupCommand = vi.fn() +/** Startup commands published with the created tabs, in creation order. */ +function queuedStartups() { + return mockCreateTab.mock.calls.flatMap((call) => call[3]?.pendingStartup ?? []) +} const mockSetActiveTabType = vi.fn() const mockSeedNativeChatAppliedSessionOptions = vi.fn() @@ -42,7 +45,6 @@ const store = { tabBarOrderByWorktree: {}, createTab: mockCreateTab, queueTabInitialCwd: vi.fn(), - queueTabStartupCommand: mockQueueTabStartupCommand, setActiveTabType: mockSetActiveTabType, setTabBarOrder: vi.fn() } @@ -130,7 +132,8 @@ describe('launchAgentInNewTab terminal tab activation', () => { undefined, { launchAgent: 'codex', - quickCommandLabel: undefined + quickCommandLabel: undefined, + pendingStartup: expect.anything() } ) expect(mockSetActiveTabType).toHaveBeenCalledExactlyOnceWith( @@ -138,6 +141,6 @@ describe('launchAgentInNewTab terminal tab activation', () => { FLOATING_TERMINAL_WORKTREE_ID ) // Chat UI model preferences belong to structured chats, not a terminal fallback. - expect(mockQueueTabStartupCommand.mock.calls[0]?.[1]).not.toHaveProperty('sessionOptions') + expect(queuedStartups()[0]).not.toHaveProperty('sessionOptions') }) }) diff --git a/src/renderer/src/lib/launch-agent-in-new-tab-windows-quoting.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab-windows-quoting.test.ts index 3004cd0d268..48fd355b220 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab-windows-quoting.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab-windows-quoting.test.ts @@ -7,7 +7,10 @@ const mockIsWebRuntimeSessionActive = vi.fn(() => false) const mockCreateWebRuntimeAgentSessionTerminal = vi.fn() const mockCreateWebRuntimeAgentSessionTerminalWithLaunchDraft = vi.fn() const mockCreateTab = vi.fn() -const mockQueueTabStartupCommand = vi.fn() +/** Startup commands published with the created tabs, in creation order. */ +function queuedStartups() { + return mockCreateTab.mock.calls.flatMap((call) => call[3]?.pendingStartup ?? []) +} const mockPasteDraftWhenAgentReady = vi.fn() const store = { @@ -67,7 +70,6 @@ const store = { ptyIdsByTabId: {} as Record, createTab: mockCreateTab, closeTab: vi.fn(), - queueTabStartupCommand: mockQueueTabStartupCommand, setActiveTabType: vi.fn(), setTabBarOrder: vi.fn(), setAgentStatus: vi.fn(), @@ -202,8 +204,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { launchPlatform: 'win32' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: "claude '--dangerously-skip-permissions' --prefill 'review Bob''s change'" }) @@ -221,8 +222,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { launchPlatform: 'win32' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: 'claude "--dangerously-skip-permissions"' }) @@ -240,8 +240,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { launchPlatform: 'win32' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: "claude '--dangerously-skip-permissions'" }) @@ -261,8 +260,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { launchPlatform: 'win32' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: 'codex "--model" "gpt-5" "fix the spinner"', agentArgsOverride: '--model gpt-5' @@ -283,8 +281,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { launchPlatform: 'win32' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: `claude '--dangerously-skip-permissions' --prefill 'review Bob'"'"'s change'` }) @@ -301,8 +298,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { launchAgentInNewTab({ requestId: 'request-7', agent: 'claude', worktreeId: 'wt-1' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: "claude '--dangerously-skip-permissions'" }) @@ -339,8 +335,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { promptDelivery: 'draft' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: `claude '--dangerously-skip-permissions' --prefill 'review Bob'"'"'s change'` }) @@ -374,7 +369,7 @@ describe('launchAgentInNewTab Windows shell quoting', () => { launchAgentInNewTab({ requestId: 'request-9', agent: 'codex', worktreeId: 'wt-1' }) - const queued = mockQueueTabStartupCommand.mock.calls.at(-1)?.[1] as { command: string } + const queued = queuedStartups().at(-1) expect(queued.command).toContain(`'don'"'"'t'`) expect(queued.command).not.toContain("'don''t'") }) diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts index 4cedc331354..51c011a54d3 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts @@ -4,7 +4,10 @@ import { toAppSshPtyId } from '../../../shared/ssh-pty-id' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' const mockCreateTab = vi.fn() -const mockQueueTabStartupCommand = vi.fn() +/** Startup commands published with the created tabs, in creation order. */ +function queuedStartups() { + return mockCreateTab.mock.calls.flatMap((call) => call[3]?.pendingStartup ?? []) +} const mockSetActiveTabType = vi.fn() const mockSetTabViewMode = vi.fn() const mockSetTabBarOrder = vi.fn() @@ -84,7 +87,6 @@ const store = { ptyIdsByTabId: {} as Record, createTab: mockCreateTab, closeTab: vi.fn(), - queueTabStartupCommand: mockQueueTabStartupCommand, setActiveTabType: mockSetActiveTabType, setTabViewMode: mockSetTabViewMode, setTabBarOrder: mockSetTabBarOrder, @@ -215,7 +217,8 @@ describe('launchAgentInNewTab', () => { launchAgentInNewTab({ requestId: 'request-1', agent: 'codex', worktreeId: 'wt-1' }) expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { - launchAgent: 'codex' + launchAgent: 'codex', + pendingStartup: expect.anything() }) }) it('keeps Floating Workspace authority on native Windows beside an active WSL project', async () => { @@ -241,7 +244,7 @@ describe('launchAgentInNewTab', () => { FLOATING_TERMINAL_WORKTREE_ID, undefined, undefined, - { launchAgent: 'codex' } + { launchAgent: 'codex', pendingStartup: expect.anything() } ) }) @@ -265,10 +268,10 @@ describe('launchAgentInNewTab', () => { expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { launchAgent: 'codex', - quickCommandLabel: undefined + quickCommandLabel: undefined, + pendingStartup: expect.anything() }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: expect.not.stringContaining('large generated prompt') }) @@ -302,7 +305,8 @@ describe('launchAgentInNewTab', () => { expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { launchAgent: 'grok', - quickCommandLabel: undefined + quickCommandLabel: undefined, + pendingStartup: expect.anything() }) expect(mockSeedNativeChatLaunchPrompt).toHaveBeenCalledWith({ tabId: 'tab-1', @@ -327,7 +331,8 @@ describe('launchAgentInNewTab', () => { expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { launchAgent: 'grok', - quickCommandLabel: undefined + quickCommandLabel: undefined, + pendingStartup: expect.anything() }) }) @@ -398,7 +403,8 @@ describe('launchAgentInNewTab', () => { expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { launchAgent: 'codex', - quickCommandLabel: 'Review' + quickCommandLabel: 'Review', + pendingStartup: expect.anything() }) }) @@ -427,7 +433,7 @@ describe('launchAgentInNewTab', () => { quickCommandLabel: 'Review' }) - const launch = mockQueueTabStartupCommand.mock.calls[0]?.[1] + const launch = queuedStartups()[0] expect(launch.command).toContain("'--profile' 'team'") expect(launch.command).not.toContain("'-m'") expect(launch.command).not.toContain('model_reasoning_effort=') @@ -459,7 +465,7 @@ describe('launchAgentInNewTab', () => { quickCommandLabel: 'Review' }) - const launch = mockQueueTabStartupCommand.mock.calls[0]?.[1] + const launch = queuedStartups()[0] expect(launch.command).not.toContain("'-m'") expect(launch.command).not.toContain('model_reasoning_effort=') expect(launch.sessionOptions).toBeUndefined() @@ -597,8 +603,7 @@ describe('launchAgentInNewTab', () => { prompt: 'fix the spinner' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: "command-code --trust '--yolo' 'fix the spinner'", initialAgentStatus: { @@ -637,8 +642,7 @@ describe('launchAgentInNewTab', () => { }) expect(result).not.toHaveProperty('promptDeliveryResult') - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: "claude '--dangerously-skip-permissions'" }) @@ -704,8 +708,7 @@ describe('launchAgentInNewTab', () => { await Promise.resolve() await Promise.resolve() - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: "command-code --trust '--yolo'" }) @@ -885,8 +888,7 @@ describe('launchAgentInNewTab', () => { promptDelivery: 'submit-after-ready' }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(queuedStartups()).toContainEqual( expect.objectContaining({ command: "codex '--model' 'gpt-5.5'" }) diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.ts b/src/renderer/src/lib/launch-agent-in-new-tab.ts index bff61b705a3..c8988afffc0 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.ts @@ -265,35 +265,34 @@ function launchAgentInNewTabInternal(args: LaunchAgentInNewTabArgs): LaunchAgent promptDeliveryResult: launched.promptDeliveryResult } } - // Why: queue startup BEFORE TerminalPane mounts — it snapshots pendingStartupByTabId in useState on first render. const tab = store.createTab(worktreeId, groupId, undefined, { launchAgent: agent, quickCommandLabel, ...(pendingActivationSpawn ? { pendingActivationSpawn: true } : {}), - ...(activate === false ? { activate: false } : {}) + ...(activate === false ? { activate: false } : {}), + pendingStartup: { + command: startupPlan.launchCommand, + ...(startupPlan.env ? { env: startupPlan.env } : {}), + launchConfig: startupPlan.launchConfig, + launchAgent: agent, + ...(agentArgs !== undefined ? { agentArgsOverride: agentArgs } : {}), + ...(startupPlan.startupCommandDelivery + ? { startupCommandDelivery: startupPlan.startupCommandDelivery } + : {}), + ...(agent === 'command-code' && hasPrompt && promptDelivery === 'auto-submit' + ? { initialAgentStatus: { agent, prompt: trimmedPrompt } } + : {}), + telemetry: { + agent_kind: tuiAgentToAgentKind(agent), + launch_source: launchSource ?? 'tab_bar_quick_launch', + request_kind: 'new' + } + } }) if (initialCwd?.trim()) { // Why: queue before mount so local, WSL, and SSH continuations preserve their subdirectory. store.queueTabInitialCwd(tab.id, initialCwd) } - store.queueTabStartupCommand(tab.id, { - command: startupPlan.launchCommand, - ...(startupPlan.env ? { env: startupPlan.env } : {}), - launchConfig: startupPlan.launchConfig, - launchAgent: agent, - ...(agentArgs !== undefined ? { agentArgsOverride: agentArgs } : {}), - ...(startupPlan.startupCommandDelivery - ? { startupCommandDelivery: startupPlan.startupCommandDelivery } - : {}), - ...(agent === 'command-code' && hasPrompt && promptDelivery === 'auto-submit' - ? { initialAgentStatus: { agent, prompt: trimmedPrompt } } - : {}), - telemetry: { - agent_kind: tuiAgentToAgentKind(agent), - launch_source: launchSource ?? 'tab_bar_quick_launch', - request_kind: 'new' - } - }) // Why: fire-and-forget the paste-after-ready delivery so callers keep the synchronous { tabId, startupPlan } signature. // Why: safe to call unconditionally — the helper short-circuits (no paste) for native-prefill agents already holding the draft. if (hasPrompt && promptDelivery === 'draft' && pasteDraftAfterLaunch === null) { diff --git a/src/renderer/src/lib/launch-ai-vault-session.test.ts b/src/renderer/src/lib/launch-ai-vault-session.test.ts index 02f5b83c5f0..e61bccaacf8 100644 --- a/src/renderer/src/lib/launch-ai-vault-session.test.ts +++ b/src/renderer/src/lib/launch-ai-vault-session.test.ts @@ -3,7 +3,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mockCreateTab = vi.fn() const mockCreateEmptySplitGroup = vi.fn() const mockCloseEmptyGroup = vi.fn() -const mockQueueTabStartupCommand = vi.fn() +/** Startup commands published with the created tabs, in creation order. */ +function queuedStartups() { + return mockCreateTab.mock.calls.flatMap((call) => call[3]?.pendingStartup ?? []) +} const mockSetActiveTabType = vi.fn() const mockSetTabBarOrder = vi.fn() const runtimeMocks = vi.hoisted(() => ({ @@ -16,7 +19,6 @@ const mockState = { createTab: mockCreateTab, createEmptySplitGroup: mockCreateEmptySplitGroup, closeEmptyGroup: mockCloseEmptyGroup, - queueTabStartupCommand: mockQueueTabStartupCommand, setActiveTabType: mockSetActiveTabType, setTabBarOrder: mockSetTabBarOrder, tabsByWorktree: {} as Record, @@ -83,8 +85,10 @@ describe('launchAiVaultSessionInNewTab', () => { command: 'claude --resume session-1' }) - expect(mockCreateTab).toHaveBeenCalledWith('wt-1', 'group-1') - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(mockCreateTab).toHaveBeenCalledWith('wt-1', 'group-1', undefined, { + pendingStartup: expect.anything() + }) + expect(queuedStartups()).toContainEqual({ command: 'claude --resume session-1', telemetry: { agent_kind: 'claude', @@ -115,9 +119,10 @@ describe('launchAiVaultSessionInNewTab', () => { }) expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { - startupCwd: 'C:\\Users\\alice\\repo' + startupCwd: 'C:\\Users\\alice\\repo', + pendingStartup: expect.anything() }) - expect(mockQueueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(queuedStartups()).toContainEqual({ command: "claude '--dangerously-skip-permissions' '--effort' 'max' '--resume' 'session-1'", env: { ANTHROPIC_BASE_URL: 'https://claude.example.test' }, envToDelete: ['CODEX_HOME'], @@ -146,7 +151,9 @@ describe('launchAiVaultSessionInNewTab', () => { }) expect(mockCreateEmptySplitGroup).toHaveBeenCalledWith('wt-1', 'group-1', 'right') - expect(mockCreateTab).toHaveBeenCalledWith('wt-1', 'group-new') + expect(mockCreateTab).toHaveBeenCalledWith('wt-1', 'group-new', undefined, { + pendingStartup: expect.anything() + }) }) it('creates runtime-hosted resume terminals through the paired host', async () => { @@ -188,7 +195,7 @@ describe('launchAiVaultSessionInNewTab', () => { activate: true }) expect(mockCreateTab).not.toHaveBeenCalled() - expect(mockQueueTabStartupCommand).not.toHaveBeenCalled() + expect(queuedStartups()).toEqual([]) if (result.tabId === null) { await expect(result.runtimeLaunch).resolves.toEqual({ status: 'created' }) diff --git a/src/renderer/src/lib/launch-ai-vault-session.ts b/src/renderer/src/lib/launch-ai-vault-session.ts index a358efb306f..342c5ccc9b4 100644 --- a/src/renderer/src/lib/launch-ai-vault-session.ts +++ b/src/renderer/src/lib/launch-ai-vault-session.ts @@ -69,19 +69,19 @@ export function launchAiVaultSessionInNewTab(args: { } } - const tab = args.cwd - ? store.createTab(args.worktreeId, targetGroupId, undefined, { startupCwd: args.cwd }) - : store.createTab(args.worktreeId, targetGroupId) - store.queueTabStartupCommand(tab.id, { - command: args.command, - ...(args.env ? { env: args.env } : {}), - ...(args.envToDelete ? { envToDelete: args.envToDelete } : {}), - ...(args.launchConfig ? { launchConfig: args.launchConfig, launchAgent: args.agent } : {}), - ...(args.providerSession ? { resumeProviderSession: args.providerSession } : {}), - telemetry: { - agent_kind: tuiAgentToAgentKind(args.agent), - launch_source: 'sidebar', - request_kind: 'resume' + const tab = store.createTab(args.worktreeId, targetGroupId, undefined, { + ...(args.cwd ? { startupCwd: args.cwd } : {}), + pendingStartup: { + command: args.command, + ...(args.env ? { env: args.env } : {}), + ...(args.envToDelete ? { envToDelete: args.envToDelete } : {}), + ...(args.launchConfig ? { launchConfig: args.launchConfig, launchAgent: args.agent } : {}), + ...(args.providerSession ? { resumeProviderSession: args.providerSession } : {}), + telemetry: { + agent_kind: tuiAgentToAgentKind(args.agent), + launch_source: 'sidebar', + request_kind: 'resume' + } } }) store.setActiveTabType('terminal', args.worktreeId) diff --git a/src/renderer/src/lib/run-quick-command-in-new-tab.test.ts b/src/renderer/src/lib/run-quick-command-in-new-tab.test.ts index e5983ec000a..4424300fb53 100644 --- a/src/renderer/src/lib/run-quick-command-in-new-tab.test.ts +++ b/src/renderer/src/lib/run-quick-command-in-new-tab.test.ts @@ -3,7 +3,6 @@ import { runQuickCommandInNewTab } from './run-quick-command-in-new-tab' type MockStoreState = { createTab: ReturnType - queueTabStartupCommand: ReturnType setActiveTabType: ReturnType setTabBarOrder: ReturnType setRecentQuickCommandForGroup: ReturnType @@ -37,7 +36,6 @@ vi.mock('@/lib/launch-agent-in-new-tab', () => ({ function createStoreState(): MockStoreState { return { createTab: vi.fn(() => ({ id: 'tab-new' })), - queueTabStartupCommand: vi.fn(), setActiveTabType: vi.fn(), setTabBarOrder: vi.fn(), setRecentQuickCommandForGroup: vi.fn(), @@ -73,10 +71,8 @@ describe('runQuickCommandInNewTab', () => { expect(result).toEqual({ tabId: 'tab-new' }) expect(mockState.createTab).toHaveBeenCalledWith('wt-1', 'group-1', undefined, { - quickCommandLabel: 'Build' - }) - expect(mockState.queueTabStartupCommand).toHaveBeenCalledWith('tab-new', { - command: 'cd packages; bun run build; cd ..' + quickCommandLabel: 'Build', + pendingStartup: { command: 'cd packages; bun run build; cd ..' } }) expect(mockState.setRecentQuickCommandForGroup).toHaveBeenCalledWith('group-1', 'build') }) @@ -139,8 +135,9 @@ describe('runQuickCommandInNewTab', () => { groupId: 'group-1' }) - expect(mockState.queueTabStartupCommand).toHaveBeenCalledWith('tab-new', { - command: 'git status' + expect(mockState.createTab).toHaveBeenCalledWith('wt-1', 'group-1', undefined, { + quickCommandLabel: 'Status', + pendingStartup: { command: 'git status' } }) }) @@ -174,7 +171,7 @@ describe('runQuickCommandInNewTab', () => { launchSource: 'quick_command', quickCommandLabel: 'Review' }) - expect(mockState.queueTabStartupCommand).not.toHaveBeenCalled() + expect(mockState.createTab).not.toHaveBeenCalled() expect(mockState.setRecentQuickCommandForGroup).toHaveBeenCalledWith('group-1', 'agent-review') }) @@ -330,6 +327,6 @@ describe('runQuickCommandInNewTab', () => { expect(result).toBeNull() expect(mocks.launchAgentInNewTab).not.toHaveBeenCalled() - expect(mockState.queueTabStartupCommand).not.toHaveBeenCalled() + expect(mockState.createTab).not.toHaveBeenCalled() }) }) diff --git a/src/renderer/src/lib/run-quick-command-in-new-tab.ts b/src/renderer/src/lib/run-quick-command-in-new-tab.ts index 1e20fd0c142..7d730a0f4a6 100644 --- a/src/renderer/src/lib/run-quick-command-in-new-tab.ts +++ b/src/renderer/src/lib/run-quick-command-in-new-tab.ts @@ -103,11 +103,8 @@ export function runQuickCommandInNewTab({ } const store = useAppStore.getState() const tab = store.createTab(worktreeId, targetGroupId, undefined, { - quickCommandLabel: command.label - }) - - store.queueTabStartupCommand(tab.id, { - command: flattenTerminalQuickCommand(command).command + quickCommandLabel: command.label, + pendingStartup: { command: flattenTerminalQuickCommand(command).command } }) // Why: match `+` button's createNewTerminalTab — without this, a worktree diff --git a/src/renderer/src/lib/worktree-activation-agent-startup.test.ts b/src/renderer/src/lib/worktree-activation-agent-startup.test.ts index ca6595d0f43..47fa6113d18 100644 --- a/src/renderer/src/lib/worktree-activation-agent-startup.test.ts +++ b/src/renderer/src/lib/worktree-activation-agent-startup.test.ts @@ -2,7 +2,8 @@ import { describe, expect, it, vi } from 'vitest' import { ensureWorktreeHasInitialTerminal } from './worktree-initial-terminal-seeding' import { createMockStore, - registerWorktreeActivationReset + registerWorktreeActivationReset, + createdTabStartups } from './worktree-activation-test-harness' registerWorktreeActivationReset() @@ -20,10 +21,11 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true }) expect(store.setActiveTab).toHaveBeenCalledWith('tab-1') - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'claude "Fix this bug"' }) expect(store.queueTabSetupSplit).not.toHaveBeenCalled() @@ -49,10 +51,11 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true, launchAgent: 'claude' }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'claude', launchAgent: 'claude' }) @@ -82,6 +85,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true, launchAgent: 'claude', ...expectedViewMode @@ -114,6 +118,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true, launchAgent: 'claude', ...expectedViewMode @@ -141,6 +146,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(createTab).toHaveBeenNthCalledWith(1, 'wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true, recordInteraction: false, launchAgent: 'claude' @@ -173,6 +179,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(createTab).toHaveBeenNthCalledWith(1, 'wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true, recordInteraction: false, launchAgent: 'claude', @@ -200,10 +207,11 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true, launchAgent: 'claude' }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'claude', telemetry: { agent_kind: 'claude-code', @@ -228,10 +236,11 @@ describe('ensureWorktreeHasInitialTerminal', () => { ) expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { + pendingStartup: expect.anything(), pendingActivationSpawn: true, launchAgent: 'codex' }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'codex', launchAgent: 'codex' }) diff --git a/src/renderer/src/lib/worktree-activation-default-tabs.test.ts b/src/renderer/src/lib/worktree-activation-default-tabs.test.ts index a6e5e5136a8..e6651635319 100644 --- a/src/renderer/src/lib/worktree-activation-default-tabs.test.ts +++ b/src/renderer/src/lib/worktree-activation-default-tabs.test.ts @@ -2,7 +2,8 @@ import { describe, expect, it, vi } from 'vitest' import { ensureWorktreeHasInitialTerminal } from './worktree-initial-terminal-seeding' import { createMockStore, - registerWorktreeActivationReset + registerWorktreeActivationReset, + createdTabStartups } from './worktree-activation-test-harness' registerWorktreeActivationReset() @@ -17,7 +18,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { pendingActivationSpawn: true }) expect(store.setActiveTab).toHaveBeenCalledWith('tab-1') - expect(store.queueTabStartupCommand).not.toHaveBeenCalled() + expect(createdTabStartups(store)).toEqual({}) expect(store.queueTabSetupSplit).not.toHaveBeenCalled() }) @@ -39,9 +40,10 @@ describe('ensureWorktreeHasInitialTerminal', () => { ensureWorktreeHasInitialTerminal(store, 'wt-1', { command: 'claude "Fix this bug"' }) expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { - pendingActivationSpawn: true + pendingActivationSpawn: true, + pendingStartup: expect.anything() }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'claude "Fix this bug"' }) }) @@ -71,7 +73,8 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(createTab).toHaveBeenCalledTimes(2) expect(createTab).toHaveBeenNthCalledWith(1, 'wt-1', undefined, undefined, { pendingActivationSpawn: true, - recordInteraction: false + recordInteraction: false, + pendingStartup: expect.anything() }) expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-1', 'Claude', { recordInteraction: false @@ -81,8 +84,8 @@ describe('ensureWorktreeHasInitialTerminal', () => { }) expect(store.setTabColor).toHaveBeenCalledWith('tab-1', '#f97316') expect(store.setTabColor).toHaveBeenCalledWith('tab-2', '#9ca3af') - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { command: 'claude' }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { command: 'pnpm dev' }) + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'claude' }) + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'pnpm dev' }) expect(store.setActiveTab).toHaveBeenLastCalledWith('tab-1') }) @@ -111,18 +114,20 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(createTab).toHaveBeenNthCalledWith(1, 'wt-1', undefined, undefined, { pendingActivationSpawn: true, recordInteraction: false, - activate: false + activate: false, + pendingStartup: expect.anything() }) expect(createTab).toHaveBeenNthCalledWith(2, 'wt-1', undefined, undefined, { recordInteraction: false, - activate: false + activate: false, + pendingStartup: expect.anything() }) expect(store.setActiveTab).not.toHaveBeenCalled() expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-2', 'Setup', { recordInteraction: false }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { command: 'pnpm dev' }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'pnpm dev' }) + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'bash /tmp/repo/.git/orca/setup-runner.sh', env: { ORCA_WORKTREE_PATH: '/tmp/worktrees/wt-1' } }) @@ -136,7 +141,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { tabs: [{ title: 'Server', command: 'pnpm dev' }] }) - expect(store.queueTabStartupCommand).not.toHaveBeenCalled() + expect(createdTabStartups(store)).toEqual({}) expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-1', 'Server', { recordInteraction: false }) @@ -159,7 +164,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.setTabCustomTitle).not.toHaveBeenCalledWith('tab-1', 'Claude', { recordInteraction: false }) - expect(store.queueTabStartupCommand).not.toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).not.toEqual({ command: 'claude' }) }) @@ -176,7 +181,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.createTab).not.toHaveBeenCalled() expect(store.setActiveTab).not.toHaveBeenCalled() - expect(store.queueTabStartupCommand).not.toHaveBeenCalled() + expect(createdTabStartups(store)).toEqual({}) expect(store.queueTabSetupSplit).not.toHaveBeenCalled() expect(store.queueTabIssueCommandSplit).not.toHaveBeenCalled() }) diff --git a/src/renderer/src/lib/worktree-activation-issue-command.test.ts b/src/renderer/src/lib/worktree-activation-issue-command.test.ts index b84e7569238..07c3337910e 100644 --- a/src/renderer/src/lib/worktree-activation-issue-command.test.ts +++ b/src/renderer/src/lib/worktree-activation-issue-command.test.ts @@ -3,7 +3,8 @@ import { ensureWorktreeHasInitialTerminal } from './worktree-initial-terminal-se import { createMockStore, registerWorktreeActivationReset, - setSetupScriptLaunchMode + setSetupScriptLaunchMode, + createdTabStartups } from './worktree-activation-test-harness' registerWorktreeActivationReset() @@ -67,7 +68,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { } ) - expect(store.queueTabStartupCommand).not.toHaveBeenCalled() + expect(createdTabStartups(store)).toEqual({}) expect(store.queueTabSetupSplit).toHaveBeenCalledWith('tab-1', { command: 'bash /tmp/repo/.git/orca/setup-runner.sh', env: { ORCA_ROOT_PATH: '/tmp/repo' }, @@ -84,7 +85,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { ensureWorktreeHasInitialTerminal(store, 'wt-1') - expect(store.queueTabStartupCommand).not.toHaveBeenCalled() + expect(createdTabStartups(store)).toEqual({}) expect(store.queueTabIssueCommandSplit).not.toHaveBeenCalled() }) }) diff --git a/src/renderer/src/lib/worktree-activation-reveal.test.ts b/src/renderer/src/lib/worktree-activation-reveal.test.ts index 64818c32a6b..f536936bf12 100644 --- a/src/renderer/src/lib/worktree-activation-reveal.test.ts +++ b/src/renderer/src/lib/worktree-activation-reveal.test.ts @@ -64,7 +64,6 @@ describe('activateAndRevealWorktree', () => { setTabCustomTitle: vi.fn(), setTabColor: vi.fn(), markDefaultTerminalTabsApplied: vi.fn(), - queueTabStartupCommand: vi.fn(), queueTabInitialCwd, queueTabSetupSplit: vi.fn(), queueTabIssueCommandSplit: vi.fn(), @@ -134,7 +133,6 @@ describe('activateAndRevealWorktree', () => { setTabCustomTitle: vi.fn(), setTabColor: vi.fn(), markDefaultTerminalTabsApplied: vi.fn(), - queueTabStartupCommand: vi.fn(), queueTabInitialCwd: vi.fn(), queueTabSetupSplit: vi.fn(), queueTabIssueCommandSplit: vi.fn(), diff --git a/src/renderer/src/lib/worktree-activation-setup-script.test.ts b/src/renderer/src/lib/worktree-activation-setup-script.test.ts index ff9bc0874b8..e7f9491d5c1 100644 --- a/src/renderer/src/lib/worktree-activation-setup-script.test.ts +++ b/src/renderer/src/lib/worktree-activation-setup-script.test.ts @@ -4,7 +4,8 @@ import { ensureWorktreeHasInitialTerminal } from './worktree-initial-terminal-se import { createMockStore, registerWorktreeActivationReset, - setSetupScriptLaunchMode + setSetupScriptLaunchMode, + createdTabStartups } from './worktree-activation-test-harness' registerWorktreeActivationReset() @@ -29,7 +30,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-2', 'Setup', { recordInteraction: false }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'bash /tmp/repo/.git/orca/setup-runner.sh', env: { ORCA_ROOT_PATH: '/tmp/repo', @@ -53,7 +54,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { } }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'bash /c/repo/.git/orca/setup-runner.sh', env: { ORCA_ROOT_PATH: 'C:\\repo', @@ -83,7 +84,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-2', 'Setup', { recordInteraction: false }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'bash -lc wrapped-setup', env: { ORCA_ROOT_PATH: '/tmp/repo' } }) @@ -112,14 +113,12 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(result).toBe('tab-1') expect(createTab).toHaveBeenCalledTimes(1) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-2', + expect(createdTabStartups(store)['tab-2']).toEqual( expect.objectContaining({ command: expect.stringContaining('bash /tmp/repo/.git/orca/setup-runner.sh') }) ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-2', + expect(createdTabStartups(store)['tab-2']).toEqual( expect.objectContaining({ command: expect.stringContaining('printf') }) @@ -141,7 +140,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { } }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'bash /mnt/c/repo/.git/orca/setup-runner.sh', env: { ORCA_ROOT_PATH: 'C:\\repo', @@ -167,8 +166,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { } ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(createdTabStartups(store)['tab-1']).toEqual( expect.objectContaining({ env: expect.objectContaining({ [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: expect.stringContaining( @@ -177,22 +175,19 @@ describe('ensureWorktreeHasInitialTerminal', () => { }) }) ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(createdTabStartups(store)['tab-1']).toEqual( expect.objectContaining({ env: expect.objectContaining({ [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: expect.stringContaining('exec claude') }) }) ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-2', + expect(createdTabStartups(store)['tab-2']).toEqual( expect.objectContaining({ command: expect.stringContaining('printf') }) ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-2', + expect(createdTabStartups(store)['tab-2']).toEqual( expect.objectContaining({ command: expect.stringContaining('bash /tmp/repo/.git/orca/setup-runner.sh') }) @@ -216,10 +211,10 @@ describe('ensureWorktreeHasInitialTerminal', () => { } ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'claude' }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'bash /tmp/repo/.git/orca/setup-runner.sh', env: { ORCA_ROOT_PATH: '/tmp/repo' } }) @@ -240,8 +235,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { } ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(createdTabStartups(store)['tab-1']).toEqual( expect.objectContaining({ env: expect.objectContaining({ [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: expect.stringContaining('exec claude') @@ -276,8 +270,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { } ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-1', + expect(createdTabStartups(store)['tab-1']).toEqual( expect.objectContaining({ env: expect.objectContaining({ [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: expect.stringContaining( @@ -344,7 +337,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-2', 'Setup', { recordInteraction: false }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-2', { + expect(createdTabStartups(store)['tab-2']).toEqual({ command: 'bash /tmp/repo/.git/orca/setup-runner.sh', env: { ORCA_ROOT_PATH: '/tmp/repo' } }) diff --git a/src/renderer/src/lib/worktree-activation-store-contract.ts b/src/renderer/src/lib/worktree-activation-store-contract.ts index 32a3f622c6c..d26c131d672 100644 --- a/src/renderer/src/lib/worktree-activation-store-contract.ts +++ b/src/renderer/src/lib/worktree-activation-store-contract.ts @@ -24,6 +24,18 @@ export type WorktreeActivationStore = Partial & { recordInteraction?: boolean viewMode?: Tab['viewMode'] activate?: boolean + pendingStartup?: { + command: string + env?: Record + launchConfig?: SleepingAgentLaunchConfig + resumeProviderSession?: AgentProviderSessionMetadata + launchToken?: string + launchAgent?: TuiAgent + draftPrompt?: string + initialAgentStatus?: { agent: TuiAgent; prompt: string } + showSessionRestoredBanner?: boolean + telemetry?: AgentStartedTelemetry + } } ) => { id: string } setActiveTab: (tabId: string) => void @@ -35,21 +47,6 @@ export type WorktreeActivationStore = Partial & { setTabColor: (tabId: string, color: string | null) => void markDefaultTerminalTabsApplied: (worktreeId: string) => void reconcileWorktreeTabModel: (worktreeId: string) => { renderableTabCount: number } - queueTabStartupCommand: ( - tabId: string, - startup: { - command: string - env?: Record - launchConfig?: SleepingAgentLaunchConfig - resumeProviderSession?: AgentProviderSessionMetadata - launchToken?: string - launchAgent?: TuiAgent - draftPrompt?: string - initialAgentStatus?: { agent: TuiAgent; prompt: string } - showSessionRestoredBanner?: boolean - telemetry?: AgentStartedTelemetry - } - ) => void queueTabSetupSplit: ( tabId: string, startup: { command: string; env?: Record; direction: SetupSplitDirection } diff --git a/src/renderer/src/lib/worktree-activation-structured-chat-surface.test.ts b/src/renderer/src/lib/worktree-activation-structured-chat-surface.test.ts index 33be3888a24..3b855050973 100644 --- a/src/renderer/src/lib/worktree-activation-structured-chat-surface.test.ts +++ b/src/renderer/src/lib/worktree-activation-structured-chat-surface.test.ts @@ -9,7 +9,8 @@ import { import { createMockStore, registerWorktreeActivationReset, - setSetupScriptLaunchMode + setSetupScriptLaunchMode, + createdTabStartups } from './worktree-activation-test-harness' const initialAppStoreState = useAppStore.getState() @@ -49,7 +50,7 @@ describe('seeding beside a caller-provided chat surface', () => { expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-1', 'Setup', { recordInteraction: false }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith('tab-1', { + expect(createdTabStartups(store)['tab-1']).toEqual({ command: 'bash /tmp/repo/.git/orca/setup-runner.sh', env: setup.envVars }) diff --git a/src/renderer/src/lib/worktree-activation-test-harness.ts b/src/renderer/src/lib/worktree-activation-test-harness.ts index 29a2cbd3227..f7e6958dcc1 100644 --- a/src/renderer/src/lib/worktree-activation-test-harness.ts +++ b/src/renderer/src/lib/worktree-activation-test-harness.ts @@ -46,13 +46,12 @@ export function registerWorktreeActivationReset(): void { export type MockActivationStore = { tabsByWorktree: Record defaultTerminalTabsAppliedByWorktreeId: Record - createTab: Mock<() => { id: string }> + createTab: Mock<(...args: unknown[]) => { id: string }> setActiveTab: Mock<() => void> setTabCustomTitle: Mock<() => void> setTabColor: Mock<() => void> markDefaultTerminalTabsApplied: Mock<() => void> reconcileWorktreeTabModel: Mock<() => { renderableTabCount: number }> - queueTabStartupCommand: Mock<() => void> queueTabInitialCwd: Mock<() => void> queueTabSetupSplit: Mock<() => void> queueTabIssueCommandSplit: Mock<() => void> @@ -68,10 +67,27 @@ export function createMockStore(overrides: Record = {}): MockAc setTabColor: vi.fn(), markDefaultTerminalTabsApplied: vi.fn(), reconcileWorktreeTabModel: vi.fn(() => ({ renderableTabCount: 0 })), - queueTabStartupCommand: vi.fn(), queueTabInitialCwd: vi.fn(), queueTabSetupSplit: vi.fn(), queueTabIssueCommandSplit: vi.fn(), ...overrides } } + +/** The startup command each created tab was published with, keyed by the created tab's id. */ +export function createdTabStartups(store: MockActivationStore): Record { + const startups: Record = {} + store.createTab.mock.calls.forEach((call, index) => { + const result = store.createTab.mock.results[index] + const options = call[3] + if ( + result?.type === 'return' && + options && + typeof options === 'object' && + 'pendingStartup' in options + ) { + startups[result.value.id] = options.pendingStartup + } + }) + return startups +} 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 786fedcd37a..c361bf4a09e 100644 --- a/src/renderer/src/lib/worktree-activation-web-runtime.test.ts +++ b/src/renderer/src/lib/worktree-activation-web-runtime.test.ts @@ -4,7 +4,8 @@ import { ensureWorktreeHasInitialTerminal } from './worktree-initial-terminal-se import type { AppStoreState } from './worktree-activation-test-harness' import { createMockStore, - registerWorktreeActivationReset + registerWorktreeActivationReset, + createdTabStartups } from './worktree-activation-test-harness' import { useAppStore } from '@/store' import { @@ -299,14 +300,12 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-2', 'Setup', { recordInteraction: false }) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-2', + expect(createdTabStartups(store)['tab-2']).toEqual( expect.objectContaining({ command: expect.stringContaining('bash /tmp/repo/.git/orca/setup-runner.sh') }) ) - expect(store.queueTabStartupCommand).toHaveBeenCalledWith( - 'tab-2', + expect(createdTabStartups(store)['tab-2']).toEqual( expect.objectContaining({ command: expect.stringContaining('printf') }) diff --git a/src/renderer/src/lib/worktree-default-terminal-tabs.ts b/src/renderer/src/lib/worktree-default-terminal-tabs.ts index 7104318ff01..d4fbde6ef2b 100644 --- a/src/renderer/src/lib/worktree-default-terminal-tabs.ts +++ b/src/renderer/src/lib/worktree-default-terminal-tabs.ts @@ -41,25 +41,31 @@ export function applyDefaultTerminalTabs( : isStartupTab && startup?.telemetry ? (agentKindToTuiAgent(startup.telemetry.agent_kind) ?? undefined) : undefined + const templateCommand = template.command?.trim() + const pendingStartup = isStartupTab + ? startup + : templateCommand && defaultTabs.runCommands + ? { command: templateCommand } + : undefined const tab = store.createTab(worktreeId, undefined, undefined, { pendingActivationSpawn: true, recordInteraction: false, ...(launchAgent ? { launchAgent } : {}), - ...(opts?.activateCreatedTabs === false ? { activate: false } : {}) + ...(opts?.activateCreatedTabs === false ? { activate: false } : {}), + ...(pendingStartup ? { pendingStartup } : {}) }) if (index === 0) { firstTabId = tab.id } + if (isStartupTab && launchAgent) { + seedNativeChatAppliedSessionOptions(tab.id, launchAgent, startup.sessionOptions) + } if (template.title) { store.setTabCustomTitle(tab.id, template.title, { recordInteraction: false }) } if (template.color) { store.setTabColor(tab.id, template.color) } - const templateCommand = template.command?.trim() - if (templateCommand && defaultTabs.runCommands && !(index === 0 && startup)) { - store.queueTabStartupCommand(tab.id, { command: templateCommand }) - } } if (!firstTabId) { @@ -68,17 +74,6 @@ export function applyDefaultTerminalTabs( if (opts?.activateCreatedTabs !== false) { store.setActiveTab(firstTabId) } - if (startup) { - const startupAgent = - startup.launchAgent ?? - (startup.telemetry - ? (agentKindToTuiAgent(startup.telemetry.agent_kind) ?? undefined) - : undefined) - if (startupAgent) { - seedNativeChatAppliedSessionOptions(firstTabId, startupAgent, startup.sessionOptions) - } - store.queueTabStartupCommand(firstTabId, startup) - } queueSetupAndIssueCommands( store, worktreeId, diff --git a/src/renderer/src/lib/worktree-initial-terminal-seeding.ts b/src/renderer/src/lib/worktree-initial-terminal-seeding.ts index cfd57ac7c98..4acaed2784b 100644 --- a/src/renderer/src/lib/worktree-initial-terminal-seeding.ts +++ b/src/renderer/src/lib/worktree-initial-terminal-seeding.ts @@ -281,22 +281,19 @@ export function ensureWorktreeHasInitialTerminal( const terminalTab = store.createTab(worktreeId, undefined, undefined, { pendingActivationSpawn: true, ...(launchAgent ? { launchAgent } : {}), - ...(opts?.activateCreatedTabs === false ? { activate: false } : {}) + ...(opts?.activateCreatedTabs === false ? { activate: false } : {}), + // Why: the initial pane begins in the requested agent session instead of an idle shell. + ...(sequencedStartup ? { pendingStartup: sequencedStartup } : {}) }) if (opts?.activateCreatedTabs !== false) { store.setActiveTab(terminalTab.id) } - - // Why: queue the seeded startup on the initial pane so the terminal begins in the requested agent session instead of an idle shell. - if (sequencedStartup) { - if (launchAgent) { - seedNativeChatAppliedSessionOptions( - terminalTab.id, - launchAgent, - sequencedStartup.sessionOptions - ) - } - store.queueTabStartupCommand(terminalTab.id, sequencedStartup) + if (sequencedStartup && launchAgent) { + seedNativeChatAppliedSessionOptions( + terminalTab.id, + launchAgent, + sequencedStartup.sessionOptions + ) } queueSetupAndIssueCommands( store, diff --git a/src/renderer/src/lib/worktree-setup-issue-command-queue.ts b/src/renderer/src/lib/worktree-setup-issue-command-queue.ts index 3a66305fb67..b98f4961654 100644 --- a/src/renderer/src/lib/worktree-setup-issue-command-queue.ts +++ b/src/renderer/src/lib/worktree-setup-issue-command-queue.ts @@ -34,7 +34,8 @@ export function queueSetupAndIssueCommands( if (mode === 'new-tab') { const setupTab = store.createTab(worktreeId, undefined, undefined, { recordInteraction: false, - ...(opts?.activateCreatedTabs === false ? { activate: false } : {}) + ...(opts?.activateCreatedTabs === false ? { activate: false } : {}), + pendingStartup: setupCommand }) // Why: createTab auto-activates the new tab; revert so focus stays on the primary terminal while Setup runs in the background. if (opts?.activateCreatedTabs !== false && terminalTabId) { @@ -42,7 +43,6 @@ export function queueSetupAndIssueCommands( } // Why: customTitle overrides the auto "Terminal N" label everywhere the tab renders, so it's the authoritative label source. store.setTabCustomTitle(setupTab.id, 'Setup', { recordInteraction: false }) - store.queueTabStartupCommand(setupTab.id, setupCommand) } else if (terminalTabId) { store.queueTabSetupSplit(terminalTabId, { ...setupCommand,