Files
orca/src/preload/api/pty-api.ts
T
Neilandstevelliu ea02d90704 fix(omp): answer startup Kitty queries before renderer handoff (#20620)
* fix(omp): answer startup Kitty queries before renderer handoff

Forward actual renderer capability through local and remote spawn. Preserve source ranges and following keyboard mode pushes, and retain independent ConPTY color authority.

Refs #17081. Secondary review: #17082.

Co-authored-by: stevelliu <stevelliu@tencent.com>

* test(omp): cover fragmented keyboard modes and ConPTY handoff

* fix: preserve keyboard startup intent without terminal colors

* fix: negotiate keyboard support for host-authoritative agent launches

* fix: keep terminal creation within line budget

* fix(omp): negotiate keyboard support for paired web launches

* test: remove obsolete message type import after main integration

* fix: validate paired launch results and retry incomplete SSH test snapshots

* fix(omp): negotiate keyboard support for background paired launches

---------

Co-authored-by: stevelliu <stevelliu@tencent.com>
2026-09-19 01:29:16 -07:00

244 lines
11 KiB
TypeScript

import type {
AgentProviderSessionMetadata,
SleepingAgentLaunchConfig
} from '../../shared/agent-session-resume'
import type { StartupCommandDelivery } from '../../shared/codex-startup-delivery'
import type { ProjectExecutionRuntimeResolution } from '../../shared/project-execution-runtime'
import type { PtyListedSession, PtySessionListScope } from '../../shared/pty-listed-session'
import type { PtyMainDeliveryDiagnostics } from '../../shared/pty-delivery-diagnostics'
import type { PtyModelRestoreNeededEvent } from '../../shared/pty-model-restore-marker'
import type {
PtyRendererDeliveryHealthReply,
PtyRendererDeliveryStateReport
} from '../../shared/pty-renderer-delivery-health'
import type { AgentKind, LaunchSource, RequestKind } from '../../shared/telemetry-events'
import type { TerminalSideEffectBatch } from '../../shared/terminal-side-effect-facts'
import type { TerminalViewAttributes } from '../../shared/terminal-view-attributes'
import type { TuiAgent } from '../../shared/tui-agent'
import type { PtyManagementApi } from './pty-management-api'
import type { TerminalProcessInspection } from '../../shared/terminal-process-inspection'
export type PtyApi = {
spawn: (opts: {
cols: number
rows: number
cwd?: string
cwdFallback?: 'worktree'
env?: Record<string, string>
envToDelete?: string[]
command?: string
commandDelivery?: 'renderer' | 'provider'
launchConfig?: SleepingAgentLaunchConfig
resumeProviderSession?: AgentProviderSessionMetadata
launchToken?: string
launchAgent?: TuiAgent
startupCommandDelivery?: StartupCommandDelivery
connectionId?: string | null
worktreeId?: string
sessionId?: string
// Why: lets a single tab open in a different shell than the user's default.
shellOverride?: string
projectRuntime?: ProjectExecutionRuntimeResolution
terminalKittyKeyboardProtocol?: boolean
terminalColorQueryReplies?: { foreground?: string; background?: string }
// Why: mark the PTY hidden before its first byte so the delivery gate owns spawn-time queries (terminal-query-authority.md §races).
initiallyHidden?: boolean
// Why: main sync-flushes the (worktreeId,tabId,leafId→ptyId) binding before pty:spawn returns to close a SIGKILL race (INVESTIGATION.md).
tabId?: string
leafId?: string
// Why: main fires `agent_started` only on spawn success, so launch metadata rides this field (telemetry-plan.md §Agent launch semantics).
telemetry?: { agent_kind: AgentKind; launch_source: LaunchSource; request_kind: RequestKind }
}) => Promise<{
id: string
/** Which lifetime of `id` this reply named; absent when the execution host predates the field. */
incarnationId?: string
launchAgent?: TuiAgent
launchConfig?: SleepingAgentLaunchConfig
snapshot?: string
snapshotCols?: number
snapshotRows?: number
snapshotPrefixAnsi?: string
snapshotFrameAnsi?: string
snapshotFrameRestoreAnsi?: string
snapshotKittyKeyboardFlags?: number
snapshotTerminalOwner?: 'shell'
snapshotSeq?: number
isReattach?: boolean
isAlternateScreen?: boolean
replay?: string
sessionExpired?: boolean
coldRestore?: { scrollback: string; cwd: string; cols?: number; rows?: number }
startupCwdFallback?: { kind: 'worktree'; cwd: string }
agentResumeUnavailable?: true
/** Host verdict on the shell-ready marker; absent when the execution host predates the field. */
shellReadyArmed?: boolean
}>
write: (id: string, data: string) => void
writeAccepted: (id: string, data: string) => Promise<boolean>
onWriteUnavailable?: (callback: (payload: { id: string }) => void) => () => void
resize: (id: string, cols: number, rows: number) => void
claimViewport: (id: string, cols: number, rows: number) => void
reportGeometry: (id: string, cols: number, rows: number) => void
signal: (id: string, signal: string) => void
clearBuffer: (id: string) => void
kill: (id: string, opts?: { keepHistory?: boolean }) => Promise<void>
ackColdRestore: (id: string) => void
ackData: (id: string, charCount: number, processedChars?: number) => void
onDeliveryResyncRequest: (callback: (payload: { requestId: number }) => void) => () => void
respondDeliveryResync: (payload: {
requestId: number
processedCharsByPty: Record<string, number>
}) => void
/** Renderer-initiated delivery health/heal lane over invoke — reaches main
* even when every main→renderer push channel is dead (field wedge). */
reportRendererDeliveryState: (
report: PtyRendererDeliveryStateReport
) => Promise<PtyRendererDeliveryHealthReply>
/** Live pty:data listener count on the preload emitter (sync) — heal-time
* discriminator between a detached listener and a dead channel. */
getPtyDataListenerCount: () => number
/** One-shot signal that this page's pty:data dispatcher is registered, so
* main can release sends held during the load/reload boot window. */
rendererDispatcherReady: () => void
setActiveRendererPty: (id: string, active: boolean) => void
setRendererPtyVisible: (id: string, visible: boolean) => void
/** Hidden-delivery gate (Phase 4): hidden=true lets main drop renderer
* byte delivery after model ingestion; reveal restores from snapshots. */
setHiddenRendererPty: (id: string, hidden: boolean) => void
/** Ref-counted-on-the-renderer delivery-interest signal that suppresses
* the hidden-delivery gate while any raw-byte consumer is registered. */
setPtyDeliveryInterest: (id: string, interested: boolean) => void
/** View-attribute bridge (Phase 5 slice 2): app-global composed terminal
* appearance push backing main's hidden-PTY OSC/DSR color replies. */
publishTerminalViewAttributes: (attributes: TerminalViewAttributes) => void
hasChildProcesses: (id: string) => Promise<boolean>
getForegroundProcess: (id: string) => Promise<string | null>
inspectProcess: (
id: string,
options?: {
expectedIncarnationId?: string
scanChildProcesses?: boolean
steadyState?: boolean
}
) => Promise<TerminalProcessInspection>
confirmForegroundProcess: (id: string) => Promise<string | null>
getCwd: (id: string) => Promise<string>
getSize: (id: string) => Promise<{ cols: number; rows: number } | null>
listSessions: (scope?: PtySessionListScope) => Promise<PtyListedSession[]>
getAuthoritativeBufferSnapshotCapabilities?: (
ids: string[]
) => Promise<{ id: string; authoritative: boolean | null }[]>
hasPty: (id: string) => Promise<boolean | null>
getMainBufferSnapshot: (
id: string,
opts?: { scrollbackRows?: number }
) => Promise<{
data: string
frameRestoreAnsi?: string
cols: number
rows: number
cwd?: string | null
seq?: number
/** Start of main's pending renderer-delivery queue at snapshot time
* (equals `seq` when empty) — bounds the renderer's post-restore
* duplicate window. */
pendingDeliveryStartSeq?: number
source?: 'headless' | 'renderer'
alternateScreen?: boolean
/** Authoritative normal buffer paired with an alternate-screen frame. */
scrollbackAnsi?: string
/** Trailing incomplete escape the emulator ingested; the restorer must
* write it after its post-replay resets, last before live chunks. */
pendingEscapeTailAnsi?: string
/** Effective kitty flags the snapshot owner proved at `seq`. Absent means
* unknown; consumers must not turn that into a known `0`. */
kittyKeyboardFlags?: number
terminalOwner?: 'shell'
} | null>
getRendererDeliveryDebugSnapshot: () => Promise<{
pendingPtyCount: number
pendingChars: number
maxPendingCharsByPty: number
rendererInFlightPtyCount: number
rendererInFlightChars: number
maxRendererInFlightCharsByPty: number
activeRendererPtyCount: number
flushScheduled: boolean
peakPendingChars: number
peakMaxPendingCharsByPty: number
peakRendererInFlightChars: number
peakMaxRendererInFlightCharsByPty: number
ackGatedFlushSkipCount: number
hiddenDeliveryGatedPtyCount: number
hiddenDeliveryGatedVisiblePtyCount: number
hiddenDeliveryGatedActivePtyCount: number
deliveryInterestPtyCount: number
hiddenDeliveryDroppedChars: number
hiddenDeliveryDroppedChunks: number
pendingDroppedChars: number
diagnostics: PtyMainDeliveryDiagnostics
rendererLifecycleResetCount: number
lastLifecycleResetClearedChars: number
rendererPtyDispatcherReady: boolean
rendererDispatcherReadyForcedCount: number
}>
resetRendererDeliveryDebug: () => Promise<void>
onData: (
callback: (data: {
id: string
data: string
seq?: number
rawLength?: number
transformed?: boolean
background?: boolean
droppedOutput?: boolean
}) => void
) => () => void
onReplay: (callback: (data: { id: string; data: string }) => void) => () => void
/** Out-of-band main→renderer signal that renderer-bound bytes were
* dropped (hidden-delivery gate / pending cap); the pane restores from
* the model snapshot. Never delivered in-band on pty:data. */
onModelRestoreNeeded: (callback: (event: PtyModelRestoreNeededEvent) => void) => () => void
/** Batched derived side-effect facts for PTYs whose bytes transit local
* main. */
onSideEffect: (callback: (batch: TerminalSideEffectBatch) => void) => () => void
/** Title-only replay snapshot for (re)attach; attention facts never replay. */
getSideEffectSnapshot: (id: string) => Promise<TerminalSideEffectBatch | null>
onExit: (
callback: (data: {
id: string
code: number
preserveRendererBinding?: boolean
/** Which lifetime of `id` died; absent when the execution host predates the field. */
incarnationId?: string
/** Set only when the owning relay disowned this id; never a claim that the process died. */
ptySourceDisowned?: true
}) => void
) => () => void
onSpawned: (callback: (data: { id: string }) => void) => () => void
onSerializeBufferRequest: (
callback: (data: {
requestId: string
ptyId: string
opts?: { scrollbackRows?: number }
}) => void
) => () => void
onClearBufferRequest: (callback: (data: { ptyId: string }) => void) => () => void
sendSerializedBuffer: (
requestId: string,
snapshot: {
data: string
cols: number
rows: number
seq?: number
lastTitle?: string
kittyKeyboardFlags?: number
} | null
) => void
declarePendingPaneSerializer: (paneKey: string) => Promise<number>
settlePaneSerializer: (paneKey: string, gen: number) => Promise<void>
clearPendingPaneSerializer: (paneKey: string, gen: number) => Promise<void>
reportRendererSerializerReady?: (ptyId: string) => Promise<void>
management: PtyManagementApi
}