import type { PilotOptions, WaitIdleOptions, WaitOptions } from "./session.js"; /** * Same interface as {@link PtyPilot}, but runs `claude` inside a detached * **tmux** session instead of a node-pty child. tmux (its own daemon) owns the * terminal, so the agent survives the shadok-ai server restarting or * crashing: on restart the server reattaches to the running tmux session and * the in-flight turn continues uninterrupted. * * The tmux session name is derived from the Claude session id, so reattach is * deterministic. Content still comes from the .jsonl tail (unchanged); tmux is * only about keeping the live process alive. */ export interface TmuxPilotOptions extends PilotOptions { /** tmux session name (stable across restarts — derive from the session id). */ tmuxName: string; } /** True when a tmux session with this exact name is alive. */ export declare function tmuxHasSession(name: string): boolean; /** Kills a tmux session by name. Idempotent: an absent session is a no-op. */ export declare function tmuxKillSession(name: string): void; /** * The working directory of a live tmux session's pane — the source of truth for * a reattached session's cwd (a worktree agent's real directory), which the * client can't always supply on resume. Null if the session is gone. */ export declare function tmuxPaneCwd(name: string): string | null; export declare class TmuxPilot { private readonly opts; private readonly name; private exitListeners; private poller; /** Consecutive captures that found the screen byte-identical — drives the * poller's back-off. Reset by anything that writes to the pane. */ private calm; private _screen; private exited; /** True when we reattached to an already-running session (survived restart). */ attached: boolean; constructor(options: TmuxPilotOptions); /** Whether a tmux session with our name is currently alive. */ private hasSession; start(): void; private tick; /** * Back to the fast cadence at once. * * Every path that writes to the pane calls this: after a keystroke the mirror * must not wait out a back-off it earned while the pane was still. */ private wake; /** Refreshes the cached rendered screen. Returns false when tmux refused. */ private capture; onExit(cb: (code: number) => void): () => void; get hasExited(): boolean; /** The currently rendered screen (cached, refreshed by the poller). */ screen(): string; /** tmux gives us the rendered pane incl. scrollback; used rarely. */ fullBuffer(): string; /** Sends literal text to the session (no submit). */ write(text: string): void; private rawFile; private rawTimer; private rawOffset; /** The pane's real character size, so the browser terminal can render the raw * stream 1:1 (mismatched cols = the TUI wraps into garbage). */ paneSize(): { cols: number; rows: number; }; /** Current screen WITH escape sequences (colours) — primes a raw viewer, * since pipe-pane only carries output produced AFTER it starts. */ seed(): string; /** Original pane size, remembered on the first resize so we can restore it * when the interactive terminal detaches (the control plane reads this pane). */ private origSize; /** Resizes the tmux window to fit the browser terminal (full-screen view). * Remembers the original size once, restored on detach. */ resizeWindow(cols: number, rows: number): void; /** Injects raw bytes into the pane (hex via send-keys -H). */ sendRaw(data: Buffer): void; /** * Streams the pane's raw output (ANSI included) by piping it to a temp file * we tail. One consumer per pilot (the server fans out to WS clients). * Returns a detach function; idempotent. */ attachRaw(onData: (chunk: Buffer) => void): () => void; private detachRaw; /** Pastes text with bracketed-paste framing (reliable for the TUI input). */ private paste; press(key: "enter" | "escape" | "up" | "down" | "left" | "right" | "tab" | "ctrl-c"): void; isWorking(): boolean; /** * Types a prompt then presses Enter — same robustness as PtyPilot: * bracketed paste with retry until the text shows, then Enter with retry * until the turn is actually sent. */ submit(text: string): Promise; /** A concise client-facing error; the full screen goes to the server log only. */ private submitError; waitFor(predicate: (screen: string) => boolean, { timeoutMs }?: WaitOptions): Promise; waitForIdle({ stableMs, timeoutMs, }?: WaitIdleOptions): Promise; /** Clean shutdown: /exit, then kill the tmux session as a fallback. */ /** * End the agent, gracefully if possible but ALWAYS for real. * * The graceful `/exit` matters — it lets claude release its session lock so a * later `--resume` works. But it goes through `submit()`, which is exactly * what fails on a TUI wedged somewhere without an input box: precisely the * case a restart exists to rescue. So the kill is unconditional, and the only * thing consulted is `hasSession()`. * * `this.exited` is deliberately NOT an early-return any more. That flag means * "I believe this ended"; believing it here returned without killing anything, * and `start()` then adopted the surviving pane — turning a restart the user * explicitly asked for into a silent reattach to the very process they wanted * gone. Three agents sat wedged on Claude Code's onboarding screen for a day * that way, and every "Reload agent" was a no-op. */ stop(): Promise; /** Kills the tmux session (ends the agent). */ kill(): void; } /** True when tmux is available on this host. */ export declare function tmuxAvailable(): boolean;