From 335d98252c7916795249acaf9a4707133a5bad89 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 16 Aug 2026 05:57:48 +0800 Subject: [PATCH] fix(cli): the advertised wait idiom fails in one of its own end states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `tty7 wait`'s headline example is tty7 wait %3 && tty7 capture %3 --plain and `exit` is one of the three states `--until` waits for by default. Run it that way and the pane is gone by the time capture runs: wait reports `exit` and succeeds, `&&` proceeds, and capture answers that nothing is running. An agent following the documented pattern gets no output for a command that produced some. The example is right for an agent — `waiting` and `done` leave the pane alive. It is wrong for a plain command, which finishes by the shell exiting. Spell both out in `wait`'s long help, and say plainly that `exit` is the end state with nothing left to read. Verified against a live server: `wait --until free` blocks for the command (5s for a 4s sleep), and the capture after it finds the marker. Also: a server running one pane had `doctor` say "1 panes" on its first line. It is the first thing a new user runs. Every other count in the tree is already plural-aware — the GUI routes these through `t_plural` with "one"/"other" branches, and the CLI's other count (`closed N panes`) handles the single case separately — so this was the last one. --- crates/tty7-cli/src/cli.rs | 13 +++++++++- crates/tty7-cli/src/commands.rs | 46 +++++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/crates/tty7-cli/src/cli.rs b/crates/tty7-cli/src/cli.rs index 56d0b234..a0a814e9 100644 --- a/crates/tty7-cli/src/cli.rs +++ b/crates/tty7-cli/src/cli.rs @@ -95,7 +95,18 @@ pub enum Command { #[command( about = "Block until a pane's agent needs input, finishes its turn, or the pane \ - exits — the orchestration primitive: `tty7 wait %3 && tty7 capture %3 --plain`" + exits — the orchestration primitive: `tty7 wait %3 && tty7 capture %3 \ + --plain`", + long_about = "Block until a pane's agent needs input, finishes its turn, or the \ + pane exits — the orchestration primitive:\n\n \ + tty7 wait %3 && tty7 capture %3 --plain\n\n\ + That pairs with an agent, whose pane is still alive in `waiting` and \ + `done`. For a plain command, wait for `free` instead:\n\n \ + tty7 wait %3 --until free && tty7 capture %3 --plain\n\n\ + `exit` is the one end state with nothing left to read: the pane's \ + output goes with the pane, and the workspace keeps only a leaf the \ + GUI can revive into a fresh shell. Capture before the shell exits, \ + or run the command under `tty7 run`, which streams it." )] Wait(WaitArgs), diff --git a/crates/tty7-cli/src/commands.rs b/crates/tty7-cli/src/commands.rs index 20e0b8bf..8bb9ede0 100644 --- a/crates/tty7-cli/src/commands.rs +++ b/crates/tty7-cli/src/commands.rs @@ -1517,8 +1517,11 @@ fn doctor(ctx: &Context, backend: &mut dyn Backend) -> Result { rows.push(vec![ "status".to_string(), format!( - "pid {}, up {}s, {} panes", - status.pid, status.uptime_secs, status.panes + "pid {}, up {}s, {} pane{}", + status.pid, + status.uptime_secs, + status.panes, + if status.panes == 1 { "" } else { "s" } ), ]); let routes = match backend.control(ControlRequest::Routes)? { @@ -3853,6 +3856,45 @@ mod tests { assert!(out.contains("set (/cfg/tty7)"), "{out}"); } + /// A server running one pane said "1 panes". + /// + /// The count comes straight off the status reply, so every fresh server + /// says it — `doctor` is the first thing a new user runs, and the first + /// line it shows them was ungrammatical. + #[test] + fn doctor_counts_one_pane_in_the_singular() { + use tty7_core::daemon::control::ServerStatus; + + let status = |panes: u64| ServerStatus { + pid: 4242, + uptime_secs: 61, + panes, + control_version: CONTROL_VERSION, + protocol_version: PROTOCOL_VERSION, + build: "26.7.5".into(), + socket: "127.0.0.1:5555".into(), + }; + let line = |panes: u64| { + let mut backend = mock(); + backend.replies.push_back(ReplyOk::Status(status(panes))); + backend.replies.push_back(ReplyOk::Routes(Vec::new())); + human(run_cli( + &["tty7", "doctor"], + &Context::default(), + &mut backend, + )) + }; + + assert!( + line(1).contains("1 pane\n") || line(1).contains("1 pane "), + "{}", + line(1) + ); + assert!(!line(1).contains("1 panes"), "{}", line(1)); + assert!(line(0).contains("0 panes"), "{}", line(0)); + assert!(line(2).contains("2 panes"), "{}", line(2)); + } + /// Missing hooks are the reason a perfectly healthy-looking agent never /// reports and `tty7 wait` sits there until it times out. `doctor` is the /// verb people run when something is not working, so it is where that has