mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 16:02:55 +00:00
Reduce hidden terminal pressure during Codex typing (#2661)
* Reduce hidden terminal pressure during Codex typing * Make terminal lag stress test tunable * Add real Codex terminal stress mode
This commit is contained in:
@@ -433,6 +433,101 @@ describe('connectPanePty', () => {
|
||||
delete (globalThis as Record<string, unknown>).__ptyConnectDiag
|
||||
})
|
||||
|
||||
it('coalesces same-class hidden title frames', async () => {
|
||||
vi.useFakeTimers()
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
transportFactoryQueue.push(createMockTransport('pty-1'))
|
||||
const pane = createPane(1)
|
||||
const manager = {
|
||||
...createManager(1),
|
||||
getActivePane: vi.fn(() => ({ id: 1 }))
|
||||
}
|
||||
const setRuntimePaneTitle = vi.fn((tabId: string, paneId: number, title: string) => {
|
||||
mockStoreState.runtimePaneTitlesByTabId[tabId] = {
|
||||
...mockStoreState.runtimePaneTitlesByTabId[tabId],
|
||||
[paneId]: title
|
||||
}
|
||||
})
|
||||
const deps = createDeps({
|
||||
isVisibleRef: { current: false },
|
||||
setRuntimePaneTitle
|
||||
})
|
||||
|
||||
const binding = connectPanePty(pane as never, manager as never, deps as never)
|
||||
const options = createdTransportOptions[0] as {
|
||||
onTitleChange: (title: string, rawTitle: string) => void
|
||||
}
|
||||
|
||||
options.onTitleChange('Codex working one', 'Codex working one')
|
||||
options.onTitleChange('Codex working two', 'Codex working two')
|
||||
|
||||
expect(setRuntimePaneTitle).toHaveBeenCalledTimes(1)
|
||||
vi.advanceTimersByTime(99)
|
||||
expect(setRuntimePaneTitle).toHaveBeenCalledTimes(1)
|
||||
vi.advanceTimersByTime(1)
|
||||
expect(setRuntimePaneTitle).toHaveBeenCalledTimes(2)
|
||||
expect(setRuntimePaneTitle).toHaveBeenLastCalledWith('tab-1', 1, 'Codex working two')
|
||||
|
||||
binding.dispose()
|
||||
})
|
||||
|
||||
it('coalesces same-state hidden agent status pings', async () => {
|
||||
vi.useFakeTimers()
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
transportFactoryQueue.push(createMockTransport('pty-1'))
|
||||
const deps = createDeps({ isVisibleRef: { current: false } })
|
||||
|
||||
const binding = connectPanePty(createPane(1) as never, createManager(1) as never, deps as never)
|
||||
const options = createdTransportOptions[0] as {
|
||||
onAgentStatus: (payload: { state: 'working'; prompt: string; toolInput?: string }) => void
|
||||
}
|
||||
|
||||
options.onAgentStatus({ state: 'working', prompt: 'p', toolInput: 'first' })
|
||||
options.onAgentStatus({ state: 'working', prompt: 'p', toolInput: 'second' })
|
||||
|
||||
expect(mockStoreState.setAgentStatus).toHaveBeenCalledTimes(1)
|
||||
vi.advanceTimersByTime(100)
|
||||
expect(mockStoreState.setAgentStatus).toHaveBeenCalledTimes(2)
|
||||
expect(mockStoreState.setAgentStatus).toHaveBeenLastCalledWith(
|
||||
makePaneKey('tab-1', LEAF_1),
|
||||
{ state: 'working', prompt: 'p', toolInput: 'second' },
|
||||
undefined
|
||||
)
|
||||
|
||||
binding.dispose()
|
||||
})
|
||||
|
||||
it('keeps hidden agent status transitions immediate', async () => {
|
||||
vi.useFakeTimers()
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
transportFactoryQueue.push(createMockTransport('pty-1'))
|
||||
const deps = createDeps({ isVisibleRef: { current: false } })
|
||||
|
||||
const binding = connectPanePty(createPane(1) as never, createManager(1) as never, deps as never)
|
||||
const options = createdTransportOptions[0] as {
|
||||
onAgentStatus: (payload: {
|
||||
state: 'working' | 'waiting' | 'done'
|
||||
prompt: string
|
||||
toolInput?: string
|
||||
}) => void
|
||||
}
|
||||
|
||||
options.onAgentStatus({ state: 'working', prompt: 'p', toolInput: 'first' })
|
||||
options.onAgentStatus({ state: 'waiting', prompt: 'p', toolInput: 'blocked' })
|
||||
options.onAgentStatus({ state: 'done', prompt: 'p', toolInput: 'complete' })
|
||||
|
||||
expect(mockStoreState.setAgentStatus).toHaveBeenCalledTimes(3)
|
||||
vi.advanceTimersByTime(100)
|
||||
expect(mockStoreState.setAgentStatus).toHaveBeenCalledTimes(3)
|
||||
expect(mockStoreState.setAgentStatus).toHaveBeenLastCalledWith(
|
||||
makePaneKey('tab-1', LEAF_1),
|
||||
{ state: 'done', prompt: 'p', toolInput: 'complete' },
|
||||
undefined
|
||||
)
|
||||
|
||||
binding.dispose()
|
||||
})
|
||||
|
||||
it('does not retain PTY connect diagnostics unless e2e debug state is enabled', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
@@ -33,7 +33,10 @@ import { recordTerminalOutput } from '@/lib/pane-manager/pane-scroll'
|
||||
import { makePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import { createTerminalCommandLifecycle } from './terminal-command-lifecycle'
|
||||
import { e2eConfig } from '@/lib/e2e-config'
|
||||
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
|
||||
import type {
|
||||
AgentStatusEntry,
|
||||
ParsedAgentStatusPayload
|
||||
} from '../../../../shared/agent-status-types'
|
||||
import { isWebTerminalSurfaceTabId } from '@/runtime/web-terminal-surface-id'
|
||||
import {
|
||||
createAgentInterruptInference,
|
||||
@@ -53,6 +56,7 @@ const PTY_CONNECT_DIAG_LIMIT = 200
|
||||
const AGENT_TASK_COMPLETE_NOTIFICATION_GRACE_MS = 250
|
||||
const AGENT_TASK_COMPLETE_NOTIFICATION_MAX_WAIT_MS = 1000
|
||||
const AGENT_TASK_COMPLETE_NOTIFICATION_DETAIL_MAX_AGE_MS = 10_000
|
||||
const HIDDEN_TERMINAL_METADATA_FLUSH_MS = 100
|
||||
let codexRestartNoticePresenceSource: Record<
|
||||
string,
|
||||
{ previousAccountLabel: string; nextAccountLabel: string }
|
||||
@@ -476,6 +480,9 @@ export function connectPanePty(
|
||||
|
||||
const onExit = (ptyId: string): void => {
|
||||
agentCompletionCoordinator.dispose()
|
||||
clearHiddenMetadataFlushTimer()
|
||||
pendingHiddenTitle = null
|
||||
pendingHiddenAgentStatus = null
|
||||
deps.syncPanePtyLayoutBinding(pane.id, null)
|
||||
deps.clearRuntimePaneTitle(deps.tabId, pane.id)
|
||||
deps.clearTabPtyId(deps.tabId, ptyId)
|
||||
@@ -514,8 +521,11 @@ export function connectPanePty(
|
||||
// Claude launches also start idle, but they have no prompt cache yet.
|
||||
let hasConsideredInitialCacheTimerSeed = false
|
||||
let allowInitialIdleCacheSeed = false
|
||||
let pendingHiddenTitle: { title: string; rawTitle: string } | null = null
|
||||
let pendingHiddenAgentStatus: ParsedAgentStatusPayload | null = null
|
||||
let hiddenMetadataFlushTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const onTitleChange = (title: string, rawTitle: string): void => {
|
||||
const applyTitleChangeNow = (title: string, rawTitle: string): void => {
|
||||
manager.setPaneGpuRendering(pane.id, !isGeminiTerminalTitle(rawTitle))
|
||||
deps.setRuntimePaneTitle(deps.tabId, pane.id, title)
|
||||
if (syncAgentTaskCompleteNotificationEnabled()) {
|
||||
@@ -546,6 +556,75 @@ export function connectPanePty(
|
||||
}
|
||||
}
|
||||
|
||||
const applyAgentStatusNow = (payload: ParsedAgentStatusPayload): void => {
|
||||
// Why: capture the store snapshot once so the title lookup and the
|
||||
// setAgentStatus call observe the same state. Re-reading getState()
|
||||
// between the two lines opens a brief window where the title could
|
||||
// shift (OSC title update landing in between) and the status would be
|
||||
// stored against a title that was never paired with it.
|
||||
const currentState = useAppStore.getState()
|
||||
const title = currentState.runtimePaneTitlesByTabId?.[deps.tabId]?.[pane.id]
|
||||
currentState.setAgentStatus(cacheKey, payload, title)
|
||||
if (syncAgentTaskCompleteNotificationEnabled()) {
|
||||
agentCompletionCoordinator.observeHookStatus(payload)
|
||||
}
|
||||
}
|
||||
|
||||
const clearHiddenMetadataFlushTimer = (): void => {
|
||||
if (hiddenMetadataFlushTimer !== null) {
|
||||
clearTimeout(hiddenMetadataFlushTimer)
|
||||
hiddenMetadataFlushTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
const flushHiddenMetadata = (): void => {
|
||||
clearHiddenMetadataFlushTimer()
|
||||
const title = pendingHiddenTitle
|
||||
const agentStatus = pendingHiddenAgentStatus
|
||||
pendingHiddenTitle = null
|
||||
pendingHiddenAgentStatus = null
|
||||
if (title) {
|
||||
applyTitleChangeNow(title.title, title.rawTitle)
|
||||
}
|
||||
if (agentStatus) {
|
||||
applyAgentStatusNow(agentStatus)
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleHiddenMetadataFlush = (): void => {
|
||||
if (hiddenMetadataFlushTimer !== null) {
|
||||
return
|
||||
}
|
||||
hiddenMetadataFlushTimer = setTimeout(() => {
|
||||
hiddenMetadataFlushTimer = null
|
||||
flushHiddenMetadata()
|
||||
}, HIDDEN_TERMINAL_METADATA_FLUSH_MS)
|
||||
}
|
||||
|
||||
const hiddenTitleNeedsImmediateApply = (title: string): boolean => {
|
||||
const currentTitle = useAppStore.getState().runtimePaneTitlesByTabId?.[deps.tabId]?.[pane.id]
|
||||
return detectAgentStatusFromTitle(currentTitle ?? '') !== detectAgentStatusFromTitle(title)
|
||||
}
|
||||
|
||||
const hiddenAgentStatusNeedsImmediateApply = (payload: ParsedAgentStatusPayload): boolean => {
|
||||
const existing = useAppStore.getState().agentStatusByPaneKey[cacheKey]
|
||||
return !existing || existing.state !== payload.state || payload.state === 'done'
|
||||
}
|
||||
|
||||
const onTitleChange = (title: string, rawTitle: string): void => {
|
||||
if (deps.isVisibleRef.current || hiddenTitleNeedsImmediateApply(title)) {
|
||||
flushHiddenMetadata()
|
||||
applyTitleChangeNow(title, rawTitle)
|
||||
return
|
||||
}
|
||||
// Why: hidden Codex panes can emit decorative title frames faster than a
|
||||
// user can observe them. Keep state transitions immediate, but coalesce
|
||||
// same-class hidden frames so background agents don't steal the renderer
|
||||
// thread from the focused xterm during typing.
|
||||
pendingHiddenTitle = { title, rawTitle }
|
||||
scheduleHiddenMetadataFlush()
|
||||
}
|
||||
|
||||
const onPtySpawn = (ptyId: string): void => {
|
||||
bindPanePtyId(pane.id, ptyId, deps.tabId)
|
||||
pane.container.dataset.ptyId = ptyId
|
||||
@@ -824,17 +903,13 @@ export function connectPanePty(
|
||||
// Without this, the OSC parser in pty-transport strips sequences from xterm
|
||||
// output but the status never reaches the store or dashboard/hover UI.
|
||||
onAgentStatus: (payload) => {
|
||||
// Why: capture the store snapshot once so the title lookup and the
|
||||
// setAgentStatus call observe the same state. Re-reading getState()
|
||||
// between the two lines opens a brief window where the title could
|
||||
// shift (OSC title update landing in between) and the status would be
|
||||
// stored against a title that was never paired with it.
|
||||
const currentState = useAppStore.getState()
|
||||
const title = currentState.runtimePaneTitlesByTabId?.[deps.tabId]?.[pane.id]
|
||||
currentState.setAgentStatus(cacheKey, payload, title)
|
||||
if (syncAgentTaskCompleteNotificationEnabled()) {
|
||||
agentCompletionCoordinator.observeHookStatus(payload)
|
||||
if (deps.isVisibleRef.current || hiddenAgentStatusNeedsImmediateApply(payload)) {
|
||||
flushHiddenMetadata()
|
||||
applyAgentStatusNow(payload)
|
||||
return
|
||||
}
|
||||
pendingHiddenAgentStatus = payload
|
||||
scheduleHiddenMetadataFlush()
|
||||
}
|
||||
}
|
||||
const transport = runtimeEnvironmentId
|
||||
@@ -1777,6 +1852,9 @@ export function connectPanePty(
|
||||
pendingTerminalInputWrite = null
|
||||
interruptInference.dispose()
|
||||
clearTitleOnlyInterruptTimer()
|
||||
clearHiddenMetadataFlushTimer()
|
||||
pendingHiddenTitle = null
|
||||
pendingHiddenAgentStatus = null
|
||||
// Why: actively resolve any in-flight passphrase-gate waits so their
|
||||
// zustand subscribers + async IIFEs don't hang for the rest of the
|
||||
// session when the pane is torn down before SSH state changes.
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('pane terminal output scheduler', () => {
|
||||
writeTerminalOutput(terminal, 'b', { foreground: false })
|
||||
|
||||
expect(terminal.write).not.toHaveBeenCalled()
|
||||
vi.advanceTimersByTime(50)
|
||||
vi.advanceTimersByTime(100)
|
||||
|
||||
expect(terminal.write).toHaveBeenCalledTimes(1)
|
||||
expect(terminal.write).toHaveBeenCalledWith('ab')
|
||||
@@ -52,7 +52,7 @@ describe('pane terminal output scheduler', () => {
|
||||
writeTerminalOutput(terminal, 'b', { foreground: false, beforeWrite })
|
||||
|
||||
expect(beforeWrite).not.toHaveBeenCalled()
|
||||
vi.advanceTimersByTime(50)
|
||||
vi.advanceTimersByTime(100)
|
||||
|
||||
expect(beforeWrite).toHaveBeenCalledTimes(1)
|
||||
expect(beforeWrite).toHaveBeenCalledWith('ab')
|
||||
@@ -84,12 +84,16 @@ describe('pane terminal output scheduler', () => {
|
||||
writeTerminalOutput(terminal, `pane-${index}`, { foreground: false })
|
||||
})
|
||||
|
||||
vi.advanceTimersByTime(50)
|
||||
vi.advanceTimersByTime(100)
|
||||
expect(terminals[0].write).toHaveBeenCalledWith('pane-0')
|
||||
expect(terminals[1].write).not.toHaveBeenCalled()
|
||||
expect(terminals[2].write).not.toHaveBeenCalled()
|
||||
|
||||
vi.advanceTimersByTime(50)
|
||||
expect(terminals[1].write).toHaveBeenCalledWith('pane-1')
|
||||
expect(terminals[2].write).not.toHaveBeenCalled()
|
||||
|
||||
vi.advanceTimersByTime(16)
|
||||
vi.advanceTimersByTime(50)
|
||||
expect(terminals[2].write).toHaveBeenCalledWith('pane-2')
|
||||
})
|
||||
|
||||
@@ -103,16 +107,23 @@ describe('pane terminal output scheduler', () => {
|
||||
writeTerminalOutput(terminals[1], 'pane-1', { foreground: false })
|
||||
writeTerminalOutput(terminals[2], 'pane-2', { foreground: false })
|
||||
|
||||
vi.advanceTimersByTime(50)
|
||||
vi.advanceTimersByTime(100)
|
||||
expect(terminals[0].write).toHaveBeenCalledTimes(1)
|
||||
expect(terminals[1].write).toHaveBeenCalledWith('pane-1')
|
||||
expect(terminals[1].write).not.toHaveBeenCalled()
|
||||
expect(terminals[2].write).not.toHaveBeenCalled()
|
||||
|
||||
// Why: a terminal with leftover bytes is deleted/re-set after each drain
|
||||
// chunk, moving it to the back of the Map so a big burst cannot starve
|
||||
// other queued panes.
|
||||
vi.advanceTimersByTime(16)
|
||||
vi.advanceTimersByTime(50)
|
||||
expect(terminals[1].write).toHaveBeenCalledWith('pane-1')
|
||||
expect(terminals[0].write).toHaveBeenCalledTimes(1)
|
||||
|
||||
vi.advanceTimersByTime(50)
|
||||
expect(terminals[2].write).toHaveBeenCalledWith('pane-2')
|
||||
expect(terminals[0].write).toHaveBeenCalledTimes(1)
|
||||
|
||||
vi.advanceTimersByTime(50)
|
||||
expect(terminals[0].write).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
@@ -134,7 +145,7 @@ describe('pane terminal output scheduler', () => {
|
||||
|
||||
writeTerminalOutput(terminal, 'stale', { foreground: false })
|
||||
discardTerminalOutput(terminal)
|
||||
vi.advanceTimersByTime(50)
|
||||
vi.advanceTimersByTime(100)
|
||||
|
||||
expect(terminal.write).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -152,7 +163,7 @@ describe('pane terminal output scheduler', () => {
|
||||
|
||||
// Why: drain runs inside setTimeout; if the throw escapes drainQueuedOutput
|
||||
// it would crash the timer callback and leave the scheduler poisoned.
|
||||
expect(() => vi.advanceTimersByTime(50)).not.toThrow()
|
||||
expect(() => vi.advanceTimersByTime(100)).not.toThrow()
|
||||
expect(throwing.write).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Advancing further must not rediscover the dead entry.
|
||||
|
||||
@@ -12,10 +12,10 @@ type QueueEntry = {
|
||||
beforeWrite?: TerminalOutputBeforeWrite
|
||||
}
|
||||
|
||||
const BACKGROUND_FLUSH_DELAY_MS = 50
|
||||
const BACKGROUND_DRAIN_INTERVAL_MS = 16
|
||||
const BACKGROUND_FLUSH_DELAY_MS = 100
|
||||
const BACKGROUND_DRAIN_INTERVAL_MS = 50
|
||||
const BACKGROUND_CHUNK_CHARS = 16 * 1024
|
||||
const MAX_WRITES_PER_DRAIN = 2
|
||||
const MAX_WRITES_PER_DRAIN = 1
|
||||
const PARSE_SETTLE_TIMEOUT_MS = 250
|
||||
|
||||
const queuedByTerminal = new Map<TerminalOutputTarget, QueueEntry>()
|
||||
|
||||
Reference in New Issue
Block a user