diff --git a/config/focus-setting-read-baseline.txt b/config/focus-setting-read-baseline.txt index 20f9e85dc52..5c1bc52ff16 100644 --- a/config/focus-setting-read-baseline.txt +++ b/config/focus-setting-read-baseline.txt @@ -333,7 +333,6 @@ 2 src/renderer/src/lib/launch-agent-web-host-tab.ts 2 src/renderer/src/lib/launch-ai-vault-session.ts 1 src/renderer/src/lib/launch-structured-agent-session.ts -1 src/renderer/src/lib/launch-worktree-background-terminals.ts 1 src/renderer/src/lib/linear-issue-url-lookup.ts 1 src/renderer/src/lib/local-preflight-context.ts 3 src/renderer/src/lib/open-agent-message-sender.ts diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index 38ddcfe1572..6ea0701fac8 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -9384,7 +9384,6 @@ "src/renderer/src/lib/launch-work-item-direct-agent.test.ts": 163, "src/renderer/src/lib/launch-work-item-direct-messages.test.ts": 48, "src/renderer/src/lib/launch-work-item-direct.test.ts": 1033, - "src/renderer/src/lib/launch-worktree-background-terminals.test.ts": 848, "src/renderer/src/lib/lazy-chunk-recovery-reload.test.ts": 200, "src/renderer/src/lib/lazy-with-retry.never-landed-reload.test.ts": 172, "src/renderer/src/lib/lazy-with-retry.test.ts": 232, diff --git a/src/main/ipc/worktree-remote.ts b/src/main/ipc/worktree-remote.ts index db38fec58da..a6cf75150b4 100644 --- a/src/main/ipc/worktree-remote.ts +++ b/src/main/ipc/worktree-remote.ts @@ -306,7 +306,7 @@ export function recordWorkspaceLineageForCreatedWorktree( } async function spawnLocalStartupAndSetupTerminals(args: { - runtime: OrcaRuntimeService | undefined + runtime: OrcaRuntimeService worktree: Worktree startup: CreateWorktreeArgs['startup'] setup: CreateWorktreeResult['setup'] @@ -315,7 +315,7 @@ async function spawnLocalStartupAndSetupTerminals(args: { createdWithAgent: CreateWorktreeArgs['createdWithAgent'] }): Promise { const { runtime, worktree, startup, setup, defaultTabs, settings, createdWithAgent } = args - if (!runtime || !startup || defaultTabs?.tabs.length) { + if (!startup || defaultTabs?.tabs.length) { return { didSpawnSetup: false } } @@ -953,7 +953,7 @@ export function createLocalWorktree( repo: Repo, store: Store, mainWindow: BrowserWindow, - runtime?: OrcaRuntimeService, + runtime: OrcaRuntimeService, timing: WorktreeCreateTimingRecorder = createWorktreeCreateTimingRecorder() ): Promise { // Why a holder fired in `finally`: consuming a prepared checkout leaves the pool one short, so a @@ -976,7 +976,7 @@ async function performLocalWorktreeCreate( mainWindow: BrowserWindow, rearm: PreparationRearmHolder, timing: WorktreeCreateTimingRecorder, - runtime?: OrcaRuntimeService + runtime: OrcaRuntimeService ): Promise { const settings = store.getSettings() const worktreePathSettings = getWorktreePathSettings( @@ -1691,6 +1691,24 @@ async function performLocalWorktreeCreate( }) ) + // Why: an automation's terminals start in main, so the window is handed none to launch. + const provisionedByHost = Boolean(args.automationProvenance) + if (provisionedByHost && (setup || defaultTabs?.tabs.length)) { + void runtime.provisionManagedWorktreeTerminals( + { + worktreeSelector: `id:${worktree.id}`, + worktreeId: worktree.id, + worktreePath: worktree.path, + ...(setup ? { setup } : {}), + ...(defaultTabs ? { defaultTabs } : {}), + hasStartupTerminal: false, + setupCommandPlatform: getSetupRunnerCommandPlatformForLaunch(setup, 'posix'), + surfaceOwner: false + }, + worktree + ) + } + notifyWorktreesChanged(mainWindow, repo.id) return { worktree: { @@ -1702,12 +1720,16 @@ async function performLocalWorktreeCreate( }, ...(worktreeLineage ? { lineage: worktreeLineage } : {}), ...(workspaceLineage ? { workspaceLineage } : {}), - ...(stagedStartup.activationSetup - ? { setup: stagedStartup.activationSetup } - : setup && !stagedStartup.didSpawnSetup - ? { setup } - : {}), - ...(defaultTabs ? { defaultTabs } : {}), + ...(provisionedByHost + ? {} + : { + ...(stagedStartup.activationSetup + ? { setup: stagedStartup.activationSetup } + : setup && !stagedStartup.didSpawnSetup + ? { setup } + : {}), + ...(defaultTabs ? { defaultTabs } : {}) + }), ...(addResult.localBaseRefRefresh ? { localBaseRefRefresh: addResult.localBaseRefRefresh } : {}), diff --git a/src/main/ipc/worktrees-automation-create-terminals.test.ts b/src/main/ipc/worktrees-automation-create-terminals.test.ts new file mode 100644 index 00000000000..56076452f07 --- /dev/null +++ b/src/main/ipc/worktrees-automation-create-terminals.test.ts @@ -0,0 +1,180 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + listWorktreesMock, + getEffectiveHooksMock, + getDefaultTabsLaunchMock, + createSetupRunnerScriptMock, + shouldRunSetupForCreateMock +} from './worktrees-test-module-mocks' +import { handlers, setupWorktreeHandlers } from './worktrees-test-harness' +import { createdWorktreeList } from './worktrees-test-fixtures' + +vi.mock('electron', async () => + (await import('./worktrees-test-module-mocks')).electronModuleMock() +) +vi.mock('../git/worktree', async () => + (await import('./worktrees-test-module-mocks')).gitWorktreeModuleMock() +) +vi.mock('../git/runner', async () => + (await import('./worktrees-test-module-mocks')).gitRunnerModuleMock() +) +vi.mock('../git/repo', async () => + (await import('./worktrees-test-module-mocks')).gitRepoModuleMock() +) +vi.mock('../git/git-username', async (importOriginal) => ({ + ...(await importOriginal>()), + resolveLocalGitUsername: (await import('./worktrees-test-module-mocks')) + .resolveLocalGitUsernameMock +})) +vi.mock('../github/client', async () => + (await import('./worktrees-test-module-mocks')).githubClientModuleMock() +) +vi.mock('../source-control/hosted-review', async () => + (await import('./worktrees-test-module-mocks')).hostedReviewModuleMock() +) +vi.mock('../providers/ssh-filesystem-dispatch', async () => + (await import('./worktrees-test-module-mocks')).sshFilesystemDispatchModuleMock() +) +vi.mock('./worktree-symlinks', async () => + (await import('./worktrees-test-module-mocks')).worktreeSymlinksModuleMock() +) +vi.mock('../hooks', async () => (await import('./worktrees-test-module-mocks')).hooksModuleMock()) +vi.mock('../setup-runner-script-text', async (importOriginal) => + (await import('./worktrees-test-module-mocks')).setupRunnerScriptTextModuleMock( + await importOriginal>() + ) +) +vi.mock('../worktree-runner-script', async (importOriginal) => + (await import('./worktrees-test-module-mocks')).worktreeRunnerScriptModuleMock( + await importOriginal>() + ) +) +vi.mock('../effective-hook-config', async (importOriginal) => + (await import('./worktrees-test-module-mocks')).effectiveHookConfigModuleMock( + await importOriginal>() + ) +) +vi.mock('../setup-hook-env-vars', async (importOriginal) => + (await import('./worktrees-test-module-mocks')).setupHookEnvVarsModuleMock( + await importOriginal>() + ) +) +vi.mock('./worktree-logic', async (importOriginal) => + (await import('./worktrees-test-module-mocks')).worktreeLogicModuleMock( + await importOriginal>() + ) +) +vi.mock('../terminal-history-deletion', async () => + (await import('./worktrees-test-module-mocks')).terminalHistoryDeletionModuleMock() +) +vi.mock('../ports/advertised-url-watcher', async () => + (await import('./worktrees-test-module-mocks')).advertisedUrlWatcherModuleMock() +) +vi.mock('../workspace-cleanup-scan-snapshot', async () => + (await import('./worktrees-test-module-mocks')).workspaceCleanupScanSnapshotModuleMock() +) +vi.mock('../workspace-space-analysis-snapshot', async () => + (await import('./worktrees-test-module-mocks')).workspaceSpaceAnalysisSnapshotModuleMock() +) +vi.mock('../workspace-cleanup-removal-snapshot-prune', async () => + (await import('./worktrees-test-module-mocks')).workspaceCleanupRemovalSnapshotPruneModuleMock() +) +vi.mock('../runtime/worktree-teardown', async () => + (await import('./worktrees-test-module-mocks')).worktreeTeardownModuleMock() +) +vi.mock('./pty/provider/registry', async () => + (await import('./worktrees-test-module-mocks')).ptyRegistryModuleMock() +) +vi.mock('./pty/provider/state-cleanup', async () => + (await import('./worktrees-test-module-mocks')).ptyStateCleanupModuleMock() +) + +vi.mock('../automations/workspace-provenance', async (importOriginal) => ({ + ...(await importOriginal>()), + resolveAutomationWorkspaceProvenance: ({ request }: { request?: unknown }) => + request ? { kind: 'created-by-automation', automationId: 'automation-1' } : undefined, + finishAutomationWorkspaceProvenanceRequest: () => {}, + releaseAutomationWorkspaceProvenanceRequest: () => {} +})) + +const automationProvenanceRequest = { + automationId: 'automation-1', + automationRunId: 'run-1', + dispatchToken: 'token-1', + createRequestId: 'create-1' +} +const defaultTabs = { tabs: [{ title: 'Dev', command: 'pnpm dev' }], runCommands: true } + +describe('automation creates: main provisions setup and default tabs', () => { + let provision: ReturnType + + beforeEach(() => { + const runtime = setupWorktreeHandlers() + provision = vi.fn().mockResolvedValue({ setupSpawned: true, setupTerminalHandle: 'term-setup' }) + Object.assign(runtime, { provisionManagedWorktreeTerminals: provision }) + listWorktreesMock.mockResolvedValue(createdWorktreeList) + getEffectiveHooksMock.mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } }) + shouldRunSetupForCreateMock.mockReturnValue(true) + getDefaultTabsLaunchMock.mockReturnValue(defaultTabs) + }) + + it('provisions once in main and hands the window nothing to launch', async () => { + const result = await handlers['worktrees:create'](null, { + repoId: 'repo-1', + name: 'improve-dashboard', + setupDecision: 'run', + automationProvenanceRequest + }) + + expect(result).not.toHaveProperty('setup') + expect(result).not.toHaveProperty('defaultTabs') + expect(provision).toHaveBeenCalledOnce() + expect(provision).toHaveBeenCalledWith( + expect.objectContaining({ + worktreePath: '/workspace/improve-dashboard', + setup: expect.objectContaining({ + runnerScriptPath: '/workspace/repo/.git/orca/setup-runner.sh' + }), + defaultTabs, + hasStartupTerminal: false, + setupCommandPlatform: 'posix', + surfaceOwner: false + }), + expect.objectContaining({ path: '/workspace/improve-dashboard' }) + ) + }) + + // Same pick the window made: the runner's path decides, a bare path falls back to posix. + it.each([ + { runnerScriptPath: 'C:\\repo\\.git\\orca\\setup-runner.cmd', platform: 'windows' }, + { runnerScriptPath: '/home/me/repo/.git/orca/setup-runner.sh', platform: 'posix' } + ])( + 'runs a $platform runner with the $platform command', + async ({ runnerScriptPath, platform }) => { + createSetupRunnerScriptMock.mockReturnValueOnce({ runnerScriptPath, envVars: {} }) + + await handlers['worktrees:create'](null, { + repoId: 'repo-1', + name: 'improve-dashboard', + setupDecision: 'run', + automationProvenanceRequest + }) + + expect(provision).toHaveBeenCalledWith( + expect.objectContaining({ setupCommandPlatform: platform }), + expect.anything() + ) + } + ) + + it('leaves a create from the app window to the window', async () => { + const result = await handlers['worktrees:create'](null, { + repoId: 'repo-1', + name: 'improve-dashboard', + setupDecision: 'run' + }) + + expect(result).toMatchObject({ setup: expect.anything(), defaultTabs }) + expect(provision).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/runtime/orca-runtime-activate-managed-worktree.ts b/src/main/runtime/orca-runtime-activate-managed-worktree.ts index 193f410cc2f..4fe3601c9ce 100644 --- a/src/main/runtime/orca-runtime-activate-managed-worktree.ts +++ b/src/main/runtime/orca-runtime-activate-managed-worktree.ts @@ -312,7 +312,7 @@ export class OrcaRuntimeWithActivateManagedWorktree extends OrcaRuntimeWithListM return deliverWorktreeStartupFollowup(this.getWorktreeStartupReadinessHost(), handle, followup) } - protected async provisionManagedWorktreeTerminals( + async provisionManagedWorktreeTerminals( args: { worktreeSelector: string worktreeId: string diff --git a/src/renderer/src/hooks/automation-dispatch-workspace.ts b/src/renderer/src/hooks/automation-dispatch-workspace.ts index ea2be2708de..191b5529ecb 100644 --- a/src/renderer/src/hooks/automation-dispatch-workspace.ts +++ b/src/renderer/src/hooks/automation-dispatch-workspace.ts @@ -1,4 +1,3 @@ -import { launchWorktreeBackgroundTerminals } from '@/lib/launch-worktree-background-terminals' import { useAppStore } from '@/store' import { translate } from '@/i18n/i18n' import { createBrowserUuid } from '@/lib/browser-uuid' @@ -244,17 +243,6 @@ export async function prepareAutomationDispatchWorkspace(args: { } context.workspaceId = worktree.id context.workspaceDisplayName = worktree.displayName - if (createResult?.setup || createResult?.defaultTabs) { - void launchWorktreeBackgroundTerminals({ - worktreeId: worktree.id, - setup: createResult.setup, - defaultTabs: createResult.defaultTabs - }).catch((error) => { - // Why: setup/defaultTabs match normal worktree creation: they are - // best-effort terminal work and must not block the automation agent. - console.warn('[automations] Failed to launch workspace setup/default tabs:', error) - }) - } return worktree } diff --git a/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts b/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts index b8c1cb49c17..1094332cfda 100644 --- a/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts +++ b/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts @@ -6,7 +6,6 @@ import { countUnchangedObserverHistoryReads } from './automation-dispatch-observ const mockDispatchEvent = vi.fn() const mockLaunchAgentBackgroundSession = vi.fn() -const mockLaunchWorktreeBackgroundTerminals = vi.fn() const mockFindReusableAutomationSession = vi.fn() const mockObserveExistingAutomationSession = vi.fn() const mockSubmitPromptToAgentPty = vi.fn() @@ -25,11 +24,6 @@ const mockStoreSubscribe = vi.fn((listener: () => void) => { return () => {} }) -const setupLaunch = { - runnerScriptPath: '/tmp/setup.sh', - envVars: { ORCA_WORKTREE_PATH: '/repo/worktree' } -} - const createdWorktree = { id: 'wt-created', repoId: 'repo-1', @@ -130,10 +124,6 @@ vi.mock('@/lib/launch-agent-background-session', () => ({ launchAgentBackgroundSession: mockLaunchAgentBackgroundSession })) -vi.mock('@/lib/launch-worktree-background-terminals', () => ({ - launchWorktreeBackgroundTerminals: mockLaunchWorktreeBackgroundTerminals -})) - vi.mock('@/lib/agent-paste-draft', () => ({ submitPromptToAgentPty: mockSubmitPromptToAgentPty })) @@ -202,8 +192,7 @@ describe('useAutomationDispatchEvents setup launch', () => { latestStoreSubscriber = null state.allWorktrees.mockReturnValue([]) state.getKnownWorktreeById.mockReturnValue(undefined) - mockCreateWorktree.mockResolvedValue({ worktree: createdWorktree, setup: setupLaunch }) - mockLaunchWorktreeBackgroundTerminals.mockResolvedValue(undefined) + mockCreateWorktree.mockResolvedValue({ worktree: createdWorktree }) mockLaunchAgentBackgroundSession.mockResolvedValue({ tabId: 'agent-tab', paneKey: 'agent-tab:7c6fb4e5-3bf1-4ff4-8259-03f7ae81c40d', @@ -248,84 +237,6 @@ describe('useAutomationDispatchEvents setup launch', () => { ).toEqual([]) }) - it('starts setup terminal launch without waiting before launching the automation agent', async () => { - const order: string[] = [] - let finishSetupLaunch: (() => void) | null = null - mockLaunchWorktreeBackgroundTerminals.mockImplementation( - () => - new Promise((resolve) => { - finishSetupLaunch = () => { - order.push('setup') - resolve() - } - }) - ) - mockLaunchAgentBackgroundSession.mockImplementation(async () => { - order.push('agent') - return { tabId: 'agent-tab', ptyId: 'agent-pty', startupPlan: {} } - }) - - await registerAndDispatch() - - expect(mockCreateWorktree).toHaveBeenCalled() - expect(mockCreateWorktree.mock.calls[0][3]).toBe('run') - expect(mockLaunchWorktreeBackgroundTerminals).toHaveBeenCalledWith({ - worktreeId: 'wt-created', - setup: setupLaunch, - defaultTabs: undefined - }) - expect(state.setActiveView).not.toHaveBeenCalled() - expect(state.setActiveWorktree).not.toHaveBeenCalled() - expect(mockLaunchAgentBackgroundSession).toHaveBeenCalledWith( - expect.objectContaining({ - worktreeId: 'wt-created', - prompt: 'run this' - }) - ) - expect(order).toEqual(['agent']) - expect(finishSetupLaunch).not.toBeNull() - const completeSetupLaunch = finishSetupLaunch as unknown as () => void - completeSetupLaunch() - await Promise.resolve() - expect(order).toEqual(['agent', 'setup']) - expect(mockMarkDispatchResult).toHaveBeenCalledWith( - expect.objectContaining({ - runId: 'run-1', - status: 'dispatched', - workspaceId: 'wt-created', - terminalSessionId: 'agent-tab' - }) - ) - }) - - it('launches setup and default tabs without activating the created worktree', async () => { - const defaultTabs = { - tabs: [{ title: 'Dev', command: 'pnpm dev' }], - runCommands: true - } - mockCreateWorktree.mockResolvedValue({ - worktree: createdWorktree, - setup: setupLaunch, - defaultTabs - }) - - await registerAndDispatch() - - expect(mockLaunchWorktreeBackgroundTerminals).toHaveBeenCalledWith({ - worktreeId: 'wt-created', - setup: setupLaunch, - defaultTabs - }) - expect(state.setActiveView).not.toHaveBeenCalled() - expect(state.setActiveWorktree).not.toHaveBeenCalled() - expect(mockLaunchAgentBackgroundSession).toHaveBeenCalledWith( - expect.objectContaining({ - worktreeId: 'wt-created', - prompt: 'run this' - }) - ) - }) - it('defaults legacy automations without a setup choice to skipping setup', async () => { await registerAndDispatch(makeAutomation({ setupDecision: undefined })) @@ -346,32 +257,6 @@ describe('useAutomationDispatchEvents setup launch', () => { ) }) - it('keeps launching the agent when background setup terminal launch fails', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined) - mockLaunchWorktreeBackgroundTerminals.mockRejectedValue(new Error('tab launch failed')) - - try { - await registerAndDispatch() - } finally { - warnSpy.mockRestore() - } - - expect(mockLaunchAgentBackgroundSession).toHaveBeenCalledWith( - expect.objectContaining({ - worktreeId: 'wt-created', - prompt: 'run this' - }) - ) - expect(mockMarkDispatchResult).toHaveBeenCalledWith( - expect.objectContaining({ - runId: 'run-1', - status: 'dispatched', - workspaceId: 'wt-created', - terminalSessionId: 'agent-tab' - }) - ) - }) - it('does not rerun setup for existing-worktree automations', async () => { const existingWorktree = { id: 'wt-existing', @@ -390,7 +275,6 @@ describe('useAutomationDispatchEvents setup launch', () => { ) expect(mockCreateWorktree).not.toHaveBeenCalled() - expect(mockLaunchWorktreeBackgroundTerminals).not.toHaveBeenCalled() expect(mockLaunchAgentBackgroundSession).toHaveBeenCalledWith( expect.objectContaining({ worktreeId: 'wt-existing', diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index c2f146c796d..b7bfb99958b 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -832,13 +832,6 @@ } } }, - "worktree": { - "background": { - "terminals": { - "setupTitle": "Setup" - } - } - }, "work": { "item": { "direct": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 81c958b7b6d..abb6d07b2e2 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -649,13 +649,6 @@ } } }, - "worktree": { - "background": { - "terminals": { - "setupTitle": "Configuración" - } - } - }, "work": { "item": { "direct": { diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index ee7828feb3b..b197a01853c 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -750,13 +750,6 @@ } } }, - "worktree": { - "background": { - "terminals": { - "setupTitle": "Configuration" - } - } - }, "work": { "item": { "direct": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index c650eab84a8..5f1c17928dc 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -649,13 +649,6 @@ } } }, - "worktree": { - "background": { - "terminals": { - "setupTitle": "設定" - } - } - }, "work": { "item": { "direct": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index aeb4264177f..53aa6c58954 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -649,13 +649,6 @@ } } }, - "worktree": { - "background": { - "terminals": { - "setupTitle": "설정" - } - } - }, "work": { "item": { "direct": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 961a3ae8674..46d7ef615c1 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -649,13 +649,6 @@ } } }, - "worktree": { - "background": { - "terminals": { - "setupTitle": "设置" - } - } - }, "work": { "item": { "direct": { diff --git a/src/renderer/src/lib/background-pane-exit-output.ts b/src/renderer/src/lib/background-pane-exit-output.ts deleted file mode 100644 index 5d077313bf1..00000000000 --- a/src/renderer/src/lib/background-pane-exit-output.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - registerEagerPtyBuffer, - type EagerPtyHandle -} from '@/components/terminal-pane/pty-dispatcher' -import { useAppStore } from '@/store' - -function persistExitedPaneOutput(tabId: string, leafId: string, output: string): void { - const store = useAppStore.getState() - const layout = store.terminalLayoutsByTabId[tabId] - if (!layout) { - return - } - const { ptyIdsByLeafId: existingPtyIds, buffersByLeafId: existingBuffers, ...rest } = layout - const nextPtyIds = { ...existingPtyIds } - delete nextPtyIds[leafId] - const trimmedOutput = output.trim() ? output : '' - store.setTabLayout(tabId, { - ...rest, - ...(Object.keys(nextPtyIds).length > 0 ? { ptyIdsByLeafId: nextPtyIds } : {}), - ...(trimmedOutput - ? { - buffersByLeafId: { - ...existingBuffers, - [leafId]: output - } - } - : existingBuffers - ? { buffersByLeafId: existingBuffers } - : {}) - }) -} - -// Why the incarnation: a relay-recycled id can hold the previous owner's exit, and draining that -// into this handler tears the pane down seconds after it launched. -export function registerBackgroundPaneBuffer( - tabId: string, - leafId: string, - pane: { ptyId: string; incarnationId?: string } -): void { - let eagerBuffer: EagerPtyHandle | null = null - const onExit = (exitPtyId: string): void => { - persistExitedPaneOutput(tabId, leafId, eagerBuffer?.flush() ?? '') - useAppStore.getState().clearTabPtyId(tabId, exitPtyId) - } - eagerBuffer = registerEagerPtyBuffer(pane.ptyId, onExit, pane.incarnationId) -} diff --git a/src/renderer/src/lib/launch-worktree-background-terminals.test.ts b/src/renderer/src/lib/launch-worktree-background-terminals.test.ts deleted file mode 100644 index af7d330fe4e..00000000000 --- a/src/renderer/src/lib/launch-worktree-background-terminals.test.ts +++ /dev/null @@ -1,418 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' - -const mockSpawn = vi.fn() -const mockKill = vi.fn() -const mockCreateTab = vi.fn() -const mockSetTabCustomTitle = vi.fn() -const mockSetTabColor = vi.fn() -const mockSetTabLayout = vi.fn() -const mockUpdateTabPtyId = vi.fn() -const mockClearTabPtyId = vi.fn() -const mockCloseTab = vi.fn() -const mockRegisterEagerPtyBuffer = vi.fn() -const mockOwnerEnvironmentId = vi.fn() - -let uuidIndex = 0 - -const setupLaunch = { - runnerScriptPath: '/tmp/setup.sh', - envVars: { ORCA_WORKTREE_PATH: '/repo/worktree' } -} - -const state = { - settings: { activeRuntimeEnvironmentId: null as string | null, setupScriptLaunchMode: 'new-tab' }, - repos: [{ id: 'repo-1', connectionId: null as string | null }], - worktreesByRepo: { - 'repo-1': [ - { - id: 'wt-1', - repoId: 'repo-1', - path: '/repo/worktree', - displayName: 'Worktree' - } - ] - }, - tabsByWorktree: { 'wt-1': [] as { id: string }[] }, - allWorktrees: vi.fn(() => state.worktreesByRepo['repo-1'] ?? []), - createTab: mockCreateTab, - setTabCustomTitle: mockSetTabCustomTitle, - setTabColor: mockSetTabColor, - setTabLayout: mockSetTabLayout, - updateTabPtyId: mockUpdateTabPtyId, - clearTabPtyId: mockClearTabPtyId, - closeTab: mockCloseTab -} - -vi.mock('@/store', () => ({ - useAppStore: { - getState: () => state - } -})) - -vi.mock('@/lib/browser-uuid', () => ({ - createBrowserUuid: () => `00000000-0000-4000-8000-${String(++uuidIndex).padStart(12, '0')}` -})) - -vi.mock('@/components/terminal-pane/pty-dispatcher', () => ({ - registerEagerPtyBuffer: mockRegisterEagerPtyBuffer -})) - -vi.mock('@/lib/worktree-runtime-owner', () => ({ - getRuntimeEnvironmentIdForWorktree: () => mockOwnerEnvironmentId() -})) - -describe('launchWorktreeBackgroundTerminals', () => { - beforeEach(() => { - vi.resetModules() - vi.clearAllMocks() - uuidIndex = 0 - state.settings = { activeRuntimeEnvironmentId: null, setupScriptLaunchMode: 'new-tab' } - state.repos = [{ id: 'repo-1', connectionId: null }] - state.worktreesByRepo['repo-1'] = [ - { - id: 'wt-1', - repoId: 'repo-1', - path: '/repo/worktree', - displayName: 'Worktree' - } - ] - state.tabsByWorktree = { 'wt-1': [] } - let tabIndex = 0 - mockCreateTab.mockImplementation(() => { - const tab = { id: `tab-${++tabIndex}` } - state.tabsByWorktree['wt-1'].push(tab) - return tab - }) - // Placement reads the row back from the store, so title and color writes must land there. - const patchTab = (tabId: string, patch: { customTitle?: string; color?: string }): void => { - state.tabsByWorktree['wt-1'] = state.tabsByWorktree['wt-1'].map((tab) => - tab.id === tabId ? { ...tab, ...patch } : tab - ) - } - mockSetTabCustomTitle.mockImplementation((tabId: string, customTitle: string) => - patchTab(tabId, { customTitle }) - ) - mockSetTabColor.mockImplementation((tabId: string, color: string) => patchTab(tabId, { color })) - mockCloseTab.mockImplementation((tabId: string) => { - state.tabsByWorktree['wt-1'] = state.tabsByWorktree['wt-1'].filter((tab) => tab.id !== tabId) - }) - let ptyIndex = 0 - mockSpawn.mockImplementation(async () => ({ id: `pty-${++ptyIndex}` })) - mockOwnerEnvironmentId.mockReturnValue(null) - vi.stubGlobal('window', { - api: { - pty: { - spawn: mockSpawn, - kill: mockKill - } - } - }) - }) - - it('spawns default tabs and setup as inactive background terminals', async () => { - const { launchWorktreeBackgroundTerminals } = - await import('./launch-worktree-background-terminals') - - await launchWorktreeBackgroundTerminals({ - worktreeId: 'wt-1', - setup: setupLaunch, - defaultTabs: { - runCommands: true, - tabs: [{ title: 'Dev', color: '#f97316', command: 'pnpm dev' }] - } - }) - - expect(mockCreateTab).toHaveBeenCalledTimes(2) - expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, { - activate: false, - recordInteraction: false - }) - expect(mockSetTabCustomTitle).toHaveBeenCalledWith('tab-1', 'Dev', { - recordInteraction: false - }) - expect(mockSetTabColor).toHaveBeenCalledWith('tab-1', '#f97316') - expect(mockSetTabCustomTitle).toHaveBeenCalledWith('tab-2', 'Setup', { - recordInteraction: false - }) - expect(mockSpawn).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - cwd: '/repo/worktree', - command: 'pnpm dev', - connectionId: null, - worktreeId: 'wt-1', - tabId: 'tab-1', - leafId: '00000000-0000-4000-8000-000000000001', - placement: { - kind: 'new-tab', - row: expect.objectContaining({ customTitle: 'Dev', color: '#f97316' }) - } - }) - ) - expect(mockSpawn).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - command: 'bash /tmp/setup.sh', - env: expect.objectContaining({ ORCA_WORKTREE_PATH: '/repo/worktree' }), - tabId: 'tab-2', - leafId: '00000000-0000-4000-8000-000000000002', - placement: { kind: 'new-tab', row: expect.objectContaining({ customTitle: 'Setup' }) } - }) - ) - expect(mockUpdateTabPtyId).toHaveBeenCalledWith('tab-1', 'pty-1') - expect(mockUpdateTabPtyId).toHaveBeenCalledWith('tab-2', 'pty-2') - expect(mockRegisterEagerPtyBuffer).toHaveBeenCalledTimes(2) - }) - - it('spawns setup in a split when setup launch mode requests a split', async () => { - state.settings = { activeRuntimeEnvironmentId: null, setupScriptLaunchMode: 'split-horizontal' } - const { launchWorktreeBackgroundTerminals } = - await import('./launch-worktree-background-terminals') - - await launchWorktreeBackgroundTerminals({ - worktreeId: 'wt-1', - setup: setupLaunch - }) - - expect(mockCreateTab).toHaveBeenCalledTimes(1) - expect(mockSpawn).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - tabId: 'tab-1', - leafId: '00000000-0000-4000-8000-000000000001', - placement: { kind: 'new-tab', row: expect.any(Object) } - }) - ) - expect(mockSpawn.mock.calls[0]?.[0]).not.toHaveProperty('command') - expect(mockSpawn).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - command: 'bash /tmp/setup.sh', - tabId: 'tab-1', - leafId: '00000000-0000-4000-8000-000000000002', - placement: { - kind: 'split', - parentLeafId: '00000000-0000-4000-8000-000000000001', - direction: 'horizontal', - proposedRoot: { - type: 'split', - direction: 'horizontal', - first: { type: 'leaf', leafId: '00000000-0000-4000-8000-000000000001' }, - second: { type: 'leaf', leafId: '00000000-0000-4000-8000-000000000002' } - } - } - }) - ) - expect(mockSetTabLayout).toHaveBeenLastCalledWith( - 'tab-1', - expect.objectContaining({ - root: expect.objectContaining({ - type: 'split', - direction: 'horizontal' - }), - ptyIdsByLeafId: { - '00000000-0000-4000-8000-000000000001': 'pty-1', - '00000000-0000-4000-8000-000000000002': 'pty-2' - }, - titlesByLeafId: { '00000000-0000-4000-8000-000000000002': 'Setup' } - }) - ) - }) - - it('spawns an initial terminal before a setup-only new-tab launch', async () => { - const { launchWorktreeBackgroundTerminals } = - await import('./launch-worktree-background-terminals') - - await launchWorktreeBackgroundTerminals({ - worktreeId: 'wt-1', - setup: setupLaunch - }) - - expect(mockCreateTab).toHaveBeenCalledTimes(2) - expect(mockSpawn.mock.calls[0]?.[0]).not.toHaveProperty('command') - expect(mockSpawn).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - command: 'bash /tmp/setup.sh', - tabId: 'tab-2', - leafId: '00000000-0000-4000-8000-000000000002' - }) - ) - expect(mockSetTabCustomTitle).toHaveBeenCalledWith('tab-2', 'Setup', { - recordInteraction: false - }) - }) - - it('uses Windows setup commands for Windows SSH runner paths', async () => { - state.repos = [{ id: 'repo-1', connectionId: 'ssh-windows' }] - state.worktreesByRepo['repo-1'] = [ - { - id: 'wt-1', - repoId: 'repo-1', - path: 'C:\\repo\\worktree', - displayName: 'Worktree' - } - ] - const { launchWorktreeBackgroundTerminals } = - await import('./launch-worktree-background-terminals') - - await launchWorktreeBackgroundTerminals({ - worktreeId: 'wt-1', - setup: { - runnerScriptPath: 'C:\\repo\\.git\\worktrees\\wt\\orca\\setup-runner.cmd', - envVars: { ORCA_WORKTREE_PATH: 'C:\\repo\\worktree' } - } - }) - - expect(mockSpawn).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - command: 'cmd.exe /c "C:\\repo\\.git\\worktrees\\wt\\orca\\setup-runner.cmd"', - connectionId: 'ssh-windows' - }) - ) - }) - - it('uses configured WSL setup commands for Windows bash runner paths', async () => { - state.repos = [{ id: 'repo-1', connectionId: null }] - state.worktreesByRepo['repo-1'] = [ - { - id: 'wt-1', - repoId: 'repo-1', - path: 'C:\\repo\\worktree', - displayName: 'Worktree' - } - ] - const { launchWorktreeBackgroundTerminals } = - await import('./launch-worktree-background-terminals') - - await launchWorktreeBackgroundTerminals({ - worktreeId: 'wt-1', - setup: { - runnerScriptPath: 'C:\\repo\\.git\\worktrees\\wt\\orca\\setup-runner.sh', - shell: { family: 'posix', executable: 'wsl.exe' }, - envVars: { ORCA_WORKTREE_PATH: 'C:\\repo\\worktree' } - } - }) - - expect(mockSpawn).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - command: 'bash /mnt/c/repo/.git/worktrees/wt/orca/setup-runner.sh', - connectionId: null - }) - ) - }) - - it('still attempts setup when a default tab fails to spawn', async () => { - const spawnError = new Error('pty unavailable') - mockSpawn - .mockRejectedValueOnce(spawnError) - .mockResolvedValueOnce({ id: 'pty-initial' }) - .mockResolvedValueOnce({ id: 'pty-setup' }) - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const { launchWorktreeBackgroundTerminals } = - await import('./launch-worktree-background-terminals') - - await launchWorktreeBackgroundTerminals({ - worktreeId: 'wt-1', - setup: setupLaunch, - defaultTabs: { - runCommands: true, - tabs: [{ title: 'Dev', command: 'pnpm dev' }] - } - }) - - expect(mockCloseTab).toHaveBeenCalledWith('tab-1', { - recordInteraction: false, - reason: 'cleanup' - }) - expect(mockSpawn).toHaveBeenCalledTimes(3) - expect(mockSpawn).toHaveBeenNthCalledWith( - 3, - expect.objectContaining({ - command: 'bash /tmp/setup.sh', - tabId: 'tab-3' - }) - ) - expect(warn).toHaveBeenCalledWith( - '[automations] Failed to launch workspace default tab:', - spawnError - ) - warn.mockRestore() - }) - - it('does not duplicate runtime-owned setup/defaultTabs handled by runtime create', async () => { - mockOwnerEnvironmentId.mockReturnValue('env-1') - const { launchWorktreeBackgroundTerminals } = - await import('./launch-worktree-background-terminals') - - await launchWorktreeBackgroundTerminals({ - worktreeId: 'wt-1', - setup: setupLaunch, - defaultTabs: { runCommands: true, tabs: [{ title: 'Dev', command: 'pnpm dev' }] } - }) - - expect(mockCreateTab).not.toHaveBeenCalled() - expect(mockSpawn).not.toHaveBeenCalled() - }) - - it('kills a PTY whose tab is closed before the spawn resolves', async () => { - let resolveSpawn!: (result: { id: string }) => void - mockSpawn.mockReturnValueOnce( - new Promise<{ id: string }>((resolve) => { - resolveSpawn = resolve - }) - ) - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const { launchWorktreeBackgroundTerminals } = - await import('./launch-worktree-background-terminals') - - const launch = launchWorktreeBackgroundTerminals({ - worktreeId: 'wt-1', - defaultTabs: { runCommands: true, tabs: [{ command: 'pnpm dev' }] } - }) - await vi.waitFor(() => expect(mockCreateTab).toHaveBeenCalledOnce()) - state.tabsByWorktree['wt-1'] = [] - resolveSpawn({ id: 'pty-after-close' }) - await launch - - expect(mockKill).toHaveBeenCalledWith('pty-after-close') - expect(mockUpdateTabPtyId).not.toHaveBeenCalled() - expect(mockRegisterEagerPtyBuffer).not.toHaveBeenCalled() - warn.mockRestore() - }) - - it('kills a late setup split PTY when its parent tab closes', async () => { - state.settings = { activeRuntimeEnvironmentId: null, setupScriptLaunchMode: 'split-horizontal' } - let resolveSetupSpawn!: (result: { id: string }) => void - mockSpawn.mockResolvedValueOnce({ id: 'pty-primary' }).mockReturnValueOnce( - new Promise<{ id: string }>((resolve) => { - resolveSetupSpawn = resolve - }) - ) - const { launchWorktreeBackgroundTerminals } = - await import('./launch-worktree-background-terminals') - - const launch = launchWorktreeBackgroundTerminals({ - worktreeId: 'wt-1', - setup: setupLaunch - }) - await vi.waitFor(() => expect(mockSpawn).toHaveBeenCalledTimes(2)) - state.closeTab('tab-1') - resolveSetupSpawn({ id: 'pty-setup-after-close' }) - await launch - - expect(mockKill).toHaveBeenCalledWith('pty-setup-after-close') - expect(mockUpdateTabPtyId).toHaveBeenCalledTimes(1) - expect(mockUpdateTabPtyId).toHaveBeenCalledWith('tab-1', 'pty-primary') - expect(mockSetTabLayout).not.toHaveBeenCalledWith( - 'tab-1', - expect.objectContaining({ - ptyIdsByLeafId: expect.objectContaining({ - '00000000-0000-4000-8000-000000000002': 'pty-setup-after-close' - }) - }) - ) - }) -}) diff --git a/src/renderer/src/lib/launch-worktree-background-terminals.ts b/src/renderer/src/lib/launch-worktree-background-terminals.ts deleted file mode 100644 index d96e1e47412..00000000000 --- a/src/renderer/src/lib/launch-worktree-background-terminals.ts +++ /dev/null @@ -1,310 +0,0 @@ -import { createBrowserUuid } from '@/lib/browser-uuid' -import { runtimeTargetForOwnerEnvironment } from '@/runtime/runtime-client-target' -import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' -import { singlePaneLayoutSnapshot } from '@/store/slices/terminal-helpers' -import { retireUnownedTerminal } from '@/lib/retire-unowned-background-terminal' -import { registerBackgroundPaneBuffer } from '@/lib/background-pane-exit-output' -import { terminalPanePlacementRow } from '@/lib/terminal-pane-placement-row' -import { useAppStore } from '@/store' -import { translate } from '@/i18n/i18n' -import { makePaneKey } from '../../../shared/stable-pane-id' -import { - buildSetupRunnerCommand, - getSetupRunnerCommandPlatformForPath -} from '../../../shared/setup-runner-command' -import type { - TerminalLayoutSnapshot, - TerminalPaneLayoutNode -} from '../../../shared/terminal-tab-types' -import type { TerminalPanePlacement } from '../../../shared/terminal-pane-placement' -import type { - WorktreeDefaultTabsLaunch, - WorktreeSetupLaunch -} from '../../../shared/worktree/launch-types' -import type { Worktree } from '../../../shared/worktree/types' - -type BackgroundPane = { - leafId: string - ptyId: string -} - -type BackgroundTab = { - tabId: string - primary: BackgroundPane -} - -type BackgroundTerminalLaunch = { - command?: string - env?: Record - title?: string - color?: string -} - -function getSetupTabTitle(): string { - return translate('auto.lib.launch.worktree.background.terminals.setupTitle', 'Setup') -} - -export type LaunchWorktreeBackgroundTerminalsArgs = { - worktreeId: string - setup?: WorktreeSetupLaunch - defaultTabs?: WorktreeDefaultTabsLaunch -} - -function buildPaneEnv( - worktreeId: string, - tabId: string, - leafId: string, - env: Record | undefined -): Record { - return { - ...env, - ORCA_PANE_KEY: makePaneKey(tabId, leafId), - ORCA_TAB_ID: tabId, - ORCA_WORKTREE_ID: worktreeId - } -} - -function buildSplitRoot( - firstLeafId: string, - secondLeafId: string, - direction: 'horizontal' | 'vertical' -): TerminalPaneLayoutNode { - return { - type: 'split', - direction, - first: { type: 'leaf', leafId: firstLeafId }, - second: { type: 'leaf', leafId: secondLeafId } - } -} - -function buildSplitLayout( - first: BackgroundPane, - second: BackgroundPane, - direction: 'horizontal' | 'vertical', - secondTitle: string -): TerminalLayoutSnapshot { - return { - root: buildSplitRoot(first.leafId, second.leafId, direction), - activeLeafId: first.leafId, - expandedLeafId: null, - ptyIdsByLeafId: { - [first.leafId]: first.ptyId, - [second.leafId]: second.ptyId - }, - titlesByLeafId: { - [second.leafId]: secondTitle - } - } -} - -function buildSetupCommand(setup: WorktreeSetupLaunch): string { - // Why: background setup tabs can launch later, so they must reuse the same shell chosen when the runner was written. - return buildSetupRunnerCommand( - setup.runnerScriptPath, - getSetupRunnerCommandPlatformForPath(setup.runnerScriptPath, 'posix'), - setup.shell - ) -} - -/** The id a background pane got, plus which lifetime of it this spawn owns. */ -type SpawnedPane = { ptyId: string; incarnationId?: string } - -async function spawnPane(args: { - worktree: Worktree - connectionId: string | null - tabId: string - leafId: string - placement: TerminalPanePlacement - command?: string - env?: Record -}): Promise { - const result = await window.api.pty.spawn({ - cols: 120, - rows: 40, - cwd: args.worktree.path, - ...(args.command ? { command: args.command } : {}), - env: buildPaneEnv(args.worktree.id, args.tabId, args.leafId, args.env), - connectionId: args.connectionId, - worktreeId: args.worktree.id, - tabId: args.tabId, - leafId: args.leafId, - placement: args.placement - }) - return { - ptyId: result.id, - ...(result.incarnationId ? { incarnationId: result.incarnationId } : {}) - } -} - -async function createBackgroundTab(args: { - worktree: Worktree - connectionId: string | null - launch: BackgroundTerminalLaunch -}): Promise { - const store = useAppStore.getState() - const tab = store.createTab(args.worktree.id, undefined, undefined, { - activate: false, - recordInteraction: false - }) - if (args.launch.title) { - store.setTabCustomTitle(tab.id, args.launch.title, { recordInteraction: false }) - } - if (args.launch.color) { - store.setTabColor(tab.id, args.launch.color) - } - - const leafId = createBrowserUuid() - store.setTabLayout(tab.id, singlePaneLayoutSnapshot(leafId)) - const created = - useAppStore.getState().tabsByWorktree[args.worktree.id]?.find(({ id }) => id === tab.id) ?? tab - let pane: SpawnedPane - try { - pane = await spawnPane({ - worktree: args.worktree, - connectionId: args.connectionId, - tabId: tab.id, - leafId, - placement: { kind: 'new-tab', row: terminalPanePlacementRow(created) }, - command: args.launch.command, - env: args.launch.env - }) - } catch (error) { - store.closeTab(tab.id, { recordInteraction: false, reason: 'cleanup' }) - throw error - } - if ( - await retireUnownedTerminal({ - owner: { tabId: tab.id }, - ptyId: pane.ptyId, - runtimeTarget: { kind: 'local' } - }) - ) { - throw new Error('The terminal tab was closed before its session finished starting.') - } - store.updateTabPtyId(tab.id, pane.ptyId) - store.setTabLayout(tab.id, singlePaneLayoutSnapshot(leafId, pane.ptyId)) - registerBackgroundPaneBuffer(tab.id, leafId, pane) - return { tabId: tab.id, primary: { leafId, ptyId: pane.ptyId } } -} - -async function addSetupSplit(args: { - worktree: Worktree - connectionId: string | null - tab: BackgroundTab - setup: WorktreeSetupLaunch - direction: 'horizontal' | 'vertical' -}): Promise { - const store = useAppStore.getState() - const setupLeafId = createBrowserUuid() - const setupPane = await spawnPane({ - worktree: args.worktree, - connectionId: args.connectionId, - tabId: args.tab.tabId, - leafId: setupLeafId, - placement: { - kind: 'split', - parentLeafId: args.tab.primary.leafId, - direction: args.direction, - proposedRoot: buildSplitRoot(args.tab.primary.leafId, setupLeafId, args.direction) - }, - command: buildSetupCommand(args.setup), - env: args.setup.envVars - }) - if ( - await retireUnownedTerminal({ - owner: { tabId: args.tab.tabId }, - ptyId: setupPane.ptyId, - runtimeTarget: { kind: 'local' } - }) - ) { - return - } - store.updateTabPtyId(args.tab.tabId, setupPane.ptyId) - store.setTabLayout( - args.tab.tabId, - buildSplitLayout( - args.tab.primary, - { leafId: setupLeafId, ptyId: setupPane.ptyId }, - args.direction, - getSetupTabTitle() - ) - ) - registerBackgroundPaneBuffer(args.tab.tabId, setupLeafId, setupPane) -} - -function getDefaultTabLaunches( - defaultTabs: WorktreeDefaultTabsLaunch | undefined -): BackgroundTerminalLaunch[] { - return (defaultTabs?.tabs ?? []).map((tab) => { - const command = tab.command?.trim() - return { - ...(tab.title ? { title: tab.title } : {}), - ...(tab.color ? { color: tab.color } : {}), - ...(command && defaultTabs?.runCommands ? { command } : {}) - } - }) -} - -export async function launchWorktreeBackgroundTerminals( - args: LaunchWorktreeBackgroundTerminalsArgs -): Promise { - if (!args.setup && !args.defaultTabs) { - return - } - const store = useAppStore.getState() - const runtimeTarget = runtimeTargetForOwnerEnvironment( - getRuntimeEnvironmentIdForWorktree(store, args.worktreeId) - ) - if (runtimeTarget.kind === 'environment') { - // Runtime-owned worktrees materialize setup/defaultTabs inside createManagedWorktree. - return - } - - const worktree = store.allWorktrees().find((entry) => entry.id === args.worktreeId) - if (!worktree) { - throw new Error('The target workspace is no longer available.') - } - const repo = store.repos.find((entry) => entry.id === worktree.repoId) - const connectionId = repo?.connectionId ?? null - const defaultLaunches = getDefaultTabLaunches(args.defaultTabs) - const launchedTabs: BackgroundTab[] = [] - - for (const launch of defaultLaunches) { - try { - launchedTabs.push(await createBackgroundTab({ worktree, connectionId, launch })) - } catch (error) { - console.warn('[automations] Failed to launch workspace default tab:', error) - } - } - - const setupMode = store.settings?.setupScriptLaunchMode ?? 'new-tab' - const shouldSplitSetup = - args.setup && (setupMode === 'split-horizontal' || setupMode === 'split-vertical') - if (shouldSplitSetup) { - const primaryTab = - launchedTabs[0] ?? (await createBackgroundTab({ worktree, connectionId, launch: {} })) - await addSetupSplit({ - worktree, - connectionId, - tab: primaryTab, - setup: args.setup!, - direction: setupMode === 'split-horizontal' ? 'horizontal' : 'vertical' - }) - return - } - - if (args.setup) { - if (launchedTabs.length === 0) { - launchedTabs.push(await createBackgroundTab({ worktree, connectionId, launch: {} })) - } - await createBackgroundTab({ - worktree, - connectionId, - launch: { - title: getSetupTabTitle(), - command: buildSetupCommand(args.setup), - env: args.setup.envVars - } - }) - } -}