supercode-cli 0.4.19

supercode — a lightweight, fully-customizable AI coding agent CLI in Rust. Any model via OpenRouter; natively continues Claude Code and Codex sessions.
//! UX-29 dev/01 — soft-interrupt / steer mid-turn without killing it.
//!
//! dev/02 (already landed, `main.rs::race_ctrl_c`) gives Ctrl-C a HARD
//! cancel: it drops the in-flight turn future. dev/01 is a DIFFERENT,
//! softer affordance: while a turn streams, the user can type a line and
//! press Enter to QUEUE a steering message, which is delivered as the next
//! turn's input once the CURRENT turn finishes normally — the turn itself
//! is never touched, dropped, or truncated.
//!
//! **Why this can't touch termios (and so can't leak raw mode).** dev/02's
//! own doc comment established the invariant this module relies on:
//! outside `rl.readline()`, the terminal is already in normal/cooked mode
//! (`ICANON`+`ISIG` both on) — `readline()` is the only thing in this
//! codebase that ever flips it to raw, and it always restores cooked mode
//! before returning. This module is only ever active DURING an
//! `agent.send()` await (i.e. strictly outside any `readline()` call), so
//! it never needs to touch `termios` at all — there is nothing for it to
//! set, and therefore nothing it could ever leak. `Ctrl-C` staying a real
//! `SIGINT` (which needs `ISIG` enabled) is completely unaffected by this
//! module by construction, not by careful coordination — dev/02's
//! `race_ctrl_c` keeps working exactly as before whether or not this
//! module's watcher is running alongside it.
//!
//! **Why it can't steal the next prompt's keystrokes.** [`watch_loop`]
//! (run on a `spawn_blocking` thread) NEVER calls `read()` unless `poll()`
//! has *just* confirmed a byte is sitting in the kernel's canonical-mode
//! read queue — so it can never block. Every iteration re-checks a stop
//! flag at most [`POLL_TIMEOUT_MS`] later, so a caller can ask it to stop
//! and then `.await` its `JoinHandle` to know — for certain, not just
//! "probably by now" — that it has stopped touching stdin, before ever
//! calling `rl.readline()` again. In canonical mode, `poll(POLLIN)` only
//! ever fires once a *complete* line (or EOF/HUP) is ready — never
//! mid-keystroke — so there's no risk of the watcher grabbing half a line
//! and leaving a corrupt remainder for `readline()` to choke on. It reads
//! ONE byte at a time (not a larger chunk) specifically so it can never
//! over-read past the first completed line into bytes belonging to a
//! SECOND line the user typed in the same window — those stay queued in
//! the kernel, untouched, for the next real reader (`rl.readline()`) to
//! see normally, same as ordinary shell type-ahead.
//!
//! **Scope.** Wired into exactly one call site: the interactive
//! `chat()` REPL loop (`main.rs`). The four one-shot `race_ctrl_c` call
//! sites (`run`, `resume <file> "prompt"`, the resume-picker's one-shot
//! prompt) are untouched — they exit after a single turn, so there is no
//! "next turn" to deliver a queued steer into, and mixing this into them
//! would risk dev/02's already-hardened one-shot exit-130 paths for no
//! benefit. TTY-gating ([`capture_available`]) additionally keeps this
//! entirely inert for non-interactive/piped/machine-format runs, which
//! never call `chat()` at all (see `Command::Chat` — always
//! `OutputFormat`-agnostic, always interactive) but are double-gated here
//! too, belt-and-suspenders, matching `hidden_input.rs`/`picker.rs`'s own
//! pattern.
//!
//! **Windows (UX-12): degraded, not ported.** [`watch_loop`]'s Unix body is
//! `libc::poll(POLLIN)` + a single-byte non-blocking `read(2)` — the same
//! "confirm a byte is ready before ever calling read" primitive
//! `picker.rs` degrades for the same reason (see its module doc): a
//! Windows console handle's readiness signal does not guarantee a
//! subsequent read won't block (it can fire on a single keystroke, then a
//! line-mode `ReadFile` blocks for the rest of the line) — porting this
//! safely needs `ReadConsoleInput`/`PeekConsoleInput`-based key-event
//! parsing, a materially different implementation, not a small cfg-gated
//! swap. Per UX-12's guidance this degrades: [`capture_available`] reports
//! `false` unconditionally on Windows, so [`spawn`] is never actually
//! called (its one caller, `main.rs::race_ctrl_c_with_steer`, always takes
//! the `!steer_enabled` early-return instead) — soft-interrupt steering is
//! simply off. Ctrl-C hard-cancel (`race_ctrl_c`, a wholly separate code
//! path) is completely unaffected; `chat` remains fully usable, just
//! without the mid-turn steer affordance.

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

/// How often [`watch_loop`] wakes to re-check whether it's been asked to
/// stop. Bounds the extra latency joining the watcher can add to
/// returning to the prompt once a turn finishes (worst case: one tick).
#[cfg(unix)]
const POLL_TIMEOUT_MS: i32 = 100;

/// A captured line is capped at this many bytes (before its terminating
/// newline) so a runaway paste with no newline can't grow this thread's
/// buffer unboundedly. The captured prefix is delivered as-is; excess
/// bytes are simply never appended (not consumed from the fd twice, not
/// lost from the kernel's queue — they were already read to get here).
#[cfg(unix)]
const MAX_LINE_BYTES: usize = 8192;

/// The pure yes/no soft-steer capture gates on: real TTYs on both the
/// input and the chrome-drawing sides — the same bar `chat()`'s own
/// `interactive` flag and `race_ctrl_c`'s callers already use. Factored
/// out so it's unit-testable without an actual terminal.
#[cfg(unix)]
pub fn capture_available(stdin_is_tty: bool, stderr_is_tty: bool) -> bool {
    stdin_is_tty && stderr_is_tty
}

