From 2be1c125de10f12caa31fbff0ecfefdaba706a64 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:03:41 -0700 Subject: [PATCH] fix(terminal): route a reveal metric flush through the stable fit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fitRevealedPane's new flush branch called safeFit directly, which is exactly what the function's contract forbids on reveal: resumeRendering has just re-attached WebGL, whose cell metrics transiently differ from the DOM renderer's, so a raw fit can propose a one-column-off grid and reflow — and xterm's wrap/unwrap is not a perfect inverse, leaving a diff-painting inline TUI corrupted. A landed flush leaves pixels unchanged with a diverged grid, the same shape as a snapshot resize, so it takes the same steady-grid repair. A real resize still fits synchronously, after the flush. Reachable via window wake, which calls fitAllRevealedPanes with no pre-flush loop. --- .../lib/pane-manager/pane-reveal-fit.test.ts | 29 +++++++++++++++---- .../src/lib/pane-manager/pane-reveal-fit.ts | 15 +++++----- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/renderer/src/lib/pane-manager/pane-reveal-fit.test.ts b/src/renderer/src/lib/pane-manager/pane-reveal-fit.test.ts index 439b5f44313..c3ac10f44b4 100644 --- a/src/renderer/src/lib/pane-manager/pane-reveal-fit.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-reveal-fit.test.ts @@ -55,10 +55,12 @@ describe('fitRevealedPane routing', () => { mocks.flushDeferredPaneMetricOptionsIfMeasurable.mockReturnValue(false) }) - it('fits when a deferred metric flush lands on a pane the size checks would skip', () => { - // Without this the no-op branch below returns early and the parked font - // change never reaches the grid — the stuck-metrics shape the deferral - // is supposed to prevent. + it('repairs on a steady grid when a deferred metric flush lands on a pane the size checks would skip', () => { + // Without the flush branch the checks below both say "nothing to do" and the + // parked font change never reaches the grid. It must route through the stable + // path, not a raw fit: pixels are unchanged and the grid diverged, so a + // synchronous fit would reflow on the WebGL/DOM metric wobble and corrupt a + // diff-painting inline TUI. mocks.flushDeferredPaneMetricOptionsIfMeasurable.mockReturnValue(true) const pane = createPane({ lastFitClientSize: { width: 800, height: 600 }, @@ -69,8 +71,25 @@ describe('fitRevealedPane routing', () => { fitRevealedPane(pane) + expect(mocks.flushDeferredPaneMetricOptionsIfMeasurable).toHaveBeenCalledWith(pane) + expect(mocks.requestStablePaneFit).toHaveBeenCalledWith(pane) + expect(mocks.safeFit).not.toHaveBeenCalled() + }) + + it('still flushes parked metric options when the pane also resized while hidden', () => { + mocks.flushDeferredPaneMetricOptionsIfMeasurable.mockReturnValue(true) + const pane = createPane({ + lastFitClientSize: { width: 800, height: 600 }, + currentSize: { width: 640, height: 480 }, + terminal: { cols: 80, rows: 24 }, + proposed: { cols: 64, rows: 20 } + }) + + fitRevealedPane(pane) + + expect(mocks.flushDeferredPaneMetricOptionsIfMeasurable).toHaveBeenCalledWith(pane) + // A real resize still takes the synchronous path, and the flush already landed. expect(mocks.safeFit).toHaveBeenCalledWith(pane) - expect(mocks.requestStablePaneFit).not.toHaveBeenCalled() }) it('fits synchronously when the fit element resized while hidden', () => { diff --git a/src/renderer/src/lib/pane-manager/pane-reveal-fit.ts b/src/renderer/src/lib/pane-manager/pane-reveal-fit.ts index 1a6dee695ca..f0125433764 100644 --- a/src/renderer/src/lib/pane-manager/pane-reveal-fit.ts +++ b/src/renderer/src/lib/pane-manager/pane-reveal-fit.ts @@ -62,18 +62,17 @@ function releaseMeasurableFitContinuations(pane: ManagedPane): void { // mismatch refits but a transient metric wobble does not reflow; // - grid already correct → leave it alone. export function fitRevealedPane(pane: ManagedPane): void { - // Why first: the pixel/grid checks below can both say "nothing to do" and - // return without fitting, which would strand metric options parked while the - // pane was unmeasurable. A landed flush changes cell size, so it must fit. - if (flushDeferredPaneMetricOptionsIfMeasurable(pane)) { - safeFit(pane) - return - } + // Why first: the checks below can both say "nothing to do" and return without + // fitting, stranding metric options parked while the pane was unmeasurable. + const flushed = flushDeferredPaneMetricOptionsIfMeasurable(pane) if (paneFitClientSizeChanged(pane)) { safeFit(pane) return } - if (!proposedGridMatchesTerminal(pane)) { + // Why the stable path for a flush: it leaves pixels unchanged but the grid + // diverged — the same shape as a snapshot resize, and a raw fit here would + // reflow on the transient WebGL/DOM metric wobble this function exists to avoid. + if (flushed || !proposedGridMatchesTerminal(pane)) { requestStablePaneFit(pane) return }