Files
orca/src/main/runtime/runtime-notifier-contract.ts
Jinwoo Hong 573537ecd4 feat(cli): make terminal close the canonical workspace teardown (#18073)
* fix(runtime): recover stale session owners and await retirement

* fix(runtime): preserve session hydration and smoke compatibility

* test(runtime): cover empty and unindexed session owners

* feat(cli): make terminal close the canonical workspace teardown

* fix(preload): align ssh termination result type

* test(runtime): assert folder hydration owner

* fix(runtime): fence legacy terminal stop by worktree host

* fix(preload): reconcile ssh result import with main

* fix(runtime): keep same-id sibling hosts out of workspace close

The stale-owner fallback in the session controller re-routed any worktree whose
catalog partition had no tabs to whichever other partition held tabs. Only
`runtime:` environment ids rotate across relay restarts; `repoId::path` legitimately
repeats across hosts, so an SSH workspace close could retire the local copy's
tabs and resume records, or flip owners mid-close and strand the SSH PTY.

Restrict the fallback to runtime hosts, and pin the session partition once per
workspace close so record clearing targets the partition that owned the tabs.

* test(runtime): give the cross-host close fixture a real resume record

* fix(preload): take main's ssh-bridge import order so the merge stays duplicate-free
2026-09-03 03:58:45 -04:00

151 lines
5.6 KiB
TypeScript

import type { SleepingAgentLaunchConfig } from '../../shared/agent-session-resume'
import type { TerminalPaneSplitSource } from '../../shared/feature-education-telemetry'
import type { TerminalRevealIdentity } from '../../shared/terminal-reveal-identity'
import type { TuiAgent } from '../../shared/tui-agent'
import type { ClientHostedBrowserRowsEvent } from '../../shared/client-hosted-browser-rows'
import type {
WorktreeBaseStatusEvent,
WorktreeRemoteBranchConflictEvent
} from '../../shared/worktree/base-ref-drift-types'
import type { CreateWorktreeResult } from '../../shared/worktree/create-types'
import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types'
import type {
RuntimeBrowserDriverState,
RuntimeMarkdownReadTabResult,
RuntimeMarkdownSaveTabResult,
RuntimeMobileSessionTabMove,
RuntimeTerminalDriverState,
RuntimeTerminalPresentation
} from '../../shared/runtime-types'
type DriverState = RuntimeTerminalDriverState
export type RuntimeNotifier = {
automationsChanged?(payload: {
selector?: { kind: 'self' } | { kind: 'ssh'; targetId: string } | { kind: 'orphan' }
reason?: 'definition' | 'run' | 'usage'
}): void
worktreesChanged(repoId: string, renamed?: { oldWorktreeId: string; newWorktreeId: string }): void
worktreeBaseStatus?(event: WorktreeBaseStatusEvent): void
worktreeRemoteBranchConflict?(event: WorktreeRemoteBranchConflictEvent): void
reposChanged(): void
activateWorktree(
repoId: string,
worktreeId: string,
setup?: CreateWorktreeResult['setup'],
startup?: WorktreeStartupLaunch,
defaultTabs?: CreateWorktreeResult['defaultTabs']
): void
createTerminal(
worktreeId: string,
opts: {
command?: string
cwd?: string
env?: Record<string, string>
title?: string
presentation?: RuntimeTerminalPresentation
}
): void
revealTerminalSession?(
worktreeId: string,
opts: {
ptyId: string
title?: string | null
cwd?: string
launchConfig?: SleepingAgentLaunchConfig
launchToken?: string
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
activate?: boolean
presentation?: RuntimeTerminalPresentation
surfaceOwner?: false
tabId?: string
leafId?: string
splitFromLeafId?: string
splitDirection?: 'horizontal' | 'vertical'
splitTelemetrySource?: TerminalPaneSplitSource
focus?: boolean
expectedProcessIdentity?: {
terminalHandle: string
incarnationId: string
}
}
):
| Promise<{ tabId: string; title?: string | null; identity?: TerminalRevealIdentity }>
| { tabId: string; title?: string | null; identity?: TerminalRevealIdentity }
| void
resolveLegacyWorkerTerminalRecovery?(
paneKey: string,
resolution: 'adopted' | 'exited' | 'rolled_back',
ptyId?: string
): void
splitTerminal(
tabId: string,
paneRuntimeId: number,
opts: {
direction: 'horizontal' | 'vertical'
command?: string
worktreeId?: string
sourceLeafId?: string
telemetrySource?: TerminalPaneSplitSource
newLeafId?: string
}
): void
renameTerminal(tabId: string, title: string | null): void
focusTerminal(tabId: string, worktreeId: string, leafId?: string | null): void
focusEditorTab?(tabId: string, worktreeId: string): void
closeSessionTab?(tabId: string, worktreeId: string): void | Promise<void>
moveSessionTab?(worktreeId: string, move: RuntimeMobileSessionTabMove): void
openFile?(
worktreeId: string,
filePath: string,
relativePath: string,
runtimeEnvironmentId?: string | null
): void
openDiff?(
worktreeId: string,
filePath: string,
relativePath: string,
staged: boolean,
runtimeEnvironmentId?: string | null
): void
readMobileMarkdownTab?(worktreeId: string, tabId: string): Promise<RuntimeMarkdownReadTabResult>
saveMobileMarkdownTab?(
worktreeId: string,
tabId: string,
baseVersion: string,
content: string
): Promise<RuntimeMarkdownSaveTabResult>
closeTerminal(tabId: string, paneRuntimeId?: number): void
closeTerminalTab?(
tabId: string,
options?: { localPtyTeardownOwnedExternally?: boolean; force?: boolean }
): Promise<void>
sleepWorktree(worktreeId: string): void
// Why: a phone opening a worktree wakes its slept agents by asking the host
// renderer to run its own navigation-free wake (experimental agent sleep);
// the runtime has no in-memory sleeping records or wake authority. Optional to
// match the many renderer-backed notifier methods only the real bridge wires.
resumeSleepingAgents?(worktreeId: string): void
terminalFitOverrideChanged(
ptyId: string,
mode: 'mobile-fit' | 'remote-desktop-fit' | 'desktop-fit',
cols: number,
rows: number
): void
// Why: presence-based lock signal — desktop renderer mounts the lock
// banner when `driver.kind === 'mobile'` and unmounts otherwise. The
// structured payload (vs a `locked: boolean`) carries the active mobile
// actor's clientId so the renderer can disambiguate multi-phone scenarios
// and so a future write coordinator can use the same signal as scheduling
// input. See docs/mobile-presence-lock.md.
terminalDriverChanged(ptyId: string, driver: DriverState): void
nativeChatLaunchDraftResolved?(
tabId: string,
resolution: { text: string; createdAt: number }
): void
browserDriverChanged?(browserPageId: string, driver: RuntimeBrowserDriverState): void
browserRemoteViewersChanged?(browserPageId: string, hasRemoteViewers: boolean): void
clientHostedBrowserRowsChanged?(event: ClientHostedBrowserRowsEvent): void
}