fix(cli): name an agent in the table the way the rest of the CLI does

The AGENT column came from a lowercased `{:?}`, which matched the agent's
slug for most variants by luck rather than by rule. Where it did not, the
table invented a third name for something that already had two: `OhMyPi`
printed as `ohmypi`, against a slug of `omp` — the spelling the hook rows,
`doctor` and the docs all use. It prints the slug now.

STATUS came the same way. The four variants are single words, so
lowercasing Debug happened to produce the documented `idle`/`working`/
`waiting`/`done`, but a variant named in two words would have printed
`needsinput` in the table against `needs-input` in the JSON for the same
pane. It goes through serde now, so the column cannot drift from what
`--json` says.

Neither the wire format nor `--json` changes: `CLIAgent` is serialised
across the control protocol, and that is a dialect not to touch in passing.

The test walks `CLIAgent::ALL` rather than listing variants, so an agent
added later is checked the moment it exists.
This commit is contained in:
l0ng-ai
2026-08-16 17:51:53 +08:00
parent bcbdf51fed
commit ac7919ed47
+81 -2
View File
@@ -1,5 +1,7 @@
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use serde_json::Value;
use tty7_core::core::cli_agent::AgentStatus;
use tty7_core::core::machine::{Machine, PaneNode, Workspace};
use tty7_core::core::session::WorkspaceId;
use tty7_core::core::tab_view::{TabLabel, TabView, strip_host_prefix, tab_views_of};
@@ -354,6 +356,23 @@ pub fn procs_tables(procs: &PaneProcs) -> String {
out
}
/// The status column, spelled the way the JSON spells it.
///
/// Taken through serde rather than `{:?}` so the two cannot drift: the four
/// current variants are single words, so lowercasing Debug happened to match
/// the documented `idle`/`working`/`waiting`/`done`, but a variant named in
/// two words would print `needsinput` in the table and `needs-input` in the
/// JSON for the same pane.
fn status_word(status: AgentStatus) -> String {
match serde_json::to_value(status) {
Ok(Value::String(word)) => word,
// `AgentStatus` is a plain unit-variant enum, so this is unreachable
// short of the derive changing shape. Say so rather than print a
// column of Rust.
_ => "?".to_string(),
}
}
pub fn agents_table(states: &[PaneAgentState]) -> String {
if states.is_empty() {
return "no agents running\n".to_string();
@@ -363,10 +382,16 @@ pub fn agents_table(states: &[PaneAgentState]) -> String {
.map(|s| {
vec![
format!("%{}", s.pane_id),
// `slug`, not a lowercased `{:?}`: the two agree for most
// variants and by luck rather than by rule, and where they
// part the table invented a third name for an agent that
// already had two — `OhMyPi` printed here as `ohmypi`, while
// its slug, the vocabulary the hook rows and the docs use, is
// `omp`.
s.agent
.map(|a| format!("{a:?}").to_lowercase())
.map(|a| a.slug().to_string())
.unwrap_or_else(|| "-".to_string()),
format!("{:?}", s.state.status).to_lowercase(),
status_word(s.state.status),
s.state.message.clone().unwrap_or_else(|| "-".to_string()),
]
})
@@ -484,6 +509,60 @@ mod tests {
);
}
#[test]
fn the_agents_table_spells_an_agent_the_way_the_rest_of_the_cli_does() {
use tty7_core::core::cli_agent::{AgentSessionState, CLIAgent};
// Walked, not listed: the table used a lowercased `{:?}`, which agreed
// with the slug for most variants by luck and invented a third name
// where it did not — `OhMyPi` printed as `ohmypi` against a slug of
// `omp`.
for agent in CLIAgent::ALL {
let state = PaneAgentState {
pane_id: 1,
agent: Some(agent),
state: AgentSessionState::default(),
};
let out = agents_table(&[state]);
let cell = out
.lines()
.nth(1)
.unwrap()
.split_whitespace()
.nth(1)
.unwrap();
assert_eq!(
cell,
agent.slug(),
"the table calls {agent:?} something the rest of the CLI does not"
);
}
}
#[test]
fn the_status_column_is_spelled_the_way_the_json_spells_it() {
use tty7_core::core::cli_agent::AgentStatus;
for status in [
AgentStatus::Idle,
AgentStatus::Working,
AgentStatus::Waiting,
AgentStatus::Done,
] {
let json = serde_json::to_value(status).unwrap();
assert_eq!(
Value::String(status_word(status)),
json,
"the table and the JSON disagree about {status:?}"
);
}
// And the four the docs promise.
assert_eq!(status_word(AgentStatus::Idle), "idle");
assert_eq!(status_word(AgentStatus::Working), "working");
assert_eq!(status_word(AgentStatus::Waiting), "waiting");
assert_eq!(status_word(AgentStatus::Done), "done");
}
#[test]
fn one_pathological_cell_does_not_widen_the_whole_table() {
// A 300-character workspace name padded every other row out past the