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' import { waitForPtyShellEcho } from './terminal-pty-readiness' import { analyzeRasterCursorCells, type TerminalRasterProbeTarget } from './terminal-cursor-raster-probe' import { nodeTerminalCommand } from './terminal-node-command' type TerminalRenderState = { coreCursorHidden: boolean | null cursorElementCount: number cursorVisibleElementCount: number cursorBlink: boolean | null blinkIntervalDuration: number | null cursorClassName: string cursorAnimationName: string cursorAnimationDuration: string rowContainerClassName: string xtermClassName: string hasWebglCanvas: boolean hasComplexScriptOutput: boolean renderer: 'dom' | 'webgl' } type CursorBlinkSample = { elapsedMs: number paintedCursorCellCount: number } 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 ` async function writeStdout(chunk) { await new Promise((resolve) => { process.stdout.write(chunk, resolve) }) if (process.platform === 'win32') { await new Promise((resolve) => setTimeout(resolve, 8)) } } await writeStdout('\\x1b[?2026h\\x1b[?25l\\x1b[2J\\x1b[H') for (const line of ${JSON.stringify(table.split('\r\n'))}) { await writeStdout(line + '\\r\\n') } await writeStdout('\\x1b[?25h\\x1b[?2026l') await writeStdout('${marker}\\r\\n') setTimeout(() => process.exit(0), 50) ` } async function readActiveTerminalRenderState(page: Page): Promise { 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 renderingDiagnostics = manager ?.getRenderingDiagnostics() .find((diagnostic) => diagnostic.paneId === pane.id) const cursorElements = Array.from( pane.container.querySelectorAll('.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 } } } const cursorElement = pane.container.querySelector('.xterm-cursor') const cursorStyle = cursorElement ? window.getComputedStyle(cursorElement) : null const rowContainer = pane.container.querySelector('.xterm-rows') const xterm = pane.container.querySelector('.xterm') return { coreCursorHidden: typeof terminal._core?.coreService?.isCursorHidden === 'boolean' ? terminal._core.coreService.isCursorHidden : null, cursorElementCount: cursorElements.length, cursorVisibleElementCount, cursorBlink: typeof pane.terminal.options.cursorBlink === 'boolean' ? pane.terminal.options.cursorBlink : null, blinkIntervalDuration: typeof pane.terminal.options.blinkIntervalDuration === 'number' ? pane.terminal.options.blinkIntervalDuration : null, cursorClassName: cursorElement?.className ?? '', cursorAnimationName: cursorStyle?.animationName ?? '', cursorAnimationDuration: cursorStyle?.animationDuration ?? '', rowContainerClassName: rowContainer?.className ?? '', xtermClassName: xterm?.className ?? '', hasWebglCanvas: renderingDiagnostics?.hasWebgl ?? false, hasComplexScriptOutput: renderingDiagnostics?.hasComplexScriptOutput ?? false, renderer: renderingDiagnostics?.hasWebgl ? 'webgl' : 'dom' } }) } async function forceCursorProbeTheme(page: Page): Promise { await 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') } pane.terminal.options.theme = { ...pane.terminal.options.theme, cursor: '#23ff45', cursorAccent: '#001000' } pane.terminal.options.cursorStyle = 'block' pane.terminal.options.cursorBlink = true pane.terminal.focus() pane.terminal.refresh(0, pane.terminal.rows - 1) }) } async function readActiveTerminalRasterTarget(page: Page): Promise { 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 screen = pane.container.querySelector('.xterm-screen') const dimensions = pane.terminal._core?._renderService?.dimensions?.css?.cell if (!screen || !dimensions) { throw new Error('Active terminal has no measurable xterm screen') } const rect = screen.getBoundingClientRect() return { clip: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, cellWidth: dimensions.width, cellHeight: dimensions.height, rows: pane.terminal.rows, cols: pane.terminal.cols } }) } async function sampleCursorBlink(page: Page): Promise { const samples: CursorBlinkSample[] = [] const target = await readActiveTerminalRasterTarget(page) const viewport = page.viewportSize() ?? undefined const start = performance.now() for (let index = 0; index < 9; index += 1) { if (index > 0) { await page.waitForTimeout(200) } const screenshot = await page.screenshot() const cells = analyzeRasterCursorCells(Buffer.from(screenshot), target, viewport) samples.push({ elapsedMs: performance.now() - start, paintedCursorCellCount: cells.length }) } return samples } async function enableRiskyTerminalRendererPath(page: Page): Promise { 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' } }) 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 manager?.setTerminalGpuAcceleration('auto') }) } 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) await waitForPtyShellEcho(orcaPage, ptyId, 20_000) 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, `${nodeTerminalCommand([scriptPath])}\r`) await waitForTerminalOutput(orcaPage, marker, 10_000) await orcaPage.waitForTimeout(250) await forceCursorProbeTheme(orcaPage) await orcaPage.waitForTimeout(50) const renderState = await readActiveTerminalRenderState(orcaPage) const blinkSamples = await sampleCursorBlink(orcaPage) testInfo.annotations.push({ type: 'opencode-emoji-table-rendering', description: JSON.stringify({ renderState, blinkSamples }) }) expect(renderState.hasComplexScriptOutput).toBe(false) expect(renderState.renderer).toBe(renderState.hasWebglCanvas ? 'webgl' : 'dom') expect(renderState.coreCursorHidden).toBe(false) if (!renderState.hasWebglCanvas) { expect(renderState.cursorVisibleElementCount).toBeGreaterThan(0) expect(renderState.cursorBlink).toBe(true) expect(renderState.cursorAnimationName).not.toBe('none') } expect(blinkSamples.some((sample) => sample.paintedCursorCellCount > 0)).toBe(true) expect(blinkSamples.some((sample) => sample.paintedCursorCellCount === 0)).toBe(true) } 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) } }) })