TUI commands and selectors
The reference TUI's consumer-facing product surface: subcommands, keybinds, status bar, layout, hooks, recording.
Full source summary
The reference TUI's consumer-facing product surface: subcommands, keybinds, status bar, layout, hooks, recording. The TUI is the wedge — the daily-driver adoption surface — and its differentiator is the wire: attach/detach, remoting, and a human and their agents sharing the same live terminals. It is held a pure consumer with no protocol privilege by ADR-0017. What's normative lives in ../spec/; this file is the human-facing reference for the tmux-shaped consumer that ships in tree.
1. CLI surface
phux is a single binary with subcommands. The naked invocation —
phux — is the common case: attach to the user’s server, lazily
spawning it if it isn’t running. With no arguments it auto-spawns a server
if the socket is missing, then starts each attach or reconnect attempt with
AttachTarget::Last. The server resolves prior activity first and otherwise
selects its configured live seed. A refusal from an older server gets at most
one lookup-only ByName retry for the configured default; the client never
translates Last into a creating attach. Auto-spawn (the client forks itself
as phux server if the socket is missing, polls 25 ms / 2 s) covers both the
naked and explicit-attach paths.
1.1 The shipped verbs
These are the main interactive and control entrypoints, annotated for
narrative. The complete inventory — every invocation path with its flags,
defaults, and help text — is the generated
docs/reference/cli.md (the same content
phux --help renders, including supervision, upgrade, tags, pairing,
agents, and workspace commands); consult it when a flag below looks
abbreviated, and trust it over this list on any disagreement:
phux # attach to default session, autostart server
phux attach [SESSION] # attach explicitly; session optional (alias: a)
phux --remote [USER@]HOST[:PORT]
# attach to a phux server on another machine,
# ssh-style (ADR-0093). Resolves to a registered
# host when there is one; otherwise pairs first
# and registers the result, so every later
# attach is a direct QUIC dial with no ssh in
# the path. PORT defaults to 8788 (the port a
# server auto-binds, ADR-0081). `user@` names
# the ssh destination used to pair -- it is not
# sent on the wire
phux attach [SESSION] --remote HOST [--code LINK] [--no-enroll]
# the verb-scoped form: name a session, pair
# from a pasted connect link instead of over
# ssh (--code, quote it), or refuse to shell out
# to ssh at all (--no-enroll)
phux attach --quic HOST:PORT [--cert-fingerprint FP] [--token HEX]
# attach to a remote server over QUIC (TLS 1.3).
# loopback trusts the dev cert; routable hosts
# require --cert-fingerprint (from `phux pair`)
phux attach --ws ws://127.0.0.1:8787
# attach over the WebSocket/TCP fallback locally
phux attach --ws wss://HOST:PORT --cert-fingerprint FP --token HEX
# attach over TLS WebSocket when UDP/QUIC is blocked
phux server [--session N] [--listen HOST:PORT] [--quic HOST:PORT]
[--connect HOST:PORT] [--hub] [--exit-after-idle SECS]
# run server in foreground
# --listen also accepts WebSocket clients (= PHUX_WS_ADDR)
# --quic also accepts QUIC clients (= PHUX_QUIC_ADDR)
# --connect selects one [[connector]] relay;
# without it every configured relay is supervised
# --hub validates [[satellites]] into the runtime
# satellite table at startup, dials each enabled
# satellite (quic/wss per ADR-0038; ssh:// over
# `ssh HOST phux stdio-bridge`), and relays
# satellite-tagged frames over the links (§4.2)
# --exit-after-idle bounds an EPHEMERAL server:
# it exits once no client has been connected for
# SECS, live panes and all. Off by default —
# without it the server lives until its last
# pane is gone (ADR-0063)
phux new [-s NAME] [-c CWD] [--] [COMMAND...]
# create a session
phux spawn [--satellite NAME | --target TARGET [--split DIR] [--ratio R]] [-c CWD] [--json] [--] [COMMAND...]
# explicit placement is local-only; absent target
# preserves legacy unplaced behavior
phux launch INTEGRATION [--print] [--target TARGET [--split DIR] [--ratio R]] [-c CWD] [--] [ARGS...]
# spawn a pane running an agent integration's
# [launch] command (ADR-0042); resolves the named
# template from an enabled plugin and routes the
# agent through its identity wrapper, so the pane
# self-declares its phux.agent/v1 identity with no
# alias. --list enumerates; --print is a
# server-free dry run of the resolved argv
phux ls # list sessions (alias: list)
phux kill TARGET # kill session/window/pane by selector
phux insert-pane TARGET NEW # insert an already-created pane (no spawn)
phux move-pane SOURCE TARGET # relocate a pane beside another
phux swap-pane FIRST SECOND # exchange two pane leaves
phux rename SESSION NEW-NAME # rename a session
phux resize TARGET COLSxROWS # set a pane's grid with no TTY (§4.2
# `window-size` for what happens when a
# client is attached)
phux snapshot [TARGET] # dump pane grid (for piping/scripting)
phux snapshot --rendered # dump the client's composited multi-pane view
phux send-keys TARGET KEYS... # send keys to a pane (scripting)
phux paste TARGET [TEXT] # paste text into a pane (TEXT or stdin)
phux run TARGET CMD... # run a command in a pane, capture $?
phux wait [TARGET] # poll a pane until a condition holds
phux watch [TARGET] # stream a pane's live events
phux rec [TARGET] -o PATH # record a pane to a cast, GIF, or APNG (§10);
# a pure observer — never attaches or resizes
phux --rec PATH # on `phux` / `phux attach` only: tee the
# attached session's composited output to PATH
phux play FILE.cast [TARGET] # create a pane whose PTY is fed from a
# recording (§10). TARGET says WHERE the new
# pane goes; it is never written to
phux ask TARGET QUESTION # report an agent ask event for a pane
phux agent install-claude # make plain interactive `claude` enter phux
phux agent uninstall-claude # remove its shim, hooks, and shell activation
phux config <init|path|show> # scaffold + inspect config
phux config check [PATH] [--json]
# report every unknown key / wrong value with
# its full dotted path and originating layer
phux config reload # validate, then apply the config to running
# clients in place (§4.3)
phux config plugins [--json] # compatibility alias: inspect plugin manifests
phux config agents [--json] # inspect configured plugin agent states
phux config run PLUGIN ACTION # execute a configured plugin action
phux plugin <COMMAND> # install/update/link/list/toggle/unlink/validate plugins
# (list alias: ls; unlink aliases: rm, remove)
phux stdio-bridge # splice stdin/stdout to the local server socket
# (the remote end of the SSH-stdio transport)
phux worktree list [--json] # worktrees + their bound session and liveness
# (alias: ls)
phux worktree new BRANCH [--path P] [--from REF] [-s NAME] [--attach] [-- CMD...]
# git worktree add, then create the bound session
phux worktree open TARGET [--attach]
# ensure the bound session exists (idempotent)
phux worktree remove TARGET [--force]
# kill the bound session, then git worktree
# remove (alias: rm)
phux doctor [--json] # diagnose the install: config, socket path,
# server reachability, plugin manifests
phux completion SHELL # print a shell completion script on stdout
# (bash, elvish, fish, powershell, zsh);
# generated from this binary's own parser, so it
# never advertises a verb the build lacks
phux host enroll HOST [--role remote|satellite] [--name N]
[--endpoint HOST:PORT] [--quic-port P]
[--no-service] [--ssh-only] [--session N] [--json]
# set up a machine over ssh end to end
# (ADR-0055): confirm phux is installed there,
# install its service unit, mint a pairing
# token, and register the result in the
# role-correct registry, so
# `phux attach HOST` needs no flags afterwards
# (--role remote, the default). Falls back to
# an ssh:// entry when the host has nothing
# dialable
phux host <add|ls|rm> # one namespace over both machine registries
# (--role remote, the default, is what
# `phux attach NAME` resolves; --role
# satellite the peers a federation hub dials;
# aliases: list, remove). Formerly the
# separate `phux remote`, `phux satellite`,
# and top-level `phux enroll` verbs, absorbed
# into this one namespace (ADR-0066)
phux service <install|reconcile|uninstall|status|logs|prune-logs>
# per-user service unit (launchd LaunchAgent on
# macOS, systemd user unit on Linux) that keeps
# a server running across logout and reboot.
# `install --hub` persists federation hub mode;
# `install --restore` adds workspace save/restore;
# `reconcile` corrects an older unit's restart
# policy in place — nothing is stopped and no
# pane is lost, unlike a reinstall (ADR-0083)
phux --version # print version
phux help [COMMAND]
The agent-facing verbs — new, placed launch/spawn, ls, snapshot,
send-keys, paste, run, wait, watch, ask, resize, and the spatial verbs above — have
their JSON
contracts and exit-code semantics documented in agents.md;
this file does not restate them.
1.2 new / kill / rename ride the wire mechanism; UX is unchanged
new, kill, and rename no longer ride dedicated session/collection
L1 verbs. Per
ADR-0030
they decompose onto the substrate, with no change to what the user types:
newisSPAWN_TERMINALplus an L3 metadata write (phux.session.create/v1, read back viaphux.session.created/v1).renameis an L3 metadata SET onphux.session.name/v1.killof a whole group is the atomicKILL_TERMINALS { ids }L1 op (tag0x09), applied all-or-nothing under the server’s single lock so no observer sees a partial teardown.
The command words, flags, and output are exactly as before; only the wire path beneath them changed.
1.3 Headless spatial edits operate on existing panes
insert-pane, move-pane, and swap-pane edit persisted L3 layout envelopes;
they do not attach and do not change another client’s local focus. Every
positional selector must resolve to exactly one local pane. Satellite topology
edits are rejected. insert-pane and swap-pane require one session;
move-pane may cross sessions, re-parenting the live Terminal on L1 before
updating the source and destination envelopes.
insert-pane TARGET NEW_PANE [--split horizontal|vertical] [--ratio R] is
named for what it honestly does: NEW_PANE must already exist (for example
from phux spawn) and must not already be in the layout. It does not
implicitly spawn. --split is the same axis flag spawn and launch take
(h / v are accepted shorthands); omitted, it defaults to horizontal (a
horizontal divider, so panes are stacked), while --split vertical means a
vertical divider and side-by-side panes. The pre-unification boolean
--horizontal / --vertical spellings have been removed. R defaults to
0.5; ratios must be finite and strictly between zero and one, checked at
parse time. move-pane SOURCE TARGET accepts the same user-facing direction
and ratio flags. A cross-session move preserves the Terminal’s process, PTY, scrollback,
metadata, and id. swap-pane FIRST SECOND preserves
the existing split geometry. All three accept --json and --socket.
Detach (C-a d) remains an interactive TUI-only action because it acts on the
calling client’s attachment.
1.3.1 phux doctor composes the checks that already exist
Every check doctor runs already existed as its own verb: config check,
plugin validate, a socket-length guard buried in the spawn path, a
GET_STATE probe inside ls. Knowing to run all four, in the right order,
and how to read each one is precisely the knowledge someone debugging phux
does not have.
$ phux doctor
ok config ~/.config/phux/config.toml is valid
ok socket-path /run/user/1000/phux/phux.sock
warn server no server at /run/user/1000/phux/phux.sock
-> start one with `phux` (auto-spawns) or `phux server`
ok plugins 2 manifest(s) valid
no failures, 1 warning(s)
Three states, not two. A check that could not run reports warn, never
ok — a stopped server is a normal state, and rendering it green would be a
lie while rendering it red would train people to ignore red lines. Only
FAIL means verified-broken, and only FAIL sets the exit code to 1, so
phux doctor can gate a setup script without failing on a machine where
phux simply is not running yet.
Every non-passing check carries a next step. A diagnosis that names a problem without naming an action is half a diagnosis.
doctor is strictly read-only. A diagnostic that repairs things is one
nobody can trust to describe the system.
The socket-path check earns its place: an over-long path fails as a connect
that times out with no explanation, and nobody guesses sockaddr_un on
their own. A socket file with nothing behind it is reported as a failure
rather than a missing server, because every CLI verb will refuse until it is
cleared.
1.4 Worktrees bind to sessions by derived name
phux worktree composes git worktree with new / ls / kill
(ADR-0054). The server learns nothing about git and stores no worktree
state; the binding between a checkout and a session is a pure function of
the worktree path. The directory basename is sanitized — anything outside
[A-Za-z0-9._-] collapses to -, runs of - collapse to one, and
selector sigils (@, #, =, .) are trimmed from the edges — so
~/src/phux-feat-auth binds to the session phux-feat-auth. Because the
name is derived and never stored, it cannot go stale when git deletes a
worktree or an operator moves the directory.
phux worktree new feat/auth # git worktree add + create the session
phux worktree list # paths, branches, derived names, liveness
phux worktree open feat/auth # idempotent: create-if-absent, else report
phux worktree remove feat/auth # kill the session, then remove the worktree
new and open are headless by default and print the session name;
pass --attach for the interactive behavior. new puts the worktree beside
the repository as <repo>-<branch> unless --path says otherwise, checks
out an existing branch or creates a missing one (from --from, else the
current HEAD), and refuses when the derived name collides with another
worktree’s — pass -s NAME to disambiguate.
remove checks cleanliness before it kills anything, so a refusal has no
side effects, then kills the bound session and waits for it to leave the
snapshot before handing over to git. That ordering is not cosmetic: git
refuses to remove a worktree whose files are held open, and a shell sitting
in that directory holds it open. It refuses the worktree you are standing in.
The bound column distinguishes three states, not two: live (a session by
that name exists), - (it does not), and ? (no server is running, which is
a different fact from “no session”).
A session created by hand in a worktree under some other name is not
recognized as bound — list shows the worktree as unbound. Closing that gap
needs pane cwd in the session snapshot, which is a wire change ADR-0054
deliberately does not make.
Status (design intent, not shipped):
windows,panes, andmessagesare listed in earlier drafts as future read verbs; none ships today.configshipsinit/path/show/reload(§4.3);config editis design intent.
The target convention. The verbs that address an existing pane —
kill, snapshot, send-keys, paste, run, wait, watch, ask, and the
spatial verbs — take selectors as
positional TARGET (omitted on snapshot/wait to mean the
focused session, or on watch for server-wide events). attach likewise takes
its [SESSION] name
positionally. new is the exception: because its trailing [COMMAND...]
is a positional var-arg, the new session’s name is the -s/--session
flag instead, keeping the command words unambiguous. So: positional target
to act on something that exists; -s to name something you are creating.
Flags before the target. send-keys, run, wait, and ask take a
trailing var-arg (the keys / command / nothing), so every flag —
--json, --timeout, --until, --idle, --socket — MUST precede the
positional TARGET; anything after it is swallowed into the trailing
words. Each command’s --help calls this out.
Output hygiene (for scripts and agents). One-shot verbs print no
banner and keep stdout clean. With --json, stdout carries ONLY the JSON
document; diagnostics go to stderr with a nonzero exit, never interleaved
into the JSON. The agent-relevant JSON surfaces are new, launch, spawn,
ls, snapshot, run, wait, JSONL watch, ask, agent, the three
spatial verbs, tag, config show/plugins/agents/run, plugin, workspace,
and satellite. Their
per-verb JSON shapes and the stable exit-code semantics are owned by
agents.md §3–§4 — this file does not restate them.
3. Selectors
A selector identifies a session, window, or pane. Selectors appear in CLI arguments, keybinding actions, and hook arguments.
| Selector | Meaning |
|---|---|
. | current — the client’s focused pane/window/session |
name | session by name |
name:N | session name, window index N |
name:N.M | session name, window N, pane index M |
name:tag | session name, window whose name is tag |
@N | opaque ID (pane/window/session) — stable for the |
| server’s lifetime | |
= | attached TUI only: previous pane (C-a =) |
#tag | every Terminal carrying L3 tag tag |
The #tag form (ADR-0027) resolves to the set of Terminals tagged
tag, exactly as a session name resolves to many panes. Tags are L3
metadata (phux.tags/v1), read and written with phux tag:
phux tag add work:1.0 build ci # tag a pane
phux tag ls . # list the focused pane's tags
phux kill #build # kill every Terminal tagged 'build'
phux tag rm @7 ci # untag
Every tag action accepts --json (the document shape lives in
agents.md §4.17). One alias policy covers every list/remove
sub-registry: tag ls/tag list and tag rm/tag remove are the same
verbs, exactly as remote, worktree, and satellite answer to ls/rm
and plugin unlink to rm/remove. launch --list deliberately stays a
flag rather than becoming a launch ls subcommand: launch enumerates
integrations as a mode of one verb, it is not a registry with its own
subcommand tree (considered and kept).
Headless CLI and MCP calls have no attached client’s focus history, so an
explicit = target is rejected with an unsupported-selector error rather than
silently aliasing .. In the attached TUI, C-a = dispatches last-pane
against a one-entry, process-local MRU; repeating it toggles between two panes,
including panes in different windows. The MRU is neither persisted nor sent on
the wire, matching ADR-0019’s client-local focus rule and accepted ADR-0049.
Shared topology writers never acquire focus authority.
All headless commands otherwise share one grammar. kill, snapshot, wait,
watch, send-keys, paste, run, ask, launch/spawn placement, and the three
spatial verbs accept the same TARGET (phux-n95) and resolve it client-side
against a GET_STATE snapshot (ADR-0021) — the server never parses a
selector. A selector that names several panes (a whole session or window)
resolves to a single selected pane: the focused pane if it is among
the matches, else the first in snapshot order. So phux send-keys work …
targets the pane you are looking at in session work, while
phux send-keys work:1.0 … targets exactly window 1, pane 0. send-keys
and run route input to that resolved pane by id — no attach, no resize
(phux-3j3). Omit the target on snapshot/wait to default to the
focused session.
The CLI infers what kind of selector is expected from the command. When ambiguity matters, prefer the most specific form. Example:
phux kill work:edit.2 # second pane in window "edit" of session "work"
phux send-keys @42 "ls" Enter # send to the local pane with stable id 42
phux snapshot devbox/@7 # read satellite pane 7 through the hub
phux run work:1.0 "cargo test"# run in window 1, pane 0 of session "work"
phux kill . # kill the focused session
# `phux kill =` errors: headless clients have no focus MRU
Interactive TUI
Use phux interactively: understand the terminal model, navigate the first session, then open focused guides for commands and customization.
TUI configuration and keybindings
The reference TUI's consumer-facing product surface: subcommands, keybinds, status bar, layout, hooks, recording.