diff --git a/src/renderer/src/components/automations/automation-run-open-target.ts b/src/renderer/src/components/automations/automation-run-open-target.ts index f70a8ad9d94..04a4fb09e09 100644 --- a/src/renderer/src/components/automations/automation-run-open-target.ts +++ b/src/renderer/src/components/automations/automation-run-open-target.ts @@ -105,6 +105,7 @@ export function resolveAutomationRunTerminalTarget( const mirroredTabId = toWebTerminalSurfaceTabId(tabId) const layout = terminals.terminalLayoutsByTabId[mirroredTabId] const mirroredPtyId = layout?.ptyIdsByLeafId?.[parsed.leafId] + // Why a live PTY only: a slept run's pane is asked of the server, which knows if it still exists. if ( !run.terminalPtyId || !terminals.hasTerminalTab(mirroredTabId) || diff --git a/src/renderer/src/components/automations/automation-run-view-state.test.ts b/src/renderer/src/components/automations/automation-run-view-state.test.ts index dc50ad0b319..a211c4d390f 100644 --- a/src/renderer/src/components/automations/automation-run-view-state.test.ts +++ b/src/renderer/src/components/automations/automation-run-view-state.test.ts @@ -91,6 +91,21 @@ describe('automation run view state', () => { }) }) + it('keeps View run for a paired server run whose pane slept, which only the server can open', () => { + const slept = makeRun({ terminalPtyId: null }) + expect( + getAutomationRunViewState({ + run: slept, + workspaceExists: true, + terminalTargetExists: false, + terminalOnPairedServer: true + }) + ).toMatchObject({ availability: 'terminal', actionLabel: 'View run', canOpen: true }) + expect( + getAutomationRunViewState({ run: slept, workspaceExists: true, terminalTargetExists: false }) + ).toMatchObject({ availability: 'workspace', actionLabel: 'Open workspace' }) + }) + it('resumes the workspace only when there is no exact terminal identity', () => { expect( getAutomationRunViewState({ diff --git a/src/renderer/src/components/automations/automation-run-view-state.ts b/src/renderer/src/components/automations/automation-run-view-state.ts index c8066c0146b..ce4cc9f7773 100644 --- a/src/renderer/src/components/automations/automation-run-view-state.ts +++ b/src/renderer/src/components/automations/automation-run-view-state.ts @@ -61,6 +61,8 @@ export function getAutomationRunViewState({ terminalOnPairedServer?: boolean }): AutomationRunViewState { const hasTerminalIdentity = Boolean(run.terminalPaneKey && run.terminalPtyId) + // Why the pane alone: a slept run drops its PTY id but keeps its pane, which the server resumes. + const hasPaneIdentity = Boolean(run.terminalPaneKey) if (run.workspaceId && workspaceExists && terminalTargetExists) { return { availability: 'terminal', @@ -70,7 +72,7 @@ export function getAutomationRunViewState({ } } - if (run.workspaceId && workspaceExists && hasTerminalIdentity && terminalOnPairedServer) { + if (run.workspaceId && workspaceExists && hasPaneIdentity && terminalOnPairedServer) { return { availability: 'terminal', actionLabel: 'View run', diff --git a/src/renderer/src/components/automations/automation-run-workspace-action.paired-server.test.ts b/src/renderer/src/components/automations/automation-run-workspace-action.paired-server.test.ts index 517369e46e4..e1d3533411f 100644 --- a/src/renderer/src/components/automations/automation-run-workspace-action.paired-server.test.ts +++ b/src/renderer/src/components/automations/automation-run-workspace-action.paired-server.test.ts @@ -95,13 +95,13 @@ function storeState(mirrored: boolean): Record { } } -function openRun(): void { +function openRun(target: AutomationRun = run): void { // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the action reads only these members. const context = { store: { repoForRow: () => undefined, worktreeForRow: () => worktree }, list: { selectedRow: { key: 'row' } } } as unknown as AutomationsPageActionContext - createAutomationRunWorkspaceAction(context)(run) + createAutomationRunWorkspaceAction(context)(target) } describe('opening a paired-server automation run', () => { @@ -142,4 +142,17 @@ describe('opening a paired-server automation run', () => { expect(mocks.toastError).toHaveBeenCalledWith('Run terminal is unavailable.') ) }) + + it('asks the server to open a run whose pane it slept, PTY id dropped', async () => { + mocks.state = storeState(false) + + openRun({ ...run, terminalPtyId: null }) + await vi.waitFor(() => expect(mocks.activateWebRuntimeSessionTab).toHaveBeenCalled()) + + expect(mocks.activateAndRevealWorktree).toHaveBeenCalledWith(WT) + expect(mocks.activateWebRuntimeSessionTab).toHaveBeenCalledWith( + expect.objectContaining({ worktreeId: WT, tabId: HOST_TAB, environmentId: ENV, leafId: LEAF }) + ) + expect(mocks.toastMessage).not.toHaveBeenCalled() + }) })