Keep Codex terminals responsive during startup (#5301)

This commit is contained in:
Brennan Benson 2026-06-15 16:40:45 -07:00 committed by GitHub
parent 3a87a9e572
commit bf31ecbad7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1240 additions and 63 deletions

View File

@ -1,12 +1,15 @@
import { describe, expect, it, vi } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Dirent, Stats } from 'node:fs'
import type * as FsPromises from 'fs/promises'
import { join } from 'path'
const { readdirMock, statMock } = vi.hoisted(() => ({
readdirMock: vi.fn<(dirPath: string) => Promise<Dirent[]>>(),
statMock: vi.fn<(filePath: string) => Promise<Stats>>()
}))
const { getLegacyCopiedCodexSessionBridgeScanPreferenceMock, readdirMock, statMock } = vi.hoisted(
() => ({
getLegacyCopiedCodexSessionBridgeScanPreferenceMock: vi.fn(),
readdirMock: vi.fn<(dirPath: string) => Promise<Dirent[]>>(),
statMock: vi.fn<(filePath: string) => Promise<Stats>>()
})
)
vi.mock('fs/promises', async () => {
const actual = await vi.importActual<typeof FsPromises>('fs/promises')
@ -29,7 +32,8 @@ vi.mock('../codex/codex-home-paths', () => ({
}))
vi.mock('../codex/codex-session-bridge', () => ({
getLegacyCopiedCodexSessionBridgeScanPreference: () => null
getLegacyCopiedCodexSessionBridgeScanPreference:
getLegacyCopiedCodexSessionBridgeScanPreferenceMock
}))
function dirent(name: string, kind: 'directory' | 'file'): Dirent {
@ -45,6 +49,13 @@ const largeSessionEntries = Array.from({ length: FILE_COUNT }, (_, index) =>
)
describe('listCodexSessionFiles large directories', () => {
beforeEach(() => {
getLegacyCopiedCodexSessionBridgeScanPreferenceMock.mockReset()
getLegacyCopiedCodexSessionBridgeScanPreferenceMock.mockReturnValue(null)
readdirMock.mockReset()
statMock.mockReset()
})
it('keeps nested session scans past the JavaScript spread-argument limit', async () => {
readdirMock.mockImplementation(async (dirPath) => {
if (dirPath === RUNTIME_SESSIONS_ROOT) {
@ -69,5 +80,6 @@ describe('listCodexSessionFiles large directories', () => {
const { listCodexSessionFiles } = await import('./scanner')
await expect(listCodexSessionFiles()).resolves.toHaveLength(FILE_COUNT)
expect(getLegacyCopiedCodexSessionBridgeScanPreferenceMock).not.toHaveBeenCalled()
})
})

View File

@ -1,6 +1,6 @@
/* eslint-disable max-lines -- Why: Codex discovery, incremental parsing, attribution, and aggregation all depend on the same event-normalization rules. Keeping them together makes the duplicate-snapshot logic easier to audit when usage totals look wrong. */
import { basename, join, win32, posix } from 'path'
import { createReadStream } from 'fs'
import { createReadStream, existsSync } from 'fs'
import { realpath, readdir, stat } from 'fs/promises'
import { createInterface } from 'readline'
import type { Repo } from '../../shared/types'
@ -55,6 +55,7 @@ type CodexUsageDeltaResolution =
| { kind: 'baseline'; nextTotals: CodexUsageRawUsage }
const YIELD_EVERY_FILES = 10
const YIELD_EVERY_DISCOVERY_ENTRIES = 100
function ensureNumber(value: unknown): number {
return typeof value === 'number' && Number.isFinite(value) ? value : 0
@ -88,17 +89,24 @@ async function canonicalizePath(pathValue: string): Promise<string> {
}
async function yieldToEventLoop(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 0))
await new Promise((resolve) => setImmediate(resolve))
}
async function walkJsonlFiles(dirPath: string): Promise<string[]> {
async function walkJsonlFiles(
dirPath: string,
progress: { entriesVisited: number } = { entriesVisited: 0 }
): Promise<string[]> {
const entries = await readdir(dirPath, { withFileTypes: true })
const files: string[] = []
for (const entry of entries) {
progress.entriesVisited += 1
if (progress.entriesVisited % YIELD_EVERY_DISCOVERY_ENTRIES === 0) {
await yieldToEventLoop()
}
const fullPath = join(dirPath, entry.name)
if (entry.isDirectory()) {
appendDiscoveredFiles(files, await walkJsonlFiles(fullPath))
appendDiscoveredFiles(files, await walkJsonlFiles(fullPath, progress))
continue
}
if (entry.isFile() && entry.name.endsWith('.jsonl')) {
@ -132,6 +140,10 @@ export function getCodexSessionDirectories(): string[] {
)
}
function hasLegacyCopiedSessionBridgeMarkers(): boolean {
return existsSync(join(getOrcaManagedCodexHomePath(), '.orca-session-copies'))
}
export async function listCodexSessionFiles(): Promise<string[]> {
const files: string[] = []
for (const dirPath of getCodexSessionDirectories()) {
@ -141,29 +153,37 @@ export async function listCodexSessionFiles(): Promise<string[]> {
// Missing or unreadable history in one home should not hide the other.
}
}
return dedupeCodexSessionFileAliases(files)
return dedupeCodexSessionFileAliases(files, hasLegacyCopiedSessionBridgeMarkers())
}
async function dedupeCodexSessionFileAliases(files: string[]): Promise<string[]> {
async function dedupeCodexSessionFileAliases(
files: string[],
hasLegacyBridgeMarkers: boolean
): Promise<string[]> {
const excludedAliases = new Set<string>()
for (const filePath of files) {
const legacyCopyBridge = getLegacyCopiedCodexSessionBridgeScanPreference(filePath)
if (!legacyCopyBridge) {
continue
}
if (legacyCopyBridge.sourceSkipBytes !== null) {
continue
}
excludedAliases.add(
await getPhysicalFileAliasKey(
legacyCopyBridge.preferManagedCopy ? legacyCopyBridge.sourcePath : filePath
if (hasLegacyBridgeMarkers) {
for (const [index, filePath] of files.entries()) {
const legacyCopyBridge = getLegacyCopiedCodexSessionBridgeScanPreference(filePath)
if ((index + 1) % YIELD_EVERY_DISCOVERY_ENTRIES === 0) {
await yieldToEventLoop()
}
if (!legacyCopyBridge) {
continue
}
if (legacyCopyBridge.sourceSkipBytes !== null) {
continue
}
excludedAliases.add(
await getPhysicalFileAliasKey(
legacyCopyBridge.preferManagedCopy ? legacyCopyBridge.sourcePath : filePath
)
)
)
}
}
const seenAliases = new Set<string>()
const uniqueFiles: string[] = []
for (const filePath of [...new Set(files)].sort()) {
for (const [index, filePath] of [...new Set(files)].sort().entries()) {
const aliasKey = await getCodexSessionFileAliasKey(filePath)
if (excludedAliases.has(aliasKey)) {
continue
@ -173,6 +193,9 @@ async function dedupeCodexSessionFileAliases(files: string[]): Promise<string[]>
}
seenAliases.add(aliasKey)
uniqueFiles.push(filePath)
if ((index + 1) % YIELD_EVERY_DISCOVERY_ENTRIES === 0) {
await yieldToEventLoop()
}
}
return uniqueFiles
}
@ -191,8 +214,14 @@ async function getPhysicalFileAliasKey(filePath: string): Promise<string> {
return `path:${await canonicalizePath(filePath)}`
}
function getLegacySourceSkipBytesByPath(files: string[]): Map<string, number> {
function getLegacySourceSkipBytesByPath(
files: string[],
hasLegacyBridgeMarkers = hasLegacyCopiedSessionBridgeMarkers()
): Map<string, number> {
const sourceSkipBytesByPath = new Map<string, number>()
if (!hasLegacyBridgeMarkers) {
return sourceSkipBytesByPath
}
for (const filePath of files) {
const legacyCopyBridge = getLegacyCopiedCodexSessionBridgeScanPreference(filePath)
if (!legacyCopyBridge || legacyCopyBridge.sourceSkipBytes === null) {
@ -734,7 +763,7 @@ function mergeSessions(
for (const session of sessions) {
const existing = target.get(session.sessionId)
if (!existing) {
target.set(session.sessionId, structuredClone(session))
target.set(session.sessionId, cloneSessionForMerge(session))
continue
}
@ -809,6 +838,15 @@ function mergeSessions(
}
}
function cloneSessionForMerge(session: CodexUsageSession): CodexUsageSession {
return {
...session,
locationBreakdown: session.locationBreakdown.map((entry) => ({ ...entry })),
modelBreakdown: session.modelBreakdown.map((entry) => ({ ...entry })),
locationModelBreakdown: session.locationModelBreakdown.map((entry) => ({ ...entry }))
}
}
function mergeDailyAggregates(
target: Map<string, CodexUsageDailyAggregate>,
dailyAggregates: CodexUsageDailyAggregate[]

View File

@ -1,6 +1,15 @@
/* eslint-disable max-lines */
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { CodexUsagePersistedState } from './types'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as Fs from 'fs'
import type {
CodexUsageDailyAggregate,
CodexUsagePersistedFile,
CodexUsagePersistedState,
CodexUsageSession
} from './types'
const { getPathMock } = vi.hoisted(() => ({
getPathMock: vi.fn(() => '/tmp/orca-test-userdata')
@ -12,11 +21,54 @@ vi.mock('electron', () => ({
}
}))
import { CodexUsageStore, normalizePersistedState } from './store'
vi.mock('fs', async () => {
const actual = await vi.importActual<typeof Fs>('fs')
return {
...actual,
writeFileSync: vi.fn(actual.writeFileSync)
}
})
vi.mock('./scanner', () => ({
createWorktreeRefs: vi.fn(() => []),
scanCodexUsageFiles: vi.fn()
}))
import { CodexUsageStore, initCodexUsagePath, normalizePersistedState } from './store'
import { scanCodexUsageFiles } from './scanner'
type ScanResult = {
processedFiles: CodexUsagePersistedFile[]
sessions: CodexUsageSession[]
dailyAggregates: CodexUsageDailyAggregate[]
}
function createDeferred<T>(): {
promise: Promise<T>
resolve: (value: T) => void
reject: (reason?: unknown) => void
} {
let resolve!: (value: T) => void
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((promiseResolve, promiseReject) => {
resolve = promiseResolve
reject = promiseReject
})
return { promise, resolve, reject }
}
function createEmptyScanResult(): ScanResult {
return {
processedFiles: [],
sessions: [],
dailyAggregates: []
}
}
function createStoreWithState(state: Partial<CodexUsagePersistedState>): CodexUsageStore {
const store = new CodexUsageStore({
getRepos: () => [],
getAllWorktreeMeta: () => ({}),
getWorktreeMeta: () => undefined
} as never)
@ -39,11 +91,79 @@ function createStoreWithState(state: Partial<CodexUsagePersistedState>): CodexUs
}
describe('CodexUsageStore', () => {
let tempUserData: string
beforeEach(() => {
tempUserData = mkdtempSync(join(tmpdir(), 'orca-codex-usage-store-'))
getPathMock.mockReturnValue(tempUserData)
initCodexUsagePath()
vi.mocked(writeFileSync).mockClear()
vi.mocked(scanCodexUsageFiles).mockReset()
vi.mocked(scanCodexUsageFiles).mockResolvedValue(createEmptyScanResult())
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-10T12:00:00.000-04:00'))
})
afterEach(() => {
vi.useRealTimers()
rmSync(tempUserData, { recursive: true, force: true })
})
it('persists a successful refresh with one compact disk write', async () => {
const store = createStoreWithState({
schemaVersion: 3,
scanState: {
enabled: true,
lastScanStartedAt: null,
lastScanCompletedAt: null,
lastScanError: null
}
})
await store.refresh(true)
expect(writeFileSync).toHaveBeenCalledTimes(1)
const persistedJson = readFileSync(join(tempUserData, 'orca-codex-usage.json'), 'utf-8')
expect(persistedJson).toBe(JSON.stringify(JSON.parse(persistedJson)))
expect(persistedJson).not.toContain('\n')
expect(JSON.parse(persistedJson).scanState).toMatchObject({
enabled: true,
lastScanStartedAt: new Date('2026-04-10T12:00:00.000-04:00').getTime(),
lastScanCompletedAt: new Date('2026-04-10T12:00:00.000-04:00').getTime(),
lastScanError: null
})
})
it('keeps scan start visible in memory while scan-start persistence is skipped', async () => {
const pendingScan = createDeferred<ScanResult>()
vi.mocked(scanCodexUsageFiles).mockReturnValueOnce(pendingScan.promise)
const store = createStoreWithState({
schemaVersion: 3,
scanState: {
enabled: true,
lastScanStartedAt: null,
lastScanCompletedAt: null,
lastScanError: 'previous failure'
}
})
const refreshPromise = store.refresh(true)
await Promise.resolve()
expect(store.getScanState()).toMatchObject({
isScanning: true,
lastScanStartedAt: new Date('2026-04-10T12:00:00.000-04:00').getTime(),
lastScanError: null
})
expect(writeFileSync).not.toHaveBeenCalled()
pendingScan.resolve(createEmptyScanResult())
await refreshPromise
expect(store.getScanState().isScanning).toBe(false)
expect(writeFileSync).toHaveBeenCalledTimes(1)
})
it('reports no data for Orca scope when only non-Orca Codex usage exists', async () => {
const store = createStoreWithState({
sessions: [

View File

@ -358,7 +358,7 @@ export class CodexUsageStore {
mkdirSync(dir, { recursive: true })
}
const tmpFile = `${usageFile}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`
writeFileSync(tmpFile, JSON.stringify(this.state, null, 2), 'utf-8')
writeFileSync(tmpFile, JSON.stringify(this.state), 'utf-8')
renameSync(tmpFile, usageFile)
}
@ -414,7 +414,7 @@ export class CodexUsageStore {
this.state.scanState.lastScanStartedAt = Date.now()
this.state.scanState.lastScanError = null
this.writeToDisk()
// Why: start-only writes rewrite the full usage cache before scan results change.
this.scanPromise = (async () => {
try {

View File

@ -85,6 +85,7 @@ vi.mock('react', async () => {
useLayoutEffect: () => {},
useCallback: <T extends (...args: never[]) => unknown>(callback: T) => callback,
useMemo: <T>(factory: () => T) => factory(),
useCallback: <T extends (...args: never[]) => unknown>(callback: T) => callback,
useRef: <T>(current: T) => ({ current }),
useState: <T>(initial: T | (() => T)) => {
const value = typeof initial === 'function' ? (initial as () => T)() : initial

View File

@ -1038,6 +1038,8 @@ export default function TerminalPane({
cwd,
startup: { command: 'codex' },
paneTransportsRef,
paneMode2031Ref,
paneLastThemeModeRef,
replayingPanesRef,
isActiveRef,
isVisibleRef,

View File

@ -2,6 +2,7 @@ import type { PtyTransport } from './pty-transport'
import type { ReplayingPanesRef } from './replay-guard'
import type { ParsedAgentStatusPayload } from '../../../../shared/agent-status-types'
import type { EventProps } from '../../../../shared/telemetry-events'
import type { TerminalColorSchemeMode } from '../../../../shared/terminal-color-scheme-protocol'
import type { TuiAgent } from '../../../../shared/types'
export type PtyConnectionDeps = {
@ -23,6 +24,8 @@ export type PtyConnectionDeps = {
restoredLeafId?: string | null
restoredPtyIdByLeafId?: Record<string, string>
paneTransportsRef: React.RefObject<Map<number, PtyTransport>>
paneMode2031Ref: React.RefObject<Map<number, boolean>>
paneLastThemeModeRef: React.RefObject<Map<number, TerminalColorSchemeMode>>
replayingPanesRef: ReplayingPanesRef
isActiveRef: React.RefObject<boolean>
isVisibleRef: React.RefObject<boolean>

View File

@ -343,6 +343,8 @@ function createDeps(overrides: Record<string, unknown> = {}) {
restoredLeafId: null,
restoredPtyIdByLeafId: {},
paneTransportsRef: { current: new Map() },
paneMode2031Ref: { current: new Map() },
paneLastThemeModeRef: { current: new Map() },
replayingPanesRef: { current: new Map() },
isActiveRef: { current: true },
isVisibleRef: { current: true },
@ -3489,7 +3491,8 @@ describe('connectPanePty', () => {
capturedDataCallback.current?.('\x1b]11;?\x1b\\startup frame\r\n')
expect(pane.terminal.write).toHaveBeenCalledWith(
expect(pane.terminal.write).toHaveBeenCalledWith('\x1b]11;?\x1b\\', expect.any(Function))
expect(pane.terminal.write).not.toHaveBeenCalledWith(
'\x1b]11;?\x1b\\startup frame\r\n',
expect.any(Function)
)
@ -3523,7 +3526,8 @@ describe('connectPanePty', () => {
capturedDataCallback.current?.('\x1b]11;?\x1b\\startup frame\r\n')
expect(pane.terminal.write).toHaveBeenCalledWith(
expect(pane.terminal.write).toHaveBeenCalledWith('\x1b]11;?\x1b\\', expect.any(Function))
expect(pane.terminal.write).not.toHaveBeenCalledWith(
'\x1b]11;?\x1b\\startup frame\r\n',
expect.any(Function)
)
@ -3565,7 +3569,7 @@ describe('connectPanePty', () => {
binding.dispose()
})
it('writes mode 2031 through hidden xterm instead of side-channel answering it', async () => {
it('side-channel answers mode 2031 when hidden Codex output is snapshot-backed', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
@ -3581,10 +3585,17 @@ describe('connectPanePty', () => {
const pane = createPane(1)
const manager = createManager(1)
const paneMode2031Ref = { current: new Map<number, boolean>() }
const paneLastThemeModeRef = { current: new Map<number, 'dark' | 'light'>() }
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({ isVisibleRef: { current: false } }) as never
createDeps({
isVisibleRef: { current: false },
paneMode2031Ref,
paneLastThemeModeRef,
startup: { command: 'codex' }
}) as never
)
await flushAsyncTicks(6)
@ -3593,8 +3604,10 @@ describe('connectPanePty', () => {
capturedDataCallback.current?.('\x1b[?2031h')
vi.advanceTimersByTime(50)
expect(transport.sendInput).not.toHaveBeenCalled()
expect(pane.terminal.write).toHaveBeenCalledWith('\x1b[?2031h')
expect(transport.sendInput).toHaveBeenCalledWith('\x1b[?997;2n')
expect(paneMode2031Ref.current.get(1)).toBe(true)
expect(paneLastThemeModeRef.current.get(1)).toBe('light')
expect(pane.terminal.write).not.toHaveBeenCalledWith('\x1b[?2031h')
} finally {
vi.useRealTimers()
}
@ -3602,6 +3615,656 @@ describe('connectPanePty', () => {
binding.dispose()
})
it('answers hidden Codex mode 2031 subscribes split across becoming visible', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
mockStoreState = {
...mockStoreState,
settings: { ...mockStoreState.settings, theme: 'light' }
}
const isVisibleRef = { current: false }
const paneMode2031Ref = { current: new Map<number, boolean>() }
const paneLastThemeModeRef = { current: new Map<number, 'dark' | 'light'>() }
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef,
paneMode2031Ref,
paneLastThemeModeRef,
startup: { command: 'codex' }
}) as never
)
await flushAsyncTicks(6)
capturedDataCallback.current?.('\x1b[?20')
isVisibleRef.current = true
capturedDataCallback.current?.('31h')
expect(transport.sendInput).toHaveBeenCalledWith('\x1b[?997;2n')
expect(paneMode2031Ref.current.get(1)).toBe(true)
expect(paneLastThemeModeRef.current.get(1)).toBe('light')
expect(pane.terminal.write).not.toHaveBeenCalledWith('31h', expect.any(Function))
binding.dispose()
})
it('does not keep mode 2031 subscribed when a skipped hidden chunk unsubscribes last', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const paneMode2031Ref = { current: new Map<number, boolean>() }
const paneLastThemeModeRef = { current: new Map<number, 'dark' | 'light'>() }
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef: { current: false },
paneMode2031Ref,
paneLastThemeModeRef,
startup: { command: 'codex' }
}) as never
)
await flushAsyncTicks(6)
capturedDataCallback.current?.('\x1b[?2031h\x1b[?2031l')
expect(transport.sendInput).not.toHaveBeenCalledWith(expect.stringMatching(/\?997/))
expect(paneMode2031Ref.current.has(1)).toBe(false)
expect(paneLastThemeModeRef.current.has(1)).toBe(false)
binding.dispose()
})
it('keeps hidden Codex redraw floods off the live xterm path', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef: { current: false },
startup: { command: 'codex' }
}) as never
)
await flushAsyncTicks(6)
vi.useFakeTimers()
try {
const hiddenCodexRedraw = `\x1b[?2026h\x1b[2J\x1b[H${'codex redraw '.repeat(8_000)}`
capturedDataCallback.current?.(hiddenCodexRedraw)
vi.advanceTimersByTime(50)
expect(pane.terminal.write).not.toHaveBeenCalledWith(hiddenCodexRedraw)
expect(window.api.pty.getMainBufferSnapshot).not.toHaveBeenCalled()
} finally {
vi.useRealTimers()
}
binding.dispose()
})
it('keeps hidden Codex terminal query chunks on the live xterm path', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef: { current: false },
startup: { command: 'codex' }
}) as never
)
await flushAsyncTicks(6)
capturedDataCallback.current?.('\x1b[c')
expect(pane.terminal.write).toHaveBeenCalledWith('\x1b[c', expect.any(Function))
binding.dispose()
})
it('keeps only coalesced hidden Codex terminal queries on the live xterm path', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef: { current: false },
startup: { command: 'codex' }
}) as never
)
await flushAsyncTicks(6)
const coalescedChunk = `\x1b[c\x1b[?2026h\x1b[2J\x1b[H${'codex redraw '.repeat(8_000)}`
capturedDataCallback.current?.(coalescedChunk)
expect(pane.terminal.write).toHaveBeenCalledWith('\x1b[c', expect.any(Function))
expect(pane.terminal.write).not.toHaveBeenCalledWith(coalescedChunk, expect.any(Function))
binding.dispose()
})
it('keeps split hidden Codex terminal queries on the live xterm path', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef: { current: false },
startup: { command: 'codex' }
}) as never
)
await flushAsyncTicks(6)
capturedDataCallback.current?.('\x1b[')
capturedDataCallback.current?.(`c\x1b[?2026h${'codex redraw '.repeat(8_000)}`)
expect(pane.terminal.write).toHaveBeenCalledWith('\x1b[c', expect.any(Function))
expect(pane.terminal.write).not.toHaveBeenCalledWith(
`c\x1b[?2026h${'codex redraw '.repeat(8_000)}`,
expect.any(Function)
)
binding.dispose()
})
it('keeps hidden Codex terminal queries split after ESC on the live xterm path', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef: { current: false },
startup: { command: 'codex' }
}) as never
)
await flushAsyncTicks(6)
capturedDataCallback.current?.('\x1b')
capturedDataCallback.current?.(`[c\x1b[?2026h${'codex redraw '.repeat(8_000)}`)
expect(pane.terminal.write).toHaveBeenCalledWith('\x1b[c', expect.any(Function))
expect(pane.terminal.write).not.toHaveBeenCalledWith(
`[c\x1b[?2026h${'codex redraw '.repeat(8_000)}`,
expect.any(Function)
)
binding.dispose()
})
it('flushes pending hidden Codex query prefixes when the pane becomes visible', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const isVisibleRef = { current: false }
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef,
startup: { command: 'codex' }
}) as never
)
try {
await flushAsyncTicks(6)
capturedDataCallback.current?.('\x1b')
isVisibleRef.current = true
capturedDataCallback.current?.('[c')
expect(pane.terminal.write).toHaveBeenCalledWith('\x1b[c', expect.any(Function))
} finally {
binding.dispose()
}
})
it('keeps split hidden-to-visible Codex stateful queries behind snapshot restore', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
typeof vi.fn
>
getMainBufferSnapshot.mockResolvedValue({
data: 'snapshot-before-cpr\r\n',
cols: 100,
rows: 30
})
const isVisibleRef = { current: false }
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef,
startup: { command: 'codex' }
}) as never
)
try {
await flushAsyncTicks(6)
capturedDataCallback.current?.('\x1b[6')
isVisibleRef.current = true
capturedDataCallback.current?.('n')
await flushAsyncTicks(20)
expect(pane.terminal.write).toHaveBeenCalledWith(
'snapshot-before-cpr\r\n',
expect.any(Function)
)
expect(pane.terminal.write).toHaveBeenCalledWith('\x1b[6n', expect.any(Function))
} finally {
binding.dispose()
}
})
it('keeps all visible bytes after a pending hidden ESC becomes non-query output', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
typeof vi.fn
>
getMainBufferSnapshot.mockResolvedValue({
data: 'snapshot-before-visible\r\n',
cols: 100,
rows: 30
})
const isVisibleRef = { current: false }
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef,
startup: { command: 'codex' }
}) as never
)
try {
await flushAsyncTicks(6)
capturedDataCallback.current?.('\x1b')
isVisibleRef.current = true
capturedDataCallback.current?.('hello')
await flushAsyncTicks(20)
expect(pane.terminal.write).toHaveBeenCalledWith('hello', expect.any(Function))
expect(pane.terminal.write).not.toHaveBeenCalledWith('ello', expect.any(Function))
} finally {
binding.dispose()
}
})
it('keeps split stateful Codex queries live after becoming visible', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const getMainBufferSnapshot = window.api.pty.getMainBufferSnapshot as unknown as ReturnType<
typeof vi.fn
>
getMainBufferSnapshot.mockResolvedValue({
data: 'snapshot-before-visible\r\n',
cols: 100,
rows: 30
})
const isVisibleRef = { current: false }
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef,
startup: { command: 'codex' }
}) as never
)
try {
await flushAsyncTicks(6)
capturedDataCallback.current?.('\x1b[')
isVisibleRef.current = true
capturedDataCallback.current?.('6n')
await flushAsyncTicks(20)
expect(pane.terminal.write).toHaveBeenCalledWith('\x1b[6n', expect.any(Function))
expect(pane.terminal.write).not.toHaveBeenCalledWith('6n', expect.any(Function))
} finally {
binding.dispose()
}
})
it('drops pending hidden Codex query prefixes when the PTY changes', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const isVisibleRef = { current: false }
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef,
startup: { command: 'codex' }
}) as never
)
try {
await flushAsyncTicks(6)
capturedDataCallback.current?.('\x1b')
;(transport.attach as unknown as (opts: { existingPtyId: string }) => void)({
existingPtyId: 'pty-new'
})
isVisibleRef.current = true
capturedDataCallback.current?.('[c')
expect(pane.terminal.write).not.toHaveBeenCalledWith('\x1b', expect.any(Function))
expect(pane.terminal.write).toHaveBeenCalledWith('[c', expect.any(Function))
} finally {
binding.dispose()
}
})
it('does not live-render split hidden Codex non-query CSI output', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef: { current: false },
startup: { command: 'codex' }
}) as never
)
await flushAsyncTicks(6)
capturedDataCallback.current?.('\x1b[?202')
capturedDataCallback.current?.('6h')
capturedDataCallback.current?.('\x1b[?203')
capturedDataCallback.current?.('1h')
expect(pane.terminal.write).not.toHaveBeenCalledWith('\x1b[?202', expect.any(Function))
expect(pane.terminal.write).not.toHaveBeenCalledWith('6h', expect.any(Function))
expect(pane.terminal.write).not.toHaveBeenCalledWith('\x1b[?203', expect.any(Function))
expect(pane.terminal.write).not.toHaveBeenCalledWith('1h', expect.any(Function))
binding.dispose()
})
it('keeps split hidden Codex OSC color queries on the live xterm path', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef: { current: false },
startup: { command: 'codex' }
}) as never
)
await flushAsyncTicks(6)
capturedDataCallback.current?.('\x1b]11;?')
capturedDataCallback.current?.(`\x1b\\\x1b[?2026h${'codex redraw '.repeat(8_000)}`)
expect(pane.terminal.write).toHaveBeenCalledWith('\x1b]11;?\x1b\\', expect.any(Function))
expect(pane.terminal.write).not.toHaveBeenCalledWith(
`\x1b\\\x1b[?2026h${'codex redraw '.repeat(8_000)}`,
expect.any(Function)
)
binding.dispose()
})
it('keeps hidden Codex OSC color queries split before the prefix on the live xterm path', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef: { current: false },
startup: { command: 'codex' }
}) as never
)
await flushAsyncTicks(6)
capturedDataCallback.current?.('\x1b]')
capturedDataCallback.current?.(`11;?\x1b\\\x1b[?2026h${'codex redraw '.repeat(8_000)}`)
expect(pane.terminal.write).toHaveBeenCalledWith('\x1b]11;?\x1b\\', expect.any(Function))
expect(pane.terminal.write).not.toHaveBeenCalledWith(
`11;?\x1b\\\x1b[?2026h${'codex redraw '.repeat(8_000)}`,
expect.any(Function)
)
binding.dispose()
})
it('keeps later hidden Codex stateless terminal queries on the live xterm path', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef: { current: false },
startup: { command: 'codex' }
}) as never
)
await flushAsyncTicks(6)
vi.useFakeTimers()
try {
vi.advanceTimersByTime(30_000)
capturedDataCallback.current?.('\x1b[5n')
vi.advanceTimersByTime(50)
expect(pane.terminal.write).toHaveBeenCalledWith('\x1b[5n', expect.any(Function))
} finally {
vi.useRealTimers()
}
binding.dispose()
})
it('keeps clean hidden Codex stateful cursor-position queries on the live xterm path', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef: { current: false },
startup: { command: 'codex' }
}) as never
)
await flushAsyncTicks(6)
capturedDataCallback.current?.('\x1b[10;20H\x1b[6n')
expect(window.api.pty.getMainBufferSnapshot).not.toHaveBeenCalled()
expect(pane.terminal.write).toHaveBeenCalledWith('\x1b[10;20H\x1b[6n', expect.any(Function))
binding.dispose()
})
it('does not answer dirty hidden Codex stateful queries from stale xterm state', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-id'
})
transportFactoryQueue.push(transport)
const pane = createPane(1)
const manager = createManager(1)
const binding = connectPanePty(
pane as never,
manager as never,
createDeps({
isVisibleRef: { current: false },
startup: { command: 'codex' }
}) as never
)
await flushAsyncTicks(6)
capturedDataCallback.current?.(`\x1b[2J\x1b[H${'codex redraw '.repeat(8_000)}`)
capturedDataCallback.current?.('\x1b[6n')
expect(window.api.pty.getMainBufferSnapshot).not.toHaveBeenCalled()
expect(pane.terminal.write).not.toHaveBeenCalledWith('\x1b[6n', expect.any(Function))
binding.dispose()
})
it('writes ordinary hidden output live instead of proactively restoring a snapshot', async () => {
const { connectPanePty } = await import('./pty-connection')
const transport = createMockTransport('pty-id')

View File

@ -110,7 +110,6 @@ const HIDDEN_OUTPUT_RESTORE_SCROLLBACK_ROWS = 5000
const HIDDEN_OUTPUT_RESTORE_PENDING_CHARS = 512 * 1024
const HIDDEN_OUTPUT_RESTORE_DEFERRED_RETRY_MS = 50
const HIDDEN_OUTPUT_RESTORE_DEFERRED_RETRY_MAX = 3
const HIDDEN_STARTUP_RENDERER_QUERY_WINDOW_MS = 10_000
const STARTUP_COMMAND_EXTENSION_RE = /\.(?:exe|cmd|bat|ps1)$/i
const TERMINAL_RENDERER_RISK_SCAN_TAIL_CHARS = 256
const CURSOR_SHOW_SEQUENCE = '\x1b[?25h'
@ -307,6 +306,168 @@ function shouldKeepHiddenStartupRendererQueriesLive(
return startup?.telemetry?.agent_kind === 'codex' || isCodexStartupCommand(startup?.command ?? '')
}
function containsHiddenStartupRendererQuery(data: string): boolean {
// Why: hidden Codex startup must not live-render ordinary redraw floods, but
// query chunks still need xterm's built-in terminal replies to unblock TUIs.
return containsCsiRendererQuery(data) || data.includes('\x1b]10;?') || data.includes('\x1b]11;?')
}
const HIDDEN_STARTUP_RENDERER_QUERY_PENDING_CHARS = 64
const HIDDEN_STARTUP_OSC_COLOR_QUERY_PREFIXES = ['\x1b]10;?', '\x1b]11;?'] as const
function findOscTerminatorIndex(data: string, offset: number): number {
for (let index = offset; index < data.length; index++) {
const code = data.charCodeAt(index)
if (code === 0x07) {
return index + 1
}
if (code === 0x1b && data[index + 1] === '\\') {
return index + 2
}
}
return -1
}
function extractHiddenStartupRendererQueryData(
data: string,
pending: string
): { statelessQueryData: string; statefulQueryData: string; pending: string } {
const input = pending + data
let statelessQueryData = ''
let statefulQueryData = ''
let offset = 0
while (offset < input.length) {
const candidateIndex = input.indexOf('\x1b', offset)
if (candidateIndex === -1) {
break
}
if (candidateIndex + 1 >= input.length) {
return { statelessQueryData, statefulQueryData, pending: input.slice(candidateIndex) }
}
if (input.startsWith('\x1b[', candidateIndex)) {
const finalByteIndex = findCsiFinalByteIndex(input, candidateIndex + 2)
if (finalByteIndex === -1) {
return {
statelessQueryData,
statefulQueryData,
pending: input.slice(
candidateIndex,
candidateIndex + HIDDEN_STARTUP_RENDERER_QUERY_PENDING_CHARS
)
}
}
const sequence = input.slice(candidateIndex, finalByteIndex + 1)
if (isStatelessRendererReplyCsiQuery(sequence)) {
statelessQueryData += sequence
} else if (isStatefulRendererReplyCsiQuery(sequence)) {
statefulQueryData += sequence
}
offset = finalByteIndex + 1
continue
}
if (input.startsWith('\x1b]', candidateIndex)) {
const remaining = input.slice(candidateIndex)
const matchingPrefix = HIDDEN_STARTUP_OSC_COLOR_QUERY_PREFIXES.find((prefix) =>
remaining.startsWith(prefix)
)
if (!matchingPrefix) {
if (
HIDDEN_STARTUP_OSC_COLOR_QUERY_PREFIXES.some((prefix) => prefix.startsWith(remaining))
) {
return { statelessQueryData, statefulQueryData, pending: remaining }
}
offset = candidateIndex + 2
continue
}
const terminatorIndex = findOscTerminatorIndex(input, candidateIndex + matchingPrefix.length)
if (terminatorIndex === -1) {
return {
statelessQueryData,
statefulQueryData,
pending: input.slice(
candidateIndex,
candidateIndex + HIDDEN_STARTUP_RENDERER_QUERY_PENDING_CHARS
)
}
}
statelessQueryData += input.slice(candidateIndex, terminatorIndex)
offset = terminatorIndex
continue
}
if (
HIDDEN_STARTUP_OSC_COLOR_QUERY_PREFIXES.some((prefix) =>
prefix.startsWith(input.slice(candidateIndex))
)
) {
return { statelessQueryData, statefulQueryData, pending: input.slice(candidateIndex) }
}
{
offset = candidateIndex + 1
continue
}
}
return { statelessQueryData, statefulQueryData, pending: '' }
}
function containsCsiRendererQuery(data: string): boolean {
let offset = data.indexOf('\x1b[')
while (offset !== -1) {
const finalByteIndex = findCsiFinalByteIndex(data, offset + 2)
if (finalByteIndex === -1) {
return false
}
const sequence = data.slice(offset, finalByteIndex + 1)
if (isStatelessRendererReplyCsiQuery(sequence) || isStatefulRendererReplyCsiQuery(sequence)) {
return true
}
offset = data.indexOf('\x1b[', finalByteIndex + 1)
}
return false
}
function containsStatefulRendererQuery(data: string): boolean {
let offset = data.indexOf('\x1b[')
while (offset !== -1) {
const finalByteIndex = findCsiFinalByteIndex(data, offset + 2)
if (finalByteIndex === -1) {
return false
}
const sequence = data.slice(offset, finalByteIndex + 1)
if (isStatefulRendererReplyCsiQuery(sequence)) {
return true
}
offset = data.indexOf('\x1b[', finalByteIndex + 1)
}
return false
}
function findCsiFinalByteIndex(data: string, offset: number): number {
for (let index = offset; index < data.length; index++) {
const code = data.charCodeAt(index)
if (code >= 0x40 && code <= 0x7e) {
return index
}
}
return -1
}
function isStatelessRendererReplyCsiQuery(sequence: string): boolean {
if (sequence.endsWith('c')) {
return true
}
return sequence === '\x1b[5n'
}
function isStatefulRendererReplyCsiQuery(sequence: string): boolean {
return sequence === '\x1b[6n' || (sequence.startsWith('\x1b[?') && sequence.endsWith('$p'))
}
let codexRestartNoticePresenceSource: Record<
string,
{ previousAccountLabel: string; nextAccountLabel: string }
@ -1873,6 +2034,8 @@ export function connectPanePty(
}
const startFreshSpawn = (): void => {
clearPaneMode2031State()
clearHiddenOutputRestoreState()
// Why: pre-signal the main process so its cooperation gate suppresses
// the daemon-snapshot seed for this paneKey. We issue declare and the
// spawn back-to-back without awaiting, because Electron's
@ -2016,9 +2179,9 @@ export function connectPanePty(
let foregroundImmediateBudgetChars = 0
let foregroundImmediateBudgetWindowStart = 0
let hiddenMode2031ScanTail = ''
const hiddenStartupRendererQueryUntil = shouldKeepHiddenStartupRendererQueriesLive(paneStartup)
? Date.now() + HIDDEN_STARTUP_RENDERER_QUERY_WINDOW_MS
: 0
const shouldSnapshotHiddenCodexOutput = shouldKeepHiddenStartupRendererQueriesLive(paneStartup)
let hiddenStartupRendererQueryPending = ''
let hiddenRendererStateDirty = false
function canUseMainBufferSnapshot(ptyId: string | null): ptyId is string {
return Boolean(ptyId) && !isRemoteRuntimePtyId(ptyId)
@ -2051,18 +2214,14 @@ export function connectPanePty(
return transport.serializeBuffer(opts)
}
function isHiddenStartupRendererQueryWindowActive(): boolean {
return (
paneStartup !== null &&
Date.now() < hiddenStartupRendererQueryUntil &&
!shouldWritePtyOutputForeground(deps.isVisibleRef.current)
)
}
function respondToSkippedMode2031Subscribe(data: string): void {
const scan = scanMode2031Sequences(hiddenMode2031ScanTail, data)
hiddenMode2031ScanTail = scan.tail
if (!scan.subscribe) {
if (scan.finalState === 'unsubscribed') {
deps.paneMode2031Ref.current.delete(pane.id)
deps.paneLastThemeModeRef.current.delete(pane.id)
}
if (scan.finalState !== 'subscribed') {
return
}
const settings = useAppStore.getState().settings
@ -2070,7 +2229,9 @@ export function connectPanePty(
// Why: hidden snapshot-backed panes skip xterm.write for PTY bytes. Answer
// mode 2031 out-of-band so TUIs still render the snapshot with the same
// theme-dependent styling they would have used in a visible pane.
deps.paneMode2031Ref.current.set(pane.id, true)
transport.sendInput(mode2031SequenceFor(mode))
deps.paneLastThemeModeRef.current.set(pane.id, mode)
recordHiddenMode2031Reply()
}
function beforeTerminalOutputWrite(): void {
@ -2169,14 +2330,19 @@ export function connectPanePty(
)
}
function writePtyOutputToXterm(data: string, foreground: boolean): void {
function writePtyOutputToXterm(
data: string,
foreground: boolean,
opts?: { hiddenStartupRendererQuery?: boolean }
): void {
if (foreground) {
resetHiddenOutputRestoreIfPtyChanged()
}
const parseHiddenStartupOutput =
!foreground &&
canUseHiddenOutputSnapshot(transport.getPtyId()) &&
isHiddenStartupRendererQueryWindowActive()
shouldSnapshotHiddenCodexOutput &&
(opts?.hiddenStartupRendererQuery === true || containsHiddenStartupRendererQuery(data))
const synchronizedOutputStarted =
shouldProtectNativeWindowsSynchronizedOutput &&
foreground &&
@ -2245,18 +2411,124 @@ export function connectPanePty(
}
function shouldSkipHiddenRendererOutput(foreground: boolean, data: string): boolean {
void foreground
void data
// Why: release correctness beats the hidden-output perf optimization.
// Real OpenCode tables still corrupt after workspace switching when PTY
// bytes bypass the renderer, so keep hidden panes on the live xterm path
// and leave snapshot skipping for a later perf branch.
return false
if (
foreground ||
!shouldSnapshotHiddenCodexOutput ||
!canUseHiddenOutputSnapshot(transport.getPtyId())
) {
return false
}
// Why: CPR/DECRQM replies depend on ordered terminal state. Keep the rare
// clean stateful-query chunk live; after skipped bytes, avoid stale replies.
return hiddenRendererStateDirty || !containsStatefulRendererQuery(data)
}
function writeHiddenStartupRendererQueries(data: string): void {
const extracted = extractHiddenStartupRendererQueryData(
data,
hiddenStartupRendererQueryPending
)
hiddenStartupRendererQueryPending = extracted.pending
if (extracted.statelessQueryData) {
writePtyOutputToXterm(extracted.statelessQueryData, false, {
hiddenStartupRendererQuery: true
})
}
// Stateful hidden queries require ordered terminal state. If this pane's
// hidden xterm is dirty, skipping is safer than sending stale CPR/DECRQM.
}
function takeHiddenStartupRendererQueryPendingForForeground(data: string): {
statelessQueryData: string
statefulQueryData: string
remainingData: string
consumedCurrentChars: number
} {
const pending = hiddenStartupRendererQueryPending
hiddenStartupRendererQueryPending = ''
if (!pending) {
return {
statelessQueryData: '',
statefulQueryData: '',
remainingData: data,
consumedCurrentChars: 0
}
}
const input = pending + data
let statelessQueryData = ''
let statefulQueryData = ''
let consumedInputChars = pending.length
let nextPending = ''
if (input.startsWith('\x1b[')) {
const finalByteIndex = findCsiFinalByteIndex(input, 2)
if (finalByteIndex === -1) {
nextPending = input.slice(0, HIDDEN_STARTUP_RENDERER_QUERY_PENDING_CHARS)
consumedInputChars = input.length
} else {
const sequence = input.slice(0, finalByteIndex + 1)
if (isStatelessRendererReplyCsiQuery(sequence)) {
statelessQueryData = sequence
} else if (isStatefulRendererReplyCsiQuery(sequence)) {
statefulQueryData = sequence
}
consumedInputChars = finalByteIndex + 1
}
} else if (input.startsWith('\x1b]')) {
const matchingPrefix = HIDDEN_STARTUP_OSC_COLOR_QUERY_PREFIXES.find((prefix) =>
input.startsWith(prefix)
)
const terminatorIndex = findOscTerminatorIndex(input, 2)
if (
!matchingPrefix &&
HIDDEN_STARTUP_OSC_COLOR_QUERY_PREFIXES.some((prefix) => prefix.startsWith(input))
) {
nextPending = input
consumedInputChars = input.length
} else if (terminatorIndex === -1) {
nextPending = input.slice(0, HIDDEN_STARTUP_RENDERER_QUERY_PENDING_CHARS)
consumedInputChars = input.length
} else {
if (matchingPrefix) {
statelessQueryData = input.slice(0, terminatorIndex)
}
consumedInputChars = terminatorIndex
}
} else if (input.length === 1) {
nextPending = input
consumedInputChars = input.length
} else {
consumedInputChars = pending.length
}
hiddenStartupRendererQueryPending = nextPending
const consumedCurrentChars = Math.max(0, consumedInputChars - pending.length)
return {
statelessQueryData,
statefulQueryData,
remainingData: data.slice(consumedCurrentChars),
consumedCurrentChars
}
}
function metaAfterConsumingCurrentChars(
meta: PtyDataMeta | undefined,
consumedCurrentChars: number
): PtyDataMeta | undefined {
if (consumedCurrentChars === 0 || typeof meta?.rawLength !== 'number') {
return meta
}
return {
...meta,
rawLength: Math.max(0, meta.rawLength - consumedCurrentChars)
}
}
function skipHiddenRendererOutput(data: string): void {
writeHiddenStartupRendererQueries(data)
respondToSkippedMode2031Subscribe(data)
markHiddenOutputRestoreNeeded()
hiddenRendererStateDirty = true
if (hiddenOutputRestoreInFlight) {
hiddenOutputRestoreFreshSnapshotNeeded = true
}
@ -2399,11 +2671,18 @@ export function connectPanePty(
function clearHiddenOutputRestoreState(): void {
clearPendingLiveChunksDuringRestore()
hiddenStartupRendererQueryPending = ''
hiddenRendererStateDirty = false
hiddenOutputRestoreNeeded = false
hiddenOutputRestorePtyId = null
hiddenOutputRestoreGeneration += 1
}
function clearPaneMode2031State(): void {
deps.paneMode2031Ref.current.delete(pane.id)
deps.paneLastThemeModeRef.current.delete(pane.id)
}
function resetHiddenOutputRestoreIfPtyChanged(): void {
if (hiddenOutputRestorePtyId === null) {
return
@ -2412,6 +2691,7 @@ export function connectPanePty(
// Why: renderer backlog is tied to the old PTY stream; after reattach,
// queued hidden bytes must not delay or replay before the new PTY.
clearHiddenOutputRestoreState()
clearPaneMode2031State()
discardTerminalOutput(pane.terminal)
}
}
@ -2475,6 +2755,7 @@ export function connectPanePty(
writeReplayData('\x1b[2J\x1b[3J\x1b[H')
writeReplayData(snapshot.data)
writeReplayData(POST_REPLAY_LIVE_SNAPSHOT_RESET)
hiddenRendererStateDirty = false
recordTerminalOutput(pane.terminal)
const currentPtyId = transport.getPtyId()
if (currentPtyId && !getFitOverrideForPty(currentPtyId)) {
@ -2660,6 +2941,24 @@ export function connectPanePty(
// output the user is watching. Throttle only when the pane or whole
// Electron document is hidden.
const foreground = shouldWritePtyOutputForeground(deps.isVisibleRef.current)
if (foreground && hiddenMode2031ScanTail) {
respondToSkippedMode2031Subscribe(data)
}
// Why: a hidden Codex query can be split just before visibility changes;
// xterm needs the completed query, while other bytes still follow restore.
const pendingForegroundQuery = foreground
? takeHiddenStartupRendererQueryPendingForForeground(data)
: null
const rendererData = pendingForegroundQuery?.remainingData ?? data
const rendererMeta = metaAfterConsumingCurrentChars(
meta,
pendingForegroundQuery?.consumedCurrentChars ?? 0
)
if (pendingForegroundQuery?.statelessQueryData) {
writePtyOutputToXterm(pendingForegroundQuery.statelessQueryData, true, {
hiddenStartupRendererQuery: true
})
}
const restoreAppliesToCurrentPty =
hiddenOutputRestorePtyId !== null && transport.getPtyId() === hiddenOutputRestorePtyId
if (shouldSkipHiddenRendererOutput(foreground, data)) {
@ -2669,14 +2968,22 @@ export function connectPanePty(
restoreAppliesToCurrentPty
) {
if (foreground) {
queueLiveChunkDuringRestore(data, meta)
if (pendingForegroundQuery?.statefulQueryData) {
queueLiveChunkDuringRestore(pendingForegroundQuery.statefulQueryData)
}
queueLiveChunkDuringRestore(rendererData, rendererMeta)
requestHiddenOutputRestoreIfNeeded()
} else if (hiddenOutputRestoreInFlight) {
hiddenOutputRestoreNeeded = true
hiddenOutputRestoreFreshSnapshotNeeded = true
}
} else {
writePtyOutputToXterm(data, foreground)
if (pendingForegroundQuery?.statefulQueryData) {
writePtyOutputToXterm(pendingForegroundQuery.statefulQueryData, true, {
hiddenStartupRendererQuery: true
})
}
writePtyOutputToXterm(rendererData, foreground)
}
schedulePendingStartupCommandDelivery()
@ -2981,6 +3288,8 @@ export function connectPanePty(
? Promise.resolve(null)
: window.api.pty.declarePendingPaneSerializer(cacheKey).catch(() => null)
let expiredReattachError = false
clearPaneMode2031State()
clearHiddenOutputRestoreState()
const reattachPromise = transport.connect({
url: '',
cols,
@ -3224,6 +3533,8 @@ export function connectPanePty(
// off — otherwise the next remount reads the same dead ptyId from
// the store and lands in this branch again in a loop.
try {
clearPaneMode2031State()
clearHiddenOutputRestoreState()
transport.attach({
existingPtyId: attachPtyId,
cols,
@ -3277,6 +3588,8 @@ export function connectPanePty(
// even if no later spawn event or layout snapshot runs.
deps.syncPanePtyLayoutBinding(pane.id, spawnedPtyId)
deps.updateTabPtyId(deps.tabId, spawnedPtyId)
clearPaneMode2031State()
clearHiddenOutputRestoreState()
transport.attach({
existingPtyId: spawnedPtyId,
cols,

View File

@ -523,6 +523,8 @@ export function useTerminalPaneLifecycle({
cwd,
startup,
paneTransportsRef,
paneMode2031Ref,
paneLastThemeModeRef,
replayingPanesRef,
isActiveRef,
isVisibleRef,

View File

@ -22,14 +22,32 @@ describe('terminal color scheme protocol', () => {
it('detects mode 2031 subscribes in compound and split private mode sequences', () => {
expect(scanMode2031Sequences('', '\x1b[?25;2031h')).toMatchObject({
subscribe: true,
finalState: 'subscribed',
tail: ''
})
const first = scanMode2031Sequences('', '\x1b[?20')
expect(first).toMatchObject({ subscribe: false, tail: '\x1b[?20' })
expect(first).toMatchObject({ subscribe: false, finalState: null, tail: '\x1b[?20' })
expect(scanMode2031Sequences(first.tail, '31h')).toMatchObject({
subscribe: true,
finalState: 'subscribed',
tail: ''
})
})
it('reports the final mode 2031 state in match order', () => {
expect(scanMode2031Sequences('', '\x1b[?2031h\x1b[?2031l')).toMatchObject({
subscribe: true,
unsubscribe: true,
finalState: 'unsubscribed',
tail: ''
})
expect(scanMode2031Sequences('', '\x1b[?2031l\x1b[?2031h')).toMatchObject({
subscribe: true,
unsubscribe: true,
finalState: 'subscribed',
tail: ''
})
})

View File

@ -22,12 +22,14 @@ export function resolveTerminalColorSchemeMode(
export type Mode2031ScanResult = {
subscribe: boolean
unsubscribe: boolean
finalState: 'subscribed' | 'unsubscribed' | null
tail: string
}
const NO_MODE_2031_SEQUENCE: Mode2031ScanResult = {
subscribe: false,
unsubscribe: false,
finalState: null,
tail: ''
}
@ -39,6 +41,7 @@ export function scanMode2031Sequences(previousTail: string, data: string): Mode2
const result: Mode2031ScanResult = {
subscribe: false,
unsubscribe: false,
finalState: null,
tail: extractPrivateModeScanTail(input)
}
// oxlint-disable-next-line no-control-regex -- terminal escape sequences require control chars
@ -51,8 +54,10 @@ export function scanMode2031Sequences(previousTail: string, data: string): Mode2
}
if ((match[2] ?? match[4]) === 'h') {
result.subscribe = true
result.finalState = 'subscribed'
} else {
result.unsubscribe = true
result.finalState = 'unsubscribed'
}
}
return result