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 f0125433764..759d9dacab7 100644 --- a/src/renderer/src/lib/pane-manager/pane-reveal-fit.ts +++ b/src/renderer/src/lib/pane-manager/pane-reveal-fit.ts @@ -6,6 +6,7 @@ import { readFitClientSize, safeFit } from './pane-fit' +import { releaseRenderPauseLatch } from './terminal-render-pause-release' import { requestStablePaneFit } from './pane-fit-resize-observer' import { clearPaneFitContinuationRetry } from './pane-fit-continuation-retry' import { resumePendingFitScrollRestoreAfterFit } from './pane-scroll' @@ -62,6 +63,12 @@ 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 before the fit, not after: while the render service is latched, every + // resize below parks the renderer half and composites the pre-hide backing + // store into the new box. Measurable only — a hidden pane must stay paused. + if (canMeasurePaneForFit(pane)) { + releaseRenderPauseLatch(pane.terminal) + } // 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) diff --git a/src/renderer/src/lib/pane-manager/terminal-render-pause-release.ts b/src/renderer/src/lib/pane-manager/terminal-render-pause-release.ts index 88b53129d87..e83642604cd 100644 --- a/src/renderer/src/lib/pane-manager/terminal-render-pause-release.ts +++ b/src/renderer/src/lib/pane-manager/terminal-render-pause-release.ts @@ -41,6 +41,30 @@ function getRenderService(terminal: unknown): PausableRenderService | null { : null } +/** + * Clears the pause latch only — no repaint, no other state touched. + * + * Why: `RenderService.handleResize` branches purely on `_isPaused`. Paused, it + * parks the renderer resize in `_pausedResizeTask`, so a reveal fit resizes the + * css box while the canvas keeps compositing its pre-hide backing store — the + * blurry reveal. Unlatched first, the same fit's `terminal.resize` takes the + * inline branch: backing store, css box and dimensions are all written inside + * xterm's own resize path, and xterm's own `_fullRefresh()` follows it. + * + * Deliberately leaves `_needsFullRefresh` armed and drives no repaint of its + * own: xterm's observer recovery is what repaints if this frame doesn't, and + * owning the repaint here is what made the previous attempt paint nothing. + * Returns true when it cleared the latch. + */ +export function releaseRenderPauseLatch(terminal: unknown): boolean { + const service = getRenderService(terminal) + if (!service || service._isPaused !== true) { + return false + } + service._isPaused = false + return true +} + /** * If xterm's renderer is paused (observer hasn't caught up to the reveal), * clear the pause latch and force a synchronous full-viewport repaint.