Global flags
Accepted anywhere on the line, before or after the subcommand.Environment
Set inside every tty7 pane, inherited by anything launched from one.
Outside a tty7 shell, address-taking verbs fail with
not inside a tty7 shell — pass an explicit %pane/@tab/workspace.
Exit codes
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 --.
--keepleaves the pane alive as a new tab afterwards. Needs a workspace, so it requires--wsor$TTY7_WS— without one it is an error, not a silent fallback.--cwdsets the working directory.--wsalso sets the pane’sTTY7_WS.- Interrupting
runcan leave the pane behind as an orphan — seepane ls --all.
{"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.
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]
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","addr"}]} —
addr is the address the socket is bound to (*, 0.0.0.0, 127.0.0.1,
[::1], or a specific interface).
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.
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:
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.
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.
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.
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>.
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
--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.
--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.
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
Not implemented yet
These parse and then exit 1 with an explanation:ws stop— the control dialect has no workspace-stop request yetmachine connect/machine disconnect— use the GUI’s connection manager