Skip to content

broker: composable wait-conditions for CLI readiness (steal from ht) #800

@willwashburn

Description

@willwashburn

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:

  1. Adding a new CLI = adding new regex/substring rules in helpers.rs. There's no shared vocabulary for "ready."
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions