From 8d25ea2afc76bdef13554ac60154c8580bb6554c Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:04:09 -0700 Subject: [PATCH] Revert "fix(terminal): clear stale parked frame on empty snapshot" This reverts commit 72de2f6765bcf6a63e0ea31f452b70ce24b911d5. --- .../terminal-pane/pty-connection.test.ts | 105 ------------- .../terminal-pane/pty-connection.ts | 9 -- .../remote-runtime-pty-transport.test.ts | 19 --- .../remote-runtime-pty-transport.ts | 4 +- .../terminal-parked-tab-watchers.ts | 2 - .../terminal-parked-viewport-frame.test.ts | 31 +--- .../terminal-parked-viewport-frame.ts | 17 +-- .../use-terminal-pane-lifecycle.ts | 1 - .../src/lib/pane-manager/pane-manager.ts | 4 +- .../pane-manager/pane-reveal-repaint.test.ts | 50 +------ .../lib/pane-manager/pane-reveal-repaint.ts | 33 ++--- ...terminal-render-model-invalidation.test.ts | 27 ---- .../terminal-render-model-invalidation.ts | 23 --- ...rminal-parked-reveal-interactivity.spec.ts | 138 +----------------- 14 files changed, 32 insertions(+), 431 deletions(-) delete mode 100644 src/renderer/src/lib/pane-manager/terminal-render-model-invalidation.test.ts delete mode 100644 src/renderer/src/lib/pane-manager/terminal-render-model-invalidation.ts diff --git a/src/renderer/src/components/terminal-pane/pty-connection.test.ts b/src/renderer/src/components/terminal-pane/pty-connection.test.ts index 18c198f1d34..96a3aef7e75 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.test.ts @@ -332,7 +332,6 @@ type MockTransport = { resize: ReturnType getPtyId: ReturnType getConnectionId: ReturnType - getRecoveryState?: ReturnType serializeBuffer?: ReturnType } @@ -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' diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index 6b5bcf7e890..6b4d65acde8 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -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, diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts index d5b09e58d07..48a5433538f 100644 --- a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts @@ -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 => {} diff --git a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts index 7d2465556c9..5f848bc9c47 100644 --- a/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts +++ b/src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts @@ -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 } diff --git a/src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers.ts b/src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers.ts index c5460292e6a..8c0d5bd082d 100644 --- a/src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers.ts +++ b/src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers.ts @@ -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 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 }) diff --git a/src/renderer/src/components/terminal-pane/terminal-parked-viewport-frame.test.ts b/src/renderer/src/components/terminal-pane/terminal-parked-viewport-frame.test.ts index 46d9f38d6ea..0dd2678b00f 100644 --- a/src/renderer/src/components/terminal-pane/terminal-parked-viewport-frame.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-parked-viewport-frame.test.ts @@ -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() } @@ -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) }) diff --git a/src/renderer/src/components/terminal-pane/terminal-parked-viewport-frame.ts b/src/renderer/src/components/terminal-pane/terminal-parked-viewport-frame.ts index 048894b557c..3ccd6ce13a2 100644 --- a/src/renderer/src/components/terminal-pane/terminal-parked-viewport-frame.ts +++ b/src/renderer/src/components/terminal-pane/terminal-parked-viewport-frame.ts @@ -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 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 diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts index 32605cbcef8..a461c6b0153 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts @@ -1505,7 +1505,6 @@ export function useTerminalPaneLifecycle({ restoredViewportBlankingPanesRef ) replayParkedTerminalViewportForTab({ - isVisible: () => isVisibleRef.current, manager, paneByLeafId: restoredPaneByLeafId, replayingPanesRef, diff --git a/src/renderer/src/lib/pane-manager/pane-manager.ts b/src/renderer/src/lib/pane-manager/pane-manager.ts index bfc1cecbdd4..0f9918525a9 100644 --- a/src/renderer/src/lib/pane-manager/pane-manager.ts +++ b/src/renderer/src/lib/pane-manager/pane-manager.ts @@ -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 { diff --git a/src/renderer/src/lib/pane-manager/pane-reveal-repaint.test.ts b/src/renderer/src/lib/pane-manager/pane-reveal-repaint.test.ts index c1aa236c652..8604a0fd6dc 100644 --- a/src/renderer/src/lib/pane-manager/pane-reveal-repaint.test.ts +++ b/src/renderer/src/lib/pane-manager/pane-reveal-repaint.test.ts @@ -10,12 +10,7 @@ type FakePaneManager = { refreshAllPanes: Mock<() => void> } -function createPane( - options: { - clearRenderModel?: ReturnType - 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() } }) diff --git a/src/renderer/src/lib/pane-manager/pane-reveal-repaint.ts b/src/renderer/src/lib/pane-manager/pane-reveal-repaint.ts index 2e7e84955e7..bfc4797aa52 100644 --- a/src/renderer/src/lib/pane-manager/pane-reveal-repaint.ts +++ b/src/renderer/src/lib/pane-manager/pane-reveal-repaint.ts @@ -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 -const pendingRevealRepaints: { - getPanes: PaneGetter - invalidatePaneId?: number -}[] = [] +const pendingRevealRepaints = new Set() 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() + const paneGetters = Array.from(pendingRevealRepaints) + pendingRevealRepaints.clear() + const livePanes = new Set() - 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, - options?: { invalidatePaneId?: number } -): void { - pendingRevealRepaints.push({ getPanes, invalidatePaneId: options?.invalidatePaneId }) +export function schedulePaneRevealRepaint(getPanes: () => Iterable): void { + pendingRevealRepaints.add(getPanes) if (revealRepaintScheduled) { return } diff --git a/src/renderer/src/lib/pane-manager/terminal-render-model-invalidation.test.ts b/src/renderer/src/lib/pane-manager/terminal-render-model-invalidation.test.ts deleted file mode 100644 index e5758d9f5eb..00000000000 --- a/src/renderer/src/lib/pane-manager/terminal-render-model-invalidation.test.ts +++ /dev/null @@ -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) - }) -}) diff --git a/src/renderer/src/lib/pane-manager/terminal-render-model-invalidation.ts b/src/renderer/src/lib/pane-manager/terminal-render-model-invalidation.ts deleted file mode 100644 index 0f4a37e56b5..00000000000 --- a/src/renderer/src/lib/pane-manager/terminal-render-model-invalidation.ts +++ /dev/null @@ -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 - } -} diff --git a/tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts b/tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts index ad0000e7204..7235d1f949c 100644 --- a/tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts +++ b/tests/e2e/paired-remote-terminal-parked-reveal-interactivity.spec.ts @@ -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 { }, 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 { - 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() - 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 { - 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 { - 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