/** * Orca surface backend. Subagents run in unfocused terminal tabs in the * current Orca worktree. * * Detection relies on ORCA_TERMINAL_HANDLE, which Orca exports inside its * managed terminals. Inside those terminals `orca` resolves to the Orca CLI * shim; outside them bare `orca` on Linux is usually the GNOME screen reader, * so resolution explicitly rejects /usr/bin/orca. */ import { execFile, execFileSync } from "node:child_process"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); /** GNOME screen reader path — never a valid Orca CLI. */ const GNOME_SCREEN_READER = "/usr/bin/orca"; let cachedCli: string | null | undefined; /** Terminal handles created as whole tabs (vs splits), mapped for tab close. */ const tabByHandle = new Map(); interface OrcaResponse { ok?: boolean; error?: { message?: unknown }; } interface OrcaTerminalCreated extends OrcaResponse { result?: { terminal?: { handle?: unknown; tabId?: unknown }; }; } interface OrcaSplitCreated extends OrcaResponse { result?: { split?: { handle?: unknown }; }; } interface OrcaTerminalRead extends OrcaResponse { result?: { terminal?: { tail?: unknown; status?: unknown }; }; } /** * Marker echoed by the startup command once the new terminal has cd'd into * the target directory and is about to exec bash. Written with a quote split * (`__PI_SURFACE_''READY__`) so the typed command line on screen never * matches, only the echoed output does. */ const READY_MARKER = "__PI_SURFACE_READY__"; function resolveCli(): string | null { if (cachedCli !== undefined) return cachedCli; const override = process.env.ORCA_CLI_COMMAND?.trim(); if (override) { cachedCli = override; return cachedCli; } try { const resolved = execFileSync("sh", ["-c", "command -v orca"], { encoding: "utf8" }).trim(); cachedCli = resolved && resolved !== GNOME_SCREEN_READER ? "orca" : null; } catch { cachedCli = null; } return cachedCli; } export function isAvailable(): boolean { return !!process.env.ORCA_TERMINAL_HANDLE && resolveCli() !== null; } export function setupHint(): string { return "Start pi inside an Orca-managed terminal with the `orca` CLI on PATH."; } function requireOrca(): string { const cli = resolveCli(); if (!isAvailable() || !cli) { throw new Error(`Orca is required for this surface. ${setupHint()}`); } return cli; } function shellEscape(value: string): string { return "'" + value.replace(/'/g, "'\\''") + "'"; } /** Startup command that lands the new terminal in pi's cwd with a bash shell. */ function startupCommand(): string { return `cd ${shellEscape(process.cwd())} && echo __PI_SURFACE_''READY__ && exec bash`; } function sleepSync(seconds: number): void { try { execFileSync("sleep", [String(seconds)], { stdio: "ignore" }); } catch {} } /** Probe marker proving the shell reads and executes typed input. */ const SHELL_UP_MARKER = "__PI_SHELL_UP__"; /** * Block until the new terminal's shell is genuinely interactive. * * Two races exist at startup: input typed before the initial shell execs bash * is swallowed with it, and input typed while bash itself initializes can be * flushed (observed as stray kitty-keyboard residue like `0u` at the prompt). * So first wait for the startup command's ready marker, then repeatedly send a * probe echo until its output appears — an executed probe also clears any * residue from the readline buffer. Falls through after the deadline as * best-effort. */ function waitForSurfaceReady(surface: string, timeoutMs = 15_000): void { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { try { if (readScreen(surface, 40).includes(READY_MARKER)) break; } catch {} sleepSync(0.25); } while (Date.now() < deadline) { try { sendCommand(surface, "echo __PI_SHELL_''UP__"); } catch { sleepSync(0.25); continue; } const probeDeadline = Math.min(Date.now() + 2_000, deadline); while (Date.now() < probeDeadline) { sleepSync(0.25); try { if (readScreen(surface, 40).includes(SHELL_UP_MARKER)) return; } catch {} } } } function parseResponse(command: string, output: string): T { let response: T; try { response = JSON.parse(output) as T; } catch { throw new Error(`Unexpected Orca ${command} output: ${output.trim() || "(empty)"}`); } if (response.ok === false) { const message = typeof response.error?.message === "string" ? response.error.message : "unknown error"; throw new Error(`Orca ${command} failed: ${message}`); } return response; } function requireId(value: unknown, command: string, field: string): string { if (typeof value !== "string" || value.length === 0) { throw new Error(`Orca ${command} did not return ${field}`); } return value; } export function parseCreatedTerminal(output: string): { handle: string; tabId: string } { const response = parseResponse("terminal create", output); return { handle: requireId(response.result?.terminal?.handle, "terminal create", "result.terminal.handle"), tabId: requireId(response.result?.terminal?.tabId, "terminal create", "result.terminal.tabId"), }; } export function parseCreatedSplit(output: string): string { const response = parseResponse("terminal split", output); return requireId(response.result?.split?.handle, "terminal split", "result.split.handle"); } export function parseReadTail(output: string): string { const response = parseResponse("terminal read", output); const status = response.result?.terminal?.status; if (typeof status === "string" && status !== "running") { throw new Error(`Orca terminal read: terminal is not running (status: ${status})`); } const tail = response.result?.terminal?.tail; if (!Array.isArray(tail)) { throw new Error("Orca terminal read did not return result.terminal.tail"); } return tail.filter((line): line is string => typeof line === "string").join("\n"); } export function createSurface(name: string): string { const cli = requireOrca(); // Target the enclosing terminal's worktree explicitly: pi's cwd (which the // CLI would otherwise resolve against) may live outside any Orca worktree. const worktreeId = process.env.ORCA_WORKTREE_ID; const output = execFileSync( cli, [ "terminal", "create", ...(worktreeId ? ["--worktree", `id:${worktreeId}`] : []), "--title", name, "--command", startupCommand(), "--json", ], { encoding: "utf8" }, ); const { handle, tabId } = parseCreatedTerminal(output); tabByHandle.set(handle, tabId); waitForSurfaceReady(handle); return handle; } export function createSurfaceSplit( name: string, direction: "left" | "right" | "up" | "down", fromSurface?: string, ): string { void name; const cli = requireOrca(); if (direction === "left" || direction === "up") { throw new Error(`Orca terminal splits only support right or down, not ${direction}`); } const source = fromSurface ?? process.env.ORCA_TERMINAL_HANDLE!; const output = execFileSync( cli, [ "terminal", "split", "--terminal", source, "--direction", direction === "right" ? "horizontal" : "vertical", "--command", startupCommand(), "--json", ], { encoding: "utf8" }, ); const handle = parseCreatedSplit(output); waitForSurfaceReady(handle); return handle; } export function sendCommand(surface: string, command: string): void { const cli = requireOrca(); const output = execFileSync( cli, ["terminal", "send", "--terminal", surface, "--text", command, "--enter", "--json"], { encoding: "utf8" }, ); parseResponse("terminal send", output); } function readScreenArgs(surface: string, lines: number): string[] { return [ "terminal", "read", "--terminal", surface, "--screen", "--limit", String(Math.max(1, lines)), "--json", ]; } export function readScreen(surface: string, lines = 50): string { const cli = requireOrca(); const output = execFileSync(cli, readScreenArgs(surface, lines), { encoding: "utf8" }); return parseReadTail(output); } export async function readScreenAsync(surface: string, lines = 50): Promise { const cli = requireOrca(); const { stdout } = await execFileAsync(cli, readScreenArgs(surface, lines), { encoding: "utf8", }); return parseReadTail(stdout); } export function closeSurface(surface: string): void { const cli = requireOrca(); const args = ["terminal", "close", "--terminal", surface]; if (tabByHandle.has(surface)) args.push("--tab"); args.push("--json"); const output = execFileSync(cli, args, { encoding: "utf8" }); parseResponse("terminal close", output); tabByHandle.delete(surface); }