diff --git a/src/renderer/src/components/terminal-pane/pty-transport.ts b/src/renderer/src/components/terminal-pane/pty-transport.ts index c2827c4c4..93e28bed6 100644 --- a/src/renderer/src/components/terminal-pane/pty-transport.ts +++ b/src/renderer/src/components/terminal-pane/pty-transport.ts @@ -24,7 +24,10 @@ import type { PtyDataMeta } from './pty-dispatcher' import { createBellDetector } from './bell-detector' -import { createAgentStatusOscProcessor, type ProcessedAgentStatusChunk } from './agent-status-osc' +import { + createAgentStatusOscProcessor, + type ProcessedAgentStatusChunk +} from '../../../../shared/agent-status-osc' import { extractIpcErrorMessage } from '@/lib/ipc-error' // Re-export public API so existing consumers keep working. @@ -373,9 +376,9 @@ export function createPtyOutputProcessor({ ): void { const rawLength = meta?.rawLength ?? data.length const suppressAttentionEvents = options.suppressAttentionEvents === true - // Why: OSC 9999 is a renderer-only control protocol. Parse it before - // xterm sees the bytes, and keep parser state across chunks so partial - // PTY reads do not drop valid status updates or print escape garbage. + // Why: OSC 9999 is an Orca control protocol. Parse it before xterm sees + // the bytes, and keep parser state across chunks so partial PTY reads do + // not drop valid status updates or print escape garbage. const processed = processAgentStatusChunk(data) data = processed.cleanData // Why: mirror the onBell / onAgentBecameIdle guard below — during eager-buffer diff --git a/src/renderer/src/lib/automation-session-observer.ts b/src/renderer/src/lib/automation-session-observer.ts index 1553ee3e4..aa9bb1080 100644 --- a/src/renderer/src/lib/automation-session-observer.ts +++ b/src/renderer/src/lib/automation-session-observer.ts @@ -1,4 +1,3 @@ -import { createAgentStatusOscProcessor } from '@/components/terminal-pane/agent-status-osc' import { subscribeToPtyData, subscribeToPtyExit } from '@/components/terminal-pane/pty-dispatcher' import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' import { getRemoteRuntimeTerminalMultiplexer } from '@/runtime/remote-runtime-terminal-multiplexer' @@ -8,6 +7,7 @@ import { getRemoteRuntimeTerminalHandle } from '@/runtime/runtime-terminal-stream' import { useAppStore } from '@/store' +import { createAgentStatusOscProcessor } from '../../../shared/agent-status-osc' import type { ParsedAgentStatusPayload } from '../../../shared/agent-status-types' export async function observeExistingAutomationSession(args: { diff --git a/src/renderer/src/lib/launch-agent-background-session.ts b/src/renderer/src/lib/launch-agent-background-session.ts index a021560e1..ded394ab2 100644 --- a/src/renderer/src/lib/launch-agent-background-session.ts +++ b/src/renderer/src/lib/launch-agent-background-session.ts @@ -13,7 +13,6 @@ import { subscribeToPtyData, subscribeToPtyExit } from '@/components/terminal-pane/pty-dispatcher' -import { createAgentStatusOscProcessor } from '@/components/terminal-pane/agent-status-osc' import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' import { toRuntimeWorktreeSelector } from '@/runtime/runtime-worktree-selector' import { singlePaneLayoutSnapshot } from '@/store/slices/terminal-helpers' @@ -22,6 +21,7 @@ import { subscribeToRuntimeTerminalData, toRemoteRuntimePtyId } from '@/runtime/runtime-terminal-stream' +import { createAgentStatusOscProcessor } from '../../../shared/agent-status-osc' import type { ParsedAgentStatusPayload } from '../../../shared/agent-status-types' import type { RuntimeTerminalCreate } from '../../../shared/runtime-types' diff --git a/src/shared/agent-status-osc.test.ts b/src/shared/agent-status-osc.test.ts new file mode 100644 index 000000000..ac14c4957 --- /dev/null +++ b/src/shared/agent-status-osc.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest' +import { createAgentStatusOscProcessor } from './agent-status-osc' + +describe('createAgentStatusOscProcessor', () => { + it('strips OSC 9999 payloads from terminal data and returns parsed statuses', () => { + const process = createAgentStatusOscProcessor() + + const result = process( + 'before\x1b]9999;{"state":"working","prompt":"ship it","agentType":"codex"}\x07after' + ) + + expect(result.cleanData).toBe('beforeafter') + expect(result.payloads).toEqual([ + { + state: 'working', + prompt: 'ship it', + agentType: 'codex' + } + ]) + }) + + it('preserves parser state across split OSC 9999 chunks', () => { + const process = createAgentStatusOscProcessor() + + expect(process('before\x1b]999').cleanData).toBe('before') + const result = process('9;{"state":"done","prompt":"ok"}\x1b\\after') + + expect(result.cleanData).toBe('after') + expect(result.payloads).toEqual([ + { + state: 'done', + prompt: 'ok' + } + ]) + }) +}) diff --git a/src/renderer/src/components/terminal-pane/agent-status-osc.ts b/src/shared/agent-status-osc.ts similarity index 88% rename from src/renderer/src/components/terminal-pane/agent-status-osc.ts rename to src/shared/agent-status-osc.ts index 1c0c9b231..959088059 100644 --- a/src/renderer/src/components/terminal-pane/agent-status-osc.ts +++ b/src/shared/agent-status-osc.ts @@ -1,5 +1,5 @@ -import type { ParsedAgentStatusPayload } from '../../../../shared/agent-status-types' -import { parseAgentStatusPayload } from '../../../../shared/agent-status-types' +import type { ParsedAgentStatusPayload } from './agent-status-types' +import { parseAgentStatusPayload } from './agent-status-types' const OSC_AGENT_STATUS_PREFIX = '\x1b]9999;' @@ -28,8 +28,8 @@ function findAgentStatusTerminator( /** * Stateful OSC 9999 parser for PTY streams. - * Why: automation background launches need the same agent-status parsing as - * mounted terminal panes, even when no terminal has been rendered yet. + * Why: hidden/model-owned terminal output needs the same agent-status parsing + * as mounted terminal panes, even when no terminal view is rendered. */ export function createAgentStatusOscProcessor(): (data: string) => ProcessedAgentStatusChunk { const MAX_PENDING = 64 * 1024