mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
* feat(cli): wait on commands, press keys, and reap orphan panes
`tty7 wait` was the orchestration primitive for agents only. A pane with
nothing reporting agent status read as `idle`, so `--until idle` returned
success instantly about a shell that was midway through a build, and there
was no state that meant "the command finished" at all.
Panes with no agent now report `no-agent`, and `free` ends the wait when the
foreground command has exited — the process-tree question `procs` could
already answer but nothing could block on. `send --key` covers the keystrokes
text cannot express, which is what a worker stopped at `waiting` is usually
asking for. `pane close` takes several panes and `--orphans` clears what an
interrupted `run` leaves behind. `doctor` finally performs the hooks check
its own help has advertised.
The skill shipped in this repo predated `wait` entirely and taught a
hand-rolled `procs` polling loop with no notion of delegation; it now covers
the loop, and its agent statuses, `ws rm` orphan claim and not-implemented
list are corrected against the code.
* fix(cli): close the gaps review found in wait, --key and pane close
Five things the first pass got wrong, in the order they bite.
`--until free --changed` waited on a command it had already missed: the
"something ran" edge is only set by a poll that catches the pane busy, and a
command that starts and finishes inside one 500ms interval never is. That is
indistinguishable from a command that never ran, so the timeout now names both
doors instead of letting a finished build read as a hang.
`free` also outranked the agent ladder, which is backwards. A pane whose depth-0
process *is* the agent — the tree cannot tell that apart from a shell at its
prompt — reads free for its whole turn, so a `waiting` the caller explicitly
asked for could be overwritten by a process-tree fact and then withheld by the
`--changed` rule that comes with it. `free` is now consulted only when none of
the requested agent states answered, which is both cheaper and what the docs
already claimed. An empty process tree is "we could not see in" rather than
"free" for the same reason `no-agent` exists.
`--key M-X` sent `ESC x`: the whole spelling was folded to lowercase, which is
free for Ctrl (the C0 rule clears the case anyway) and wrong for Alt, where the
character rides through as itself.
`send --help` listed the key vocabulary by hand next to the table it is a list
of; it had already drifted by one alias. It is generated now.
And a `pane close` batch that could not close everything raised an error, which
left `--json` holding prose exactly when a cleanup script needs to know which
panes are still its problem. It exits 1 with `{"closed":[…],"failed":[…]}`, with
the complaint still on stderr so `-q` reports it.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
322 lines
14 KiB
Plaintext
322 lines
14 KiB
Plaintext
---
|
||
title: "Command reference"
|
||
description: "Every verb, its flags, and the JSON it emits under --json."
|
||
---
|
||
|
||
## Global flags
|
||
|
||
Accepted anywhere on the line, before or after the subcommand.
|
||
|
||
| Flag | Effect |
|
||
|---|---|
|
||
| `-m, --machine <MACHINE>` | Route to a linked machine over the local server's existing link. Matches the full link key (`me@devbox:22`) or the bare host (`devbox`). SSH links only; a down link, or a jump/proxy chain, is refused with a reason rather than dialled fresh. |
|
||
| `--json` | One JSON object on stdout instead of the human table. |
|
||
| `-q, --quiet` | No output on success. Errors still go to stderr. |
|
||
|
||
## Environment
|
||
|
||
Set inside every tty7 pane, inherited by anything launched from one.
|
||
|
||
| Variable | Meaning |
|
||
|---|---|
|
||
| `TTY7_PANE` | This pane's id, e.g. `71` or `%71` (both accepted). Default target of `split`, `send`, `capture`, `procs`, `wait`, `pane close`. |
|
||
| `TTY7_WS` | This pane's workspace id. Default for `run --keep`, `tab new`, `ws tree`. |
|
||
| `TTY7_CONFIG_DIR` | The server's config dir — how the CLI finds the right server. You never pass a socket path. |
|
||
|
||
Outside a tty7 shell, address-taking verbs fail with
|
||
`not inside a tty7 shell — pass an explicit %pane/@tab/workspace`.
|
||
|
||
## Exit codes
|
||
|
||
| Code | Meaning |
|
||
|---|---|
|
||
| `0` | Success |
|
||
| `1` | The command failed; one line on stderr, prefixed `tty7:` |
|
||
| `2` | Usage error — unknown verb, missing argument, bad type |
|
||
| `124` | `tty7 wait` timed out (the `timeout(1)` convention) |
|
||
| `141` | Unix only: the reader hung up — piping into `head -1`, say — and SIGPIPE ended it, exactly as it ends `cat`. Not a failure. Windows reports 0 for the same thing, having no signal to imitate. |
|
||
| *other* | Only from `tty7 run`, which passes the child's exit code through |
|
||
|
||
If `run` cannot learn the child's code it prints a note to stderr and exits 1
|
||
with `"exit_code_known": false` in the JSON — that is how you tell a real 1 from
|
||
a stand-in.
|
||
|
||
## Top-level verbs
|
||
|
||
### `tty7 [PATH]`
|
||
|
||
No subcommand means the GUI. A running window is asked to come forward and open
|
||
a tab at `PATH`; if none is registered, the app is launched instead.
|
||
JSON: `{"path","delivered","launched"}` — `delivered` says an existing window
|
||
took it, `launched` that a new process was started.
|
||
|
||
Without `PATH` it just activates the app. `-m` is refused: this verb drives the
|
||
GUI on *this* machine.
|
||
|
||
### `tty7 ls`
|
||
|
||
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.
|
||
|
||
### `tty7 run [--keep] [--cwd DIR] [--ws WORKSPACE] -- CMD...`
|
||
|
||
Spawns a pane running `CMD`, streams its output to stdout, waits, and exits with
|
||
its code. The command must come after `--`.
|
||
|
||
- `--keep` leaves the pane alive as a new tab afterwards. Needs a workspace, so
|
||
it requires `--ws` or `$TTY7_WS` — without one it is an error, not a silent
|
||
fallback.
|
||
- `--cwd` sets the working directory. `--ws` also sets the pane's `TTY7_WS`.
|
||
- Interrupting `run` can leave the pane behind as an orphan — see
|
||
`pane ls --all`.
|
||
|
||
JSON: `{"pane","exit","exit_code_known","kept"}`, printed **after** the streamed
|
||
output. The combined stream is not valid JSON — read the last line.
|
||
|
||
### `tty7 new [PATH] [--open]`
|
||
|
||
Creates a workspace plus its first tab and shell, at `PATH` if given. Prints the
|
||
workspace id. JSON: `{"id","pane","opened"}`.
|
||
|
||
`--open` also puts a window on it, if a GUI is running on this machine. Without
|
||
it the workspace still appears in the switcher; it just waits to be opened.
|
||
|
||
### `tty7 split [%PANE] (--v|--h) [--ratio R]`
|
||
|
||
Alias of `pane split`. Splits `%PANE` (default `$TTY7_PANE`), spawning a shell
|
||
in the same cwd. Exactly one axis is required — `--v`/`--vertical` puts the new
|
||
pane below, `--h`/`--horizontal` to the right. `--ratio` (default `0.5`) is the
|
||
share kept by the *existing* pane. Prints `%NN`. JSON: `{"pane"}`.
|
||
|
||
### `tty7 send [%PANE] [TEXT] [--enter] [--key KEY]…`
|
||
|
||
Types `TEXT` into the pane as keystrokes; `--enter` appends CR. With one
|
||
argument the text is the argument and the pane comes from `$TTY7_PANE` — but a
|
||
lone `%42` is rejected as a missing-text error rather than typed, unless a
|
||
`--key` gives it something to do.
|
||
|
||
`--key` presses a key instead of typing characters, which is what a pane wants
|
||
once something is already running in it: answering a prompt that only takes
|
||
arrow keys, closing a TUI with `escape`, stopping a build with `C-c`. Repeat it
|
||
for a sequence, and it composes with `TEXT` — the text goes first.
|
||
|
||
| | |
|
||
|---|---|
|
||
| Named | `enter` `escape` `tab` `backtab` `space` `backspace` `delete` `up` `down` `right` `left` `home` `end` `pageup` `pagedown` |
|
||
| Chords | `C-<char>` (Ctrl, e.g. `C-c`), `M-<char>` (Alt) |
|
||
| Aliases | `return` `cr` `esc` `del` `bs` `shift-tab` `pgup` `pgdn` |
|
||
|
||
Names are case-insensitive, and an unknown one is a usage error (exit `2`)
|
||
raised before anything is sent — half a key sequence in a live pane is worse
|
||
than none. Each keystroke is delivered as its own event, 200 ms apart, so a
|
||
raw-mode TUI reads a sequence as a sequence rather than as a paste.
|
||
|
||
JSON: `{"pane","sent","enter","keys"}`.
|
||
|
||
### `tty7 capture [%PANE] [--plain] [--scrollback]`
|
||
|
||
The pane's replay. Two independent choices:
|
||
|
||
**How much** — the newest scrollback segment by default, the whole ring with
|
||
`--scrollback`. The ring splits into segments on resize, so for a pane that was
|
||
never resized the two are identical.
|
||
|
||
**In what form** — without `--plain`, the stored bytes with ANSI escapes intact,
|
||
decoded as UTF-8 (invalid bytes become U+FFFD). With `--plain`, those bytes
|
||
replayed through a terminal grid and printed as the text they produced.
|
||
|
||
Either way it is a snapshot, not a stream: it collects the replay, settles for
|
||
~300 ms, and returns. Call it again for a newer one.
|
||
JSON: `{"pane","text"}`.
|
||
|
||
### `tty7 procs [%PANE]`
|
||
|
||
The process tree inside the pane, indented by depth, `*` on the foreground
|
||
process — then a second table of ports those processes are listening on. Prints
|
||
`nothing running in this pane` when both are empty.
|
||
|
||
JSON: `{"procs":[{"pid","name","depth","foreground"}],"ports":[{"port","pid","name"}]}`.
|
||
|
||
### `tty7 agents`
|
||
|
||
Every pane running a recognised coding agent. Table:
|
||
`PANE AGENT STATUS MESSAGE`, status one of `idle` / `working` / `waiting` /
|
||
`done`. JSON: `{"agents":[...]}`, plus a `"diagnostics"` array when an agent's
|
||
status hook is missing or out of date — that is why an agent can be listed with
|
||
a status that never moves.
|
||
|
||
### `tty7 wait [%PANE] [--until STATE,…] [--changed] [--timeout SECS] [--interval MS]`
|
||
|
||
Blocks until the pane reaches one of the named states.
|
||
|
||
| Flag | Default | |
|
||
|---|---|---|
|
||
| `--until` | `waiting,done,exit` | See the state table below |
|
||
| `--changed` | off | Only wake on a state the pane moved into *after* the wait began |
|
||
| `--timeout` | none | Give up after N seconds, exiting `124` |
|
||
| `--interval` | `500` | Poll interval in ms (50–3,600,000) |
|
||
|
||
The states come from two places. Four are the agent's own status, as reported
|
||
by its [hooks](/agents/status); the last three are facts about the pane:
|
||
|
||
| State | Means |
|
||
|---|---|
|
||
| `idle` `working` `waiting` `done` | The agent's status |
|
||
| `no-agent` | Nothing is reporting status here — a plain shell, or an agent whose hooks are not installed |
|
||
| `free` | The foreground command has exited; the pane is back to its bare shell |
|
||
| `exit` | The pane itself is gone. Ends every wait whether it was asked for or not |
|
||
|
||
`free` is how you wait for a **command** rather than an agent, and it is the
|
||
one state that costs a second request per poll — so it is only checked when you
|
||
name it, and only when none of the agent states you asked for already matched.
|
||
With `--changed` it means "something ran and then finished", which is what you
|
||
want directly after a `send`; a command quick enough to finish inside one
|
||
`--interval` is never seen running, and the timeout says so.
|
||
|
||
The reply carries the agent's message and native session id. The JSON's `stale`
|
||
flag says whether the answer might belong to the previous turn.
|
||
[Orchestration →](/agents/orchestration)
|
||
|
||
### `tty7 events`
|
||
|
||
Streams server events until interrupted, one per line — pane exits, agent status
|
||
changes, workspace preemption, layout deltas. `--json` makes it NDJSON. Blocks
|
||
forever; run it with a timeout or in the background.
|
||
|
||
### `tty7 status`
|
||
|
||
Same as `server status`: pid, uptime, pane count, dialect versions, build,
|
||
socket path. JSON is the `ServerStatus` object itself (`pid`, `uptime_secs`,
|
||
`panes`, `control_version`, `protocol_version`, `build`, `socket`).
|
||
|
||
### `tty7 doctor`
|
||
|
||
The install check: the three environment variables, whether the server answers,
|
||
whether its control and protocol versions match this binary, pid/uptime/panes,
|
||
how many machine links exist, and where each agent's
|
||
[status hooks](/agents/status) stand. Adds a note when you are not inside a
|
||
tty7 shell.
|
||
|
||
The hooks row is the one that explains a mystery: without them an agent reports
|
||
nothing, so `tty7 agents` shows it standing still and `tty7 wait` sits there
|
||
until it times out. Outdated hooks fail the same quiet way. Hooks are a local
|
||
install, so under `-m` the row reads `unknown`.
|
||
|
||
JSON: `{"context":{"config_dir","workspace","pane"},"server":{"reachable","dialect_ok","build","status","routes"},"hooks":{"installed","outdated","not_installed"}}`
|
||
— the context fields are booleans, not values, and each `hooks` field is a list
|
||
of agent slugs.
|
||
|
||
## `ws` — workspaces
|
||
|
||
Address a workspace by name, by full id, or by a unique id prefix (the 8-char
|
||
prefix `tty7 ls` prints). An ambiguous name or prefix is an error that lists the
|
||
candidates.
|
||
|
||
| Command | Effect | JSON |
|
||
|---|---|---|
|
||
| `ws ls` | Every workspace | `{"workspaces":[...]}` |
|
||
| `ws tree [WORKSPACE]` | One workspace as a tree: tabs, split axes and ratios, panes with cwds | The whole workspace object: `{"id","name","last_active","active_tab","tabs":[{"id","name","sidebar_group","root",…}]}` |
|
||
| `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 | `{"removed"}` |
|
||
| `ws attach WORKSPACE` | Become its controlling client | `{"attached","took_over_from"}` |
|
||
| `ws detach WORKSPACE` | Let go without interrupting anything | `{"detached"}` |
|
||
|
||
<Warning>
|
||
`ws rm` does **not** kill the panes it held — they keep running as orphans
|
||
with no workspace. Find them with `pane ls --all` and close them one by one.
|
||
</Warning>
|
||
|
||
Prefer `tty7 new <path>` over `ws new` when you want something usable: `ws new`
|
||
leaves an empty workspace you then have to populate, while `tty7 new --json`
|
||
hands back both ids at once.
|
||
|
||
The `root` node in `ws tree --json` is externally tagged, so a leaf is
|
||
`{"Leaf":{"pane":31}}` and a split is `{"Split":{"axis","ratio","a","b"}}` with
|
||
`a`/`b` nested the same way.
|
||
|
||
## `tab` — tabs
|
||
|
||
`@N` numbers tabs across the **whole machine** in tree order, densely from `@1`.
|
||
The numbering shifts whenever any workspace or tab is created or removed, so
|
||
resolve it immediately before use. A full tab UUID also works: `@<uuid>`.
|
||
|
||
| Command | Effect | JSON |
|
||
|---|---|---|
|
||
| `tab ls [WORKSPACE]` | Tabs of a workspace | `{"workspace","tabs":[{"ordinal","id","name","label","agent","group","panes":[…]}]}` |
|
||
| `tab new [WORKSPACE] [--cwd DIR]` | Add a tab with a fresh shell | `{"tab","pane"}` |
|
||
| `tab close @TAB` | Close the tab and every pane in it | `{"closed"}` |
|
||
| `tab rename @TAB NAME` | Name or rename | `{"tab","name"}` |
|
||
| `tab move @TAB INDEX` | Reposition within its workspace | `{"tab","to"}` |
|
||
|
||
`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.
|
||
|
||
`label` falls back through the best evidence available — the name if someone set
|
||
one, else the agent running there, else the last segment of the cwd, else the
|
||
foreground process. `name` stays literal, so a script can tell a real name from
|
||
a stand-in.
|
||
|
||
## `pane` — panes
|
||
|
||
| Command | Effect | JSON |
|
||
|---|---|---|
|
||
| `pane ls [WORKSPACE]` | Panes with their workspace, tab, cwd, live flag | `{"panes":[…]}` |
|
||
| `pane ls --all` | The server's whole pane registry, including orphans | `{"panes":[…],"orphans":N}` |
|
||
| `pane split …` | Identical to top-level `split` | `{"pane"}` |
|
||
| `pane close [%PANE…]` | Close panes; their shells are hung up | `{"closed":[…]}` |
|
||
| `pane close --orphans` | Close every pane no workspace holds | `{"closed":[…]}` |
|
||
|
||
`--all` is the one that shows leaks. Each entry is
|
||
`{"pane","workspace","orphan","owner","title","cwd","live"}`: `owner` is the id
|
||
of the workspace that owns the pane, and `orphan: true` means no workspace holds
|
||
it. An interrupted `run` and a removed workspace both leave orphans here.
|
||
|
||
`--orphans` is the reaper for exactly those. It closes what `pane ls --all`
|
||
lists as orphaned and nothing else — panes a workspace holds are untouched —
|
||
and reports an empty list rather than an error when there is nothing to clean
|
||
up, so a script does not have to guard it. A pane that cannot be closed does
|
||
not abandon the rest of the batch: the rest are still attempted, the complaint
|
||
goes to stderr, and the verb exits 1 with `{"closed":[…],"failed":[…]}` — the
|
||
list a retry needs.
|
||
|
||
<Warning>
|
||
`--orphans` closes every orphan on the machine, and an orphan can still be
|
||
doing real work — an interrupted `run` leaves the command running. Look at
|
||
`pane ls --all` first.
|
||
</Warning>
|
||
|
||
`title` is usually the running command — `claude`, `nvim`, `cargo` — which makes
|
||
`pane ls --all --json` a quick way to find "the pane running X".
|
||
|
||
## `machine` — remotes
|
||
|
||
`machine ls` lists the local machine plus every link the server holds:
|
||
`MACHINE KIND CONNECTED`. JSON: `{"machines":[{"key","kind","connected"}]}`.
|
||
|
||
## `server` — the daemon
|
||
|
||
| Command | Effect |
|
||
|---|---|
|
||
| `server status` | Same as `tty7 status` |
|
||
| `server logs` | Tail the server log; prints the path, and says so when logging was never enabled (`TTY7_LOG=info` before the server starts) |
|
||
| `server start` | Bring up a server on this machine |
|
||
| `server stop` | Stop it — **every pane on the machine dies** |
|
||
| `server restart` | Stop, then start — same consequence |
|
||
|
||
<Warning>
|
||
Do not run `start`, `stop`, or `restart` on someone else's behalf. They change
|
||
or destroy what the user's GUI is attached to.
|
||
</Warning>
|
||
|
||
## Not implemented yet
|
||
|
||
These parse and then exit 1 with an explanation:
|
||
|
||
- `ws stop` — the control dialect has no workspace-stop request yet
|
||
- `machine connect` / `machine disconnect` — use the GUI's connection manager
|