fix(terminal): gate metric writes on the pixel box, not the fit floor

canApplyPaneMetricOptions reused canMeasurePaneForFit, whose >=8 cols /
>=4 rows floor exists to stop a fit pinning the PTY to a sliver. But the
divider clamp is 50px, which clears the 48px pixel floor and proposes
~5 cols — so a pane dragged to the clamp deferred every font change and
never flushed: it never hides, and its box never changes, so no reveal
and no ResizeObserver entry ever arrives. It rendered a stale font until
widened, where pre-PR the write was unconditional.

Gate metric writes on display plus the pixel box only. Hidden panes and
the transient worktree-switch overlay are near-zero, so they still
defer — the deferral's purpose is unchanged. The cols/rows floor stays
on the fit, including the post-flush re-check in performSafeFit.

Apply and flush share the same predicate, so no "applies but never
flushes" state can open up.
This commit is contained in:
Brennan Benson
2026-08-06 14:11:47 -07:00
parent 2be1c125de
commit 58cb556acc
2 changed files with 45 additions and 7 deletions
@@ -20,13 +20,18 @@ export function getProposedPaneDimensions(
}
}
export function canMeasurePaneForFit(pane: ManagedPane): boolean {
function hasPaneFitPixelBox(pane: ManagedPane): boolean {
const measure = pane.container?.getBoundingClientRect
if (typeof measure === 'function') {
const rect = measure.call(pane.container)
if (rect.width < MIN_PANE_FIT_WIDTH_PX || rect.height < MIN_PANE_FIT_HEIGHT_PX) {
return false
}
if (typeof measure !== 'function') {
return true
}
const rect = measure.call(pane.container)
return rect.width >= MIN_PANE_FIT_WIDTH_PX && rect.height >= MIN_PANE_FIT_HEIGHT_PX
}
export function canMeasurePaneForFit(pane: ManagedPane): boolean {
if (!hasPaneFitPixelBox(pane)) {
return false
}
const dims = getProposedPaneDimensions(pane)
if (!dims) {
@@ -37,8 +42,14 @@ export function canMeasurePaneForFit(pane: ManagedPane): boolean {
return dims.cols >= MIN_PANE_FIT_COLS && dims.rows >= MIN_PANE_FIT_ROWS
}
/** Why only the pixel box, not the fit floor: a pane held at the 50px divider
* clamp proposes ~5 cols, so the fit floor would reject it forever — it never
* hides and its box never changes, so nothing would flush and it would render
* a stale font until widened. A hidden pane or the transient worktree-switch
* overlay is near-zero, so the pixel floor still defers those. The cols/rows
* floor stays where it belongs: on the fit. */
export function canApplyPaneMetricOptions(pane: ManagedPane): boolean {
return !isManagedPaneDisplayNone(pane) && canMeasurePaneForFit(pane)
return !isManagedPaneDisplayNone(pane) && hasPaneFitPixelBox(pane)
}
/** Why the pending check comes first: it is an O(1) WeakMap lookup, while the
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import type { ManagedPane, ManagedPaneInternal } from './pane-manager-types'
import { toPublicPane } from './pane-public-view'
import { canApplyPaneMetricOptions, canMeasurePaneForFit } from './pane-fit-measurability'
import {
applyOrDeferPaneMetricOptions,
flushDeferredPaneMetricOptions,
@@ -138,3 +139,29 @@ describe('pane-metric-options-deferral', () => {
expect(pane.terminal.options.fontSize).toBeUndefined()
})
})
describe('canApplyPaneMetricOptions gating', () => {
function makeSizedPane(rect: { width: number; height: number }, cols: number): ManagedPane {
return {
id: 2,
terminal: { options: {} },
container: { getBoundingClientRect: () => rect },
fitAddon: { proposeDimensions: () => ({ cols, rows: 20 }) }
} as unknown as ManagedPane
}
it('applies to a pane clamped narrow by a divider drag', () => {
// 50px is the divider clamp: over the pixel floor but ~5 cols, under the
// fit floor. Gating on the fit floor would strand it on a stale font,
// because it never hides and its box never changes.
expect(canApplyPaneMetricOptions(makeSizedPane({ width: 50, height: 600 }, 5))).toBe(true)
})
it('still defers on a near-zero box (hidden pane / worktree-switch overlay)', () => {
expect(canApplyPaneMetricOptions(makeSizedPane({ width: 0, height: 0 }, 0))).toBe(false)
})
it('leaves the cols/rows floor on the fit itself', () => {
expect(canMeasurePaneForFit(makeSizedPane({ width: 50, height: 600 }, 5))).toBe(false)
})
})