fix(cli): the advertised wait idiom fails in one of its own end states

`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.
This commit is contained in:
l0ng-ai
2026-08-16 05:57:48 +08:00
parent a343915734
commit 335d98252c
2 changed files with 56 additions and 3 deletions
+12 -1
View File
@@ -95,7 +95,18 @@ pub enum Command {
#[command( #[command(
about = "Block until a pane's agent needs input, finishes its turn, or the pane \ 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), Wait(WaitArgs),
+44 -2
View File
@@ -1517,8 +1517,11 @@ fn doctor(ctx: &Context, backend: &mut dyn Backend) -> Result<Outcome> {
rows.push(vec![ rows.push(vec![
"status".to_string(), "status".to_string(),
format!( format!(
"pid {}, up {}s, {} panes", "pid {}, up {}s, {} pane{}",
status.pid, status.uptime_secs, status.panes status.pid,
status.uptime_secs,
status.panes,
if status.panes == 1 { "" } else { "s" }
), ),
]); ]);
let routes = match backend.control(ControlRequest::Routes)? { let routes = match backend.control(ControlRequest::Routes)? {
@@ -3853,6 +3856,45 @@ mod tests {
assert!(out.contains("set (/cfg/tty7)"), "{out}"); 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 /// Missing hooks are the reason a perfectly healthy-looking agent never
/// reports and `tty7 wait` sits there until it times out. `doctor` is the /// 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 /// verb people run when something is not working, so it is where that has