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 bfde8f99a5e..784f329a102 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.test.ts @@ -9094,10 +9094,7 @@ describe('connectPanePty', () => { expect(transport.resize.mock.calls).toEqual([]) }) - it('reports the destination grid from a background measure-lease pane', async () => { - // Why this test: the report is deliberately NOT gated on visibility here. A hidden - // pane that still measures is under the background measure lease, so its grid is - // authoritative — and reveal never re-sends it, because the pixels never changed. + it('refits but does not report the destination grid from a background measure-lease pane', async () => { const { pane, transport, replayCallback } = await connectRemoteSnapshotGridPane( 'remote:web-env-1@@pty-replay-grid-hidden', { visible: false } @@ -9106,9 +9103,12 @@ describe('connectPanePty', () => { replayCallback.current?.('remote snapshot bytes', { snapshotCols: 80, snapshotRows: 24 }) await flushAsyncTicks(20) + // A measure-lease pane is fully sized, so the fit lands — but reporting from a + // hidden pane would persist as a claim on the next subscribe. Its grid stays + // unreported until the next layout change after reveal; see the send condition. expect(pane.fitAddon.fit).toHaveBeenCalled() expect(pane.terminal.cols).toBe(120) - expect(transport.resize).toHaveBeenCalledWith(120, 40) + expect(transport.resize).not.toHaveBeenCalled() }) it('arms no fit retry after replaying into an unmeasurable pane', async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index 5d0ddb7c8fd..0f0b0052184 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -5813,16 +5813,21 @@ export function connectPanePty( } const destinationCols = pane.terminal.cols const destinationRows = pane.terminal.rows - // Why NOT gated on visibility, unlike the sibling fits: reaching here - // means the fit measured a real box, which for a hidden pane means the - // background measure lease deliberately laid it out full size — an - // authoritative grid, not hidden-layout churn. Reveal will not re-send - // it either: the pixels never changed, so fitRevealedPane skips, and - // remote PTYs are excluded from ptySizeReassertion. This is the only - // chance to report. (This path never signals SIGWINCH.) + // Why the visibility gate, matching the sibling at the hidden-snapshot + // restore: the transport remembers every viewport handed to resize() + // before the claim branch and replays it with claim intent on the next + // subscribe, so a send from a hidden pane reaches other clients as a + // durable claim. Accepted tradeoff: a background measure-lease pane is + // fully sized, so its fit succeeds and its grid then goes unreported + // until the next layout change after reveal — fitRevealedPane will not + // re-fit a pane whose pixels never changed (opacity does not affect + // getBoundingClientRect) and remote PTYs have no ptySizeReassertion + // drift repair. A stale grid until the next resize is the more + // conservative failure than an unrevocable wrong-grid claim. if ( destinationCols > 0 && destinationRows > 0 && + isRendererPtyResizeAuthoritative() && !shouldSuppressDesktopPtyResize() ) { transport.resize(destinationCols, destinationRows)