From 25bf0f8fc74a67e8ba02fb00b87692b7601e0a9c Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 7 Aug 2026 01:08:51 -0700 Subject: [PATCH] fix(terminal): flush heavy reveal metrics after WebGL resume --- .../terminal-visibility-resume.test.ts | 15 +++++++++++++-- .../terminal-pane/terminal-visibility-resume.ts | 17 ++++++++--------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts b/src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts index 656bef73c3e..c6b82f2470c 100644 --- a/src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts @@ -136,6 +136,17 @@ describe('resumeTerminalVisibility reveal repaint', () => { expect(manager.fitAllPanes).not.toHaveBeenCalled() }) + it('leaves heavy metric flushing to the reveal fit after rendering resumes', () => { + const manager = createManager() + manager.getPanes.mockReturnValue([{ terminal: {} }]) + + resumeTerminalVisibility(resumeArgs(manager, false)) + + expect(manager.resumeRendering).toHaveBeenCalledTimes(1) + expect(manager.fitAllRevealedPanes).toHaveBeenCalledTimes(1) + expect(flushDeferredPaneMetricOptionsIfMeasurable).not.toHaveBeenCalled() + }) + it('does not fit on a light tab reveal', () => { const manager = createManager() resumeTerminalVisibility(resumeArgs(manager, true)) @@ -145,8 +156,8 @@ describe('resumeTerminalVisibility reveal repaint', () => { }) it('flushes hidden-era metric options on reveal and refits the light path', () => { - // P0 bold/blurry-font shape: a font change while hidden defers; the reveal - // must land it and refit, or cols/rows stay pinned to the old metrics. + // A font change while hidden must land and refit on reveal, or cols/rows + // stay pinned to the old metrics. const manager = createManager() manager.getPanes.mockReturnValue([{ terminal: {} }]) flushDeferredPaneMetricOptionsIfMeasurable.mockReturnValueOnce(true) diff --git a/src/renderer/src/components/terminal-pane/terminal-visibility-resume.ts b/src/renderer/src/components/terminal-pane/terminal-visibility-resume.ts index aa8f3732e1b..01d3627d2c1 100644 --- a/src/renderer/src/components/terminal-pane/terminal-visibility-resume.ts +++ b/src/renderer/src/components/terminal-pane/terminal-visibility-resume.ts @@ -72,16 +72,13 @@ export function resumeTerminalVisibility({ // not jump to the wrong history entry. captureViewportPositions(!wasVisible) withSuppressedScrollTracking(() => { - // Why before the branch: metric options deferred while hidden must land ahead - // of any fit/repaint below so the first visible frame uses correct metrics. - // Still-unmeasurable panes keep their deferral; the fit path flushes those. - let flushedDeferredMetrics = false - for (const pane of manager.getPanes()) { - if (flushDeferredPaneMetricOptionsIfMeasurable(pane)) { - flushedDeferredMetrics = true - } - } if (shouldUseLightTabResume) { + let flushedDeferredMetrics = false + for (const pane of manager.getPanes()) { + if (flushDeferredPaneMetricOptionsIfMeasurable(pane)) { + flushedDeferredMetrics = true + } + } // Why: intra-worktree tab switches only toggle the overlay. Keeping // synchronous drain and atlas rebuilds off this path avoids racing the // overlay's delayed geometry fit. Still request hidden-output recovery: @@ -100,6 +97,8 @@ export function resumeTerminalVisibility({ focusActivePane(manager) } } else { + // fitAllRevealedPanes flushes after WebGL reattaches, avoiding a redundant + // full refresh in the suspended DOM renderer while preserving first paint. resumeTerminalVisibilityHeavy(manager, isActive) } enforceTerminalViewportIntents(manager)