/** * @file panePaste.ts — lets another part of the Workspace page paste text into * a workspace's terminal (a diff-line comment from the Changes panel). Every * mounted TerminalView registers its run id; the text goes to the pane of that * workspace focused last — clicked into, or made the Workspace's focused pane * (a tab, Alt+1–4, the sidebar) — else any of its panes. It is a paste — * bracketed, so `claude` takes it as one block in its composer — and never an * Enter: the user reads it and sends it. Only a pane on SCREEN takes it (one * hidden behind a full-width Changes panel or a replay would swallow it * unseen); the caller says where it went, or that it went nowhere. The one * exception is `submitToRun`: a pane's own Chat view (`PaneChat.tsx`) sends * what the user typed there, so it pastes AND presses Enter — and its Stop * button presses Esc through `interruptRun`. * @author Nguyễn Ngọc Trí Vĩ */ interface Pane { paste: (text: string, submit?: boolean) => void; visible: () => boolean; key?: (data: string) => void; } const panes = new Map(); let lastFocused: string | null = null; /** Registers a mounted terminal (`visible`: is it on screen now; `key`: send * raw input, as a key press); returns its unregister. */ export function registerPane( runId: string, paste: (text: string, submit?: boolean) => void, visible: () => boolean = () => true, key?: (data: string) => void ): () => void { const entry = { paste, visible, key }; panes.set(runId, entry); return () => { if (panes.get(runId) === entry) panes.delete(runId); if (lastFocused === runId) lastFocused = null; }; } export function notePaneFocus(runId: string): void { lastFocused = runId; } /** The tmux names of workspace `laneId`'s panes: `agenthub-lane-[-slot]`. */ function ofLane(runId: string, laneId: number): boolean { const base = `agenthub-lane-${laneId}`; return runId === base || runId.startsWith(`${base}-`); } /** Pastes `text` into the pane of `runId` when it is mounted and on screen; * false otherwise (not mounted yet, or hidden). */ export function pasteToRun(runId: string, text: string): boolean { const p = panes.get(runId); if (!p?.visible()) return false; p.paste(text); return true; } /** Sends `text` as a message to the pane of `runId`: a paste, then Enter. * False when the pane is not mounted or not attached. */ export function submitToRun(runId: string, text: string): boolean { const p = panes.get(runId); if (!p?.visible()) return false; p.paste(text, true); return true; } /** Presses Esc in the pane of `runId` — how `claude` and `codex` stop the turn * they are running. False when the pane is not mounted or not attached. */ export function interruptRun(runId: string): boolean { const p = panes.get(runId); if (!p?.visible() || !p.key) return false; p.key("\x1b"); return true; } /** Pastes `text` into one of the workspace's VISIBLE panes — the one focused * last when it is — and returns its run id; null when none is on screen. */ export function pasteToLane(laneId: number, text: string): string | null { const ok = (id: string) => ofLane(id, laneId) && !!panes.get(id)?.visible(); const id = lastFocused && ok(lastFocused) ? lastFocused : [...panes.keys()].find(ok); if (!id) return null; panes.get(id)!.paste(text); return id; }