phux
guidesinteractive TUI

TUI commands and selectors

The reference TUI's consumer-facing product surface: subcommands, keybinds, status bar, layout, hooks, recording.

evolving document
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:

  • new is SPAWN_TERMINAL plus an L3 metadata write (phux.session.create/v1, read back via phux.session.created/v1).
  • rename is an L3 metadata SET on phux.session.name/v1.
  • kill of a whole group is the atomic KILL_TERMINALS { ids } L1 op (tag 0x09), 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, and messages are listed in earlier drafts as future read verbs; none ships today. config ships init / path / show / reload (§4.3); config edit is 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.

SelectorMeaning
.current — the client’s focused pane/window/session
namesession by name
name:Nsession name, window index N
name:N.Msession name, window N, pane index M
name:tagsession name, window whose name is tag
@Nopaque ID (pane/window/session) — stable for the
server’s lifetime
=attached TUI only: previous pane (C-a =)
#tagevery 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

View exact source