mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 00:02:41 +00:00
Stabilize terminal fit resize observer (#2497)
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user