diff --git a/src/renderer/src/lib/worktree-creation-flow-execute.ts b/src/renderer/src/lib/worktree-creation-flow-execute.ts index 839f454eef5..d4409d10549 100644 --- a/src/renderer/src/lib/worktree-creation-flow-execute.ts +++ b/src/renderer/src/lib/worktree-creation-flow-execute.ts @@ -19,7 +19,10 @@ import type { WorktreeCreationRequest } from '@/lib/pending-worktree-creation' import { createBrowserUuid } from '@/lib/browser-uuid' import { resolveBackendDraftStartup } from '@/lib/worktree-draft-startup-view-mode' import { buildWorktreeCreationStartupOpt } from '@/lib/worktree-creation-flow-startup' -import { launchStructuredWorktreeSession } from '@/lib/worktree-creation-structured-session' +import { + launchStructuredWorktreeSession, + type WorktreeCreationStructuredSessionResult +} from '@/lib/worktree-creation-structured-session' import { completeWorktreeCreation } from '@/lib/worktree-creation-completion' import { markStructuredWorktreeLaunchUnconfirmed } from '@/lib/worktree-creation-structured-recovery' import { ensureWebRuntimeWorktreeTerminalAfterWake } from '@/lib/web-runtime-worktree-terminal-after-wake' @@ -164,28 +167,41 @@ export async function executeWorktreeCreation( (completionState.activeView === 'terminal' && completionState.activePendingCreationId === null)) + // Why: the worktree exists past this point and nothing awaits this caller, so + // each follow-up step is best-effort — an escaped throw would strand the + // creation surface over the finished workspace instead of reaching completion. let activation: ActivateAndRevealResult | false = false - let primaryTabId: string | null + let primaryTabId: string | null = null if (shouldActivateOnCompletion && !structuredLaunch) { - activation = activateAndRevealWorktree(worktree.id, { - sidebarRevealBehavior: 'auto', - ...(preparedRequest.agent !== null ? { agent: preparedRequest.agent } : {}), - ...(result.setup ? { setup: result.setup } : {}), - ...(result.defaultTabs ? { defaultTabs: result.defaultTabs } : {}), - ...(startupOpt ? { startup: startupOpt } : {}), - ...(preparedRequest.issueCommand ? { issueCommand: preparedRequest.issueCommand } : {}), - ...(backendSpawned ? { backendStartupTerminalSpawned: true } : {}) - }) - primaryTabId = activation === false ? null : activation.primaryTabId - } else { - // Keep chat creation on its pending surface until the session is ready. - const hasExplicitTerminalWork = Boolean( - startupOpt || result.setup || preparedRequest.issueCommand || result.defaultTabs - ) - primaryTabId = - preparedRequest.agent !== null && !hasExplicitTerminalWork - ? null - : ensureWorktreeHasInitialTerminal( + try { + activation = activateAndRevealWorktree(worktree.id, { + sidebarRevealBehavior: 'auto', + ...(preparedRequest.agent !== null ? { agent: preparedRequest.agent } : {}), + ...(result.setup ? { setup: result.setup } : {}), + ...(result.defaultTabs ? { defaultTabs: result.defaultTabs } : {}), + ...(startupOpt ? { startup: startupOpt } : {}), + ...(preparedRequest.issueCommand ? { issueCommand: preparedRequest.issueCommand } : {}), + ...(backendSpawned ? { backendStartupTerminalSpawned: true } : {}) + }) + primaryTabId = activation === false ? null : activation.primaryTabId + } catch (error) { + console.error('worktree create: activate-and-reveal failed', worktree.id, error) + // Activation can publish the worktree before a later step throws. Do not + // infer a primary tab from default-tab ordering; only a fresh seed may + // return one here. + const stateAfterActivationFailure = useAppStore.getState() + const existingTabs = stateAfterActivationFailure.tabsByWorktree[worktree.id] ?? [] + const launchAgent = startupOpt?.launchAgent ?? preparedRequest.agent + const verifiedLaunchTabId = + result.startupTerminal?.tabId ?? + (launchAgent ? existingTabs.find((tab) => tab.launchAgent === launchAgent)?.id : undefined) + if (verifiedLaunchTabId) { + // Startup terminal ids and stamped agent tabs are the only safe primary + // ids when activation returned no result. + primaryTabId = verifiedLaunchTabId + } else if (existingTabs.length === 0) { + try { + primaryTabId = ensureWorktreeHasInitialTerminal( useAppStore.getState(), worktree.id, startupOpt, @@ -193,17 +209,67 @@ export async function executeWorktreeCreation( preparedRequest.issueCommand, result.defaultTabs, { - activateCreatedTabs: false, ...(preparedRequest.agent !== null ? { callerProvidesSurface: true } : {}), ...(backendSpawned ? { backendStartupTerminalSpawned: true } : {}) } ) + } catch (recoveryError) { + console.error( + 'worktree create: activation recovery seeding failed', + worktree.id, + recoveryError + ) + } + } + if (!backendSpawned) { + try { + ensureWebRuntimeWorktreeTerminalAfterWake(worktree.id, { + startup: startupOpt, + agent: preparedRequest.agent + }) + } catch (recoveryError) { + console.error( + 'worktree create: activation recovery after-wake seeding failed', + worktree.id, + recoveryError + ) + } + } + } + } else { + // Keep chat creation on its pending surface until the session is ready. + const hasExplicitTerminalWork = Boolean( + startupOpt || result.setup || preparedRequest.issueCommand || result.defaultTabs + ) + if (preparedRequest.agent === null || hasExplicitTerminalWork) { + try { + primaryTabId = ensureWorktreeHasInitialTerminal( + useAppStore.getState(), + worktree.id, + startupOpt, + result.setup, + preparedRequest.issueCommand, + result.defaultTabs, + { + activateCreatedTabs: false, + ...(preparedRequest.agent !== null ? { callerProvidesSurface: true } : {}), + ...(backendSpawned ? { backendStartupTerminalSpawned: true } : {}) + } + ) + } catch (error) { + console.error('worktree create: initial terminal seeding failed', worktree.id, error) + } + } if (!structuredLaunch && !backendSpawned) { - ensureWebRuntimeWorktreeTerminalAfterWake(worktree.id, { - startup: startupOpt, - agent: preparedRequest.agent, - activate: false - }) + try { + ensureWebRuntimeWorktreeTerminalAfterWake(worktree.id, { + startup: startupOpt, + agent: preparedRequest.agent, + activate: false + }) + } catch (error) { + console.error('worktree create: after-wake terminal seeding failed', worktree.id, error) + } } } @@ -213,25 +279,34 @@ export async function executeWorktreeCreation( agentLaunchRoute === 'structured-native-chat' && isAgentSessionHandleProvider(preparedRequest.agent) ) { - const structuredSession = await launchStructuredWorktreeSession({ - creationId, - request: preparedRequest, - agentLaunchRoute, - worktreeId: worktree.id, - shouldActivateOnCompletion, - fallbackStartupOpt, - activation, - primaryTabId - }) - structuredLaunchAccepted = structuredSession.accepted - activation = structuredSession.activation - primaryTabId = structuredSession.primaryTabId - if (structuredSession.cancelled) { - return + let structuredSession: WorktreeCreationStructuredSessionResult | null = null + try { + structuredSession = await launchStructuredWorktreeSession({ + creationId, + request: preparedRequest, + agentLaunchRoute, + worktreeId: worktree.id, + shouldActivateOnCompletion, + fallbackStartupOpt, + activation, + primaryTabId + }) + } catch (error) { + // Why: plan.launch is guarded inside, but its sync prologue is not; treat + // an escaped throw like a failed launch (accepted) and still complete. + console.error('worktree create: structured session launch failed', worktree.id, error) } - if (structuredSession.visibilityUnknown) { - markStructuredWorktreeLaunchUnconfirmed(creationId, worktree.id) - return + if (structuredSession) { + structuredLaunchAccepted = structuredSession.accepted + activation = structuredSession.activation + primaryTabId = structuredSession.primaryTabId + if (structuredSession.cancelled) { + return + } + if (structuredSession.visibilityUnknown) { + markStructuredWorktreeLaunchUnconfirmed(creationId, worktree.id) + return + } } } diff --git a/src/renderer/src/lib/worktree-creation-flow-stranded-surface.test.ts b/src/renderer/src/lib/worktree-creation-flow-stranded-surface.test.ts new file mode 100644 index 00000000000..143645e9008 --- /dev/null +++ b/src/renderer/src/lib/worktree-creation-flow-stranded-surface.test.ts @@ -0,0 +1,377 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { + PendingWorktreeCreation, + WorktreeCreationRequest +} from '@/lib/pending-worktree-creation' +import { shouldShowWorktreeCreationSurface } from '@/lib/worktree-creation-surface' + +// Guards executeWorktreeCreation's post-create tail: callers fire and forget, +// so a throw after createWorktree succeeds must be contained per-step and the +// creation must still reach completeWorktreeCreation, which tears the creation +// surface down. Also covers the caller-side .catch() backstop: a rejection that +// still escapes (e.g. pre-create preparation) becomes a visible error state +// plus toast instead of a panel silently stuck at "creating". + +type TestActiveView = 'terminal' | 'tasks' + +const store = { + settings: { + activeRuntimeEnvironmentId: null as string | null, + experimentalNativeChat: undefined as boolean | undefined, + openAgentTabsInChatByDefault: undefined as boolean | undefined + }, + activeView: 'terminal' as TestActiveView, + activePendingCreationId: 'creation-1' as string | null, + repos: [] as { id: string; connectionId: string | null }[], + pendingWorktreeCreations: {} as Record, + beginPendingWorktreeCreation: vi.fn((entry: PendingWorktreeCreation) => { + store.pendingWorktreeCreations[entry.creationId] = entry + store.activePendingCreationId = entry.creationId + }), + updatePendingWorktreeCreation: vi.fn( + (creationId: string, patch: Partial) => { + const entry = store.pendingWorktreeCreations[creationId] + if (entry) { + store.pendingWorktreeCreations[creationId] = { ...entry, ...patch } + } + } + ), + // Mirrors pending-worktree-creation.ts: drop the entry and the active pointer. + removePendingWorktreeCreation: vi.fn((creationId: string) => { + delete store.pendingWorktreeCreations[creationId] + if (store.activePendingCreationId === creationId) { + store.activePendingCreationId = null + } + }), + setActivePendingWorktreeCreation: vi.fn((creationId: string | null) => { + store.activePendingCreationId = creationId + }), + setActiveView: vi.fn((view: TestActiveView) => { + store.activeView = view + }), + setSidebarOpen: vi.fn(), + updateWorktreeMeta: vi.fn(), + createWorktree: vi.fn(), + tabsByWorktree: {} as Record, + unifiedTabsByWorktree: {} +} + +vi.mock('@/store', () => ({ + useAppStore: { + getState: () => store + } +})) + +vi.mock('sonner', () => ({ + toast: { error: vi.fn() } +})) + +vi.mock('@/lib/worktree-activation', () => ({ + activateAndRevealWorktree: vi.fn() +})) + +vi.mock('@/lib/worktree-initial-terminal-seeding', () => ({ + ensureWorktreeHasInitialTerminal: vi.fn() +})) + +vi.mock('@/lib/web-runtime-worktree-terminal-after-wake', () => ({ + ensureWebRuntimeWorktreeTerminalAfterWake: vi.fn() +})) + +vi.mock('@/lib/workspace-activation-terminal-focus', () => ({ + queueWorkspaceActivationTerminalFocus: vi.fn() +})) + +vi.mock('@/lib/new-workspace', () => ({ + ensureAgentStartupInTerminal: vi.fn() +})) + +vi.mock('@/lib/worktree-creation-agent-seeds', () => ({ + seedAgentTabStateAfterWorktreeCreate: vi.fn() +})) + +vi.mock('@/lib/ephemeral-vm-workspace-target', () => ({ + prepareEphemeralVmWorkspaceTarget: vi.fn() +})) + +vi.mock('@/lib/ephemeral-vm-worktree-creation', () => ({ + prepareRequestForCreate: vi.fn( + async (_creationId: string, request: WorktreeCreationRequest) => request + ), + attachEphemeralVmRuntimeToWorkspace: vi.fn(async () => undefined), + cleanupEphemeralVmRuntimeForFailedCreate: vi.fn(async () => undefined) +})) + +vi.mock('@/lib/worktree-creation-structured-recovery', () => ({ + markStructuredWorktreeLaunchUnconfirmed: vi.fn(), + retryStructuredWorktreeLaunch: vi.fn() +})) + +import { toast } from 'sonner' +import { activateAndRevealWorktree } from '@/lib/worktree-activation' +import { ensureWorktreeHasInitialTerminal } from '@/lib/worktree-initial-terminal-seeding' +import { ensureWebRuntimeWorktreeTerminalAfterWake } from '@/lib/web-runtime-worktree-terminal-after-wake' +import { ensureAgentStartupInTerminal } from '@/lib/new-workspace' +import { prepareRequestForCreate } from '@/lib/ephemeral-vm-worktree-creation' +import { executeWorktreeCreation } from './worktree-creation-flow-execute' +import { runBackgroundWorktreeCreation } from './worktree-creation-flow' + +function makeRequest(overrides: Partial = {}): WorktreeCreationRequest { + return { + repoId: 'repo-1', + name: 'feature', + setupDecision: 'inherit', + agent: null, + pendingFirstAgentMessageRename: false, + note: '', + startupPlan: null, + quickPrompt: '', + quickTelemetry: null, + ...overrides + } as WorktreeCreationRequest +} + +function seedPendingCreation(request: WorktreeCreationRequest): void { + store.pendingWorktreeCreations = { + 'creation-1': { + creationId: 'creation-1', + phase: 'fetching', + status: 'creating', + startedAt: 1, + indeterminate: false, + loaderVisible: true, + request + } + } + store.activePendingCreationId = 'creation-1' +} + +function surfaceInput(activeView: TestActiveView): { + activeView: TestActiveView + activePendingCreationId: string | null + hasActivePendingCreation: boolean +} { + return { + activeView, + activePendingCreationId: store.activePendingCreationId, + hasActivePendingCreation: + store.activePendingCreationId !== null && + store.pendingWorktreeCreations[store.activePendingCreationId] !== undefined + } +} + +beforeEach(() => { + // resetAllMocks: implementations from prior tests (the injected throws) must not leak. + vi.resetAllMocks() + vi.spyOn(console, 'error').mockImplementation(() => undefined) + store.activeView = 'terminal' + store.repos = [{ id: 'repo-1', connectionId: null }] + store.tabsByWorktree = {} + store.pendingWorktreeCreations = {} + store.activePendingCreationId = null + store.createWorktree.mockResolvedValue({ + worktree: { id: 'wt-1', repoId: 'repo-1' } + }) +}) + +describe('a throw after createWorktree succeeds no longer strands the creation surface', () => { + it('activating branch: a throw in activateAndRevealWorktree recovers a terminal and completes', async () => { + const request = makeRequest() + seedPendingCreation(request) + vi.mocked(ensureWorktreeHasInitialTerminal).mockReturnValue('recovered-tab') + vi.mocked(activateAndRevealWorktree).mockImplementation(() => { + throw new Error('activation exploded') + }) + + await executeWorktreeCreation('creation-1', request) + + expect(console.error).toHaveBeenCalledWith( + 'worktree create: activate-and-reveal failed', + 'wt-1', + expect.any(Error) + ) + expect(ensureWorktreeHasInitialTerminal).toHaveBeenCalledWith( + store, + 'wt-1', + undefined, + undefined, + undefined, + undefined, + {} + ) + // Contained: completion still tears the surface down. + expect(store.removePendingWorktreeCreation).toHaveBeenCalledWith('creation-1', { + cleanupVm: false + }) + expect(store.pendingWorktreeCreations['creation-1']).toBeUndefined() + expect(store.activePendingCreationId).toBeNull() + expect(shouldShowWorktreeCreationSurface(surfaceInput('terminal'))).toBe(false) + }) + + it('activating branch: leaves existing default tabs untouched after a partial failure', async () => { + const request = makeRequest({ issueCommand: { command: 'echo setup' } }) + seedPendingCreation(request) + store.tabsByWorktree = { 'wt-1': [{ id: 'existing-tab' }] } + vi.mocked(activateAndRevealWorktree).mockImplementation(() => { + throw new Error('reveal exploded after tab creation') + }) + + await executeWorktreeCreation('creation-1', request) + + expect(ensureWorktreeHasInitialTerminal).not.toHaveBeenCalled() + expect(store.removePendingWorktreeCreation).toHaveBeenCalledWith('creation-1', { + cleanupVm: false + }) + }) + + it('activating branch: routes draft and follow-up delivery to the stamped agent tab', async () => { + const request = makeRequest({ + agent: 'codex', + startupPlan: { + agent: 'codex', + launchCommand: 'codex', + expectedProcess: 'codex', + draftPrompt: 'draft context', + followupPrompt: 'follow-up context', + launchConfig: { agentArgs: '', agentEnv: {} } + } + }) + seedPendingCreation(request) + store.tabsByWorktree = { + 'wt-1': [{ id: 'default-tab' }, { id: 'agent-tab', launchAgent: 'codex' }] + } + vi.mocked(activateAndRevealWorktree).mockImplementation(() => { + throw new Error('reveal exploded after default tabs were created') + }) + + await executeWorktreeCreation('creation-1', request) + + expect(ensureWorktreeHasInitialTerminal).not.toHaveBeenCalled() + expect(ensureAgentStartupInTerminal).toHaveBeenCalledWith( + expect.objectContaining({ primaryTabId: 'agent-tab' }) + ) + }) + + it('background branch: a throw in after-wake seeding is contained after tabs are seeded', async () => { + // User left the terminal view mid-create, so the non-activating branch runs. + store.activeView = 'tasks' + const request = makeRequest() + seedPendingCreation(request) + vi.mocked(ensureWorktreeHasInitialTerminal).mockReturnValue('tab-1') + vi.mocked(ensureWebRuntimeWorktreeTerminalAfterWake).mockImplementation(() => { + throw new Error('after-wake exploded') + }) + + await executeWorktreeCreation('creation-1', request) + + // Tabs were seeded for the new worktree... + expect(ensureWorktreeHasInitialTerminal).toHaveBeenCalledWith( + store, + 'wt-1', + undefined, + undefined, + undefined, + undefined, + expect.objectContaining({ activateCreatedTabs: false }) + ) + expect(console.error).toHaveBeenCalledWith( + 'worktree create: after-wake terminal seeding failed', + 'wt-1', + expect.any(Error) + ) + // ...and the creation still completed instead of stranding the entry. + expect(store.removePendingWorktreeCreation).toHaveBeenCalledWith('creation-1', { + cleanupVm: false + }) + expect(store.pendingWorktreeCreations['creation-1']).toBeUndefined() + expect(store.activePendingCreationId).toBeNull() + expect(shouldShowWorktreeCreationSurface(surfaceInput('terminal'))).toBe(false) + }) + + it('concurrent create: a throw completing a backgrounded creation still tears its entry down', async () => { + // A second submitted create repointed activePendingCreationId, so this + // creation's completion takes the non-activating branch on the terminal view. + store.activeView = 'terminal' + const request = makeRequest() + seedPendingCreation(request) + store.activePendingCreationId = 'creation-2' + store.createWorktree.mockResolvedValue({ + worktree: { id: 'wt-1', repoId: 'repo-1' }, + setup: { runnerScriptPath: '/tmp/setup.sh' } + }) + vi.mocked(ensureWorktreeHasInitialTerminal).mockReturnValue('tab-1') + vi.mocked(ensureWebRuntimeWorktreeTerminalAfterWake).mockImplementation(() => { + throw new Error('after-wake exploded') + }) + + await executeWorktreeCreation('creation-1', request) + + // Blank terminal + Setup tab are seeded by this one synchronous call. + expect(activateAndRevealWorktree).not.toHaveBeenCalled() + expect(ensureWorktreeHasInitialTerminal).toHaveBeenCalledWith( + store, + 'wt-1', + undefined, + { runnerScriptPath: '/tmp/setup.sh' }, + undefined, + undefined, + expect.objectContaining({ activateCreatedTabs: false }) + ) + // The entry is gone; the pointer stays on the other in-flight creation. + expect(store.pendingWorktreeCreations['creation-1']).toBeUndefined() + expect(store.activePendingCreationId).toBe('creation-2') + expect(shouldShowWorktreeCreationSurface(surfaceInput('terminal'))).toBe(false) + }) + + it('control: with no throw the same flow completes and tears the surface down', async () => { + store.activeView = 'tasks' + const request = makeRequest() + seedPendingCreation(request) + vi.mocked(ensureWorktreeHasInitialTerminal).mockReturnValue('tab-1') + + await executeWorktreeCreation('creation-1', request) + + expect(store.removePendingWorktreeCreation).toHaveBeenCalledWith('creation-1', { + cleanupVm: false + }) + expect(store.pendingWorktreeCreations['creation-1']).toBeUndefined() + expect(store.activePendingCreationId).toBeNull() + expect(shouldShowWorktreeCreationSurface(surfaceInput('terminal'))).toBe(false) + }) + + it('backstop: a rejection that escapes the execute promise becomes a visible inline error', async () => { + // Pre-create preparation runs before the in-function try/catch. + vi.mocked(prepareRequestForCreate).mockRejectedValue(new Error('prepare exploded')) + + const creationId = runBackgroundWorktreeCreation(makeRequest()) + + await vi.waitFor(() => { + expect(store.pendingWorktreeCreations[creationId]).toMatchObject({ + status: 'error', + error: 'prepare exploded' + }) + }) + expect(toast.error).not.toHaveBeenCalled() + expect(store.removePendingWorktreeCreation).not.toHaveBeenCalled() + expect(console.error).toHaveBeenCalledWith( + 'worktree create: unhandled failure', + creationId, + expect.any(Error) + ) + }) + + it('backstop: a rejection after leaving the panel is announced with a toast', async () => { + store.activeView = 'tasks' + vi.mocked(prepareRequestForCreate).mockRejectedValue(new Error('prepare exploded')) + + const creationId = runBackgroundWorktreeCreation(makeRequest()) + // The pending surface is revealed synchronously; move away before the + // rejected preparation reaches the fire-and-forget backstop. + store.activeView = 'tasks' + + await vi.waitFor(() => { + expect(toast.error).toHaveBeenCalledWith('prepare exploded') + }) + expect(store.pendingWorktreeCreations[creationId]).toMatchObject({ status: 'error' }) + }) +}) diff --git a/src/renderer/src/lib/worktree-creation-flow.ts b/src/renderer/src/lib/worktree-creation-flow.ts index dfde4e7fe58..7a46e708417 100644 --- a/src/renderer/src/lib/worktree-creation-flow.ts +++ b/src/renderer/src/lib/worktree-creation-flow.ts @@ -1,3 +1,4 @@ +import { toast } from 'sonner' import { useAppStore } from '@/store' import { findPendingLinkedWorkItemCreationId, @@ -11,11 +12,34 @@ import { getWorktreeCreationIndeterminate } from '@/lib/worktree-creation-flow-startup' import { retryStructuredWorktreeLaunch } from '@/lib/worktree-creation-structured-recovery' +import { + formatWorkspaceCreateError, + getWorkspaceCreateErrorToastMessage +} from '@/lib/workspace-create-error-format' type ContinueBackgroundWorktreeCreationOptions = { revealCreationSurface?: boolean } +// Why: nothing awaits these creations, so an escaped rejection would otherwise +// strand the pending entry — and the creation surface — with no error shown. +function startWorktreeCreation(creationId: string, request: WorktreeCreationRequest): void { + executeWorktreeCreation(creationId, request).catch((error: unknown) => { + console.error('worktree create: unhandled failure', creationId, error) + const store = useAppStore.getState() + if (!store.pendingWorktreeCreations[creationId]) { + return + } + const message = getWorkspaceCreateErrorToastMessage(formatWorkspaceCreateError(error)) + store.updatePendingWorktreeCreation(creationId, { status: 'error', error: message }) + // Why: the panel renders this error inline while its surface is visible; + // only announce it separately after the user has navigated away. + if (!(store.activeView === 'terminal' && store.activePendingCreationId === creationId)) { + toast.error(message) + } + }) +} + function revealPendingCreation( creationId: string, request: WorktreeCreationRequest, @@ -62,7 +86,7 @@ export function runBackgroundWorktreeCreation(request: WorktreeCreationRequest): // client over plain HTTP). createBrowserUuid falls back to getRandomValues. const creationId = createBrowserUuid() revealPendingCreation(creationId, request, getInitialWorktreeCreationPhase(request)) - void executeWorktreeCreation(creationId, request) + startWorktreeCreation(creationId, request) return creationId } @@ -101,7 +125,7 @@ export function continueBackgroundWorktreeCreation( store.setActiveView('terminal') store.setSidebarOpen(true) } - void executeWorktreeCreation(creationId, request) + startWorktreeCreation(creationId, request) return true } @@ -133,5 +157,5 @@ export function retryBackgroundWorktreeCreation(creationId: string): void { ) return } - void executeWorktreeCreation(creationId, entry.request) + startWorktreeCreation(creationId, entry.request) }