From 305a4aaabc0deb5030228987962bfadbc05abddc Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:15:30 -0700 Subject: [PATCH] fix(terminal): report restored remote replay grid --- .../terminal-pane/pty-connection.test.ts | 49 +++++++++++++++++-- .../terminal-pane/pty-connection.ts | 46 ++++++++++++----- 2 files changed, 80 insertions(+), 15 deletions(-) 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 84571eb1e15..558c1b80d6c 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.test.ts @@ -17256,13 +17256,20 @@ describe('connectPanePty', () => { disposable.dispose() }) - it('paints remote replay at its snapshot grid before returning to the live pane grid', async () => { + it('paints remote replay at its snapshot grid and reports the live grid from a measurable hidden pane', async () => { const { connectPanePty } = await import('./pty-connection') enableActiveRuntimeEnvironment() const transport = createMockTransport('remote:env-1@@terminal-grid') const capturedReplayCallback: { current: - | ((data: string, meta?: { snapshotCols?: number; snapshotRows?: number }) => void) + | (( + data: string, + meta?: { + snapshotCols?: number + snapshotRows?: number + pendingEscapeTailAnsi?: string + } + ) => void) | null } = { current: null } transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => { @@ -17285,9 +17292,10 @@ describe('connectPanePty', () => { callback?.() }) as typeof pane.terminal.write const manager = createManager(1) - const deps = createDeps() + const deps = createDeps({ isVisibleRef: { current: false } }) const disposable = connectPanePty(pane as never, manager as never, deps as never) await flushAsyncTicks(6) + transport.resize.mockClear() capturedReplayCallback.current?.('snapshot payload', { snapshotCols: 60, @@ -17304,6 +17312,40 @@ describe('connectPanePty', () => { ) expect(pane.terminal.cols).toBe(180) expect(pane.terminal.rows).toBe(50) + expect(transport.resize).toHaveBeenCalledWith(180, 50) + + vi.mocked(pane.terminal.resize).mockClear() + capturedReplayCallback.current?.('unknown-size payload', { + snapshotCols: 0, + snapshotRows: 0 + }) + await flushAsyncTicks(12) + expect(pane.terminal.resize).not.toHaveBeenCalledWith(0, 0) + + vi.mocked(pane.terminal.resize).mockClear() + capturedReplayCallback.current?.('', { + snapshotCols: 30, + snapshotRows: 10, + pendingEscapeTailAnsi: '\x1b[3' + }) + await flushAsyncTicks(12) + expect(pane.terminal.resize).not.toHaveBeenCalledWith(30, 10) + + const { setDriverForPty } = await import('@/lib/pane-manager/mobile-driver-state') + setDriverForPty('remote:env-1@@terminal-grid', { kind: 'mobile', clientId: 'phone-1' }) + transport.resize.mockClear() + try { + capturedReplayCallback.current?.('mobile-owned payload', { + snapshotCols: 60, + snapshotRows: 20 + }) + await flushAsyncTicks(12) + expect(pane.terminal.cols).toBe(180) + expect(pane.terminal.rows).toBe(50) + expect(transport.resize).not.toHaveBeenCalled() + } finally { + setDriverForPty('remote:env-1@@terminal-grid', { kind: 'idle' }) + } disposable.dispose() }) @@ -25807,6 +25849,7 @@ describe('connectPanePty', () => { expect(getSize).not.toHaveBeenCalled() expect(pane.fitAddon.proposeDimensions).toHaveBeenCalled() + expect(pane.fitAddon.fit).not.toHaveBeenCalled() expect(transport.resize).toHaveBeenCalledWith(120, 40) }) diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index 79ea0bcfaf3..9f8c4239534 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -4350,8 +4350,29 @@ export function connectPanePty( } } } + const reportRemoteViewportAfterFit = ( + ptyId: string | null, + expectedStreamGeneration: number + ): void => { + if ( + !ptyId || + !isRemoteRuntimePtyId(ptyId) || + disposed || + transport.getPtyId() !== ptyId || + transportStreamGeneration !== expectedStreamGeneration || + shouldSuppressDesktopPtyResize() + ) { + return + } + const cols = pane.terminal.cols + const rows = pane.terminal.rows + if (Number.isFinite(cols) && Number.isFinite(rows) && cols > 0 && rows > 0) { + transport.resize(cols, rows) + } + } const reportRemoteViewportOnReveal = (): void => { const revealedPtyId = transport.getPtyId() + const revealedStreamGeneration = transportStreamGeneration if ( !revealedPtyId || !isRemoteRuntimePtyId(revealedPtyId) || @@ -4362,14 +4383,7 @@ export function connectPanePty( return } safeFitAndThen(pane, 'remote-reveal-viewport', () => { - if (disposed || transport.getPtyId() !== revealedPtyId || shouldSuppressDesktopPtyResize()) { - return - } - const cols = pane.terminal.cols - const rows = pane.terminal.rows - if (Number.isFinite(cols) && Number.isFinite(rows) && cols > 0 && rows > 0) { - transport.resize(cols, rows) - } + reportRemoteViewportAfterFit(revealedPtyId, revealedStreamGeneration) }) } let pendingForegroundGridDriftCheckRaf: number | null = null @@ -5649,7 +5663,8 @@ export function connectPanePty( let replayDrainQueued = false const drainReplayDataQueue = async ( expectedPtyId: string | null, - expectedStreamGeneration: number + expectedStreamGeneration: number, + noteSnapshotGridReplay: () => void ): Promise => { let appliedCurrentPayload = false while (pendingReplayData !== null) { @@ -5692,9 +5707,11 @@ export function connectPanePty( ) if ( snapshotDimensions && + data.length > 0 && (pane.terminal.cols !== snapshotDimensions.cols || pane.terminal.rows !== snapshotDimensions.rows) ) { + noteSnapshotGridReplay() suppressStructuralReplayPtyResize = true try { pane.terminal.resize(snapshotDimensions.cols, snapshotDimensions.rows) @@ -5760,6 +5777,7 @@ export function connectPanePty( pendingReplayData?.streamGeneration ?? transportStreamGeneration beginReattachLiveDataDeferral(scheduledStreamGeneration) let replayCompleted = false + let replayUsedSnapshotGrid = false replayWriteQueue = replayWriteQueue .catch(() => undefined) .then(() => @@ -5767,7 +5785,10 @@ export function connectPanePty( async () => { replayCompleted = await drainReplayDataQueue( scheduledPtyId, - scheduledStreamGeneration + scheduledStreamGeneration, + () => { + replayUsedSnapshotGrid = true + } ) }, { @@ -5776,8 +5797,9 @@ export function connectPanePty( transport.getPtyId() === scheduledPtyId && transportStreamGeneration === scheduledStreamGeneration, afterRestore: () => { - if (replayCompleted) { - safeFit(pane) + if (replayCompleted && replayUsedSnapshotGrid && safeFit(pane)) { + // Why: a measurable hidden pane is authoritative; only presence ownership gates this report (#12768). + reportRemoteViewportAfterFit(scheduledPtyId, scheduledStreamGeneration) } } }