diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.test.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.test.ts index bfb329a4d..c563b543d 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.test.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.test.ts @@ -388,6 +388,77 @@ describe('useTerminalPaneGlobalEffects', () => { expect(terminal.write).toHaveBeenCalledWith('new-output', expect.any(Function)) }) + it('does not replay stale hydration after the pane transport is replaced', async () => { + const terminal = { + name: 'terminal-a', + options: { scrollback: 1000 }, + write: vi.fn((_data: string, callback?: () => void) => callback?.()) + } + const pane = { id: 1, terminal } + const oldTransport = { getPtyId: vi.fn(() => 'pty-1') } + const newTransport = { getPtyId: vi.fn(() => 'pty-2') } + const paneTransports = new Map([ + [1, oldTransport] + ]) + const manager = { + getPanes: vi.fn(() => [pane]), + resumeRendering: vi.fn(), + suspendRendering: vi.fn(), + fitAllPanes: vi.fn(), + getActivePane: vi.fn(() => null), + setActivePane: vi.fn() + } + let resolveSnapshot!: (snapshot: { data: string; cols: number; rows: number } | null) => void + const snapshotPromise = new Promise<{ data: string; cols: number; rows: number } | null>( + (resolve) => { + resolveSnapshot = resolve + } + ) + window.api.pty.serializeHeadlessBuffer = vi.fn((id: string) => { + if (id === 'pty-1') { + return snapshotPromise + } + return Promise.resolve(null) + }) as typeof window.api.pty.serializeHeadlessBuffer + const baseArgs = { + tabId: 'tab-1', + worktreeId: 'wt-1', + isActive: true, + isVisible: true, + paneCount: 1, + managerRef: { current: manager as never }, + containerRef: { current: null }, + paneTransportsRef: { current: paneTransports as never }, + replayingPanesRef: { current: new Map() }, + isActiveRef: { current: false }, + isVisibleRef: { current: false }, + toggleExpandPane: vi.fn() + } + queueHiddenTerminalOutput(terminal, 'pty-1', 'old-output') + + beginHookRender() + useTerminalPaneGlobalEffects(baseArgs) + + paneTransports.set(1, newTransport) + queueHiddenTerminalOutput(terminal, 'pty-2', 'new-output') + resolveSnapshot({ data: 'stale-headless-output', cols: 120, rows: 40 }) + await Promise.resolve() + await Promise.resolve() + + expect(terminal.write).not.toHaveBeenCalledWith('stale-headless-output', expect.any(Function)) + terminal.write.mockClear() + + beginHookRender() + useTerminalPaneGlobalEffects(baseArgs) + await Promise.resolve() + await Promise.resolve() + + expect(window.api.pty.serializeHeadlessBuffer).toHaveBeenCalledWith('pty-2', { + scrollbackRows: 1000 + }) + expect(terminal.write).toHaveBeenCalledWith('new-output', expect.any(Function)) + }) + it('restores from the pre-hide scroll state when hidden layout changes the viewport', () => { const terminalA = { name: 'terminal-a' } const manager = { diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts index 643ea6092..341bd3b26 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts @@ -88,8 +88,16 @@ export function useTerminalPaneGlobalEffects({ if (!hydration) { return } - const transport = paneTransportsRef.current.get(pane.id) - if (transport?.getPtyId() !== hydration.ptyId) { + const getCurrentPtyId = (): string | null => { + const currentPane = managerRef.current + ?.getPanes() + .find((candidate) => candidate.id === pane.id) + if (currentPane?.terminal !== pane.terminal) { + return null + } + return paneTransportsRef.current.get(pane.id)?.getPtyId() ?? null + } + if (getCurrentPtyId() !== hydration.ptyId) { clearHiddenTerminalOutputForPty(pane.terminal, hydration.ptyId) return } @@ -104,7 +112,7 @@ export function useTerminalPaneGlobalEffects({ cancelHiddenTerminalHydration(pane.terminal, hydration) return } - if (transport.getPtyId() !== hydration.ptyId) { + if (getCurrentPtyId() !== hydration.ptyId) { clearHiddenTerminalOutputForPty(pane.terminal, hydration.ptyId) return } @@ -138,7 +146,7 @@ export function useTerminalPaneGlobalEffects({ cancelHiddenTerminalHydration(pane.terminal, hydration) return } - if (transport.getPtyId() !== hydration.ptyId) { + if (getCurrentPtyId() !== hydration.ptyId) { clearHiddenTerminalOutputForPty(pane.terminal, hydration.ptyId) return } @@ -156,7 +164,7 @@ export function useTerminalPaneGlobalEffects({ markHiddenTerminalFallbackReplayed(pane.terminal, hydration) }) }, - [isVisibleRef, paneTransportsRef, replayingPanesRef] + [isVisibleRef, managerRef, paneTransportsRef, replayingPanesRef] ) useEffect(() => {