Stabilize OpenCode terminal rendering for release

This commit is contained in:
Neil
2026-06-07 19:26:47 -07:00
parent e35deb9720
commit eaa4a7a065
11 changed files with 223 additions and 270 deletions
@@ -12,7 +12,6 @@ const mocks = vi.hoisted(() => ({
getTerminalOutputEpoch: vi.fn(() => 0),
handleTerminalFileDrop: vi.fn(),
requestTerminalBacklogRecovery: vi.fn(),
setActiveTerminalOutputTarget: vi.fn(),
restoreScrollState: vi.fn(),
restoreScrollStateAfterLayout: vi.fn()
}))
@@ -57,8 +56,7 @@ vi.mock('./pane-helpers', () => ({
vi.mock('@/lib/pane-manager/pane-terminal-output-scheduler', () => ({
flushTerminalOutput: mocks.flushTerminalOutput,
requestTerminalBacklogRecovery: mocks.requestTerminalBacklogRecovery,
setActiveTerminalOutputTarget: mocks.setActiveTerminalOutputTarget
requestTerminalBacklogRecovery: mocks.requestTerminalBacklogRecovery
}))
vi.mock('@/lib/pane-manager/pane-scroll', () => ({
@@ -230,12 +228,11 @@ describe('useTerminalPaneGlobalEffects', () => {
})
it('reports the active local PTY to the main output scheduler', () => {
const terminal = { name: 'terminal-a' }
const manager = {
getPanes: vi.fn(() => [{ id: 1, terminal }]),
getPanes: vi.fn(() => [{ id: 1, terminal: { name: 'terminal-a' } }]),
resumeRendering: vi.fn(),
suspendRendering: vi.fn(),
getActivePane: vi.fn(() => ({ id: 1, terminal }))
getActivePane: vi.fn(() => ({ id: 1, terminal: { name: 'terminal-a' } }))
}
const transport = { getPtyId: vi.fn(() => 'pty-active') }
const paneTransports = new Map([[1, transport]])
@@ -257,7 +254,6 @@ describe('useTerminalPaneGlobalEffects', () => {
})
expect(window.api.pty.setActiveRendererPty).toHaveBeenCalledWith('pty-active', true)
expect(mocks.setActiveTerminalOutputTarget).toHaveBeenCalledWith(terminal, true)
})
it('restores from the pre-hide scroll state when hidden layout changes the viewport', () => {
@@ -12,8 +12,7 @@ import type { PtyTransport } from './pty-transport'
import { handleTerminalFileDrop } from './terminal-drop-handler'
import {
flushTerminalOutput,
requestTerminalBacklogRecovery,
setActiveTerminalOutputTarget
requestTerminalBacklogRecovery
} from '@/lib/pane-manager/pane-terminal-output-scheduler'
import { handleFocusTerminalPaneDetail } from './focus-terminal-pane-event'
import { surfaceStaleAgentRow } from './stale-agent-row'
@@ -138,32 +137,17 @@ export function useTerminalPaneGlobalEffects({
useEffect(() => {
const manager = managerRef.current
const syncActiveOutputTargets = (activePaneId: number | null): void => {
for (const pane of manager?.getPanes() ?? []) {
setActiveTerminalOutputTarget(pane.terminal, pane.id === activePaneId)
}
for (const [paneId, transport] of paneTransportsRef.current) {
const ptyId = transport.getPtyId()
if (!ptyId || ptyId.startsWith('remote:')) {
continue
}
window.api.pty.setActiveRendererPty?.(ptyId, paneId === activePaneId)
}
}
if (!isActive || !isVisible || !manager) {
syncActiveOutputTargets(null)
const activePane = isActive && isVisible ? manager?.getActivePane() : null
const ptyId = activePane
? (paneTransportsRef.current.get(activePane.id)?.getPtyId() ?? null)
: null
if (!ptyId || ptyId.startsWith('remote:')) {
return
}
const activePane = manager.getActivePane()
const activePaneId = activePane?.id ?? null
// Why: active output hints must clear every pane when a tab hides; split
// focus can change after this effect's active-pane snapshot.
syncActiveOutputTargets(activePaneId)
return () => {
syncActiveOutputTargets(null)
}
// Why: main uses this as a scheduler hint only, so the foreground pane's
// renderer output gets first chance at the bounded ACK reserve.
window.api.pty.setActiveRendererPty?.(ptyId, true)
return () => window.api.pty.setActiveRendererPty?.(ptyId, false)
}, [isActive, isVisible, managerRef, paneTransportsRef])
useEffect(() => {
@@ -1,31 +1,10 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
setActiveTerminalOutputTarget: vi.fn()
}))
vi.mock('@/lib/pane-manager/pane-terminal-output-scheduler', () => ({
setActiveTerminalOutputTarget: mocks.setActiveTerminalOutputTarget
}))
import { describe, expect, it, vi } from 'vitest'
import {
reportActiveRendererPtyForPane,
shouldDetachPaneTransportOnUnmount,
splitPaneWithOneShotStartup,
suppressIntentionalPaneCloseExit
} from './use-terminal-pane-lifecycle'
beforeEach(() => {
vi.clearAllMocks()
;(globalThis as unknown as { window: unknown }).window = {
api: {
pty: {
setActiveRendererPty: vi.fn()
}
}
}
})
describe('splitPaneWithOneShotStartup', () => {
it('only exposes startup to the intentional split and clears it afterwards', () => {
const deps: { startup?: { command: string; env?: Record<string, string> } | null } = {
@@ -167,52 +146,3 @@ describe('suppressIntentionalPaneCloseExit', () => {
expect(suppressPtyExit).not.toHaveBeenCalled()
})
})
describe('reportActiveRendererPtyForPane', () => {
it('marks the active visible pane as the renderer output target', () => {
const terminalA = { name: 'terminal-a' }
const terminalB = { name: 'terminal-b' }
const manager = {
getPanes: vi.fn(() => [
{ id: 1, terminal: terminalA },
{ id: 2, terminal: terminalB }
])
}
const paneTransports = new Map([
[1, { getPtyId: vi.fn(() => 'pty-1') }],
[2, { getPtyId: vi.fn(() => 'remote:env@@pty-2') }]
])
reportActiveRendererPtyForPane(paneTransports as never, manager as never, 2, true)
expect(mocks.setActiveTerminalOutputTarget).toHaveBeenCalledWith(terminalA, false)
expect(mocks.setActiveTerminalOutputTarget).toHaveBeenCalledWith(terminalB, true)
expect(window.api.pty.setActiveRendererPty).toHaveBeenCalledWith('pty-1', false)
expect(window.api.pty.setActiveRendererPty).not.toHaveBeenCalledWith(
'remote:env@@pty-2',
expect.anything()
)
})
it('clears every renderer output target while hidden or inactive', () => {
const terminalA = { name: 'terminal-a' }
const terminalB = { name: 'terminal-b' }
const manager = {
getPanes: vi.fn(() => [
{ id: 1, terminal: terminalA },
{ id: 2, terminal: terminalB }
])
}
const paneTransports = new Map([
[1, { getPtyId: vi.fn(() => 'pty-1') }],
[2, { getPtyId: vi.fn(() => 'pty-2') }]
])
reportActiveRendererPtyForPane(paneTransports as never, manager as never, 2, false)
expect(mocks.setActiveTerminalOutputTarget).toHaveBeenCalledWith(terminalA, false)
expect(mocks.setActiveTerminalOutputTarget).toHaveBeenCalledWith(terminalB, false)
expect(window.api.pty.setActiveRendererPty).toHaveBeenCalledWith('pty-1', false)
expect(window.api.pty.setActiveRendererPty).toHaveBeenCalledWith('pty-2', false)
})
})
@@ -61,7 +61,6 @@ import { getRemoteRuntimePtyEnvironmentId } from '@/runtime/runtime-terminal-str
import { getConnectionId } from '@/lib/connection-context'
import { isPaneReplaying, type ReplayingPanesRef } from './replay-guard'
import { fitAndFocusPanes, fitPanes } from './pane-helpers'
import { setActiveTerminalOutputTarget } from '@/lib/pane-manager/pane-terminal-output-scheduler'
import { registerRuntimeTerminalTab, scheduleRuntimeGraphSync } from '@/runtime/sync-runtime-graph'
import { e2eConfig } from '@/lib/e2e-config'
import {
@@ -94,22 +93,16 @@ function extractUncHost(value: string | undefined): string | null {
return match?.[1] || null
}
export function reportActiveRendererPtyForPane(
function reportActiveRendererPtyForPane(
paneTransports: Map<number, PtyTransport>,
manager: PaneManager | null,
activePaneId: number | null,
activeAllowed: boolean
activePaneId: number | null
): void {
const activeTargetPaneId = activeAllowed ? activePaneId : null
for (const pane of manager?.getPanes() ?? []) {
setActiveTerminalOutputTarget(pane.terminal, activeTargetPaneId === pane.id)
}
for (const [paneId, transport] of paneTransports) {
const ptyId = transport.getPtyId()
if (!ptyId || ptyId.startsWith('remote:')) {
continue
}
window.api.pty.setActiveRendererPty?.(ptyId, activeTargetPaneId === paneId)
window.api.pty.setActiveRendererPty?.(ptyId, activePaneId === paneId)
}
}
@@ -829,9 +822,6 @@ export function useTerminalPaneLifecycle({
queueResizeAll(true)
},
onPaneClosed: (paneId, closedPane) => {
if (closedPane?.terminal) {
setActiveTerminalOutputTarget(closedPane.terminal, false)
}
const linkProviderDisposable = linkProviderDisposablesRef.current.get(paneId)
if (linkProviderDisposable) {
linkProviderDisposable.dispose()
@@ -891,7 +881,6 @@ export function useTerminalPaneLifecycle({
mouseHideDisposablesRef.current.delete(paneId)
}
const transport = paneTransportsRef.current.get(paneId)
const closingPtyId = transport?.getPtyId() ?? null
const panePtyBinding = panePtyBindings.get(paneId)
if (panePtyBinding) {
panePtyBinding.dispose()
@@ -914,9 +903,6 @@ export function useTerminalPaneLifecycle({
transport,
useAppStore.getState().suppressPtyExit
)
if (closingPtyId && !closingPtyId.startsWith('remote:')) {
window.api.pty.setActiveRendererPty?.(closingPtyId, false)
}
if (ptyId) {
// Why: user/CLI pane closes intentionally tear down this PTY after
// PaneManager has already promoted the sibling. Suppress that exit
@@ -957,19 +943,12 @@ export function useTerminalPaneLifecycle({
// stay stuck on the closed pane's last title.
const newActivePane = managerRef.current?.getActivePane()
if (newActivePane) {
reportActiveRendererPtyForPane(
paneTransportsRef.current,
managerRef.current,
newActivePane.id,
isActiveRef.current && isVisibleRef.current
)
reportActiveRendererPtyForPane(paneTransportsRef.current, newActivePane.id)
const paneTitles = useAppStore.getState().runtimePaneTitlesByTabId[tabId] ?? {}
const activeTitle = paneTitles[newActivePane.id]
if (activeTitle) {
updateTabTitle(tabId, activeTitle)
}
} else {
reportActiveRendererPtyForPane(paneTransportsRef.current, managerRef.current, null, false)
}
scheduleRuntimeGraphSync()
},
@@ -978,12 +957,7 @@ export function useTerminalPaneLifecycle({
if (shouldPersistLayout) {
persistLayoutSnapshot()
}
reportActiveRendererPtyForPane(
paneTransportsRef.current,
managerRef.current,
pane.id,
isActiveRef.current && isVisibleRef.current
)
reportActiveRendererPtyForPane(paneTransportsRef.current, pane.id)
// Why: when the user switches focus between split panes, update the
// tab title to the newly active pane's last-known title so the tab
// label reflects the focused agent — not a stale title from the
@@ -26,7 +26,6 @@ export type PaneSpawnHints = {
export type ClosedPaneInfo = {
paneId: number
leafId: TerminalLeafId
terminal?: Terminal
}
export type PaneManagerOptions = {
@@ -5,7 +5,6 @@ import type { TerminalLeafId } from '../../../../shared/stable-pane-id'
const captureScrollState = vi.hoisted(() => vi.fn())
const wrapInSplit = vi.hoisted(() => vi.fn())
const openTerminal = vi.hoisted(() => vi.fn())
const disposePane = vi.hoisted(() => vi.fn())
const disposeWebgl = vi.hoisted(() => vi.fn())
const clearPendingSplitScrollRestore = vi.hoisted(() => vi.fn())
const scheduleSplitScrollRestore = vi.hoisted(() => vi.fn())
@@ -23,7 +22,7 @@ vi.mock('./pane-tree-ops', () => ({
}))
vi.mock('./pane-lifecycle', () => ({
disposePane,
disposePane: vi.fn(),
openTerminal
}))
@@ -46,7 +45,6 @@ vi.mock('./pane-divider', () => ({
}))
import { splitManagedPane } from './pane-split-close'
import { closeManagedPane } from './pane-split-close'
const TEST_LEAF_ID = '11111111-1111-4111-8111-111111111111' as TerminalLeafId
@@ -55,7 +53,6 @@ class MockElement {
dataset: Record<string, string> = {}
parentElement: MockElement | null = null
style: Record<string, string> = {}
remove = vi.fn()
private descendants: MockElement[] = []
constructor(private readonly classNames: string[]) {
@@ -119,11 +116,6 @@ function createPane(id: number, webglAddon: unknown): ManagedPaneInternal {
describe('splitManagedPane', () => {
beforeEach(() => {
vi.clearAllMocks()
disposePane.mockImplementation(
(pane: ManagedPaneInternal, panes: Map<number, ManagedPaneInternal>) => {
panes.delete(pane.id)
}
)
})
it('prepares every pane under a moved mounted subtree for split reparenting', () => {
@@ -201,44 +193,3 @@ describe('splitManagedPane', () => {
)
})
})
describe('closeManagedPane', () => {
beforeEach(() => {
vi.clearAllMocks()
disposePane.mockImplementation(
(pane: ManagedPaneInternal, panes: Map<number, ManagedPaneInternal>) => {
panes.delete(pane.id)
}
)
})
it('reports the closed terminal in the close callback', () => {
const closedPane = createPane(1, null)
const siblingPane = createPane(2, null)
const root = new MockElement(['root'])
;(closedPane.container as unknown as MockElement).parentElement = root
const panes = new Map<number, ManagedPaneInternal>([
[closedPane.id, closedPane],
[siblingPane.id, siblingPane]
])
const onPaneClosed = vi.fn()
closeManagedPane({
paneId: closedPane.id,
activePaneId: closedPane.id,
panes,
root: root as unknown as HTMLElement,
styleOptions: {},
managerOptions: { onPaneClosed },
getDragCallbacks: () => ({}) as never,
releasePaneIdentity: vi.fn(),
setActivePaneId: vi.fn()
})
expect(onPaneClosed).toHaveBeenCalledWith(closedPane.id, {
paneId: closedPane.id,
leafId: closedPane.leafId,
terminal: closedPane.terminal
})
})
})
@@ -180,11 +180,7 @@ export function closeManagedPane(args: CloseManagedPaneArgs): void {
safeFit(p)
}
updateMultiPaneState(args.getDragCallbacks())
args.managerOptions.onPaneClosed?.(args.paneId, {
paneId: args.paneId,
leafId: closedLeafId,
terminal: pane.terminal
})
args.managerOptions.onPaneClosed?.(args.paneId, { paneId: args.paneId, leafId: closedLeafId })
args.managerOptions.onLayoutChanged?.()
}
@@ -612,45 +612,6 @@ describe('pane terminal output scheduler', () => {
expect(terminals[2].write).toHaveBeenCalledWith('pane-2')
})
it('drains the active terminal before older queued background terminals', async () => {
vi.useFakeTimers()
const { setActiveTerminalOutputTarget, writeTerminalOutput } = await loadScheduler()
const terminals = [createTerminal(), createTerminal(), createTerminal()]
terminals.forEach((terminal, index) => {
writeTerminalOutput(terminal, `pane-${index}`, { foreground: false })
})
setActiveTerminalOutputTarget(terminals[2], true)
vi.advanceTimersByTime(50)
expect(terminals[2].write).toHaveBeenCalledWith('pane-2')
expect(terminals[0].write).toHaveBeenCalledWith('pane-0')
expect(terminals[1].write).not.toHaveBeenCalled()
setActiveTerminalOutputTarget(terminals[2], false)
})
it('still drains the active terminal first with one hundred queued terminals', async () => {
vi.useFakeTimers()
const { setActiveTerminalOutputTarget, writeTerminalOutput } = await loadScheduler()
const terminals = Array.from({ length: 100 }, () => createTerminal())
const activeTerminal = terminals[99]
terminals.forEach((terminal, index) => {
writeTerminalOutput(terminal, `pane-${index}`, { foreground: false })
})
setActiveTerminalOutputTarget(activeTerminal, true)
vi.advanceTimersByTime(50)
expect(activeTerminal.write).toHaveBeenCalledWith('pane-99')
expect(terminals[0].write).toHaveBeenCalledWith('pane-0')
expect(activeTerminal.write.mock.invocationCallOrder[0]).toBeLessThan(
terminals[0].write.mock.invocationCallOrder[0]
)
})
it('rotates terminals with remaining backlog behind untouched queued terminals', async () => {
vi.useFakeTimers()
const { writeTerminalOutput } = await loadScheduler()
@@ -879,26 +840,6 @@ describe('pane terminal output scheduler', () => {
expect(terminal.write).not.toHaveBeenCalled()
})
it('clears active priority when terminal output is discarded', async () => {
vi.useFakeTimers()
const { discardTerminalOutput, setActiveTerminalOutputTarget, writeTerminalOutput } =
await loadScheduler()
const terminalA = createTerminal()
const terminalB = createTerminal()
setActiveTerminalOutputTarget(terminalB, true)
discardTerminalOutput(terminalB)
writeTerminalOutput(terminalA, 'new-a', { foreground: false })
writeTerminalOutput(terminalB, 'new-b', { foreground: false })
vi.advanceTimersByTime(50)
expect(terminalB.write).toHaveBeenCalledWith('new-b')
expect(terminalA.write).toHaveBeenCalledWith('new-a')
expect(terminalA.write.mock.invocationCallOrder[0]).toBeLessThan(
terminalB.write.mock.invocationCallOrder[0]
)
})
it('survives a write to a disposed terminal during background drain', async () => {
vi.useFakeTimers()
const { writeTerminalOutput } = await loadScheduler()
@@ -84,7 +84,6 @@ const BACKGROUND_BACKLOG_WARNING =
'\x18\x1b[0m\r\n[Orca skipped hidden terminal output because the backlog exceeded 2 MB.]\r\n'
const queuedByTerminal = new Map<TerminalOutputTarget, QueueEntry>()
const activeOutputTargets = new WeakSet<TerminalOutputTarget>()
const backlogRecoveryByTerminal = new WeakMap<
TerminalOutputTarget,
TerminalBacklogRecoveryRequest
@@ -526,13 +525,6 @@ function hasDrainableBacklog(): boolean {
}
function takeNextDrainableEntry(): QueueEntry | null {
for (const entry of queuedByTerminal.values()) {
if (!activeOutputTargets.has(entry.terminal) || !isEntryDrainable(entry)) {
continue
}
queuedByTerminal.delete(entry.terminal)
return entry
}
for (const entry of queuedByTerminal.values()) {
if (!isEntryDrainable(entry)) {
continue
@@ -624,17 +616,6 @@ function drainQueuedOutput(): void {
}
}
export function setActiveTerminalOutputTarget(
terminal: TerminalOutputTarget,
active: boolean
): void {
if (active) {
activeOutputTargets.add(terminal)
} else {
activeOutputTargets.delete(terminal)
}
}
export function writeTerminalOutput(
terminal: TerminalOutputTarget,
data: string,
@@ -931,7 +912,6 @@ export function waitForTerminalOutputParsed(terminal: TerminalOutputTarget): Pro
export function discardTerminalOutput(terminal: TerminalOutputTarget): void {
exposeDebugApi()
queuedByTerminal.delete(terminal)
activeOutputTargets.delete(terminal)
discardForegroundRenderSettle(terminal)
recordQueueDebugPressure()
}
@@ -95,6 +95,7 @@ export async function startSyntheticOpenCodeInjection({
`\x1b[1;2H\x1b[38;2;255;138;0m${spinner} OpenCode synthetic agent ${paneIndex}\x1b[0m`,
`\x1b[${row};4H\x1b[38;2;231;237;247m${body.padEnd(118, '#')}\x1b[0m`,
`\x1b[23;2H\x1b[38;2;106;169;255mstream ${String(frame).padStart(4, '0')} ${'#'.repeat(96)}\x1b[0m`,
'\x1b[?25h',
'\x1b[?2026l'
].join('')
}
@@ -0,0 +1,201 @@
import { randomUUID } from 'node:crypto'
import { rmSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import type { Page } from '@stablyai/playwright-test'
import { test, expect } from './helpers/orca-app'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import {
sendToTerminal,
waitForActivePanePtyId,
waitForActiveTerminalManager,
waitForTerminalOutput
} from './helpers/terminal'
type TerminalRenderState = {
coreCursorHidden: boolean | null
cursorElementCount: number
cursorVisibleElementCount: number
hasWebglCanvas: boolean
hasComplexScriptOutput: boolean
renderer: 'dom' | 'webgl'
}
const EMOJI_TABLE_MARKER = 'ORCA_EMOJI_TABLE_RENDER_DONE'
function emojiTableScript(marker: string): string {
const table = [
'| Emoji | Name | Age | Occupation | City | Favorite Color | Pet | Hobby |',
'| --- | --- | ---: | --- | --- | --- | --- | --- |',
'| 😀 | Alice Johnson | 28 | Engineer | New York | Blue | 🐕 Dog | 🎸 Guitar |',
'| 😂 | Bob Smith | 34 | Designer | London | Green | 🐱 Cat | 📚 Reading |',
'| 🥰 | Carol Davis | 22 | Student | Paris | Pink | 🐰 Rabbit | 🎨 Painting |',
'| 😎 | Dave Wilson | 45 | Architect | Tokyo | Black | 🐢 Turtle | 🏃 Running |',
'| 🤩 | Eve Martinez | 31 | Writer | Berlin | Purple | 🐦 Bird | ✈️ Traveling |',
'| 😜 | Frank Brown | 27 | Developer | Sydney | Red | 🐹 Hamster | 🎮 Gaming |',
'| 🥳 | Grace Lee | 39 | Teacher | Seoul | Yellow | 🐟 Fish | 🌱 Gardening |',
'| 🤔 | Henry Taylor | 41 | Doctor | Toronto | White | 🐕 Dog | 🍳 Cooking |',
'| 😴 | Ivy Anderson | 26 | Nurse | Chicago | Orange | 🐱 Cat | 🧘 Yoga |',
'| 🤗 | Jack Thomas | 33 | Lawyer | Boston | Navy | 🐢 Turtle | 📸 Photography |',
'| 😈 | Karen White | 29 | Artist | Miami | Teal | 🐹 Hamster | 🧶 Knitting |',
'| 😮 | Leo Harris | 37 | Pilot | Dubai | Gold | 🐦 Bird | 🚁 Drones |',
'| 🤠 | Mia Clark | 24 | Barista | Seattle | Coral | 🐰 Rabbit | 🎤 Singing |',
'| 😍 | Olivia Hall | 30 | Marketer | Austin | Pink | 🐱 Cat | 🏄 Surfing |'
].join('\r\n')
return `
process.stdout.write('\\x1b[?2026h\\x1b[?25l')
process.stdout.write('\\x1b[2J\\x1b[H')
process.stdout.write(${JSON.stringify(table)})
process.stdout.write('\\r\\n${marker}\\r\\n')
process.stdout.write('\\x1b[?25h\\x1b[?2026l')
setTimeout(() => process.exit(0), 50)
`
}
async function readActiveTerminalRenderState(page: Page): Promise<TerminalRenderState> {
return page.evaluate(() => {
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('No active terminal pane')
}
const cursorElements = Array.from(
pane.container.querySelectorAll<HTMLElement>('.xterm-cursor, .xterm-cursor-layer *')
)
const cursorVisibleElementCount = cursorElements.filter((element) => {
const style = window.getComputedStyle(element)
const rect = element.getBoundingClientRect()
return (
style.display !== 'none' &&
style.visibility !== 'hidden' &&
Number(style.opacity || '1') > 0 &&
rect.width > 0 &&
rect.height > 0
)
}).length
const terminal = pane.terminal as {
_core?: {
coreService?: { isCursorHidden?: boolean }
}
}
return {
coreCursorHidden:
typeof terminal._core?.coreService?.isCursorHidden === 'boolean'
? terminal._core.coreService.isCursorHidden
: null,
cursorElementCount: cursorElements.length,
cursorVisibleElementCount,
hasWebglCanvas: pane.container.querySelector('.xterm-webgl canvas') !== null,
hasComplexScriptOutput: pane.hasComplexScriptOutput === true,
renderer: pane.webglAddon ? 'webgl' : 'dom'
}
})
}
async function enableRiskyTerminalRendererPath(page: Page): Promise<void> {
await page.evaluate(() => {
const store = window.__store
if (!store) {
throw new Error('window.__store unavailable')
}
const state = store.getState()
store.setState({
settings: {
...state.settings!,
terminalGpuAcceleration: 'auto',
theme: 'dark'
}
})
})
}
test.describe('OpenCode emoji table terminal rendering', () => {
test('keeps emoji table output visually sane and restores the cursor', async ({
orcaPage,
testRepoPath
}, testInfo) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
await enableRiskyTerminalRendererPath(orcaPage)
const ptyId = await waitForActivePanePtyId(orcaPage)
const runId = randomUUID()
const marker = `${EMOJI_TABLE_MARKER}_${runId}`
const scriptPath = path.join(testRepoPath, `.orca-opencode-emoji-table-${runId}.mjs`)
writeFileSync(scriptPath, emojiTableScript(marker))
try {
await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`)
await waitForTerminalOutput(orcaPage, marker, 10_000)
await orcaPage.waitForTimeout(250)
const renderState = await readActiveTerminalRenderState(orcaPage)
testInfo.annotations.push({
type: 'opencode-emoji-table-rendering',
description: JSON.stringify(renderState)
})
expect(renderState.renderer).toBe('dom')
expect(renderState.hasWebglCanvas).toBe(false)
expect(renderState.coreCursorHidden).toBe(false)
expect(renderState.cursorVisibleElementCount).toBeGreaterThan(0)
} finally {
rmSync(scriptPath, { force: true })
}
})
test('local real OpenCode demo keeps table rendering and cursor visible', async ({
orcaPage
}, testInfo) => {
test.skip(
process.env.ORCA_E2E_REAL_OPENCODE !== '1',
'Set ORCA_E2E_REAL_OPENCODE=1 to exercise the locally installed OpenCode TUI'
)
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage, 30_000)
await enableRiskyTerminalRendererPath(orcaPage)
const ptyId = await waitForActivePanePtyId(orcaPage)
await sendToTerminal(
orcaPage,
ptyId,
'opencode run --demo --interactive "Give me markdown table dummy data a long table with emojis in it"\r'
)
try {
await waitForTerminalOutput(orcaPage, 'Give me markdown table', 15_000)
await waitForTerminalOutput(orcaPage, 'Emoji', 60_000)
await waitForTerminalOutput(orcaPage, 'Alice', 60_000)
await orcaPage.waitForTimeout(1_500)
await testInfo.attach('real-opencode-demo-table', {
body: await orcaPage.screenshot({ fullPage: true }),
contentType: 'image/png'
})
const renderState = await readActiveTerminalRenderState(orcaPage)
testInfo.annotations.push({
type: 'real-opencode-demo-rendering',
description: JSON.stringify(renderState)
})
expect(renderState.coreCursorHidden).toBe(false)
expect(renderState.cursorVisibleElementCount).toBeGreaterThan(0)
} finally {
await sendToTerminal(orcaPage, ptyId, '\x03').catch(() => undefined)
}
})
})