/** Herdr surface backend. Subagents run in unfocused tabs in the current workspace. */ import { execFile, execFileSync } from "node:child_process"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); const commandAvailability = new Map(); const tabByPane = new Map(); interface HerdrTabCreated { result?: { root_pane?: { pane_id?: unknown }; tab?: { tab_id?: unknown }; }; } interface HerdrPaneCreated { result?: { pane?: { pane_id?: unknown }; }; } function hasCommand(command: string): boolean { const cached = commandAvailability.get(command); if (cached !== undefined) return cached; let available = false; try { execFileSync("sh", ["-c", `command -v ${command}`], { stdio: "ignore" }); available = true; } catch { available = false; } commandAvailability.set(command, available); return available; } export function isAvailable(): boolean { return ( process.env.HERDR_ENV === "1" && !!process.env.HERDR_WORKSPACE_ID && !!process.env.HERDR_PANE_ID && hasCommand("herdr") ); } export function setupHint(): string { return "Start pi inside a Herdr-managed pane with the `herdr` CLI on PATH."; } function requireHerdr(): void { if (!isAvailable()) { throw new Error(`Herdr is required for this surface. ${setupHint()}`); } } function parseJson(command: string, output: string): T { try { return JSON.parse(output) as T; } catch { throw new Error(`Unexpected Herdr ${command} output: ${output.trim() || "(empty)"}`); } } function requireId(value: unknown, command: string, field: string): string { if (typeof value !== "string" || value.length === 0) { throw new Error(`Herdr ${command} did not return ${field}`); } return value; } export function parseCreatedTab(output: string): { paneId: string; tabId: string } { const response = parseJson("tab create", output); return { paneId: requireId(response.result?.root_pane?.pane_id, "tab create", "result.root_pane.pane_id"), tabId: requireId(response.result?.tab?.tab_id, "tab create", "result.tab.tab_id"), }; } export function parseCreatedPane(output: string): string { const response = parseJson("pane split", output); return requireId(response.result?.pane?.pane_id, "pane split", "result.pane.pane_id"); } export function createSurface(name: string): string { requireHerdr(); const workspace = process.env.HERDR_WORKSPACE_ID!; const output = execFileSync( "herdr", [ "tab", "create", "--workspace", workspace, "--cwd", process.cwd(), "--label", name, "--no-focus", ], { encoding: "utf8" }, ); const { paneId, tabId } = parseCreatedTab(output); tabByPane.set(paneId, tabId); return paneId; } export function createSurfaceSplit( name: string, direction: "left" | "right" | "up" | "down", fromSurface?: string, ): string { requireHerdr(); if (direction === "left" || direction === "up") { throw new Error(`Herdr pane splits only support right or down, not ${direction}`); } const source = fromSurface ?? process.env.HERDR_PANE_ID!; const output = execFileSync( "herdr", [ "pane", "split", "--pane", source, "--direction", direction, "--cwd", process.cwd(), "--no-focus", ], { encoding: "utf8" }, ); const paneId = parseCreatedPane(output); try { execFileSync("herdr", ["pane", "rename", paneId, name], { encoding: "utf8" }); } catch { // A cosmetic label must not break a successful split. } return paneId; } export function sendCommand(surface: string, command: string): void { requireHerdr(); execFileSync("herdr", ["pane", "run", surface, command], { encoding: "utf8" }); } export function readScreen(surface: string, lines = 50): string { requireHerdr(); return execFileSync( "herdr", ["pane", "read", surface, "--source", "recent-unwrapped", "--lines", String(Math.max(1, lines))], { encoding: "utf8" }, ); } export async function readScreenAsync(surface: string, lines = 50): Promise { requireHerdr(); const { stdout } = await execFileAsync( "herdr", ["pane", "read", surface, "--source", "recent-unwrapped", "--lines", String(Math.max(1, lines))], { encoding: "utf8" }, ); return stdout; } export function closeSurface(surface: string): void { requireHerdr(); const tabId = tabByPane.get(surface); if (tabId) { execFileSync("herdr", ["tab", "close", tabId], { encoding: "utf8" }); tabByPane.delete(surface); return; } execFileSync("herdr", ["pane", "close", surface], { encoding: "utf8" }); }