From 91c2cac80dfa67ad94fca3886f99ad382b9db38d Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 16 Aug 2026 10:42:03 +0800 Subject: [PATCH] fix(cli): say where a workspace id nobody typed came from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `tty7 tab new` in a shell whose workspace has been removed answered: no workspace with id 65b90fd4-421f-4cfb-9848-d263c9a80959 on this machine Nothing in what was typed contains a uuid. `workspace_or_context` falls back to `$TTY7_WS`, so the id came from the shell — and bare like that it reads as an internal error rather than something to act on. Every sibling message in this file carries a hint; this one had none. Found by running the CLI against a daemon that was not the one my shell belonged to, which is the same shape as the two cases the comment above `run --keep` already names: a workspace since removed, or a shell opened against another machine. That insight was in the source and never reached the reader. The id stays whole rather than shortened the way an ambiguity is — it is the exact string to compare against `echo $TTY7_WS`. --- crates/tty7-cli/src/resolve.rs | 46 +++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/crates/tty7-cli/src/resolve.rs b/crates/tty7-cli/src/resolve.rs index 8e116dad..8cd5c81f 100644 --- a/crates/tty7-cli/src/resolve.rs +++ b/crates/tty7-cli/src/resolve.rs @@ -60,13 +60,33 @@ fn nothing_named(text: &str) -> String { } } +/// What to say when an id resolves to nothing. +/// +/// Its sibling [`nothing_named`] answers a word someone typed. This one often +/// answers a word nobody typed: [`crate::address::workspace_or_context`] falls +/// back to `$TTY7_WS`, so `tab new` inside a shell whose workspace has since +/// been removed — or a shell opened against another machine — fails with a +/// uuid that appears nowhere in what was typed. Left bare, that reads as an +/// internal error rather than something the reader can act on, which is why +/// the second sentence is here at all. +/// +/// The id stays whole rather than shortened the way an ambiguity is: this is +/// the exact string to compare against `echo $TTY7_WS`. +fn no_workspace_with_id(id: &WorkspaceId) -> String { + format!( + "no workspace with id {id} on this machine — `tty7 ls` lists them. If you did not type \ + this id, it is the $TTY7_WS of the shell you are in, whose workspace has been removed \ + or belongs to another machine" + ) +} + pub fn workspace<'m>(machine: &'m Machine, addr: &WorkspaceAddress) -> Result<&'m Workspace> { match addr { WorkspaceAddress::Id(id) => machine .workspaces .iter() .find(|ws| ws.id == *id) - .ok_or_else(|| anyhow::anyhow!("no workspace with id {id} on this machine")), + .ok_or_else(|| anyhow::anyhow!("{}", no_workspace_with_id(id))), WorkspaceAddress::Named(text) => { let by_name: Vec<_> = machine .workspaces @@ -171,6 +191,30 @@ mod tests { use crate::address::parse_workspace; use crate::testbed::two_workspace_machine; + /// The reader is holding a uuid they did not type; the message has to say + /// where it came from, not just that it failed. + #[test] + fn an_id_that_resolves_to_nothing_says_where_it_came_from() { + let m = two_workspace_machine(); + let missing: WorkspaceId = "0d4e1a54-0000-4000-8000-00000000dead".parse().unwrap(); + let err = workspace(&m, &WorkspaceAddress::Id(missing)) + .expect_err("no workspace has that id") + .to_string(); + + assert!( + err.contains(&missing.to_string()), + "whole, to compare: {err}" + ); + assert!( + err.contains("`tty7 ls`"), + "a way to see the real ones: {err}" + ); + assert!( + err.contains("$TTY7_WS"), + "where an untyped id comes from: {err}" + ); + } + #[test] fn tab_ordinals_number_the_machine_in_tree_order() { let m = two_workspace_machine();