Skip to content

Latest commit

 

History

History
417 lines (326 loc) · 20.8 KB

File metadata and controls

417 lines (326 loc) · 20.8 KB
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, tab ls, 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, clamped to 0.05–0.95 — a --ratio 70 silently becomes 0.95, not an error. Prints %NN. JSON: {"pane"}.

tty7 send [%PANE] [TEXT] [--enter] [--key KEY]…

Types TEXT into the pane as keystrokes; --enter is shorthand for --key enter — it appends CR to the text, or presses Enter on its own when there is none, so tty7 send %42 --enter runs whatever pane 42 already has typed. With one argument the text is the argument and the pane comes from $TTY7_PANE — but a lone %42 (or bare 42, the shape pane ls --json prints) is rejected as a missing-text error rather than typed, unless a --key gives it something to do. --enter is that key only for the %-marked spelling: tty7 send 83 --enter is refused, because it reads as much like typing 83 into your own pane as like pressing Enter in pane 83, and the error names both ways to say which (send %83 --enter, send %PANE 83 --enter). A % followed by a digit that still doesn't parse (%3x) is an address error, never text for your own pane — while text that merely starts with % (%s/foo/bar/, %!sort) types as given, as does anything unmarked that is not a plain number (3x, +5). To type an address-shaped string, name the pane as well: tty7 send %42 %3x.

--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 pgdown

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. One case exception: Alt is a prefixed ESC, so its character goes out exactly as written and M-X is not M-x (Ctrl is unaffected — C-c and C-C are the same byte). 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] [--tail N]

The pane's replay. Three 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.

The default form is bounded by the pane's last **resize**, which is an event in the window rather than in the pane's output. A resize seals the segment holding everything printed so far and opens a new one, and on Unix the shell answers the SIGWINCH by repainting its prompt — so straight after a resize the newest segment can hold that repaint and nothing else, while the command's output sits in the segment behind it. Nothing in the byte stream tells a repaint apart from real output, so `capture` cannot decide this for you. When you are reading a pane whose window may have changed size — anything under the GUI — ask for `--scrollback`, and reach for `--tail N` if you wanted the tail rather than the ring.

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.

How many lines — all of them by default, the last N with --tail N, which answers "how did the last command end?" without a pipe through tail(1) (a program Windows does not have). The trim happens last, after --plain has decided what a line is: a wrapped line is one line to the grid, so --plain --tail 1 gives you the whole of the last line and not its final row. N must be at least 1 — a tail of nothing would read as a blank pane. The daemon still replays the ring in full; the saving is the pipe, not the wire.

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","bytes"} — text is what was printed, --tail included; bytes is how much the replay carried, counted before --plain rendered it or --tail trimmed it. It is what tells an empty text apart: 0 is a pane that has printed nothing, and a non-zero count with no text is a screen whose bytes produced nothing visible (a pane that was cleared, say). The same case also prints one line on stderr, so a script that never reads the JSON still sees it.

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","addr"}],"context":{...}} — addr is the address the socket is bound to (*, 0.0.0.0, 127.0.0.1, [::1], or a specific interface).

context says how much of the pane the process list actually covers: {"remote","local_pty","at_prompt","remote_prompt_seen"}. The tree is always this machine's, so on a pane that is the near end of a connection it lists the tunnel rather than the work — local_pty is false for a pane routed to a remote daemon (nothing here to walk), and remote names the host otherwise. at_prompt is the pane's own shell integration talking, absent until it emits its first mark. Older servers omit context entirely.

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; 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 Nothing is running in front of the pane's shell — it will take input now
exit The pane itself is gone. Ends every wait whether it was asked for or not
unknown Only ever reported, never awaited: freeness could not be determined at all. See below

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.

Two things answer it. On a pane whose shell is on this machine it is the process tree: nothing deeper than the shell means free. On a remote or SSH pane that tree describes the near end of the connection — the ssh itself, busy for as long as you are logged in, or nothing at all for a pane routed to a remote daemon — so there it is the far shell's own prompt marks that decide. A prompt mark on such a pane can only have come from the far side, because the near shell cannot be at a prompt while the connection holds its pty. Marks outrank a deeper process on a local pane too, which is why free means "will take input" rather than strictly "back to the bare shell": a pane sitting at a nested shell's prompt is free.

When the far host has no shell integration loaded there is nothing on this side that can tell an idle remote prompt from a running remote command. wait says so — status: unknown, exit 1, and a free_unknown string naming the host — rather than polling to the end of --timeout. It gives one poll of grace first, for a handshake still in flight, and it only gives up when free was the only state that could still answer: --until done,free keeps waiting on done.

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.

JSON: {"pane","status","matched","stale","activity","message","session_id"}. A timeout exits 124 with the same object plus "timed_out": true — matched is false there, and stale still says whether the pane moved while you watched, and free_unknown carries the reason when free was asked for and never resolved. 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 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 and hang up its panes {"removed"}
ws attach WORKSPACE Become its controlling client {"attached","took_over_from"}
ws detach WORKSPACE Let go without interrupting anything {"detached"}
`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 workspace — find them with `pane ls --all` and close them one by one.

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 new [WORKSPACE] --pane %PANE Add a tab around a pane already running {"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"}

--pane re-homes instead of spawning: it builds the tab around a pane the server is already running, which is how an orphan gets back on screen. Only a pane no tab holds is accepted — use pane split to add to a tab that exists. With no WORKSPACE and no TTY7_WS, the pane goes back to the workspace it was spawned for, which is the owner that pane ls --all prints. The seed is rebuilt from the pane registry, so the tab gets the pane's recorded cwd (unless --cwd overrides it) but not the shell or SSH spec the pane started with: the tree drops a pane's record when the tab holding it closes, and the registry is what is left. That only matters if the shell later dies, since a restore would then have nothing to restore from.

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 may attach to the pane (absent when none may), and orphan: true means no workspace holds it. An interrupted run leaves orphans here, as does a ws rm that reported panes it could not hang up.

An orphan is not necessarily rubbish — its shell may still be doing real work — so there are two ways out of the list. tab new --pane %<id> puts one back into a tab, which is the recovery path when the panes came out from under a workspace rather than out of an interrupted run. pane close is the other.

--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.

`--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.

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
Do not run `start`, `stop`, or `restart` on someone else's behalf. They change or destroy what the user's GUI is attached to.

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