diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index d7b36d0f020..a7432fad614 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -3337,7 +3337,7 @@ export function connectPanePty( if (transport.isConnected() && transport.getPtyId() !== null) { return } - const storePtyId = useAppStore.getState().ptyIdsByTabId[deps.tabId]?.[0] ?? null + const storePtyId = useAppStore.getState().ptyIdsByTabId?.[deps.tabId]?.[0] ?? null void requestTerminalPaneRecovery({ tabId: deps.tabId, ptyId: transport.getPtyId() ?? storePtyId, @@ -3351,7 +3351,7 @@ export function connectPanePty( const unregisterUndeliverableWriteHandler = registerUndeliverableWriteHandler( pane.terminal, (reason) => { - const storePtyId = useAppStore.getState().ptyIdsByTabId[deps.tabId]?.[0] ?? null + const storePtyId = useAppStore.getState().ptyIdsByTabId?.[deps.tabId]?.[0] ?? null void requestTerminalPaneRecovery({ tabId: deps.tabId, ptyId: transport.getPtyId() ?? storePtyId, diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-recovery.test.ts b/src/renderer/src/components/terminal-pane/terminal-pane-recovery.test.ts index 8d1d511dd20..45eff4e4ffb 100644 --- a/src/renderer/src/components/terminal-pane/terminal-pane-recovery.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-pane-recovery.test.ts @@ -149,6 +149,19 @@ describe('requestTerminalPaneRecovery', () => { expect(mocks.remountTerminalTabForRecovery).not.toHaveBeenCalled() }) + it('never throws when the store surface is partial (timer/callback contexts)', async () => { + // Regression: recovery fires from stall-watch timers and write callbacks; + // an environment with a partial store (mocked suites, teardown races) must + // get a false return, not an unhandled TypeError. + mocks.remountTerminalTabForRecovery.mockImplementation(() => { + throw new TypeError('remountTerminalTabForRecovery is not a function') + }) + + await expect( + requestTerminalPaneRecovery({ tabId: 'tab-1', ptyId: 'pty-1', reason: 'write-stalled' }) + ).resolves.toBe(false) + }) + it('does not consume budget when the tab no longer exists', async () => { mocks.remountTerminalTabForRecovery.mockReturnValue(false) diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-recovery.ts b/src/renderer/src/components/terminal-pane/terminal-pane-recovery.ts index d7b20d81e7c..65afa9fe006 100644 --- a/src/renderer/src/components/terminal-pane/terminal-pane-recovery.ts +++ b/src/renderer/src/components/terminal-pane/terminal-pane-recovery.ts @@ -78,7 +78,15 @@ export async function requestTerminalPaneRecovery(request: RecoveryRequest): Pro return false } } - const remounted = useAppStore.getState().remountTerminalTabForRecovery(request.tabId) + let remounted = false + try { + remounted = useAppStore.getState().remountTerminalTabForRecovery(request.tabId) + } catch { + // Why: recovery fires from timer and write-callback contexts (stall watch, + // replay guard, onData) — it is best-effort by contract and must never + // surface a throw there (partial store surfaces in tests, teardown races). + return false + } if (!remounted) { return false }