Problem
Today, every CLI we drive in the broker (Claude, Codex, Gemini, …) needs a bespoke "is it ready for input?" detector in src/helpers.rs::detect_cli_ready — Claude wants "Welcome back" + a bare ❯ line, Gemini wants "Type your message or @path/to/file", codex wants >, etc. Idle detection is a separate, coarse "no output for N seconds" timer (reset_idle_on_output in src/pty_worker.rs).
Two problems:
- Adding a new CLI = adding new regex/substring rules in helpers.rs. There's no shared vocabulary for "ready."
- Real readiness is usually a conjunction of conditions ("the welcome banner has been drawn AND output has been quiet for 200ms"), but our code can only express one at a time.
Prior art
montanaflynn/headless-terminal (internal/wait/wait.go, ~270 LOC) exposes a small composable taxonomy of wait conditions:
--wait-text REGEX — wait until the screen contains a match
--wait-cursor R,C — wait until the cursor lands at a row/col
--wait-idle DUR — output has been quiet for DUR
--wait-change — any output change since send
--wait-exit — process has exited
All AND-composed: a single start-time predicate plus a reset-on-chunk idle timer, racing on Done(). This is the part agents (and we) get wrong; ht has thought through it more carefully than we have.
Proposal
Port the wait taxonomy to Rust and replace the per-CLI ready hacks. Sketch:
// new module, e.g. src/wait.rs
pub enum WaitCondition {
Text(Regex),
Cursor { row: u16, col: u16 }, // requires #3 (VT grid) for cursor
Idle(Duration),
Change,
Exit,
}
pub struct WaitSet(Vec<WaitCondition>); // AND-composed
Then "Claude is ready" becomes WaitSet::new().text("Welcome back").idle(Duration::from_millis(200)) instead of bespoke detection in helpers.rs.
Files to touch
- New:
src/wait.rs
- Replace:
src/helpers.rs::detect_cli_ready (currently per-CLI string matching)
- Update:
src/pty_worker.rs injection path to use the new primitive instead of the existing idle-only model
- The
Cursor variant depends on having a real VT grid — see follow-up issue for that. Ship Text/Idle/Change/Exit first; Cursor lands once vt100 is wired up.
Effort
Medium. Pure logic port, no new deps. Tests can run against recorded byte streams from real Claude/Codex sessions.
Why now
Removes per-CLI fragility, gives us a single primitive for "wait for X" that integration tests, the steer mode, and SDK-exposed waits can all share. Also a prerequisite for cleaning up the inject + readiness loop in pty_worker.rs.
Problem
Today, every CLI we drive in the broker (Claude, Codex, Gemini, …) needs a bespoke "is it ready for input?" detector in
src/helpers.rs::detect_cli_ready— Claude wants"Welcome back"+ a bare❯line, Gemini wants"Type your message or @path/to/file", codex wants>, etc. Idle detection is a separate, coarse "no output for N seconds" timer (reset_idle_on_outputinsrc/pty_worker.rs).Two problems:
Prior art
montanaflynn/headless-terminal (
internal/wait/wait.go, ~270 LOC) exposes a small composable taxonomy of wait conditions:--wait-text REGEX— wait until the screen contains a match--wait-cursor R,C— wait until the cursor lands at a row/col--wait-idle DUR— output has been quiet for DUR--wait-change— any output change since send--wait-exit— process has exitedAll AND-composed: a single start-time predicate plus a reset-on-chunk idle timer, racing on
Done(). This is the part agents (and we) get wrong; ht has thought through it more carefully than we have.Proposal
Port the wait taxonomy to Rust and replace the per-CLI ready hacks. Sketch:
Then "Claude is ready" becomes
WaitSet::new().text("Welcome back").idle(Duration::from_millis(200))instead of bespoke detection in helpers.rs.Files to touch
src/wait.rssrc/helpers.rs::detect_cli_ready(currently per-CLI string matching)src/pty_worker.rsinjection path to use the new primitive instead of the existing idle-only modelCursorvariant depends on having a real VT grid — see follow-up issue for that. Ship Text/Idle/Change/Exit first; Cursor lands once vt100 is wired up.Effort
Medium. Pure logic port, no new deps. Tests can run against recorded byte streams from real Claude/Codex sessions.
Why now
Removes per-CLI fragility, gives us a single primitive for "wait for X" that integration tests, the steer mode, and SDK-exposed waits can all share. Also a prerequisite for cleaning up the inject + readiness loop in pty_worker.rs.