Stabilize terminal fit resize observer (#2497)

This commit is contained in:
Jinwoo Hong
2026-05-29 17:34:01 -07:00
committed by GitHub
parent ff5903b84a
commit 080a270700
2 changed files with 134 additions and 8 deletions
@@ -32,7 +32,10 @@ function flushAnimationFrames(timestamp = 16): void {
}
}
function createPane(): ManagedPaneInternal {
function createPane(
proposeDimensions: () => { cols: number; rows: number } = () => ({ cols: 80, rows: 24 }),
options: { rect?: { width: number; height: number } } = {}
): ManagedPaneInternal {
const leafId = '11111111-1111-4111-8111-111111111111' as never
return {
id: 1,
@@ -43,7 +46,9 @@ function createPane(): ManagedPaneInternal {
rows: 24
} as never,
container: { dataset: {} } as never,
xtermContainer: {} as never,
xtermContainer: {
getBoundingClientRect: () => options.rect ?? ({ width: 800, height: 600 } as DOMRect)
} as never,
linkTooltip: {} as never,
terminalGpuAcceleration: 'auto',
gpuRenderingEnabled: true,
@@ -52,7 +57,7 @@ function createPane(): ManagedPaneInternal {
hasComplexScriptOutput: false,
fitAddon: {
fit: vi.fn(),
proposeDimensions: vi.fn(() => ({ cols: 80, rows: 24 }))
proposeDimensions: vi.fn(proposeDimensions)
} as never,
fitResizeObserver: null,
pendingObservedFitRafId: null,
@@ -101,7 +106,7 @@ describe('attachPaneFitResizeObserver', () => {
vi.restoreAllMocks()
})
it('coalesces repeated observer callbacks into a single fit per frame', () => {
it('coalesces repeated observer callbacks and fits once the grid is stable', () => {
const pane = createPane()
attachPaneFitResizeObserver(pane)
@@ -116,6 +121,57 @@ describe('attachPaneFitResizeObserver', () => {
expect(pane.fitAddon.fit).toHaveBeenCalledTimes(1)
})
it('waits through a transient grid wobble before fitting', () => {
const proposed = [
{ cols: 80, rows: 24 },
{ cols: 81, rows: 24 },
{ cols: 81, rows: 24 }
]
const pane = createPane(() => proposed.shift() ?? { cols: 81, rows: 24 })
attachPaneFitResizeObserver(pane)
mockResizeObservers[0]?.trigger()
flushAnimationFrames()
expect(pane.fitAddon.fit).not.toHaveBeenCalled()
flushAnimationFrames()
expect(pane.fitAddon.fit).toHaveBeenCalledTimes(1)
})
it('skips observer fits while the terminal has no visible geometry', () => {
const pane = createPane(() => ({ cols: 80, rows: 24 }), {
rect: { width: 0, height: 0 }
})
attachPaneFitResizeObserver(pane)
mockResizeObservers[0]?.trigger()
flushAnimationFrames()
expect(requestAnimationFrame).not.toHaveBeenCalled()
expect(pane.fitAddon.fit).not.toHaveBeenCalled()
})
it('throttles an endlessly unstable grid instead of fitting every frame', () => {
let cols = 80
const pane = createPane(() => {
cols = cols === 80 ? 81 : 80
return { cols, rows: 24 }
})
attachPaneFitResizeObserver(pane)
mockResizeObservers[0]?.trigger()
for (let i = 0; i < 10; i += 1) {
flushAnimationFrames()
}
expect(pane.fitAddon.fit).toHaveBeenCalledTimes(1)
expect(pane.pendingObservedFitRafId).toBeNull()
})
it('disconnects the observer and cancels any queued fit', () => {
const pane = createPane()
@@ -1,6 +1,34 @@
import type { ManagedPaneInternal } from './pane-manager-types'
import { safeFit } from './pane-tree-ops'
type ProposedDimensions = {
cols: number
rows: number
}
const MAX_STABILITY_FRAMES = 8
function getProposedDimensions(pane: ManagedPaneInternal): ProposedDimensions | null {
try {
return pane.fitAddon.proposeDimensions() ?? null
} catch {
return null
}
}
function dimensionsEqual(a: ProposedDimensions | null, b: ProposedDimensions | null): boolean {
return a?.cols === b?.cols && a?.rows === b?.rows
}
function terminalDimensionsEqual(pane: ManagedPaneInternal, dims: ProposedDimensions): boolean {
return pane.terminal.cols === dims.cols && pane.terminal.rows === dims.rows
}
function hasVisibleFitGeometry(pane: ManagedPaneInternal): boolean {
const rect = pane.xtermContainer.getBoundingClientRect?.()
return !rect || (rect.width > 0 && rect.height > 0)
}
export function attachPaneFitResizeObserver(pane: ManagedPaneInternal): void {
detachPaneFitResizeObserver(pane)
@@ -12,12 +40,54 @@ export function attachPaneFitResizeObserver(pane: ManagedPaneInternal): void {
if (pane.pendingObservedFitRafId !== null) {
return
}
if (!hasVisibleFitGeometry(pane)) {
return
}
// Why: keep xterm fit work off the divider pointermove hot path and let
// the browser coalesce drag-driven size changes the same way Superset does.
pane.pendingObservedFitRafId = requestAnimationFrame(() => {
pane.pendingObservedFitRafId = null
safeFit(pane)
})
//
// Windows can report a short-lived one-column anchor/scrollbar wobble when
// the right sidebar is open. Requiring a stable proposed grid before fitting
// prevents Codex from receiving a rapid SIGWINCH loop and visibly vibrating.
let previous = getProposedDimensions(pane)
let frameCount = 0
const waitForStableGrid = (): void => {
pane.pendingObservedFitRafId = requestAnimationFrame(() => {
if (!hasVisibleFitGeometry(pane)) {
pane.pendingObservedFitRafId = null
return
}
const next = getProposedDimensions(pane)
frameCount += 1
if (!next) {
pane.pendingObservedFitRafId = null
safeFit(pane)
return
}
if (terminalDimensionsEqual(pane, next)) {
pane.pendingObservedFitRafId = null
return
}
if (dimensionsEqual(previous, next)) {
pane.pendingObservedFitRafId = null
safeFit(pane)
return
}
previous = next
if (frameCount >= MAX_STABILITY_FRAMES) {
pane.pendingObservedFitRafId = null
safeFit(pane)
return
}
waitForStableGrid()
})
}
waitForStableGrid()
})
observer.observe(pane.xtermContainer)