From 58cb556accc7ff9a05dd94ffcbe653d06fa9adc4 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:11:47 -0700 Subject: [PATCH] fix(terminal): gate metric writes on the pixel box, not the fit floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../pane-manager/pane-fit-measurability.ts | 25 ++++++++++++----- .../pane-metric-options-deferral.test.ts | 27 +++++++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/lib/pane-manager/pane-fit-measurability.ts b/src/renderer/src/lib/pane-manager/pane-fit-measurability.ts index fcd6a4615d3..57186374409 100644 --- a/src/renderer/src/lib/pane-manager/pane-fit-measurability.ts +++ b/src/renderer/src/lib/pane-manager/pane-fit-measurability.ts @@ -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 diff --git a/src/renderer/src/lib/pane-manager/pane-metric-options-deferral.test.ts b/src/renderer/src/lib/pane-manager/pane-metric-options-deferral.test.ts index f3ffa1cf4c9..e07ae670d18 100644 --- a/src/renderer/src/lib/pane-manager/pane-metric-options-deferral.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-metric-options-deferral.test.ts @@ -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) + }) +})