/// Windows: always `false` — see the module doc's "Windows" section. Takes
/// the same signature as the Unix version (parameters unused) so callers
/// don't need their own `cfg` branches.
#[cfg(windows)]
pub fn capture_available(stdin_is_tty: bool, stderr_is_tty: bool) -> bool {
    let _ = (stdin_is_tty, stderr_is_tty);
    false
}

/// Spawn a background watcher for ONE in-flight turn. Returns a stop flag
/// and the `JoinHandle` to race the turn against; callers own both:
/// `stop.store(true, ..)` to ask it to exit, then `.await` the handle
/// (regardless of who asked it to stop, or whether it already resolved on
/// its own) to know for certain it is no longer touching stdin before the
/// next `rl.readline()` call.
///
/// Never touches `termios` — see the module doc for why that's safe.
pub fn spawn() -> (Arc<AtomicBool>, tokio::task::JoinHandle<Option<String>>) {
    let stop = Arc::new(AtomicBool::new(false));
    let stop_bg = stop.clone();
    let handle = tokio::task::spawn_blocking(move || watch_loop(&stop_bg));
    (stop, handle)
}

/// Runs on a `spawn_blocking` thread. Polls stdin for a single queued
/// steering line; see the module doc for the no-block / no-over-read
/// safety argument. Returns `None` if asked to stop before a line arrived,
/// or on EOF/HUP (e.g. Ctrl-D pressed on an empty line mid-turn — no
/// steering line was typed, so there's nothing to queue).
#[cfg(unix)]
fn watch_loop(stop: &AtomicBool) -> Option<String> {
    let mut buf: Vec<u8> = Vec::new();
    loop {
        if stop.load(Ordering::Relaxed) {
            return None;
        }
        let mut pfd = libc::pollfd {
            fd: libc::STDIN_FILENO,
            events: libc::POLLIN,
            revents: 0,
        };
        // SAFETY: `pfd` is one valid, stack-local `pollfd` for stdin, and
        // `n == 1`; `poll` returns after at most `POLL_TIMEOUT_MS` even if
        // stdin never becomes readable, so this can never block
        // indefinitely (that bound is exactly why `stop` above is re-
        // checked promptly instead of this thread parking forever).
        let n = unsafe { libc::poll(&mut pfd, 1, POLL_TIMEOUT_MS) };
        if n <= 0 {
            continue; // timeout, or a transient EINTR/error — recheck `stop`.
        }
        if pfd.revents & libc::POLLIN == 0 {
            if pfd.revents & (libc::POLLHUP | libc::POLLERR | libc::POLLNVAL) != 0 {
                return None; // stdin closed/errored — nothing to capture.
            }
            continue;
        }
        let mut byte = 0u8;
        // SAFETY: `poll` just confirmed exactly one byte is ready to read
        // on this fd, so this single-byte `read` returns immediately
        // without blocking; `byte` is a valid 1-byte out-buffer.
        let r = unsafe {
            libc::read(
                libc::STDIN_FILENO,
                &mut byte as *mut u8 as *mut libc::c_void,
                1,
            )
        };
        if r <= 0 {
            // EOF (Ctrl-D on an empty canonical-mode buffer) or a
            // transient read error: nothing usable was captured.
            return None;
        }
        if byte == b'\n' {
            return Some(String::from_utf8_lossy(&buf).trim().to_string());
        }
        if buf.len() < MAX_LINE_BYTES {
            buf.push(byte);
        }
    }
}

/// Windows stub — see module doc "Windows" section. Unreachable at
/// runtime: this function's only caller, [`spawn`], is only invoked from
/// `main.rs::race_ctrl_c_with_steer` when `capture_available()` is `true`,
/// which never happens on Windows. Kept (rather than `unreachable!()`) so
/// a hypothetical future direct call fails safe — "no line captured" —
/// instead of panicking.
#[cfg(windows)]
fn watch_loop(stop: &AtomicBool) -> Option<String> {
    let _ = stop.load(Ordering::Relaxed);
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn capture_available_requires_both_ttys() {
        assert!(capture_available(true, true));
        assert!(!capture_available(false, true));
        assert!(!capture_available(true, false));
        assert!(!capture_available(false, false));
    }

    /// `watch_loop` must return promptly (bounded by `POLL_TIMEOUT_MS`,
    /// not hang) when asked to stop and stdin never produces a line —
    /// this is the property the "no leaked/leaked-forever reader thread"
    /// guarantee in the module doc rests on. Runs against the TEST
    /// process's real stdin (whatever it is under `cargo test` — a pipe
    /// or `/dev/null`, never a line-producing TTY), so this never reads a
    /// real line; it only proves the stop path terminates quickly.
    #[test]
    fn watch_loop_stops_promptly_when_asked() {
        let stop = Arc::new(AtomicBool::new(false));
        let stop_bg = stop.clone();
        let handle = std::thread::spawn(move || watch_loop(&stop_bg));
        std::thread::sleep(std::time::Duration::from_millis(20));
        stop.store(true, Ordering::Relaxed);
        let start = std::time::Instant::now();
        let result = handle.join().expect("watch_loop thread panicked");
        // Bounded by a couple of poll ticks, not "hangs forever".
        assert!(
            start.elapsed() < std::time::Duration::from_millis(2000),
            "watch_loop did not stop promptly: {:?}",
            start.elapsed()
        );
        assert!(result.is_none(), "no line was ever typed; expected None");
    }
}