mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(terminal): keep xterm's render pause latched for a pane with no layout box
resetWebglTextureAtlas() released xterm's paused-render gate for every pane of a visible manager, including panes that are display:none (a collapsed sibling of an expanded pane, a restore that stays display:none for its whole reattach). forceRepaintThroughRenderPause exists for a pane that is already DOM-visible while xterm's IntersectionObserver lags a frame. On a pane with no box it paints the freshly cleared render model into nothing, and because the observer only fires on a state change it never re-pauses the service. It also clears _needsFullRefresh, which is the only thing that makes _handleIntersectionChange repaint on reveal and flush the deferred _pausedResizeTask. Latch instead for those panes: terminal.refresh() re-arms _needsFullRefresh and xterm repaints from it on reveal.
This commit is contained in:
@@ -63,6 +63,42 @@ function createFittablePane(): ManagedPaneInternal {
|
||||
return pane
|
||||
}
|
||||
|
||||
type FakeRenderService = {
|
||||
_isPaused: boolean
|
||||
_needsFullRefresh: boolean
|
||||
refreshRows: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
/** A pane whose xterm render service is paused, as it is whenever the pane has
|
||||
* no layout box: `display` decides whether the element is one xterm's
|
||||
* IntersectionObserver would report as intersecting. */
|
||||
function createPausedPane(display: 'block' | 'none'): {
|
||||
pane: ManagedPaneInternal
|
||||
renderService: FakeRenderService
|
||||
} {
|
||||
const pane = createPane()
|
||||
const renderService: FakeRenderService = {
|
||||
_isPaused: true,
|
||||
_needsFullRefresh: false,
|
||||
// Mirrors xterm: a refresh while paused only latches the pending full repaint.
|
||||
refreshRows: vi.fn(() => {
|
||||
if (renderService._isPaused) {
|
||||
renderService._needsFullRefresh = true
|
||||
}
|
||||
})
|
||||
}
|
||||
const view = { getComputedStyle: () => ({ display }) }
|
||||
const element = { ownerDocument: { defaultView: view }, parentElement: null }
|
||||
pane.container = element as never
|
||||
pane.xtermContainer = element as never
|
||||
pane.terminal = {
|
||||
...pane.terminal,
|
||||
refresh: vi.fn(() => renderService.refreshRows(0, pane.terminal.rows - 1)),
|
||||
_core: { _renderService: renderService }
|
||||
} as never
|
||||
return { pane, renderService }
|
||||
}
|
||||
|
||||
describe('terminal WebGL addon lifecycle', () => {
|
||||
beforeEach(() => {
|
||||
resetTerminalWebglSuggestion()
|
||||
@@ -132,6 +168,36 @@ describe('terminal WebGL addon lifecycle', () => {
|
||||
expect(pane.terminal.refresh).toHaveBeenCalledWith(0, 23)
|
||||
})
|
||||
|
||||
it('keeps the render pause latched when resetting a pane that has no layout box', () => {
|
||||
// Regression: the atlas reset released xterm's pause for every pane of a
|
||||
// visible manager, including a collapsed sibling of an expanded pane. That
|
||||
// painted the freshly cleared model into an element with no box, and left
|
||||
// the service unpaused for good — the IntersectionObserver only fires on a
|
||||
// change, so it never re-pauses. Clearing _needsFullRefresh with it drops
|
||||
// the repaint the observer owes the pane on reveal, and the deferred
|
||||
// _pausedResizeTask that rides along with it.
|
||||
const { pane, renderService } = createPausedPane('none')
|
||||
|
||||
resetWebglTextureAtlas(pane)
|
||||
|
||||
expect(renderService._isPaused).toBe(true)
|
||||
expect(renderService._needsFullRefresh).toBe(true)
|
||||
expect(pane.terminal.refresh).toHaveBeenCalledWith(0, 23)
|
||||
})
|
||||
|
||||
it('still forces the paused render through for a pane that has a layout box', () => {
|
||||
// The reveal case the release exists for: DOM-visible, but xterm's observer
|
||||
// has not caught up, so a plain refresh() would be swallowed.
|
||||
const { pane, renderService } = createPausedPane('block')
|
||||
|
||||
resetWebglTextureAtlas(pane)
|
||||
|
||||
expect(renderService._isPaused).toBe(false)
|
||||
expect(renderService._needsFullRefresh).toBe(false)
|
||||
expect(renderService.refreshRows).toHaveBeenCalledWith(0, 23, true)
|
||||
expect(pane.terminal.refresh).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('skips the reset while WebGL is latched off after a context loss', () => {
|
||||
const pane = createPane()
|
||||
pane.webglDisabledAfterContextLoss = true
|
||||
|
||||
@@ -2,6 +2,7 @@ import { WebglAddon } from '@xterm/addon-webgl'
|
||||
import type { ManagedPaneInternal } from './pane-manager-types'
|
||||
import { recordTerminalWebglDiagnostic } from '../../../../shared/terminal-webgl-diagnostics'
|
||||
import { getLivePaneCensus } from './pane-manager-registry'
|
||||
import { isManagedPaneDisplayNone } from './pane-display-visibility'
|
||||
import { forceRepaintThroughRenderPause } from './terminal-render-pause-release'
|
||||
import {
|
||||
getTerminalWebglAutoDecision,
|
||||
@@ -151,7 +152,17 @@ export function resetWebglTextureAtlas(pane: ManagedPaneInternal): void {
|
||||
// paused-render gate and the cleared model never repaints (stale bottom rows
|
||||
// until a drag-select forces a redraw). Force the paused render through
|
||||
// first; only fall back to refresh() when the terminal was not gated.
|
||||
if (!forceRepaintThroughRenderPause(pane.terminal)) {
|
||||
//
|
||||
// Why the display check: that release is only right for a pane that is
|
||||
// DOM-visible. A pane with no box at all (collapsed sibling of an expanded
|
||||
// pane, a restore that stays display:none for its whole reattach) is
|
||||
// legitimately paused, and releasing it paints the just-cleared model into
|
||||
// nothing and then leaves the service unpaused for good — the observer only
|
||||
// fires on a change, so it never re-pauses. Clearing _needsFullRefresh with
|
||||
// it also drops the full repaint the observer owes the pane on reveal, and
|
||||
// the deferred _pausedResizeTask that flushes alongside it. Latching is what
|
||||
// xterm's own gate does, and the reveal repaints from the latch.
|
||||
if (isManagedPaneDisplayNone(pane) || !forceRepaintThroughRenderPause(pane.terminal)) {
|
||||
// Why: refresh even without a WebGL addon so recovery never silently
|
||||
// no-ops — a DOM-rendered pane can hold stale pixels after reveal too.
|
||||
pane.terminal.refresh(0, pane.terminal.rows - 1)
|
||||
|
||||
Reference in New Issue
Block a user