fix(terminal): route a reveal metric flush through the stable fit

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.
This commit is contained in:
Brennan Benson
2026-08-06 14:03:41 -07:00
parent ea0233513c
commit 2be1c125de
2 changed files with 31 additions and 13 deletions
@@ -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', () => {
@@ -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
}