/** * Detection of the "Claude is working" state from the rendered TUI screen. * * Kept free of any dependency so it can be unit-tested directly * (debug/detect.test.mjs) against captured screen fixtures. */ /** True when the screen indicates that Claude is currently working. */ export declare function screenShowsWork(screen: string): boolean; /** * One step of the "is this turn over?" loop, shared by both pilots. * * A turn ends on an ABSENCE of change: the screen must stop showing work AND * stay byte-identical for `stableMs`. That is a proxy, and a deliberately * conservative one — a TUI can look calm for an instant between two tool calls, * and calling the turn there would cut it in half. * * `stableMs` is a parameter and not a constant because the bar is not always * the same. When the user pressed the interrupt key, the end state is not being * guessed at: we asked for it. The caller may then lower the window, and the * turn is released in a fraction of the time. What it may NOT do is excuse a * screen that still shows work — that check is here, above the window, so no * caller can shorten its way past it. * * Pure on purpose: the loop it replaces lived twice (both pilots) and could * only be exercised by spawning a real process. */ export declare function idleStep(screen: string, lastScreen: string, stableSince: number, now: number, stableMs: number): { done: boolean; stableSince: number; lastScreen: string; }; /** * True while the text we just typed is still sitting in the input box (the * last "❯ …" line). Used to confirm a submit: before Enter the probe is in the * box; once Enter is accepted the box clears, so `!inputHasProbe` means "sent". * This is robust to a fast turn scrolling the echo out of the transcript — * unlike looking for the probe anywhere on screen, which false-negatived and * dumped the whole screen as an error. */ export declare function inputHasProbe(screen: string, probe: string): boolean; /** * The current content of the input box (the last "❯ …" line), without the "❯". * Submit detection keys off this being non-empty (something is typed/pasted) * then empty again (sent) — content-agnostic, so it also works when the TUI * collapses a big paste into a "[Pasted text +N lines]" placeholder (the * literal text isn't on screen to look for). */ export declare function inputText(screen: string): string; /** * Why a screen refused a prompt — named, when the shape is recognisable. * * `submit` reports "the text never appeared in the input box", which describes * the SYMPTOM and points at the input box, where there is nothing to find. Three * agents were once wedged on Claude Code's first-run screen — no input box at * all, every prompt echoed into a masked field — and that message sent the * investigation towards session startup, twice, before anyone looked at the * pane. Naming what is actually on screen turns hours into seconds. * * Returns null when nothing is recognised: a vague message beats a confident * wrong one. */ export declare function describeStuckScreen(screen: string): string | null; /** * How long to wait before reading a session's screen again. * * The screen watcher used to run at a flat 300 ms for EVERY session, which put * the cadence one consumer needs — a human watching the engine room — on all of * them at once, forever. Since `screen()` is a synchronous `tmux capture-pane`, * the cost lands on the server's event loop and grows with the number of agents * that EXIST rather than with what anyone is looking at: measured at 6 ms a * capture, twenty-one idle agents blocked 42% of the loop and every HTTP * request queued behind it for a second or more. * * An idle agent's screen is byte-identical for hours (measured: ten out of ten * unchanged over 1.2 s), so the watcher can back off and lose nothing. Anything * that could move the screen resets the streak to zero. */ export declare const SCREEN_FAST_MS = 300; export declare const SCREEN_SLOW_MS = 2000; /** Unchanged polls tolerated before backing off at all — a short burst of * stillness is normal mid-turn and must not slow the mirror down. */ export declare const SCREEN_CALM_AFTER = 3; export declare function nextScreenDelay(unchangedStreak: number, busy: boolean): number; /** The option number the ❯ cursor is currently on in a single-select dialog. */ export declare function selectedOptionN(screen: string): number | null; /** The slice of a pilot `moveToOption` needs: read the screen, press a key, and * wait until the screen satisfies a predicate. Both PtyPilot and TmuxPilot match * it; a fake one lets the overshoot regression be tested without a real TUI. */ export interface DialogPilot { screen(): string; press(key: "up" | "down"): void; waitFor(predicate: (screen: string) => boolean, opts?: { timeoutMs?: number; }): Promise; } /** * Moves the ❯ cursor onto option `n` — one press per step, WAITING for the * cursor to actually move before the next press. Returns whether it landed on n. * * The wait is the whole point. A fixed delay after each press overshot on the * tmux transport: its screen is a mirror refreshed on a ~300ms poll * (SCREEN_FAST_MS), so a 160ms read came back stale — the cursor still on the old * option — and the loop pressed down again and again, sailing past the target. * Single-select answers then landed on the LAST option, so the web form "did * nothing" and the question re-appeared. node-pty's screen is synchronous and hid * it entirely. `waitFor` polls until the move lands, which is correct on both. */ export declare function moveToOption(pilot: DialogPilot, n: number): Promise; /** The slice of a pilot `typeIntoBox` needs: put text in the box, clear the box, * and wait until the screen satisfies a predicate. Both PtyPilot and TmuxPilot * match it; a fake one lets the doubled-prompt regression be tested without a * real TUI. */ export interface TypingPilot { /** Bracketed-paste the text into the input box — never submits it. */ paste(text: string): void; /** Ctrl-U: clear whatever is in the input box. */ clearInput(): void; waitFor(predicate: (screen: string) => boolean, opts?: { timeoutMs?: number; }): Promise; } export interface TypeOptions { attempts?: number; /** How long to wait for a paste to show up in the box. */ appearMs?: number; /** How long to let an in-flight paste land before clearing. */ settleMs?: number; /** How long to wait for the box to actually read empty after Ctrl-U. */ clearMs?: number; } /** * Puts `text` in the input box, retrying until it is actually there. Returns * whether it landed; the caller decides what a failure means. * * The retry is what makes it robust — the TUI flushes stdin received before its * keyboard handler is ready — and the RECOVERY between attempts is where it used * to go wrong. A slow first paste (a freshly respawned pane, a loaded machine) * can still be in flight when the appearance check times out: the old code sent * Ctrl-U immediately, so it cleared an EMPTY box, the paste landed a moment * later, and the next attempt pasted on top of it — leaving the partial text * PLUS the full text in the box, which is how a prompt came through doubled. * Nothing about that is visible afterwards: the transcript just shows one * mangled message. * * So the recovery does the two things the fast path cannot: it WAITS for any * in-flight paste to land before clearing, and it CONFIRMS the box reads empty * before pasting again. A box that will not clear is left to the next attempt * rather than pasted into blindly. * * It lives here, next to `idleStep` and `moveToOption`, for the reason those do: * it was written twice (node-pty and tmux), and could only be exercised by * spawning a real process. */ export declare function typeIntoBox(pilot: TypingPilot, text: string, { attempts, appearMs, settleMs, clearMs }?: TypeOptions): Promise;