mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
Prevent stale terminal redraw fragments (#6449)
This commit is contained in:
@@ -25,7 +25,11 @@ import { safeFit } from '@/lib/pane-manager/pane-tree-ops'
|
||||
import { getFitOverrideForPty, bindPanePtyId } from '@/lib/pane-manager/mobile-fit-overrides'
|
||||
import { isPtyLocked } from '@/lib/pane-manager/mobile-driver-state'
|
||||
import { isPaneReplaying, replayIntoTerminal, replayIntoTerminalAsync } from './replay-guard'
|
||||
import { terminalOutputPrefersRenderRefresh } from '@/lib/pane-manager/terminal-complex-script'
|
||||
import {
|
||||
terminalOutputPrefersRenderRefresh,
|
||||
terminalRewriteOutputRenderRefreshDecision,
|
||||
terminalRewriteOutputPrefersRenderRefresh
|
||||
} from '@/lib/pane-manager/terminal-complex-script'
|
||||
import {
|
||||
PANE_PTY_RESIZE_HOLD_FLUSH_EVENT,
|
||||
queuePanePtyResizeIfHeld,
|
||||
@@ -2594,6 +2598,8 @@ export function connectPanePty(
|
||||
let hiddenOutputRestoreGeneration = 0
|
||||
let foregroundImmediateBudgetChars = 0
|
||||
let foregroundImmediateBudgetWindowStart = 0
|
||||
let foregroundRewriteChunkEndedWithCarriageReturn = false
|
||||
let foregroundRewriteCsiScanTail = ''
|
||||
let hiddenMode2031ScanTail = ''
|
||||
const shouldSnapshotHiddenCodexOutput = shouldKeepHiddenStartupRendererQueriesLive(paneStartup)
|
||||
let hiddenStartupRendererQueryPending = ''
|
||||
@@ -2710,35 +2716,31 @@ export function connectPanePty(
|
||||
}
|
||||
|
||||
function containsWindowsRewriteControl(data: string): boolean {
|
||||
if (data.includes('\r') || data.includes('\b')) {
|
||||
return true
|
||||
}
|
||||
let escapeIndex = data.indexOf('\x1b[')
|
||||
while (escapeIndex !== -1) {
|
||||
for (let index = escapeIndex + 2; index < data.length; index++) {
|
||||
const char = data[index]
|
||||
if (char >= '0' && char <= '9') {
|
||||
continue
|
||||
}
|
||||
if (char === ';' || char === '?') {
|
||||
continue
|
||||
}
|
||||
if (char === 'J' || char === 'K') {
|
||||
return true
|
||||
}
|
||||
break
|
||||
}
|
||||
escapeIndex = data.indexOf('\x1b[', escapeIndex + 2)
|
||||
}
|
||||
return false
|
||||
return data.includes('\r') || terminalRewriteOutputPrefersRenderRefresh(data)
|
||||
}
|
||||
|
||||
function foregroundRewriteOutputPrefersRenderRefresh(data: string): boolean {
|
||||
const decision = terminalRewriteOutputRenderRefreshDecision(data, {
|
||||
previousChunkEndsWithCarriageReturn: foregroundRewriteChunkEndedWithCarriageReturn,
|
||||
previousRewriteCsiScanTail: foregroundRewriteCsiScanTail
|
||||
})
|
||||
foregroundRewriteChunkEndedWithCarriageReturn = decision.nextChunkEndsWithCarriageReturn
|
||||
foregroundRewriteCsiScanTail = decision.nextRewriteCsiScanTail
|
||||
return decision.prefersRenderRefresh
|
||||
}
|
||||
|
||||
function shouldForceForegroundRenderRefresh(data: string): boolean {
|
||||
const rewriteOutputPrefersRenderRefresh = foregroundRewriteOutputPrefersRenderRefresh(data)
|
||||
if (foregroundAnsiOutputPrefersRenderRefresh(data)) {
|
||||
// Why: Codex-style background SGR panels can paint cell fills while
|
||||
// glyphs lag behind; refresh only renderer-risk ANSI chunks, not all output.
|
||||
return true
|
||||
}
|
||||
if (rewriteOutputPrefersRenderRefresh) {
|
||||
// Why: resize fixes these panes because xterm's buffer is right but
|
||||
// in-place redraw cells can remain stale in the renderer until repaint.
|
||||
return true
|
||||
}
|
||||
return (
|
||||
shouldApplyNativeWindowsRewriteRefresh &&
|
||||
containsNonAsciiOutput(data) &&
|
||||
@@ -2779,21 +2781,24 @@ export function connectPanePty(
|
||||
// cursor-only restores need row invalidation even outside DEC 2026.
|
||||
const nativeWindowsCursorRestore =
|
||||
shouldProtectNativeWindowsSynchronizedOutput && foreground && containsCursorRestore(data)
|
||||
const foregroundOutput = foreground || parseHiddenStartupOutput
|
||||
const foregroundRenderRefreshNeeded =
|
||||
foregroundOutput && shouldForceForegroundRenderRefresh(data)
|
||||
synchronizedForegroundOutputActive = nextSynchronizedForegroundOutputActive
|
||||
if (hiddenMode2031ScanTail) {
|
||||
if (!foreground && hiddenMode2031ScanTail) {
|
||||
respondToSkippedMode2031Subscribe(data)
|
||||
}
|
||||
writeTerminalOutput(pane.terminal, data, {
|
||||
foreground: foreground || parseHiddenStartupOutput,
|
||||
foreground: foregroundOutput,
|
||||
beforeWrite: beforeTerminalOutputWrite,
|
||||
onBackgroundBacklogDropped: markHiddenOutputRestoreNeeded,
|
||||
latencySensitive:
|
||||
!foreground || parseHiddenStartupOutput ? true : isLatencySensitiveForegroundOutput(data),
|
||||
forceForegroundRefresh:
|
||||
(foreground || parseHiddenStartupOutput) &&
|
||||
foregroundOutput &&
|
||||
(synchronizedForegroundOutput ||
|
||||
nativeWindowsCursorRestore ||
|
||||
shouldForceForegroundRenderRefresh(data)),
|
||||
foregroundRenderRefreshNeeded),
|
||||
followupForegroundRefresh: nativeWindowsCursorRestore,
|
||||
stripTransientCursorShows: shouldProtectNativeWindowsSynchronizedOutput && foreground,
|
||||
coalesceForeground: synchronizedForegroundOutput && synchronizedOutputEnded,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { terminalOutputPrefersRenderRefresh } from './terminal-complex-script'
|
||||
import {
|
||||
terminalOutputPrefersRenderRefresh,
|
||||
terminalRewriteOutputRenderRefreshDecision,
|
||||
terminalRewriteOutputPrefersRenderRefresh
|
||||
} from './terminal-complex-script'
|
||||
|
||||
describe('terminalOutputPrefersRenderRefresh', () => {
|
||||
it('detects Arabic terminal output', () => {
|
||||
@@ -69,3 +73,156 @@ describe('terminalOutputPrefersRenderRefresh', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('terminalRewriteOutputPrefersRenderRefresh', () => {
|
||||
it('detects in-place carriage-return redraws', () => {
|
||||
expect(terminalRewriteOutputPrefersRenderRefresh('\r• Working')).toBe(true)
|
||||
expect(terminalRewriteOutputPrefersRenderRefresh('prefix\r\x1b[2K• Working')).toBe(true)
|
||||
})
|
||||
|
||||
it('does not treat normal CRLF output as an in-place redraw', () => {
|
||||
expect(terminalRewriteOutputPrefersRenderRefresh('line one\r\nline two\r\n')).toBe(false)
|
||||
})
|
||||
|
||||
it('waits on a trailing carriage return so split CRLF output does not refresh early', () => {
|
||||
expect(terminalRewriteOutputPrefersRenderRefresh('line one\r')).toBe(false)
|
||||
expect(terminalRewriteOutputPrefersRenderRefresh('\nline two')).toBe(false)
|
||||
})
|
||||
|
||||
it('detects terminal erase rewrites and backspace updates', () => {
|
||||
expect(terminalRewriteOutputPrefersRenderRefresh('\x1b[2K• Working')).toBe(true)
|
||||
expect(terminalRewriteOutputPrefersRenderRefresh('\x1b[2J\x1b[Hredraw')).toBe(true)
|
||||
expect(terminalRewriteOutputPrefersRenderRefresh('progress 10%\b\b20%')).toBe(true)
|
||||
})
|
||||
|
||||
it('still detects split Codex-style rewrites through the erase-line chunk', () => {
|
||||
expect(terminalRewriteOutputPrefersRenderRefresh('\r')).toBe(false)
|
||||
expect(terminalRewriteOutputPrefersRenderRefresh('\x1b[2K• Working')).toBe(true)
|
||||
})
|
||||
|
||||
it('ignores ordinary cursor movement and style output', () => {
|
||||
expect(terminalRewriteOutputPrefersRenderRefresh('\x1b[10;2Hcursor move')).toBe(false)
|
||||
expect(terminalRewriteOutputPrefersRenderRefresh('\x1b[32mplain green\x1b[0m')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('terminalRewriteOutputRenderRefreshDecision', () => {
|
||||
it('refreshes when a trailing carriage return continues as a split redraw', () => {
|
||||
const trailingCarriageReturn = terminalRewriteOutputRenderRefreshDecision('\r', {
|
||||
previousChunkEndsWithCarriageReturn: false,
|
||||
previousRewriteCsiScanTail: ''
|
||||
})
|
||||
expect(trailingCarriageReturn).toEqual({
|
||||
nextChunkEndsWithCarriageReturn: true,
|
||||
nextRewriteCsiScanTail: '',
|
||||
prefersRenderRefresh: false
|
||||
})
|
||||
|
||||
expect(
|
||||
terminalRewriteOutputRenderRefreshDecision('• Working without erase-line', {
|
||||
previousChunkEndsWithCarriageReturn: true,
|
||||
previousRewriteCsiScanTail: ''
|
||||
})
|
||||
).toEqual({
|
||||
nextChunkEndsWithCarriageReturn: false,
|
||||
nextRewriteCsiScanTail: '',
|
||||
prefersRenderRefresh: true
|
||||
})
|
||||
})
|
||||
|
||||
it('does not refresh split CRLF output', () => {
|
||||
const trailingCarriageReturn = terminalRewriteOutputRenderRefreshDecision('line one\r', {
|
||||
previousChunkEndsWithCarriageReturn: false,
|
||||
previousRewriteCsiScanTail: ''
|
||||
})
|
||||
expect(trailingCarriageReturn).toEqual({
|
||||
nextChunkEndsWithCarriageReturn: true,
|
||||
nextRewriteCsiScanTail: '',
|
||||
prefersRenderRefresh: false
|
||||
})
|
||||
|
||||
expect(
|
||||
terminalRewriteOutputRenderRefreshDecision('\nline two', {
|
||||
previousChunkEndsWithCarriageReturn: true,
|
||||
previousRewriteCsiScanTail: ''
|
||||
})
|
||||
).toEqual({
|
||||
nextChunkEndsWithCarriageReturn: false,
|
||||
nextRewriteCsiScanTail: '',
|
||||
prefersRenderRefresh: false
|
||||
})
|
||||
})
|
||||
|
||||
it('refreshes when a rewrite erase sequence is split across chunks', () => {
|
||||
const trailingRewriteCsi = terminalRewriteOutputRenderRefreshDecision('\r\x1b[', {
|
||||
previousChunkEndsWithCarriageReturn: false,
|
||||
previousRewriteCsiScanTail: ''
|
||||
})
|
||||
expect(trailingRewriteCsi).toEqual({
|
||||
nextChunkEndsWithCarriageReturn: false,
|
||||
nextRewriteCsiScanTail: '\x1b[',
|
||||
prefersRenderRefresh: true
|
||||
})
|
||||
|
||||
expect(
|
||||
terminalRewriteOutputRenderRefreshDecision('2K• Working', {
|
||||
previousChunkEndsWithCarriageReturn: false,
|
||||
previousRewriteCsiScanTail: '\x1b['
|
||||
})
|
||||
).toEqual({
|
||||
nextChunkEndsWithCarriageReturn: false,
|
||||
nextRewriteCsiScanTail: '',
|
||||
prefersRenderRefresh: true
|
||||
})
|
||||
})
|
||||
|
||||
it('carries rewrite erase sequence tails split before CSI introducer or params', () => {
|
||||
expect(
|
||||
terminalRewriteOutputRenderRefreshDecision('\x1b', {
|
||||
previousChunkEndsWithCarriageReturn: false,
|
||||
previousRewriteCsiScanTail: ''
|
||||
})
|
||||
).toEqual({
|
||||
nextChunkEndsWithCarriageReturn: false,
|
||||
nextRewriteCsiScanTail: '\x1b',
|
||||
prefersRenderRefresh: false
|
||||
})
|
||||
|
||||
expect(
|
||||
terminalRewriteOutputRenderRefreshDecision('2J', {
|
||||
previousChunkEndsWithCarriageReturn: false,
|
||||
previousRewriteCsiScanTail: '\x1b['
|
||||
})
|
||||
).toEqual({
|
||||
nextChunkEndsWithCarriageReturn: false,
|
||||
nextRewriteCsiScanTail: '',
|
||||
prefersRenderRefresh: true
|
||||
})
|
||||
})
|
||||
|
||||
it('drops overlong rewrite CSI tails', () => {
|
||||
expect(
|
||||
terminalRewriteOutputRenderRefreshDecision(`\x1b[${'1'.repeat(80)}`, {
|
||||
previousChunkEndsWithCarriageReturn: false,
|
||||
previousRewriteCsiScanTail: ''
|
||||
})
|
||||
).toEqual({
|
||||
nextChunkEndsWithCarriageReturn: false,
|
||||
nextRewriteCsiScanTail: '',
|
||||
prefersRenderRefresh: false
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves pending trailing carriage return state across empty chunks', () => {
|
||||
expect(
|
||||
terminalRewriteOutputRenderRefreshDecision('', {
|
||||
previousChunkEndsWithCarriageReturn: true,
|
||||
previousRewriteCsiScanTail: '\x1b['
|
||||
})
|
||||
).toEqual({
|
||||
nextChunkEndsWithCarriageReturn: true,
|
||||
nextRewriteCsiScanTail: '\x1b[',
|
||||
prefersRenderRefresh: false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,8 +3,23 @@
|
||||
// without switching renderers based on the text content.
|
||||
const EMOJI_PRESENTATION_PATTERN = /\p{Emoji_Presentation}/u
|
||||
const ESCAPE_CHARACTER = String.fromCharCode(0x1b)
|
||||
const REWRITE_CSI_SCAN_TAIL_MAX_CHARS = 64
|
||||
const SGR_SEQUENCE_PATTERN = new RegExp(`${ESCAPE_CHARACTER}\\[([0-9:;]*)m`, 'g')
|
||||
|
||||
function containsStandaloneCarriageReturn(data: string): boolean {
|
||||
let index = data.indexOf('\r')
|
||||
while (index !== -1) {
|
||||
if (index === data.length - 1) {
|
||||
return false
|
||||
}
|
||||
if (data[index + 1] !== '\n') {
|
||||
return true
|
||||
}
|
||||
index = data.indexOf('\r', index + 1)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function isInRange(value: number, start: number, end: number): boolean {
|
||||
return value >= start && value <= end
|
||||
}
|
||||
@@ -86,6 +101,99 @@ function containsBackgroundSgr(data: string): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
function containsRewriteEraseSequence(data: string): boolean {
|
||||
let escapeIndex = data.indexOf('\x1b[')
|
||||
while (escapeIndex !== -1) {
|
||||
for (let index = escapeIndex + 2; index < data.length; index++) {
|
||||
const char = data[index]
|
||||
if (char >= '0' && char <= '9') {
|
||||
continue
|
||||
}
|
||||
if (char === ';' || char === '?') {
|
||||
continue
|
||||
}
|
||||
// Why: erase-in-line/screen rewrites can leave stale renderer cells until
|
||||
// the next resize; xterm's buffer is correct, but the visible layer needs repainting.
|
||||
if (char === 'J' || char === 'K') {
|
||||
return true
|
||||
}
|
||||
break
|
||||
}
|
||||
escapeIndex = data.indexOf('\x1b[', escapeIndex + 2)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function trailingIncompleteRewriteCsiTail(data: string): string {
|
||||
const escapeIndex = data.lastIndexOf(ESCAPE_CHARACTER)
|
||||
if (escapeIndex === -1) {
|
||||
return ''
|
||||
}
|
||||
const tail = data.slice(escapeIndex)
|
||||
if (tail === ESCAPE_CHARACTER) {
|
||||
return tail
|
||||
}
|
||||
if (!tail.startsWith('\x1b[')) {
|
||||
return ''
|
||||
}
|
||||
if (tail.length > REWRITE_CSI_SCAN_TAIL_MAX_CHARS) {
|
||||
return ''
|
||||
}
|
||||
for (let index = 2; index < tail.length; index++) {
|
||||
const char = tail[index]
|
||||
if (char >= '0' && char <= '9') {
|
||||
continue
|
||||
}
|
||||
if (char === ';' || char === '?') {
|
||||
continue
|
||||
}
|
||||
return ''
|
||||
}
|
||||
return tail
|
||||
}
|
||||
|
||||
export function terminalRewriteOutputPrefersRenderRefresh(data: string): boolean {
|
||||
if (data.includes('\b') || containsStandaloneCarriageReturn(data)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return containsRewriteEraseSequence(data)
|
||||
}
|
||||
|
||||
export type TerminalRewriteOutputRenderRefreshDecision = {
|
||||
nextChunkEndsWithCarriageReturn: boolean
|
||||
nextRewriteCsiScanTail: string
|
||||
prefersRenderRefresh: boolean
|
||||
}
|
||||
|
||||
export type TerminalRewriteOutputRenderRefreshState = {
|
||||
previousChunkEndsWithCarriageReturn: boolean
|
||||
previousRewriteCsiScanTail: string
|
||||
}
|
||||
|
||||
export function terminalRewriteOutputRenderRefreshDecision(
|
||||
data: string,
|
||||
state: TerminalRewriteOutputRenderRefreshState
|
||||
): TerminalRewriteOutputRenderRefreshDecision {
|
||||
if (!data) {
|
||||
return {
|
||||
nextChunkEndsWithCarriageReturn: state.previousChunkEndsWithCarriageReturn,
|
||||
nextRewriteCsiScanTail: state.previousRewriteCsiScanTail,
|
||||
prefersRenderRefresh: false
|
||||
}
|
||||
}
|
||||
const scanData = state.previousRewriteCsiScanTail
|
||||
? `${state.previousRewriteCsiScanTail}${data}`
|
||||
: data
|
||||
return {
|
||||
nextChunkEndsWithCarriageReturn: data.endsWith('\r'),
|
||||
nextRewriteCsiScanTail: trailingIncompleteRewriteCsiTail(scanData),
|
||||
prefersRenderRefresh:
|
||||
(state.previousChunkEndsWithCarriageReturn && data[0] !== '\n') ||
|
||||
terminalRewriteOutputPrefersRenderRefresh(scanData)
|
||||
}
|
||||
}
|
||||
|
||||
export function terminalOutputPrefersRenderRefresh(data: string): boolean {
|
||||
if (containsBackgroundSgr(data)) {
|
||||
return true
|
||||
|
||||
@@ -39,8 +39,17 @@ type SchedulerDebugWindow = Window & {
|
||||
}
|
||||
}
|
||||
|
||||
type RefreshProbeWindow = SchedulerDebugWindow & {
|
||||
__terminalRefreshProbe?: {
|
||||
count: () => number
|
||||
dispose: () => void
|
||||
}
|
||||
}
|
||||
|
||||
const REDRAW_FRAME_COUNT = 270
|
||||
const REDRAW_PAYLOAD_CHARS = 520
|
||||
const REWRITE_REDRAW_FRAME_COUNT = REDRAW_FRAME_COUNT
|
||||
const REWRITE_REDRAW_PAYLOAD_CHARS = REDRAW_PAYLOAD_CHARS
|
||||
const TIMER_SAMPLE_MS = 16
|
||||
const MAX_RENDERER_TIMER_DRIFT_MS = 500
|
||||
const FOREGROUND_IMMEDIATE_BUDGET_CHARS = 128 * 1024
|
||||
@@ -82,6 +91,18 @@ async function measureRendererDuringBurst(page: Page, paneKey: string): Promise<
|
||||
return measureRendererDuringFrames(page, paneKey, frames)
|
||||
}
|
||||
|
||||
async function measureRendererDuringRewriteBurst(
|
||||
page: Page,
|
||||
paneKey: string
|
||||
): Promise<BurstMeasurement> {
|
||||
const frames = Array.from({ length: REWRITE_REDRAW_FRAME_COUNT }, (_, frame) => {
|
||||
const text = `• Working ${String(frame).padStart(4, '0')}`
|
||||
const payload = 'x'.repeat(REWRITE_REDRAW_PAYLOAD_CHARS)
|
||||
return `\r\x1b[2K${text} ${payload}`
|
||||
})
|
||||
return measureRendererDuringFrames(page, paneKey, frames)
|
||||
}
|
||||
|
||||
async function measureRendererDuringFrames(
|
||||
page: Page,
|
||||
paneKey: string,
|
||||
@@ -133,6 +154,58 @@ async function measureRendererDuringFrames(
|
||||
)
|
||||
}
|
||||
|
||||
async function installActivePaneRefreshProbe(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
;(window as RefreshProbeWindow).__terminalRefreshProbe?.dispose()
|
||||
const state = window.__store?.getState()
|
||||
const worktreeId = state?.activeWorktreeId
|
||||
const tabId =
|
||||
state?.activeTabType === 'terminal'
|
||||
? state.activeTabId
|
||||
: worktreeId
|
||||
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
|
||||
: null
|
||||
const manager = tabId ? window.__paneManagers?.get(tabId) : null
|
||||
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
|
||||
if (!pane) {
|
||||
throw new Error('Active terminal pane is unavailable')
|
||||
}
|
||||
const terminal = pane.terminal as unknown as {
|
||||
_core?: { refresh?: (start: number, end: number, sync?: boolean) => void }
|
||||
}
|
||||
const originalCoreRefresh = terminal._core?.refresh?.bind(terminal._core)
|
||||
if (!terminal._core || !originalCoreRefresh) {
|
||||
throw new Error('Active terminal core refresh hook is unavailable')
|
||||
}
|
||||
let refreshCount = 0
|
||||
terminal._core.refresh = (start, end, sync) => {
|
||||
if (sync === true) {
|
||||
refreshCount += 1
|
||||
}
|
||||
originalCoreRefresh(start, end, sync)
|
||||
}
|
||||
;(window as RefreshProbeWindow).__terminalRefreshProbe = {
|
||||
count: () => refreshCount,
|
||||
dispose: () => {
|
||||
if (terminal._core) {
|
||||
terminal._core.refresh = originalCoreRefresh
|
||||
}
|
||||
delete (window as RefreshProbeWindow).__terminalRefreshProbe
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function readRefreshProbeCount(page: Page): Promise<number> {
|
||||
return page.evaluate(() => (window as RefreshProbeWindow).__terminalRefreshProbe?.count() ?? 0)
|
||||
}
|
||||
|
||||
async function disposeActivePaneRefreshProbe(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
;(window as RefreshProbeWindow).__terminalRefreshProbe?.dispose()
|
||||
})
|
||||
}
|
||||
|
||||
function loadCapturedOpenCodeSmallRedrawFrames(): string[] {
|
||||
if (!existsSync(OPENCODE_CAPTURE_PATH)) {
|
||||
return []
|
||||
@@ -176,6 +249,35 @@ function annotateMeasurement(
|
||||
}
|
||||
|
||||
test.describe('Terminal foreground redraw freeze repro', () => {
|
||||
test('Codex-style line rewrites request a visible row refresh', async ({ orcaPage }) => {
|
||||
await waitForSessionReady(orcaPage)
|
||||
await waitForActiveWorktree(orcaPage)
|
||||
await ensureTerminalVisible(orcaPage)
|
||||
await waitForActiveTerminalManager(orcaPage, 30_000)
|
||||
|
||||
const { paneKey } = await waitForActivePaneHookDescriptor(orcaPage)
|
||||
await waitForTerminalPtyDataInjector(orcaPage, paneKey)
|
||||
await installActivePaneRefreshProbe(orcaPage)
|
||||
try {
|
||||
const refreshBaseline = await readRefreshProbeCount(orcaPage)
|
||||
await resetSchedulerDebug(orcaPage)
|
||||
const measurement = await measureRendererDuringRewriteBurst(orcaPage, paneKey)
|
||||
const scheduler = await readSchedulerDebug(orcaPage)
|
||||
|
||||
expect(measurement.injectedFrames).toBe(REWRITE_REDRAW_FRAME_COUNT)
|
||||
expect(measurement.maxTimerDriftMs).toBeLessThan(MAX_RENDERER_TIMER_DRIFT_MS)
|
||||
expect(scheduler.deferredForegroundEnqueueCount).toBeGreaterThan(0)
|
||||
await expect
|
||||
.poll(async () => (await readRefreshProbeCount(orcaPage)) - refreshBaseline, {
|
||||
timeout: 5_000,
|
||||
message: 'Codex-style terminal rewrites did not request an xterm refresh'
|
||||
})
|
||||
.toBeGreaterThan(0)
|
||||
} finally {
|
||||
await disposeActivePaneRefreshProbe(orcaPage)
|
||||
}
|
||||
})
|
||||
|
||||
test('active OpenTUI-style redraw bursts do not monopolize the renderer', async ({
|
||||
orcaPage
|
||||
}, testInfo) => {
|
||||
|
||||
Reference in New Issue
Block a user