From a5229e18fac2e64d3ccdb142ecfe548ff74a2d8d Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 15 Aug 2026 16:53:18 +0800 Subject: [PATCH] docs(cli): say what `attach`, `ATTACHED` and `tab move INDEX` actually do MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by driving a dev instance with the CLI rather than by reading, which is the only way any of these would have surfaced. `ws attach` is documented as "become its controlling client", and the CLI cannot be one: the claim belongs to the connection that made it and the connection ends when the command does, so `tty7 ls` reads unattached again the moment it returns. Its real and only lasting effect is displacing whoever held it — which is exactly what the human output says (`took over from HOST`) and what the docs never did. `ATTACHED` said it names "a GUI window, or another client". A GUI only claims a workspace it is showing as a *remote* one; a window on a workspace of its own machine claims nothing, because there is no second client to arbitrate against. So with a window sitting on it `tty7 ls` prints `-`, and the column read as "nothing has this open" when it means "no remote client holds this". `tab move @TAB INDEX` never said which end `INDEX` counts from. It is 0-based — beside an `@N` address that is 1-based, on the same command line — so `tab move @1 2` moves the first tab to the third slot. Verified against a running server: `@1 0` leaves it, `@1 1` puts it second, past the end clamps to last the way `split --ratio` already documents. `to` in the JSON echoes the number you asked for rather than where the tab landed, which is worth saying since the clamp makes those differ. Also records why `WorkspaceDetach` throws away the one thing it computes: the reply has always been `Unit`, and the dialect is spoken to whatever build was pushed to a remote machine, so widening it to `Bool` would break every server already out there. 2932 tests pass. --- crates/tty7-core/src/host/server.rs | 10 +++++++++- docs/cli/reference.mdx | 28 +++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/crates/tty7-core/src/host/server.rs b/crates/tty7-core/src/host/server.rs index 5b360355..f1ec7375 100644 --- a/crates/tty7-core/src/host/server.rs +++ b/crates/tty7-core/src/host/server.rs @@ -671,7 +671,15 @@ fn run_request( Vec::new(), ), ControlRequest::WorkspaceDetach { id } => { - detach_workspace(conn, &id)?; + // `detach_workspace` can say whether there was a claim to release, + // and the answer is dropped on purpose. `Unit` is what this request + // has always replied, and the dialect is spoken across versions — + // a remote `tty7-server` is whatever build was pushed to that + // machine, so widening the reply to `Bool` would make a new client + // fail against every server already out there. Detach is idempotent + // from the caller's side anyway: "you do not hold this" is the + // outcome either way. + let _released = detach_workspace(conn, &id)?; (ReplyOk::Unit, Vec::new()) } ControlRequest::GuiOpen { path, workspace } => ( diff --git a/docs/cli/reference.mdx b/docs/cli/reference.mdx index a7cf88b5..d0ad49e6 100644 --- a/docs/cli/reference.mdx +++ b/docs/cli/reference.mdx @@ -58,8 +58,13 @@ GUI on *this* machine. Same as `ws ls`. Table: `WORKSPACE NAME TABS PANES ATTACHED`. JSON: `{"workspaces":[{"id","name","tabs","panes","attached"}]}`. -`ATTACHED` names the host holding the workspace — a GUI window, or another -client — and is `-` when nobody is. +`ATTACHED` names the client that has *claimed* the workspace over a control +link, and a claim lasts exactly as long as that client's link. A GUI claims a +workspace it is showing as a **remote** one; a GUI window on a workspace of its +own machine does not claim anything, because there is no second client to +arbitrate against. So `-` for a local workspace means "no remote client holds +this", not "nothing has it open" — with a window sitting on it, `tty7 ls` still +prints `-`. ### `tty7 run [--keep] [--cwd DIR] [--ws WORKSPACE] -- CMD...` @@ -243,9 +248,19 @@ candidates. | `ws new [NAME]` | An empty workspace (no tab, no pane) | `{"id","name"}` | | `ws rename WORKSPACE NAME` | Name or rename | `{"id","name"}` | | `ws rm WORKSPACE` | Delete the workspace and hang up its panes | `{"removed"}` | -| `ws attach WORKSPACE` | Become its controlling client | `{"attached","took_over_from"}` | +| `ws attach WORKSPACE` | Take it back from whoever holds it | `{"attached","took_over_from"}` | | `ws detach WORKSPACE` | Let go without interrupting anything | `{"detached"}` | +`ws attach` is a takeover, not a subscription: the claim belongs to the +connection that made it, and the CLI's connection ends when the command does. +So it prints `took over from HOST` when it displaced someone and nothing when +it did not, and either way the workspace reads as unattached again immediately +afterwards. Displacing the holder is the whole of what it does from here — +staying attached is a thing only a client that keeps its link open can do. + +For the same reason `ws detach` reports `{"detached"}` whether or not there was +a claim to release: it says "you do not hold this", which was already true. + `ws rm` hangs up the panes the workspace held. If the command reports that some panes could not be hung up, they keep running as orphans with no @@ -274,6 +289,13 @@ resolve it immediately before use. A full tab UUID also works: `@`. | `tab rename @TAB NAME` | Name or rename | `{"tab","name"}` | | `tab move @TAB INDEX` | Reposition within its workspace | `{"tab","to"}` | +`INDEX` counts from **0**, unlike the `@N` address beside it — `tab move @1 0` +leaves the first tab where it is, and `tab move @1 1` puts it second. It is a +position in the workspace's own tab list, not an `@N`, so it does not move a tab +between workspaces. Past the end it clamps to last rather than failing, the way +`split --ratio` does; `to` in the JSON echoes the number you asked for, so read +the position back from `tab ls` rather than from the reply. + `GROUP` is the heading the GUI's sidebar files the tab under, shown by its last segment. Read-only from here: with the default repo grouping the GUI recomputes it from the tab's working directory.