Revert "fix(terminal): clear stale parked frame on empty snapshot"

This reverts commit 72de2f6765.
This commit is contained in:
Brennan Benson
2026-08-06 01:04:09 -07:00
parent da2c6bfcd1
commit 8d25ea2afc
14 changed files with 32 additions and 431 deletions
@@ -332,7 +332,6 @@ type MockTransport = {
resize: ReturnType<typeof vi.fn>
getPtyId: ReturnType<typeof vi.fn>
getConnectionId: ReturnType<typeof vi.fn>
getRecoveryState?: ReturnType<typeof vi.fn>
serializeBuffer?: ReturnType<typeof vi.fn>
}
@@ -21083,110 +21082,6 @@ describe('connectPanePty', () => {
expect(transport.getPtyId).toHaveReturnedWith(remotePtyId)
})
it.each(['disconnected', 'offline'] as const)(
'preserves a parked paired frame and PTY identity while recovery is %s',
async (phase) => {
const { connectPanePty } = await import('./pty-connection')
const remotePtyId = 'remote:env-1@@terminal-1'
const transport = createMockTransport()
transport.connect.mockResolvedValue(undefined)
transport.getRecoveryState = vi.fn(() => ({ phase, epoch: 1, attempt: 1 }))
transportFactoryQueue.push(transport)
await parkTabForReveal('tab-1', remotePtyId)
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: remotePtyId }] },
ptyIdsByTabId: { 'tab-1': [remotePtyId] },
terminalLayoutsByTabId: {
'tab-1': {
root: { type: 'leaf', leafId: LEAF_1 },
activeLeafId: LEAF_1,
expandedLeafId: null,
ptyIdsByLeafId: { [LEAF_1]: remotePtyId }
}
},
runtimeStatusByEnvironmentId: new Map([
[
'env-1',
{
checkedAt: Date.now(),
status: { capabilities: ['terminal.paired-parking.v1'] }
}
]
])
}
const pane = createPane(1)
pane.container.dataset.parkedViewportFrame = 'true'
const { writes } = captureCallbackTerminalWrites(pane)
const deps = createDeps({
restoredLeafId: LEAF_1,
restoredPtyIdByLeafId: { [LEAF_1]: remotePtyId }
})
connectPanePty(pane as never, createManager(1) as never, deps as never)
await flushAsyncTicks(20)
expect(transport.connect).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ sessionId: remotePtyId })
)
expect(deps.clearExitedPanePtyLayoutBinding).not.toHaveBeenCalled()
expect(deps.syncPanePtyLayoutBinding).not.toHaveBeenCalledWith(1, null)
expect(deps.clearTabPtyId).not.toHaveBeenCalled()
expect(writes.join('')).not.toContain(
buildFreshShellViewportBlankingSequence(pane.terminal.rows)
)
expect(pane.container.dataset.parkedViewportFrame).toBe('true')
expect(mockStoreState.tabsByWorktree['wt-1'][0]?.ptyId).toBe(remotePtyId)
expect(mockStoreState.terminalLayoutsByTabId?.['tab-1']?.ptyIdsByLeafId?.[LEAF_1]).toBe(
remotePtyId
)
}
)
it('replaces an ended paired session after a parked reattach returns no PTY', async () => {
const { connectPanePty } = await import('./pty-connection')
const remotePtyId = 'remote:env-1@@terminal-1'
const replacementPtyId = 'remote:env-1@@terminal-2'
const transport = createMockTransport()
transport.connect.mockImplementation(async ({ sessionId }) => {
if (sessionId) {
transport.getPtyId.mockReturnValue(null)
return undefined
}
transport.getPtyId.mockReturnValue(replacementPtyId)
return replacementPtyId
})
transport.getRecoveryState = vi.fn(() => ({ phase: 'ended', epoch: 1, attempt: 0 }))
transportFactoryQueue.push(transport)
await parkTabForReveal('tab-1', remotePtyId)
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: remotePtyId }] },
ptyIdsByTabId: { 'tab-1': [remotePtyId] },
runtimeStatusByEnvironmentId: new Map([
[
'env-1',
{
checkedAt: Date.now(),
status: { capabilities: ['terminal.paired-parking.v1'] }
}
]
])
}
const deps = createDeps({
restoredLeafId: LEAF_1,
restoredPtyIdByLeafId: { [LEAF_1]: remotePtyId }
})
connectPanePty(createPane(1) as never, createManager(1) as never, deps as never)
await flushAsyncTicks(20)
expect(transport.connect).toHaveBeenCalledTimes(2)
expect(transport.connect.mock.calls[1]?.[0]).not.toHaveProperty('sessionId')
expect(deps.clearExitedPanePtyLayoutBinding).toHaveBeenCalledWith(1, remotePtyId)
expect(deps.clearTabPtyId).toHaveBeenCalledWith('tab-1', remotePtyId)
})
it('restores a local main-model snapshot after a contentless park reattach', async () => {
const { connectPanePty } = await import('./pty-connection')
const localPtyId = 'global-floating-terminal@@terminal-1'
@@ -8162,15 +8162,6 @@ export function connectPanePty(
const ptyId =
connectResult?.id ?? (typeof result === 'string' ? result : transport.getPtyId())
if (!ptyId) {
// Why: paired recovery retains the valid parked frame while host liveness is unknown.
const recoveryPhase = transport.getRecoveryState?.().phase
if (
mountFollowsTerminalPark &&
isRemoteRuntimePtyId(staleSessionId) &&
(recoveryPhase === 'disconnected' || recoveryPhase === 'offline')
) {
return false
}
warnTerminalLifecycleAnomaly('restored PTY reattach returned no PTY id', {
tabId: deps.tabId,
worktreeId: deps.worktreeId,
@@ -893,25 +893,6 @@ describe('createRemoteRuntimePtyTransport', () => {
transport.destroy?.()
})
it('forwards an empty authoritative snapshot so a cached parked viewport clears', async () => {
const { createRemoteRuntimePtyTransport } = await import('./remote-runtime-pty-transport')
const onReplayData = vi.fn()
const transport = createRemoteRuntimePtyTransport('env-1', {
worktreeId: 'wt-1',
tabId: 'web-terminal-host-tab-1',
leafId: 'pane:1'
})
const connect = transport.connect({ url: '', callbacks: { onReplayData } })
await vi.waitFor(() => expect(subscriptionSendBinary).toHaveBeenCalled())
emitSnapshot(latestSubscribePayload().streamId, '', { cols: 132, rows: 43 })
await connect
expect(onReplayData).toHaveBeenCalledOnce()
expect(onReplayData).toHaveBeenCalledWith('', { snapshotCols: 132, snapshotRows: 43 })
transport.destroy?.()
})
it('keeps the current mirror input queue when an older connect settles', async () => {
const healthyRuntimeCall = runtimeCall.getMockImplementation()
let releaseFirstActivation = (): void => {}
@@ -1858,8 +1858,8 @@ export function createRemoteRuntimePtyTransport(
}
},
onSnapshot: (data, meta) => {
// Why: an empty snapshot is still authoritative and must clear a cached parked viewport.
if (isCurrentSubscription()) {
// Why: an empty snapshot can still carry a pending mid-escape tail that must replay so the next live chunk completes it.
if ((data || meta?.pendingEscapeTailAnsi) && isCurrentSubscription()) {
if (subscribedPtyId && bufferPtyShutdownReplayData(subscribedPtyId, data)) {
return
}
@@ -55,7 +55,6 @@ export type { ParkableTerminalTabModel } from './terminal-parked-watcher-reconci
export type ParkedTerminalPtyEligibility = (ptyId: string) => boolean
export function replayParkedTerminalViewportForTab(args: {
isVisible: () => boolean
manager: PaneManager
paneByLeafId: ReadonlyMap<string, number>
replayingPanesRef: ReplayingPanesRef
@@ -73,7 +72,6 @@ export function replayParkedTerminalViewportForTab(args: {
state.terminalLayoutsByTabId[args.tabId]?.ptyIdsByLeafId ?? {},
tab?.ptyId ?? null
),
isVisible: args.isVisible,
paneByLeafId: args.paneByLeafId,
replayingPanesRef: args.replayingPanesRef
})
@@ -8,18 +8,9 @@ import {
replayParkedTerminalViewportFrames
} from './terminal-parked-viewport-frame'
const mocks = vi.hoisted(() => ({
replayIntoTerminalAsync: vi.fn(
(
_pane: unknown,
_replayingPanesRef: unknown,
_data: string,
_options: { shouldReleaseRenderPause: () => boolean }
) => Promise.resolve()
)
}))
const mocks = vi.hoisted(() => ({ replayIntoTerminal: vi.fn() }))
vi.mock('./replay-guard', () => ({ replayIntoTerminalAsync: mocks.replayIntoTerminalAsync }))
vi.mock('./replay-guard', () => ({ replayIntoTerminal: mocks.replayIntoTerminal }))
function makePane(
id = 1,
@@ -46,7 +37,7 @@ function makePane(
}
beforeEach(() => {
mocks.replayIntoTerminalAsync.mockClear()
mocks.replayIntoTerminal.mockClear()
})
describe('parked terminal viewport frames', () => {
@@ -78,14 +69,12 @@ describe('parked terminal viewport frames', () => {
expect(local.serialize).not.toHaveBeenCalled()
})
it('replays a matching-grid frame and repaints after reveal layout settles', async () => {
it('replays a matching-grid frame and skips a stale geometry', () => {
const matching = makePane(1, 'matching')
const stale = makePane(2, 'stale')
const scheduleRevealRepaint = vi.fn()
const manager = {
getPanes: () => [matching.pane, stale.pane],
hasWebglRenderer: () => false,
scheduleRevealRepaint
hasWebglRenderer: () => false
} as unknown as PaneManager
const replayingPanesRef = { current: new Map<number, number>() }
@@ -95,7 +84,6 @@ describe('parked terminal viewport frames', () => {
{ leafId: 'matching', frame: { data: 'matching frame', cols: 120, rows: 40 } },
{ leafId: 'stale', frame: { data: 'stale frame', cols: 80, rows: 24 } }
],
isVisible: () => true,
paneByLeafId: new Map([
['matching', 1],
['stale', 2]
@@ -104,16 +92,13 @@ describe('parked terminal viewport frames', () => {
})
expect(replayed).toBe(1)
expect(mocks.replayIntoTerminalAsync).toHaveBeenCalledOnce()
expect(mocks.replayIntoTerminalAsync).toHaveBeenCalledWith(
expect(mocks.replayIntoTerminal).toHaveBeenCalledOnce()
expect(mocks.replayIntoTerminal).toHaveBeenCalledWith(
matching.pane,
replayingPanesRef,
'matching frame',
expect.objectContaining({ shouldReleaseRenderPause: expect.any(Function) })
expect.any(Object)
)
expect(mocks.replayIntoTerminalAsync.mock.calls[0]?.[3].shouldReleaseRenderPause()).toBe(true)
await Promise.resolve()
expect(scheduleRevealRepaint).toHaveBeenCalledWith({ invalidatePaneId: matching.pane.id })
expect(consumeParkedTerminalViewportFrameMarker(matching.pane)).toBe(true)
expect(consumeParkedTerminalViewportFrameMarker(matching.pane)).toBe(false)
})
@@ -2,7 +2,7 @@ import type { ManagedPane, PaneManager } from '@/lib/pane-manager/pane-manager'
import type { PtyTransport } from './pty-transport'
import { isRemoteRuntimePtyId } from '@/runtime/runtime-terminal-inspection'
import { serializeWithAbsoluteCursor } from '../../../../shared/terminal-serialize-absolute-cursor'
import { replayIntoTerminalAsync, type ReplayingPanesRef } from './replay-guard'
import { replayIntoTerminal, type ReplayingPanesRef } from './replay-guard'
const MAX_PARKED_VIEWPORT_FRAME_CHARS = 256 * 1024
const PARKED_VIEWPORT_FRAME_DATASET_KEY = 'parkedViewportFrame'
@@ -65,7 +65,6 @@ export function captureParkedTerminalViewportFrame(
export function replayParkedTerminalViewportFrames(args: {
manager: PaneManager
frames: readonly ParkedTerminalViewportFrameByLeaf[]
isVisible: () => boolean
paneByLeafId: ReadonlyMap<string, number>
replayingPanesRef: ReplayingPanesRef
}): number {
@@ -76,20 +75,10 @@ export function replayParkedTerminalViewportFrames(args: {
if (!pane || pane.terminal.cols !== frame.cols || pane.terminal.rows !== frame.rows) {
continue
}
const replay = replayIntoTerminalAsync(pane, args.replayingPanesRef, frame.data, {
shouldRefreshViewportSynchronously: () => !args.manager.hasWebglRenderer(pane.id),
shouldReleaseRenderPause: args.isVisible
replayIntoTerminal(pane, args.replayingPanesRef, frame.data, {
shouldRefreshViewportSynchronously: () => !args.manager.hasWebglRenderer(pane.id)
})
pane.container.dataset[PARKED_VIEWPORT_FRAME_DATASET_KEY] = 'true'
void replay.then(() => {
if (
pane.container.dataset[PARKED_VIEWPORT_FRAME_DATASET_KEY] === 'true' &&
args.isVisible()
) {
// Why: invalidate the stale WebGL cell model only after reveal layout settles.
args.manager.scheduleRevealRepaint({ invalidatePaneId: pane.id })
}
})
replayed += 1
}
return replayed
@@ -1505,7 +1505,6 @@ export function useTerminalPaneLifecycle({
restoredViewportBlankingPanesRef
)
replayParkedTerminalViewportForTab({
isVisible: () => isVisibleRef.current,
manager,
paneByLeafId: restoredPaneByLeafId,
replayingPanesRef,
@@ -375,11 +375,11 @@ export class PaneManager {
return this.atlasRecoveryVisible && !this.destroyed
}
scheduleRevealRepaint(options?: { invalidatePaneId?: number }): void {
scheduleRevealRepaint(): void {
// Why: the settled-frame callback can fire after destroy(); repainting
// disposed panes could throw in attach and latch the global WebGL
// attach backoff, downgrading unrelated new panes to the DOM renderer.
schedulePaneRevealRepaint(() => (this.destroyed ? [] : this.panes.values()), options)
schedulePaneRevealRepaint(() => (this.destroyed ? [] : this.panes.values()))
}
scheduleRevealPresent(): void {
@@ -10,12 +10,7 @@ type FakePaneManager = {
refreshAllPanes: Mock<() => void>
}
function createPane(
options: {
clearRenderModel?: ReturnType<typeof vi.fn>
webglAddon?: FakeWebglAddon | null
} = {}
): ManagedPaneInternal {
function createPane(options: { webglAddon?: FakeWebglAddon | null } = {}): ManagedPaneInternal {
const leafId = '33333333-3333-4333-8333-333333333333' as never
return {
id: 1,
@@ -25,8 +20,7 @@ function createPane(
cols: 80,
rows: 24,
refresh: vi.fn(),
loadAddon: vi.fn(),
_core: { _renderService: { clear: options.clearRenderModel ?? vi.fn() } }
loadAddon: vi.fn()
} as never,
container: {} as never,
xtermContainer: {} as never,
@@ -164,46 +158,6 @@ describe('schedulePaneRevealRepaint', () => {
expect(pane.terminal.refresh).toHaveBeenCalled()
})
it('invalidates a requested WebGL pane only after reveal layout settles', () => {
const clearRenderModel = vi.fn()
const originalAddon = { clearTextureAtlas: vi.fn() }
const pane = createPane({ clearRenderModel, webglAddon: originalAddon })
registerPaneManager(() => [pane])
schedulePaneRevealRepaint(() => [pane], { invalidatePaneId: pane.id })
flushFrame()
expect(clearRenderModel).not.toHaveBeenCalled()
flushFrame()
expect(clearRenderModel).toHaveBeenCalledOnce()
expect(pane.webglAddon).toBe(originalAddon)
})
it('coalesces invalidation requests for different panes into one recovery', () => {
const firstClear = vi.fn()
const secondClear = vi.fn()
const firstPane = createPane({
clearRenderModel: firstClear,
webglAddon: { clearTextureAtlas: vi.fn() }
})
const secondPane = createPane({
clearRenderModel: secondClear,
webglAddon: { clearTextureAtlas: vi.fn() }
})
secondPane.id = 2
const manager = registerPaneManager(() => [firstPane, secondPane])
const getPanes = (): ManagedPaneInternal[] => [firstPane, secondPane]
schedulePaneRevealRepaint(getPanes, { invalidatePaneId: firstPane.id })
schedulePaneRevealRepaint(getPanes, { invalidatePaneId: secondPane.id })
flushFrame()
flushFrame()
expect(firstClear).toHaveBeenCalledOnce()
expect(secondClear).toHaveBeenCalledOnce()
expect(manager.resetWebglTextureAtlases).toHaveBeenCalledOnce()
})
it('resolves the pane list at repaint time, not at scheduling time', () => {
const stalePane = createPane({ webglAddon: { clearTextureAtlas: vi.fn() } })
const livePane = createPane({ webglAddon: { clearTextureAtlas: vi.fn() } })
@@ -1,14 +1,10 @@
import type { ManagedPaneInternal } from './pane-manager-types'
import { reattachWebglIfNeeded } from './pane-webgl-reattach'
import { resetAndRefreshAllTerminalWebglAtlases } from './pane-manager-registry'
import { invalidateTerminalRenderModel } from './terminal-render-model-invalidation'
type PaneGetter = () => Iterable<ManagedPaneInternal>
const pendingRevealRepaints: {
getPanes: PaneGetter
invalidatePaneId?: number
}[] = []
const pendingRevealRepaints = new Set<PaneGetter>()
let revealRepaintScheduled = false
function scheduleSettledFrame(callback: () => void): void {
@@ -41,25 +37,23 @@ function forEachPaneOnSettledFrame(
function flushPaneRevealRepaints(): void {
revealRepaintScheduled = false
const paneGetters = pendingRevealRepaints.splice(0)
const livePanes = new Map<ManagedPaneInternal, boolean>()
const paneGetters = Array.from(pendingRevealRepaints)
pendingRevealRepaints.clear()
const livePanes = new Set<ManagedPaneInternal>()
for (const { getPanes, invalidatePaneId } of paneGetters) {
for (const getPanes of paneGetters) {
try {
for (const pane of getPanes()) {
livePanes.set(pane, livePanes.get(pane) === true || pane.id === invalidatePaneId)
livePanes.add(pane)
}
} catch {
/* ignore — a manager may be destroyed while its repaint is pending */
}
}
for (const [pane, shouldInvalidate] of livePanes) {
for (const pane of livePanes) {
try {
reattachWebglIfNeeded(pane)
if (shouldInvalidate && pane.webglAddon && !pane.webglDisabledAfterContextLoss) {
invalidateTerminalRenderModel(pane.terminal)
}
} catch {
/* ignore — one pane's teardown must not block global recovery */
}
@@ -75,14 +69,13 @@ function flushPaneRevealRepaints(): void {
* Why: while a pane is hidden, parsed output can update the WebGL renderer's
* per-cell model without ever presenting a frame. At reveal the model diff
* reports those cells unchanged, so plain refreshes skip them and the canvas
* keeps compositing pre-hide pixels. Once layout settles, invalidate the
* replayed pane's model before one registry-wide atlas reset and refresh.
* keeps compositing pre-hide pixels until a selection or resize rebuilds the
* model. Once layout settles, reattach every revealed renderer before one
* registry-wide atlas reset so no delayed pane-local clear can invalidate a
* sibling terminal's rebuilt model.
*/
export function schedulePaneRevealRepaint(
getPanes: () => Iterable<ManagedPaneInternal>,
options?: { invalidatePaneId?: number }
): void {
pendingRevealRepaints.push({ getPanes, invalidatePaneId: options?.invalidatePaneId })
export function schedulePaneRevealRepaint(getPanes: () => Iterable<ManagedPaneInternal>): void {
pendingRevealRepaints.add(getPanes)
if (revealRepaintScheduled) {
return
}
@@ -1,27 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { invalidateTerminalRenderModel } from './terminal-render-model-invalidation'
describe('invalidateTerminalRenderModel', () => {
it('clears an available render service', () => {
const clear = vi.fn()
expect(invalidateTerminalRenderModel({ _core: { _renderService: { clear } } })).toBe(true)
expect(clear).toHaveBeenCalledOnce()
})
it('safely ignores unavailable or disposed render services', () => {
expect(invalidateTerminalRenderModel(null)).toBe(false)
expect(invalidateTerminalRenderModel({ _core: {} })).toBe(false)
expect(
invalidateTerminalRenderModel({
_core: {
_renderService: {
clear: () => {
throw new Error('disposed')
}
}
}
})
).toBe(false)
})
})
@@ -1,23 +0,0 @@
type MaybeClearableRenderService = {
clear?: () => void
}
type TerminalWithRenderService = {
_core?: {
_renderService?: MaybeClearableRenderService
}
}
/** Clears xterm's retained cell model so the next refresh redraws every cell. */
export function invalidateTerminalRenderModel(terminal: unknown): boolean {
const service = (terminal as TerminalWithRenderService | null)?._core?._renderService
if (!service || typeof service.clear !== 'function') {
return false
}
try {
service.clear()
return true
} catch {
return false
}
}
@@ -25,7 +25,6 @@ import { randomUUID } from 'node:crypto'
import os from 'node:os'
import path from 'node:path'
import type { Page } from '@stablyai/playwright-test'
import { PNG } from 'pngjs'
import {
HOST_TERMINAL_SURFACE_SEPARATOR,
toWebTerminalSurfaceTabId
@@ -52,8 +51,6 @@ writeFileSync(
'const size = () => `${process.stdout.columns}x${process.stdout.rows}`',
'const record = (line) => appendFileSync(sink, `${line}\\n`)',
'record(`READY:${size()}`)',
"process.stdout.write('\\x1b[2J\\x1b[H')",
"process.stdout.write('\\x1b[44m \\x1b[0m\\r\\n')",
'process.stdout.write(`READY:${size()}\\r\\n`)',
"process.stdout.on('resize', () => {",
' record(`SIZE:${size()}`)',
@@ -85,7 +82,7 @@ function shellQuote(value: string): string {
function fixtureCommand(sinkPath: string): string {
const command = [process.execPath, fixturePath, sinkPath]
return process.platform === 'win32'
? `& ${command.map((value) => `"${value.replaceAll('"', '""')}"`).join(' ')}`
? command.map((value) => `"${value.replaceAll('"', '""')}"`).join(' ')
: command.map(shellQuote).join(' ')
}
@@ -207,26 +204,6 @@ async function readPaneContent(page: Page, webTabId: string): Promise<string> {
}, webTabId)
}
async function readPaintedPaneViewport(
page: Page,
webTabId: string
): Promise<{ renderPaused: boolean; text: string }> {
return page.evaluate((id) => {
const manager = window.__paneManagers?.get(id)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
if (!pane) {
return { renderPaused: false, text: '' }
}
const buffer = pane.terminal.buffer.active
const text = Array.from(
{ length: pane.terminal.rows },
(_, row) => buffer.getLine(buffer.viewportY + row)?.translateToString(true) ?? ''
).join('\n')
const renderService = Reflect.get(pane.terminal, '_core')?._renderService
return { renderPaused: renderService?._isPaused === true, text }
}, webTabId)
}
async function readPaneDiagnostics(
page: Page,
worktreeId: string,
@@ -271,95 +248,6 @@ async function waitForPaneMarker(
return false
}
async function waitForPaintedPaneMarker(
page: Page,
webTabId: string,
marker: string,
budgetMs: number
): Promise<boolean> {
const deadline = Date.now() + budgetMs
while (Date.now() < deadline) {
const viewport = await readPaintedPaneViewport(page, webTabId)
if (!viewport.renderPaused && viewport.text.includes(marker)) {
return true
}
await new Promise((resolve) => setTimeout(resolve, 250))
}
return false
}
function countForegroundPixels(buffer: Buffer): number {
const image = PNG.sync.read(buffer)
const buckets = new Map<string, { count: number; red: number; green: number; blue: number }>()
for (let offset = 0; offset < image.data.length; offset += 4) {
if ((image.data[offset + 3] ?? 0) < 128) {
continue
}
const red = image.data[offset] ?? 0
const green = image.data[offset + 1] ?? 0
const blue = image.data[offset + 2] ?? 0
const key = `${red >> 3},${green >> 3},${blue >> 3}`
const bucket = buckets.get(key) ?? { count: 0, red, green, blue }
bucket.count += 1
buckets.set(key, bucket)
}
const background = [...buckets.values()].sort((left, right) => right.count - left.count)[0]
if (!background) {
return 0
}
let foregroundPixels = 0
for (let offset = 0; offset < image.data.length; offset += 4) {
const distance =
Math.abs((image.data[offset] ?? 0) - background.red) +
Math.abs((image.data[offset + 1] ?? 0) - background.green) +
Math.abs((image.data[offset + 2] ?? 0) - background.blue)
if ((image.data[offset + 3] ?? 0) >= 128 && distance > 48) {
foregroundPixels += 1
}
}
return foregroundPixels
}
async function readPaneForegroundPixels(page: Page, webTabId: string): Promise<number> {
const clip = await page.evaluate((id) => {
const manager = window.__paneManagers?.get(id)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0] ?? null
const screen = pane?.terminal.element?.querySelector('.xterm-screen')
if (!(screen instanceof HTMLElement)) {
return null
}
const rect = screen.getBoundingClientRect()
return {
x: Math.max(0, rect.left),
y: Math.max(0, rect.top),
width: Math.max(1, Math.min(rect.width, window.innerWidth - rect.left)),
height: Math.max(1, Math.min(rect.height, 64, window.innerHeight - rect.top))
}
}, webTabId)
if (!clip) {
return 0
}
return countForegroundPixels(await page.screenshot({ clip }))
}
async function waitForPaneForegroundPixels(
page: Page,
webTabId: string,
minimum: number,
budgetMs: number
): Promise<number> {
const deadline = Date.now() + budgetMs
let foregroundPixels = 0
do {
foregroundPixels = Math.max(foregroundPixels, await readPaneForegroundPixels(page, webTabId))
if (foregroundPixels > minimum) {
return foregroundPixels
}
await new Promise((resolve) => setTimeout(resolve, 50))
} while (Date.now() < deadline)
return foregroundPixels
}
function readPtyGridFromContent(content: string): { cols: number; rows: number } | null {
const sizes = [...content.matchAll(/(?:READY|SIZE):(\d+)x(\d+)/g)]
const last = sizes.at(-1)
@@ -456,12 +344,6 @@ async function seedScenario(
message: 'target terminal never painted its READY marker'
})
.toContain('READY:')
await expect
.poll(async () => (await readPaintedPaneViewport(client.page, target.webTabId)).text, {
timeout: 60_000,
message: 'target terminal READY marker never reached the visible viewport'
})
.toContain('READY:')
return { target, decoys }
}
@@ -609,12 +491,6 @@ test('paired client fast-paints a parked terminal before the runtime responds',
const previousParkDelay = process.env.ORCA_E2E_TERMINAL_PARKING_DELAY_MS
process.env.ORCA_E2E_TERMINAL_PARKING_DELAY_MS = String(PARK_DELAY_MS)
const client = await launchPairedElectronClient(offer, testInfo, 'parked-fast-paint')
const clientWindow = await client.app.browserWindow(client.page)
await clientWindow.evaluate((window) => {
window.show()
window.focus()
})
await expect.poll(() => clientWindow.evaluate((window) => window.isVisible())).toBe(true)
const createdTerminals: string[] = []
try {
const worktreeId = await orcaPage.evaluate(() => {
@@ -655,20 +531,10 @@ test('paired client fast-paints a parked terminal before the runtime responds',
const startedAt = Date.now()
await openClientTab(client.page, worktreeId, target.webTabId)
const painted = await waitForPaintedPaneMarker(client.page, target.webTabId, 'READY:', 2_000)
expect(
painted,
await waitForPaneMarker(client.page, target.webTabId, 'READY:', 2_000),
'cold-parked viewport stayed blank while the paired runtime was disconnected'
).toBe(true)
expect(
await waitForPaneForegroundPixels(
client.page,
target.webTabId,
1_000,
Math.max(1, 2_000 - (Date.now() - startedAt))
),
'cached viewport reached the buffer but did not paint terminal pixels'
).toBeGreaterThan(1_000)
console.log(`[paired-reveal] cached viewport painted in ${Date.now() - startedAt}ms`)
} finally {
await client.page