diff --git a/src/renderer/src/components/terminal-pane/pty-connection.test.ts b/src/renderer/src/components/terminal-pane/pty-connection.test.ts index 1d42057d539..f3b5edb149a 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.test.ts @@ -55,10 +55,10 @@ async function drainFakeTimerWork(limit = 20): Promise { // Why: a reveal remount reads the still-live park watcher entry at connect time to // tell itself apart from an in-place reattach; the host disposes it a beat later. -async function parkTabForReveal(tabId: string, ptyId: string): Promise { +async function parkTabForReveal(tabId: string, ptyId: string, worktreeId = 'wt-1'): Promise { const { parkedWatchersByTabId } = await import('./terminal-parked-watcher-registry') parkedWatchersByTabId.set(tabId, { - worktreeId: 'wt-1', + worktreeId, tabPtyId: ptyId, paneIdByPtyId: new Map([[ptyId, 1]]), disposersByPtyId: new Map([[ptyId, () => {}]]) @@ -20104,6 +20104,62 @@ describe('connectPanePty', () => { expect(transport.getPtyId).toHaveReturnedWith(remotePtyId) }) + it('restores a local main-model snapshot after a contentless park reattach', async () => { + const { connectPanePty } = await import('./pty-connection') + const localPtyId = 'global-floating-terminal@@terminal-1' + const transport = createMockTransport(localPtyId) + transport.connect.mockImplementation(async ({ sessionId }: { sessionId?: string }) => { + transport.getPtyId.mockReturnValue(localPtyId) + return sessionId ? { id: localPtyId, isReattach: true } : null + }) + transportFactoryQueue.push(transport) + const getMainBufferSnapshot = vi.mocked(window.api.pty.getMainBufferSnapshot) + getMainBufferSnapshot.mockResolvedValue({ + data: 'FLOATING-PARK-RESTORE-OK\r\n', + cols: 113, + rows: 32, + seq: 558, + source: 'headless' + }) + await parkTabForReveal('tab-1', localPtyId, 'global-floating-terminal') + mockStoreState = { + ...mockStoreState, + tabsByWorktree: { + 'global-floating-terminal': [{ id: 'tab-1', ptyId: localPtyId }] + }, + ptyIdsByTabId: { 'tab-1': [localPtyId] }, + terminalLayoutsByTabId: { + 'tab-1': { + root: { type: 'leaf', leafId: LEAF_1 }, + activeLeafId: LEAF_1, + expandedLeafId: null, + ptyIdsByLeafId: { [LEAF_1]: localPtyId } + } + } + } + + const pane = createPane(1) + const { parseCallbacks, writes } = captureCallbackTerminalWrites(pane) + const deps = createDeps({ + worktreeId: 'global-floating-terminal', + restoredLeafId: LEAF_1, + restoredPtyIdByLeafId: { [LEAF_1]: localPtyId } + }) + connectPanePty(pane as never, createManager(1) as never, deps as never) + for (let step = 0; step < 30; step += 1) { + parseCallbacks.shift()?.() + await flushAsyncTicks(2) + } + + expect(getMainBufferSnapshot).toHaveBeenCalledOnce() + expect(getMainBufferSnapshot).toHaveBeenCalledWith(localPtyId, { scrollbackRows: 5000 }) + expect(transport.connect).toHaveBeenCalledWith( + expect.objectContaining({ sessionId: localPtyId }) + ) + expect(deps.syncPanePtyLayoutBinding).toHaveBeenCalledWith(1, localPtyId) + expect(writes.join('')).toContain('FLOATING-PARK-RESTORE-OK') + }) + it('falls back to relay replay when the SSH model snapshot stalls', async () => { const { connectPanePty } = await import('./pty-connection') const { SSH_REATTACH_MODEL_SNAPSHOT_TIMEOUT_MS } = await import('./ssh-reattach-model-restore') diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index daf09427ca7..a9f72134751 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -7781,7 +7781,9 @@ export function connectPanePty( // the subscribe screen without keeping the old xterm mounted. let prefetchedParkModelSnapshot: PtyBufferSnapshot | null = null if (revealFollowsTerminalPark && (!hasStructuralReplay || isRemoteRuntimePtyId(ptyId))) { - if (isRemoteRuntimePtyId(ptyId)) { + if (parseAppSshPtyId(ptyId)) { + prefetchedParkModelSnapshot = await fetchSshMainModelReattachSnapshot() + } else { try { prefetchedParkModelSnapshot = await serializeHiddenOutputSnapshot(ptyId, { scrollbackRows: resolveHiddenRestoreScrollbackRows(pane.terminal.options.scrollback) @@ -7789,8 +7791,6 @@ export function connectPanePty( } catch { prefetchedParkModelSnapshot = null } - } else { - prefetchedParkModelSnapshot = await fetchSshMainModelReattachSnapshot() } if (!isCurrentReattachPayload()) { return false