Files
orca/src/shared/runtime-terminal-contracts.ts
T
Jinwoo Hong ae0f3675a1 fix(remote): focus host-delegated split panes (#16886)
* fix(remote): focus host-delegated split panes

Return the authoritative leaf identity from terminal.split, record viewer-local focus intent behind the captured pairing revision, and replay the mirrored layout before focusing the exact pane. Preserve old-host fallback and prevent delayed split responses from stealing focus after the viewer moves away.

Add deterministic runtime, renderer, concurrency, compatibility, and headed paired-Electron coverage for Cmd+D, header splits, and immediate PTY input routing.

Fixes #16510

* fix(remote): preserve split focus across tab groups

Resolve the initiating source tab and leaf from the remote PTY, while keeping the viewer's current focus as a separate anti-steal baseline. This lets context-menu/header splits from non-focused group tabs focus their result without allowing delayed responses to override a later navigation.

* test(remote): drive split focus with key events

* test(remote): use the platform split shortcut

* fix(remote): fence concurrent split focus intent

* fix(remote): harden split focus ordering

* fix(remote): preserve split focus after runtime refactor

* fix(remote): fence stale split focus gestures

* test(remote): keep split focus regression within line budget
2026-08-30 03:07:20 -04:00

323 lines
8.6 KiB
TypeScript

import type { AgentSessionPtyWriteRefusal } from './agent-session-pty-write-admission'
import type {
AgentProviderSessionMetadata,
SleepingAgentLaunchConfig
} from './agent-session-resume'
import type { StartupCommandDelivery } from './codex-startup-delivery'
import type { ExecutionHostId } from './execution-host'
import type { PtyIncarnationId } from './pty-incarnation'
import type { RuntimeMobileSessionTabsResult } from './runtime-session-contracts'
import type { TabGroupLayoutNode } from './tab-types'
import type { TerminalExitCause } from './terminal-exit-cause'
import type { TerminalPaneLayoutNode } from './terminal-tab-types'
import type { TuiAgent } from './tui-agent'
export type RuntimeTerminalSummary = {
handle: string
ptyId: string | null
incarnationId?: string | null
orphaned?: boolean
worktreeId: string
worktreePath: string
branch: string
tabId: string
leafId: string
title: string | null
connected: boolean
writable: boolean
lastOutputAt: number | null
preview: string
/** Host-resolved agent identity for action consumers; absent when unknown or unsupported. */
agentIdentity?: TuiAgent
/** Absent while running or when the host predates the field; never infer a clean finish. */
exitCause?: TerminalExitCause
/** Absent when the host predates the field or could not name the execution host. */
executionHostId?: ExecutionHostId
}
export type RuntimeTerminalVisualTerminalNode = {
type: 'terminal'
handle: string
tabId: string
leafId: string
title: string | null
connected: boolean
active: boolean
}
export type RuntimeTerminalVisualPaneNode =
| RuntimeTerminalVisualTerminalNode
| {
type: 'pane-split'
direction: Extract<TerminalPaneLayoutNode, { type: 'split' }>['direction']
first: RuntimeTerminalVisualPaneNode
second: RuntimeTerminalVisualPaneNode
}
export type RuntimeTerminalVisualTab = {
tabId: string
title: string | null
activeLeafId: string | null
panes: RuntimeTerminalVisualPaneNode
}
export type RuntimeTerminalVisualGroupNode = {
type: 'group'
groupId: string | null
activeTabId: string | null
tabs: RuntimeTerminalVisualTab[]
}
export type RuntimeTerminalVisualLayoutNode =
| RuntimeTerminalVisualGroupNode
| {
type: 'split'
direction: Extract<TabGroupLayoutNode, { type: 'split' }>['direction']
first: RuntimeTerminalVisualLayoutNode
second: RuntimeTerminalVisualLayoutNode
}
export type RuntimeTerminalVisualLayout = {
worktreeId: string
worktreePath: string
root: RuntimeTerminalVisualLayoutNode
}
export type RuntimeTerminalListHostScope = {
hostIds: ExecutionHostId[]
omittedHostIds: ExecutionHostId[]
}
export type RuntimeTerminalListResult = {
terminals: RuntimeTerminalSummary[]
visualLayouts?: RuntimeTerminalVisualLayout[]
topologyRevisions?: Record<string, number>
totalCount: number
truncated: boolean
/** Absent from hosts that predate the field; treat that scope as unverifiable. */
hostScope?: RuntimeTerminalListHostScope
}
export type RuntimeTerminalOrphanAdoptionClaim = {
terminal: string
ptyId: string
incarnationId: PtyIncarnationId
tabId: string
leafId: string
}
export type RuntimeTerminalOrphanTopologyTab = {
tabId: string
root: TerminalPaneLayoutNode
activeLeafId: string
expandedLeafId: string | null
}
export type RuntimeTerminalOrphanTopologyGroup = {
id: string
activeTabId: string
tabOrder: string[]
recentTabIds?: string[]
}
export type RuntimeTerminalOrphanTopology = {
tabs: RuntimeTerminalOrphanTopologyTab[]
groups: RuntimeTerminalOrphanTopologyGroup[]
groupLayout?: TabGroupLayoutNode
}
export type RuntimeTerminalOrphanAdoptionRequest = {
worktree: string
expectedTopologyRevision: number
claims: RuntimeTerminalOrphanAdoptionClaim[]
activeTabId?: string
activeGroupId?: string
topology?: RuntimeTerminalOrphanTopology
}
export type RuntimeTerminalOrphanAdoptionResult = {
adopted: boolean
topologyRevision: number
snapshot: RuntimeMobileSessionTabsResult
}
export type RuntimeWorktreeTerminalSleepResult = {
stopped: number
stoppedPtyIds: string[]
livePtyIds: string[]
} & (
| { postStopVerified: true; postStopFailure?: never; remainingLivePtyIds?: never }
| {
postStopVerified: false
postStopFailure: 'terminal_liveness_unavailable'
remainingLivePtyIds?: never
}
| {
postStopVerified: false
postStopFailure: 'terminal_worktree_sleep_still_live'
remainingLivePtyIds: string[]
}
)
export type RuntimeTerminalInteractiveWaitSource = 'hook' | 'prompt-text' | 'title'
export type RuntimeTerminalInteractiveWait = {
source: RuntimeTerminalInteractiveWaitSource
reason?: RuntimeTerminalWaitBlockedReason
since?: number
}
export type RuntimeTerminalShow = RuntimeTerminalSummary & {
paneRuntimeId: number
ptyId: string | null
rendererGraphEpoch: number
/** Null means evaluated with no wait; absent means not evaluated. */
agentWait?: RuntimeTerminalInteractiveWait | null
}
export type RuntimeTerminalState = 'running' | 'exited' | 'unknown'
export type RuntimeTerminalRead = {
handle: string
status: RuntimeTerminalState
tail: string[]
truncated: boolean
limited?: boolean
oldestCursor?: string
nextCursor: string | null
latestCursor?: string
returnedLineCount?: number
source?: 'stream' | 'screen' | 'screen-unavailable'
/** UI-only composer text, excluded from `tail`. */
draft?: string
}
export type RuntimeTerminalRename = {
handle: string
tabId: string
title: string | null
}
export type RuntimeTerminalSend = {
handle: string
accepted: boolean
bytesWritten: number
refusedReason?: 'no-agent' | 'permission'
/**
* Present only when a durable agent-session lease refused the write. Additive and optional: an
* old client sees the `accepted: false` it already handles and ignores this field.
*/
agentSessionRefusal?: AgentSessionPtyWriteRefusal
}
export type RuntimeTerminalAgentStatusState = 'working' | 'permission' | 'idle' | null
export type RuntimeTerminalAgentStatus = {
handle: string
isRunningAgent: boolean
status: RuntimeTerminalAgentStatusState
}
export type RuntimeTerminalPresentation = 'background' | 'focused'
type RuntimeTerminalCreateBaseRequestPayload = {
requestId: string
worktreeId?: string
afterTabId?: string
targetGroupId?: string
command?: string
cwd?: string
env?: Record<string, string>
envToDelete?: string[]
launchConfig?: SleepingAgentLaunchConfig
resumeProviderSession?: AgentProviderSessionMetadata
launchToken?: string
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
startupCommandDelivery?: StartupCommandDelivery
title?: string
activate?: boolean
presentation?: RuntimeTerminalPresentation
surfaceOwner?: false
}
export type RuntimeTerminalCreateRequestPayload =
| (RuntimeTerminalCreateBaseRequestPayload & { source?: undefined })
| (RuntimeTerminalCreateBaseRequestPayload & {
worktreeId: string
source: 'runtime-session'
})
export type RuntimeTerminalCreate = {
handle: string
tabId?: string
paneKey?: string | null
ptyId?: string | null
worktreeId: string
title: string | null
executionHostId?: ExecutionHostId
hostPlatform?: NodeJS.Platform
surface?: 'background' | 'visible'
warning?: string
agentSessionDisposition?: 'created' | 'adopted'
isReattach?: true
/** Spawn process identity for host-internal ownership proof. */
processId?: number
}
export type RuntimeTerminalSplit = {
handle: string
tabId: string
paneRuntimeId: number
// Why: paired callers need the host-created leaf identity to focus the exact pane.
leafId?: string
}
export type RuntimeTerminalResolvePane = {
handle: string
tabId: string
leafId: string
ptyId: string | null
connected?: boolean
worktreeId?: string
executionHostId?: ExecutionHostId
hostPlatform?: NodeJS.Platform
}
export type RuntimeTerminalFocus = {
handle: string
tabId: string
worktreeId: string
navigated?: boolean
}
export type RuntimeTerminalClose = {
handle: string
tabId: string
closeMode?: 'tab'
ptyKilled: boolean
ptyStopVerdict?: 'live' | 'unverifiable'
ptyStopReason?: string
}
export type RuntimeTerminalWaitCondition = 'exit' | 'tui-idle'
export type RuntimeTerminalWaitBlockedReason =
| 'codex-update-prompt'
| 'codex-trust-workspace'
| 'codex-cwd-prompt'
| 'codex-model-migration-prompt'
| 'codex-hooks-review-prompt'
| 'codex-interactive-prompt'
| 'agent-approval-prompt'
export type RuntimeTerminalWait = {
handle: string
condition: RuntimeTerminalWaitCondition
satisfied: boolean
status: RuntimeTerminalState
exitCode: number | null
exitCause?: TerminalExitCause
blockedReason?: RuntimeTerminalWaitBlockedReason
}