Control CLI
This is the reference for the rozi commands that inspect and drive a running rozi: listing panes, typing into them, reading their output, rearranging the layout, and showing pickers and toasts. It is for anyone writing shell scripts, hooks, services, or extensions. For a gentler start, read Scripting; to write a client that speaks the wire format directly, see Control protocol.
rozi list-panes # the panes in the rozi you are running
rozi split --title Tests --argv cargo test # open a pane running a program
rozi --session dev capture-pane --target 3 # read a pane in a session nobody is viewingDiagnostic report
Run rozi doctor to print an aligned plain-text report for a bug report. It includes the rozi version, extension API and session protocol range, OS and architecture, install source, a recognized terminal name, configuration status and warning count, local session counts, SSH availability, and the installed extension count. Unknown terminal names appear as unknown. Inside a rozi pane, the terminal appears as rozi (inside a pane).
The report omits session names, file paths, hostnames, usernames, configuration contents, and raw environment values. It does not start or attach to a session or write files. Configuration warnings are counted in config_warnings without printing their text. The config field says valid, valid, 1 warning or valid, N warnings for a config loaded with warnings such as ignored keys, or rejected when the config could not be loaded. Use rozi doctor --config <PATH> or ROZI_CONFIG to check another config file. A report with rejected configuration still exits successfully.
Two endpoints
A control command talks to one of two places, called its endpoint:
- A running UI — the rozi window you are looking at. It can do everything, including moving focus and drawing pickers and toasts.
- A named session server — the background process that keeps a session's panes alive (see Sessions and clients). Select it with
--session <NAME>. It needs no window at all, so a detached session can be listed, captured, typed into, and given new panes from a cron job or an SSH login.
| Endpoint | Selected by | Serves |
|---|---|---|
| A running UI | --socket, ROZI_SOCKET, or discovery | Every command. |
| A named session server | --session <NAME> or --here | The commands a server can answer without a screen. |
rozi --session dev list-panes
rozi --session dev capture-pane --target 3
rozi --session dev send-keys --target 3 'cargo test' Enter
rozi --session dev split --argv cargo watch -x testBoth endpoints return the same {ok, code, data, error} document and the same tables, so a script reads one format either way. code appears on failures and is stable for automation.
A session endpoint gives a script no authority an attached client would not have. Opening a pane still needs layout control to be free (only one client at a time holds it; see Shared sessions), typing still respects the session's input lock, and a request made on behalf of an extension is refused (see Extensions and detached sessions).
The commands marked no in the command table need a UI. Against a session they fail with a message saying why: focus, the active workspace, toasts, pickers, actions, and capture-ui belong to a UI, and a session server has no screen.
--session and --socket name different endpoints and cannot be combined. A bare session name is a launch target, not a control target: rozi dev starts a UI, so rozi dev list-panes is refused and suggests --session dev instead.
Sessions on another machine
Add --remote <HOST> to reach a named session on another machine:
rozi --remote workbox --session dev list-panes
rozi --remote workbox --session dev agents prompt --target 3 --wait quiescent --timeout 10m "run the tests"Every command a local session answers, a remote one answers the same way, with the same output and the same exit status. The request is forwarded over SSH and the answer is rendered locally, so a remote command in a terminal prints the same table a local one does.
It uses the same SSH connection as remote attach: saved hosts, connection multiplexing, the discovered remote binary, and the same askpass rules. There is no daemon, network port, or account. --remote without --session is refused, because a control command addresses a session server and the far host's UI is not one.
agents wait and agents prompt --wait run on the remote session server and keep their own deadline; the SSH hop does not cut them short.
The remote host needs a rozi that supports forwarding. An older one is reported as a version mismatch rather than a broken command. Run rozi api describe there and look for remote-control in its capabilities.
Verify caller context
Run rozi context --format json to identify the session and pane the command runs in. The data object contains inside_rozi, status, session, pane, agent, and ui. A verified pane is a reference containing session_instance, pane_id, and generation. agent is the pane's current agent record when one can be selected.
status is verified, outside, incomplete, stale-session, or stale-pane. Missing caller information is outside; partial information is incomplete. A retired session instance, a server on another rozi version, or a pane that has exited is stale. A stale pane has no verified pane or agent.
ui.available says whether the caller's UI can be reached. When the caller's session is known, ui.displays_session and ui.holds_session distinguish the session on screen from an attachment held in the background. Those fields are null when unknown. A missing or dead ROZI_SOCKET does not prevent verification of a live caller session. session.terminal_engine and ui.terminal_engine name the terminal engine each one runs.
Put --here before a control command to target your own verified session, including a detached session or one the UI holds in the background:
rozi context --format json
rozi --here agents list --format json
rozi --here capture-pane
rozi --here split --argv cargo testFor splits, layout changes, pane close, and input, --here uses the caller's UI when that UI displays the verified session. It first tries ROZI_SOCKET. If that UI is unavailable or displays another session, it searches for exactly one live UI displaying the caller's session. This works after a UI replacement, for panes created while detached, and with UIs displaying other sessions. It preserves controller, input-lock, and read-only rules. A session switch before execution refuses the command. If no unique matching UI is found, it uses direct session control; layout control must then be free and input locks still apply. Other commands use direct session control.
UI authority is available only through a UI on the same host as the command. Inside a remotely attached pane, --here verifies and targets the remote session, but cannot borrow the local UI's controller or input-lock authority across SSH. If that authority is required and no usable UI is on the session host, it returns unsupported with guidance. Run the mutation through the UI's socket on the UI host, detach the controller for layout changes, or unlock input for sends and prompts. Read operations and mutations allowed by direct session control still work.
--here refuses absent or stale caller identity. It cannot be combined with --session, --socket, or --remote. It supports the same commands as --session. For commands with an optional pane target, including agents report and agents release, an omitted --target means the calling pane. Explicit targets name panes in the caller's session. Commands without --here keep the usual UI selection rules.
Endpoint discovery
Without --session or --here, control commands choose a UI endpoint in this order:
--socket PATHROZI_SOCKET, while that UI is running- From a session pane, the only UI showing the pane's session
- The only live control endpoint in the runtime directory
Discovery fails if the runtime directory contains no live endpoints, or more than one and none of them is the only UI showing the calling pane's session. Pass --socket when several UIs are running. Put --socket PATH before the command.
status, agents report, and agents release are the exception. Run from a pane with no ROZI_SOCKET — a remote pane, or one in a session with no window attached — and without --target, they go to the session server the pane runs in, which ROZI_SESSION_INSTANCE identifies. The session keeps the latest state, so every window attached to it shows it, including one on another machine and one that attaches later.
| Platform | Endpoint named by ROZI_SOCKET |
|---|---|
| Linux | Unix-domain socket under $XDG_RUNTIME_DIR/rozi, /run/user/<uid>/rozi, or a private per-user temporary directory |
| macOS | Unix-domain socket in rozi's private runtime directory |
| Windows | Discovery entry under %LOCALAPPDATA%\rozi\run for a current-user named pipe |
On Windows, pass the discovery-entry path to the CLI. Do not read the entry or construct a pipe name yourself.
Every pane receives ROZI=1, ROZI_PANE, ROZI_TERMINAL=1, TERM_PROGRAM=rozi, and TERM_PROGRAM_VERSION, and a local pane also receives ROZI_SOCKET and ROZI_BIN when control is available. A session pane also receives ROZI_SESSION_INSTANCE, an opaque id of the session server it runs in, so a UI holding several sessions can tell whose pane is asking. It is empty in a scratch or popup pane. Remote panes do not receive the local client's ROZI_SOCKET or ROZI_BIN, and neither does a pane opened by rozi --session <NAME> split or restored after a restart, because there is no UI for them to name. Commands from such a pane reach the UI showing its session once one attaches, its statuses and agent reports reach the session without one, and rozi --here reaches the session either way.
Target selection
Commands that accept --target use it first.
Against a UI endpoint, the CLI otherwise sends the calling pane's ROZI_PANE, and rozi falls back to the focused pane. Without --target, status, agents report, and agents release from a pane of a session the UI keeps in the background apply to the calling pane in its own session, while another session is on screen. status --target names a pane of the session on screen.
Against a session endpoint, ROZI_PANE is ignored. A pane id does not say which session it belongs to, so a script in pane 3 of work running rozi --session dev send-text … would otherwise type into pane 3 of dev. A session endpoint uses --target, or the only pane of a one-pane session, and otherwise fails with the ids to choose from:
session `dev` has 3 panes and no focused pane; pass --target (ids: 1, 2, 5)agents report and agents release always require --target with --session, even for a one-pane session. Without --session in a pane that has no UI to name, they target the calling pane in its own session; see Endpoint discovery.
A pane addressing its own session names itself:
rozi --session dev status working --target "$ROZI_PANE"Target a pane explicitly when a script drives a pane it created:
ROZI_CMD=${ROZI_BIN:-rozi}
pane=$("$ROZI_CMD" split --argv bash | jq -r '.data.id')
"$ROZI_CMD" send-text --target "$pane" 'printf "ready\n"'
"$ROZI_CMD" send-keys --target "$pane" EnterInput sent while a pane's process is starting is queued. Input to an exited or failed process is rejected.
Commands
The --session column says whether the command also works against a session server with no UI attached.
| Command | Purpose | --session |
|---|---|---|
context [--format text|json] | Verify caller identity and report UI availability. | caller only |
list-panes [--scope current|attached] [--format text|json] | List panes visible to this endpoint. attached needs a UI. | yes |
layout get [--workspace 1-9] [--format text|json] | Report workspaces and where each pane sits. | yes |
layout set --workspace 1-9 [<LAYOUT>] [--master-ratio R] [--if-revision N] | Set a workspace's tiling layout, its master share, or both. | yes |
pane set --target ID [--floating B] [--fullscreen B] [--rect X,Y,W,H | --rect-fraction X,Y,W,H] [--split-ratio R | --width-ratio R] [--if-revision N] | Float, tile, place, size, or fullscreen a pane. | yes |
pane move --target ID --workspace 1-9 [--if-revision N] | Move a pane to another workspace. | yes |
pane swap --target ID --with ID [--if-revision N] | Exchange two tiled panes. | yes |
pane close --target ID [--if-revision N] | Close a pane without asking. | yes |
pane reveal --target ID | Scroll a Scrollable strip to a pane without focusing it. | no |
agents list [--format text|json] | List effective agent runtimes and exact references. | yes |
agents list --all [--format text|json] | List every agent in every session and on every connected host. | no |
agents go --location JSON | Go to an agent wherever it runs, by a location from agents list --all. | no |
agents get TARGET [--format text|json] | Read one semantic agent record. | yes |
agents read TARGET [--scrollback N|full] [--format text|json] | Capture an agent's terminal. | yes |
agents wait TARGET --until STATE --timeout DURATION [--format text|json] | Wait atomically for semantic state. | yes |
agents prompt TARGET [--wait STATE --timeout DURATION] [--capture text|ansi] [--result] [--allow-working] [--format text|json] TEXT | Validate, submit, and optionally wait atomically. | yes |
agents response TARGET [--format text|json] | Read the latest retained agent response, including its source and fidelity. | yes |
agents ask TARGET [--for ID] [--format text|json] -- TEXT | Ask another agent without waiting; prints the request. | yes |
agents ask TARGET --wait --timeout DUR [--format text|json] -- TEXT | Ask, then wait for the answer as agents await does. | yes |
agents reply ID --stdin | Answer a request sent to the calling agent with the text on stdin. | yes |
agents reply ID --later | Keep a delivered request open for a reply after the calling agent's turn ends. | yes |
agents await ID --timeout DURATION [--format text|json] | Wait until a request is answered, failed, or cancelled, and print its answer. | yes |
agents request ID [--format text|json] | Show one request. | yes |
agents requests [--incoming|--outgoing] [--format text|json] | List the session's requests, or those to or from the calling agent. | yes |
agents cancel ID | Cancel a request you asked. | yes |
agents report --agent ID --integration TOKEN --state STATE --seq N [--reason TEXT] [--native-session ID] [--unless-quiet STATE [--ended]] [--target ID] | Publish incarnation- and sequence-fenced native agent state. | yes |
agents release --integration TOKEN --seq N [--target ID] | Release integration authority. | yes |
metrics [--format text|json] | Read bounded client and cached server resource counters. | yes |
focus <PANE_ID> | Focus a pane. | no |
send-text [--target <PANE_ID>] [WAIT] [--capture text|ansi|png|spans] <TEXT> | Send literal UTF-8 text. | yes |
send-keys [--target <PANE_ID>] [-l|--literal] [WAIT] [--capture text|ansi|png|spans] [--] <KEY|TEXT>... | Send named keys and text. | yes |
split [OPTIONS] [COMMAND | --argv PROGRAM [ARG...]] | Open a pane. | yes |
run-action <ACTION_ID> | Run a built-in, configured, or extension command ID. | no |
invoke <ACTION> [--args-json JSON|-] [--timeout DUR] [--format text|json] | Call a deployed action and print its result. | yes |
actions list [--json|--format text|json] | List a session's deployed actions, with their schemas in JSON. | yes |
invocation list [--format text|json] | List the action calls running in the session. | yes |
invocation cancel <INVOCATION_ID> | Stop a running action call. | yes |
capture-pane [--target ID] [--scrollback N|full] [--last-output] [--render text|ansi|png|spans] [--scale 1-3] [--image-pixels] [WAIT] [--output FILE] [--format text|json] | Capture a pane as text, ANSI, PNG, or styled runs. | yes |
capture-ui [--render text|ansi|png|spans] [--scale 1-3] [--image-pixels] [--output FILE] [--format text|json] | Capture the whole UI as it is drawn. | no |
record start [pane] [--target ID] [--output FILE [--force]] [--max-fps N] [--duration DUR] [--max-bytes SIZE] | Start recording a pane. | yes |
record pane --target ID [--output FILE] [OPTIONS] | Record a pane until Ctrl+C. | only |
record list [--format text|json] | List running recordings. | yes |
record mark TEXT [--id ID | --target ID] | Label the current moment of running recordings. | yes |
record stop [--id ID | --target ID] | Stop recordings once their files are complete. | yes |
record start ui [--output FILE [--force]] [--max-fps N] [--duration DUR] [--max-bytes SIZE] [--hide-indicator] | Record the UI as it paints, chrome included. | no |
record mark --ui TEXT | Label the current moment of the UI recording. | no |
record stop --ui | Stop the UI recording once its file is complete. | no |
record export FILE --to png-frames DIR | --to video-frames DIR [--scale 1-3] | --to cast OUT [--force] | Export a recording. | — |
record play FILE [--speed N] [--from MARK|TIME] | Replay a recording in this terminal. | — |
switch-workspace <1-9> | Switch the active workspace. | no |
move-to-workspace <1-9> | Move the focused pane. | no |
status [--target <PANE_ID>] <VALUE> [--reason TEXT] | Report status for a pane. | yes |
status --clear [--target <PANE_ID>] | Clear reported status. | yes |
notify <MESSAGE> [--title TEXT] [--level info|error] | Notify a UI or a named session. | yes |
show-command <ID>, hide-command <ID> | Show or hide one of the calling extension's commands in the command palette. | no |
subscribe [EVENT...] [--snapshot] [--after INSTANCE:SEQ] | Stream events as NDJSON. An empty list subscribes to all events. --snapshot and --after need a session endpoint. | yes |
pick [--title TEXT] [--placeholder TEXT] [--json] | Open a modal picker using stdin and stdout. | no |
prompt --title TEXT [--placeholder TEXT] [--value TEXT] [--masked] [--json] | Ask for a line of text in a modal prompt. | no |
publish | Publish Activity rows over stdin and receive activations on stdout. --kind agent declares the rows agents; --name tells apart several publishers in one pane. | no |
sidebar publish --tab TAB [--name NAME] | Publish rows to a sidebar stream tab over stdin and receive what the user does with them on stdout. | no |
extensions status [--json] [--session <NAME>] | Report where the UI runs placed extension processes; with --session, report the extensions that session holds. | no |
extensions deploy <ID> --session <NAME> [--remote HOST] | Store a verified copy of an installed extension in a session, which keeps it across detach and restart. | — |
extensions undeploy <ID> --session <NAME> [--remote HOST] | Remove a session's copy of an extension. | — |
ports list [--format text|json] | List active and failed client-owned SSH port forwards in the running UI. | no |
ports forward <PORT> [--remote HOST] [--host HOST] [--local-port PORT] [--format text|json] | Open or reuse a client-owned SSH port forward onto 127.0.0.1 in the running UI. | no |
ports close <PORT> [--remote HOST] [--host HOST] [--format text|json] | Close a client-owned SSH port forward in the running UI. | no |
api describe | Print the API versions and capabilities of the installed binary. | — |
api schema | Print the JSON Schema of the installed binary's control API. | — |
api commands | List the installed binary's control commands. | — |
api command <ID> | Describe one control command: its request, what its reply carries, and the definitions they use. | — |
layout set and the pane commands also accept --format text|json. WAIT is [--wait-for TEXT] [--settle DURATION] --timeout DURATION; see Wait for output.
In the agents rows, TARGET is --target ID (a pane id) or --ref JSON (an exact agent reference from agents list); a bare pane id is refused. agents wait and agents prompt run inside the session server. Against a UI, they are passed to the session on screen, or to the session a --ref names. See Inspect and wait for agents for states, references, and integration reports.
The record commands that record run inside the session server. Against a UI, record start, stop, list, and mark are passed to the session it is attached to, and act on the pane the command runs in unless given --target or --id. record pane needs --session. record start ui, record mark --ui, and record stop --ui record the UI itself, which writes the file on its own machine. record export and record play read a file and take no endpoint. See Record a pane or the UI.
Control commands reject the launch-only options --read-only and --pick. --config PATH selects the configuration for split; other control commands reject it. Select a session with --here or --session <NAME>, optionally qualified by --remote <HOST> for a named session.
Check the installed API
rozi api describe prints the release, control API version, schema version, the range of session protocol generations, and capabilities of the installed binary. It does not connect to a UI or session:
{
"release": "0.0.44",
"api": 1,
"schema": 6,
"session_protocol": 52,
"session_protocol_min": 52,
"capabilities": [
"agent-prompt-results",
"agent-requests",
"agent-responses",
"agent-waits",
"attached-control",
"attached-session-scope",
"caller-context",
"capture-render",
"capture-scale",
"capture-spans",
"capture-ui",
"capture-wait",
"command-visibility",
"detached-session-create",
"extension-link-handlers",
"extension-state-dir",
"global-agents",
"layout-control",
"pane-control",
"pane-reveal",
"pick-prompt-from-query",
"pick-query-events",
"prompt",
"publish-kind",
"published-activity",
"record-pane",
"record-ui",
"remote-control",
"remote-extension-runtime",
"runtime-events",
"session-control",
"session-deployments",
"session-no-resurrect",
"session-notifications",
"session-services",
"sidebar-streams",
"sidebar-tab-settings",
"split-placement",
"split-size"
]
}schema is the version of the JSON schema and changes independently of session_protocol, which only concerns how two rozi binaries talk to each other. Two binaries can share a session when each one's session_protocol falls inside the other's range, from session_protocol_min to session_protocol.
detached-session-create means rozi sessions new <NAME> --detach starts a session in the background without opening a UI. An older binary refuses --detach.
session-no-resurrect means sessions new <NAME> --detach accepts --no-resurrect. An older binary refuses it.
session-notifications means notify accepts a session endpoint and its subscription snapshots include a retained notification inbox.
session-actions means a session server runs deployed actions: it accepts invoke, actions list, invocation list, and invocation cancel.
session-services means explicitly deployed placement = "session" services run without a UI and use credentials issued by that session.
session-deployments means a session server accepts extensions deploy, extensions undeploy, and extensions status --session: it keeps its own verified copy of an extension. An older session server refuses them, and rozi extensions deploy reports that the session cannot hold deployments.
extension-link-handlers means installed extensions can contribute link handlers through their manifest.
extension-state-dir means extension processes receive a state directory in ROZI_EXTENSION_STATE_DIR, and ROZI_SESSION_NAME. A host needs it to run placed contributions.
publish-kind means publish accepts --kind and --name: published rows are activities unless declared agents, and several publishers can share a pane. An older binary treats every published row as an agent and keeps one publisher per pane.
remote-extension-runtime means the binary can run placed extension processes, as a client and as a host. Both need it.
pick-prompt-from-query means a picker action's prompt honors from_query. An older binary ignores it and starts the prompt from value.
pick-query-events means a picker honors report_query and reports its search text. An older binary ignores it and reports nothing.
prompt means the UI answers prompt. An older binary refuses it as an unknown command.
runtime-events means subscribe and hooks accept the agent-state-changed and foreground-changed events. An older binary refuses a subscription that names either.
attached-session-scope means list-panes accepts --scope attached and extension manifests accept session_scope. An older binary refuses the option and rejects a manifest that uses the field.
sidebar-tab-settings means extension manifests accept a setting's table form and settings on a sidebar tab. An older binary rejects a manifest that uses either.
Output and exit status
list-panes, layout get, metrics, capture-pane, capture-ui, record list, and extensions status print human-readable output to a terminal and stable JSON when redirected. Use --format text or --format json to choose explicitly, or --json for extensions status. extensions deploy prints its last chunk's report the same way.
Other successful one-shot commands print a short acknowledgement on a terminal and the JSON response when redirected. A failure prints its error to stderr; with JSON output, the failure document is also written to stdout.
| Exit status | Meaning |
|---|---|
0 | The command succeeded. |
1 | The command was invalid or refused, the reply had ok: false, a wait timed out, or a capture could not be written. |
2 | No endpoint was reached: discovery failed, the connection failed, the reply was empty or not JSON, or a PNG was about to be written to a terminal. |
pick and prompt have their own statuses; see Pickers and Prompts. So does a session subscription.
list-panes describes only the endpoint that answered. From a UI, it includes the current attachment and client-local scratch panes, not every named session. From --session, it includes every pane in that session, including panes whose process has exited, which report exited (<CODE>) instead of ready. Use rozi sessions list to discover session servers.
--scope attached asks a UI for the panes of every session it has attached, on screen or not, and its client-local scratch panes. A pane's on_screen says whether its session is the one showing. Pane IDs repeat across sessions, and a command naming a pane by ID reaches the session on screen.
Human-readable help and reports use rozi's palette when written to a terminal; redirected output stays plain. NO_COLOR, CLICOLOR=0, and TERM=dumb disable styling. CLICOLOR_FORCE enables it for a consumer that renders ANSI color from a pipe. JSON, publish and subscribe streams, and version output are never styled. The one-shot detach summary uses the same palette when rozi restores the terminal.
JSON schema
Every shape on this page is described by docs/schema/rozi-control-v1.schema.json: requests, the response envelope, error codes, each command's data payload, published activity rows, agent records and references, and the event envelope.
The schema is generated from the source, so it always matches the binary it ships with. rozi api schema prints that binary's copy, without connecting to a UI or session. rozi api commands lists its commands by the cmd each is sent as, and rozi api command <ID> gives one command's request schema, the payload names its reply carries in reply ([] marks an array, and an empty list means no named payload), and every definition those use. Contributors regenerate it with:
cargo run --features schema-gen --bin rozi-api-schemaTwo conventions matter when you validate against it:
- Objects accept unknown properties. Responses gain fields over time, and a client validating against an older copy keeps working. Do not reject a document for carrying a field you do not recognize.
- Enumerations are closed. Error codes, agent states, wait conditions, and event names are fixed vocabularies. A new value is an API change and moves the schema version. The file name follows the control API version, so
rozi-control-v1.schema.jsonkeeps its name whilex-rozi-schema-versioncounts its revisions.
ControlResponse.data is untyped in the envelope, because one envelope carries every command's answer. The schema names each payload separately — PaneInfo, AgentInfo, PaneCapture, AgentWaitResult, and the rest — so pick the one for the command you sent.
Open a pane
A successful shared pane spawn includes its workspace placement. Closing the requesting client after success leaves the pane in that workspace for other clients and later attachments.
split opens a pane. It leaves focus unchanged unless --focus is present. For a local endpoint, the directory defaults to the configured cwd, or the calling shell's directory when cwd is unset. Relative --cwd paths resolve from the calling shell's directory. With --remote, paths resolve on the remote host.
| Option | Effect |
|---|---|
--cwd DIR | Start the pane in DIR. |
--title TEXT | Set the pane title. |
--workspace 1-9 | Open the pane in that workspace. |
--target ID | Split pane ID, in its workspace. Cannot go with --workspace. |
--split-ratio R | Give the new pane this share of the split it opens. Dwindle only. |
--focus | Focus the new pane. |
--keep-open | Keep the pane after its process exits. |
--size COLSxROWS | Start the pane at this terminal size, 1 to 1000 cells per side. |
--format text|json | Print the reply as text or JSON. The default is JSON when piped. |
--argv PROGRAM [ARG...] | Run a program directly, without a shell. |
A positional COMMAND is interpreted by the configured command_shell. --argv launches a program directly and consumes all remaining arguments, so every pane option must come before it.
rozi split --cwd "/repo with spaces" --title Tests --keep-open 'cargo test'
rozi split --workspace 3 --focus --argv cargo test -- --nocaptureThe reply waits up to five seconds for the pane's process to be ready. pty_ready: false means the pane exists but has not reported ready yet.
Choose where a pane opens
split divides a pane the same way splitting it by hand does. In a Dwindle workspace the pane is split along its longer side, so a wide pane gets a neighbour to its right and a tall one gets a neighbour below; see Dwindle. Master, Grid, Columns, Rows, Scrollable, and Monocle workspaces add the new pane at the end of their order instead.
From a UI endpoint, split divides the pane the command runs in, or the focused pane when it runs outside rozi. With --workspace, it divides that workspace's focused pane. --target ID divides pane ID instead and opens the new pane in that pane's workspace. The split direction still follows the pane's shape, and focus stays where it was.
--split-ratio R gives the new pane that share of the split, from 0.2 to 0.8, in the same layout change that adds it. It is the share pane set --split-ratio sets. It works only when the new pane lands in a Dwindle workspace beside another tiled pane. Otherwise split fails with unsupported and opens nothing.
--target and --split-ratio need a session that already has panes. Before any session is attached, split opens its pane alone in a new session, so it refuses both. While the scratchpad is open, split refuses to divide a pane hidden behind it.
# Open a narrow log pane beside the editor in pane 4
rozi split --target 4 --split-ratio 0.3 --argv tail -f /var/log/app.logrozi api describe lists split-placement when the binary supports --target and --split-ratio. An older binary ignores both.
Choose the size of a pane
A pane takes the size of the tile a UI draws it in. A pane nobody is looking at — one in a workspace that is not on screen, or one in a detached session — has no tile, so it keeps the size it started at. Without --size, that is 120×32 in a UI, or the size of the session's other panes in a detached session.
--size sets that starting size, including sizes larger than any window, so a script can capture a program's whole screen at once:
rozi sessions new capture --detach --no-resurrect
pane=$(rozi --session capture split --size 100x220 --argv my-tui | jq -r '.data.id')
rozi --session capture capture-pane --target "$pane" --settle 500ms --timeout 10s
rozi sessions kill captureThe size holds until a UI draws the pane. Opening it in the workspace on screen, or switching to its workspace later, resizes it to its tile like any other pane.
Open a pane in a detached session
With --session, the server places the pane itself, so a client attaching later finds it already in the layout. [[rules]] apply exactly as they do to a pane a person opens: a rule may float the pane, make it fullscreen, and choose its workspace, and an explicit --workspace still wins over the rule. Without either, the pane lands in workspace 1. With no focus to follow, the new pane is added at the end of the workspace's tiling, and the split ratios already set survive. --target splits the pane it names, as it does through a UI.
[[rules]] and the configured shell are read from the server's config at the moment of the split, so an edited rule applies to the next one without restarting the session.
A session endpoint refuses split when:
- Another client holds layout control. Detach it, or open the pane from that client. Reading and typing do not need layout control and keep working.
--focusis given. There is no focus to move.- The session has an invalid layout. Restore a valid session snapshot before adding panes.
A pane opened this way receives ROZI, ROZI_PANE, ROZI_SESSION_INSTANCE, ROZI_TERMINAL, TERM_PROGRAM, and TERM_PROGRAM_VERSION. It does not get ROZI_SOCKET or ROZI_BIN, since there is no UI, or anything [environment] forward adds from the short-lived CLI process. It has no SSH variables such as SSH_CONNECTION, even when an SSH connection started the session, except that a pane opened with --here gets the calling pane's SSH_CONNECTION. On Linux with a systemd user session, it gets the desktop variables, such as DISPLAY and WAYLAND_DISPLAY, of the desktop running on that machine at that moment. With no running desktop, it has none.
Layout
layout get reports every workspace and where each of its panes sits. --workspace N narrows the report to one workspace.
rozi layout get --format json
rozi --session dev layout get --workspace 2 --format jsonThe report keeps two questions apart:
- How the session is arranged.
workspacesdescribes the session's shared layout. The session server owns it and every client follows it, so both endpoints give the same answer. - What one UI shows.
clientand each pane'sview_rectdescribe a single UI's screen: its focus, the workspace it shows, and where it draws each pane. They appear only when a UI answered.
Each accepted change to the shared layout gets a new, increasing revision number. Scripts use it to detect changes made by someone else (see Changing the layout). A client that follows another client's layout reports the layout it last received; the controlling client reports the one it commits.
{
"session": "dev",
"revision": 18,
"canvas": { "cols": 160, "rows": 47 },
"workspaces": [
{
"index": 1,
"name": null,
"layout": "dwindle",
"synchronized": false,
"panes": [
{
"id": 7,
"reference": { "session_instance": "…", "pane_id": 7, "generation": 1 },
"order": 0,
"floating": false,
"pinned": false,
"fullscreen": false,
"rect": { "x": 0, "y": 0, "width": 96, "height": 47 },
"rect_fraction": { "x": 0.0, "y": 0.0, "width": 0.6, "height": 1.0 },
"view_rect": { "x": 0, "y": 1, "width": 95, "height": 46 }
}
]
}
],
"client": {
"active_workspace": 1,
"focused_pane": 7,
"controller": true,
"committed": true,
"viewport": { "cols": 160, "rows": 48 }
}
}| Field | Meaning |
|---|---|
revision | The layout revision described. Null until something places a pane. A controlling UI first sends any change it is still holding back, and reports the revision that change will have. |
canvas | The shared canvas that rect is measured against: the pane area of the client that last controlled the layout. |
workspaces | Every workspace in the layout, including empty ones, so a script can see a workspace's layout before using it. index is one-based. A UI always reports all nine. A layout the server started for a headless split holds only the workspaces it placed panes in; the others take each client's configured default layout. |
layout | dwindle, master, grid, columns, rows, scrollable, or monocle. |
master_ratio | Master workspaces only: the master pane's share of the width. |
order | The pane's position in the tiling order that every layout except Dwindle arranges panes in. Null for a floating pane. |
pinned | Whether the pane is pinned. A pinned pane floats, is never fullscreen, and moves to each workspace the controlling client switches to. |
rect | Where the pane sits on the shared canvas, in whole cells. Gaps, borders, and the workbar are left out, because each client draws those differently. |
rect_fraction | The pane's position as fractions of the canvas, rounded to six decimal places. Floating panes are stored this way, so their fractions are exact. |
split_ratio | Dwindle workspaces only: the pane's share of the split that directly holds it. Absent for a floating pane or a lone tile. |
width_ratio | Scrollable workspaces only: the pane's column width as a fraction of the viewport. |
view_rect | Where this UI draws the pane, in cells of its own terminal, gaps and chrome included. Only for panes in the workspace the UI shows. |
unplaced_panes | Session endpoint only: panes the server runs that no layout places yet. |
client.controller | Whether this UI holds layout control. A UI without a shared session controls its own layout. |
client.committed | False until the server has confirmed revision. A controlling UI sends a change without waiting for the confirmation. |
Tiled panes are listed in tiling order, then floating panes. Some geometry needs care:
- Scrollable: the strip of columns can be wider than the canvas, so a
rectcan extend past the right edge andrect_fractioncan exceed1.0. The sharedrectalways starts the strip at its first column;view_rectshows where this UI has scrolled it to follow focus. - Monocle: every tiled pane has the same
rect. The focused one is drawn on top. - Fullscreen:
rectis where the pane returns to afterwards.view_rectcovers the screen while it is fullscreen. - Followers: a client that follows another centres the controller's canvas in its own window, so its
view_rectvalues can start at a negative position or run past its edges.
Changing the layout
layout set chooses a workspace's tiling layout. pane set floats, tiles, places, sizes, or fullscreens one pane. pane move, pane swap, and pane close move a pane to another workspace, exchange two panes, and close one.
rozi layout set --workspace 2 master
rozi pane set --target 7 --floating true --rect 10,5,80,24
rozi pane set --target 7 --fullscreen true
rozi --session dev pane set --target 3 --floating false --if-revision 18Every write names what it changes: layout set needs --workspace, and every pane command needs --target. Neither falls back to focus or ROZI_PANE, and neither moves focus.
A write sets a state rather than toggling it, and options you leave out keep their current value. Repeating a write, or asking for a state that already holds, succeeds with changed: false and creates no new revision. A refused write changes nothing.
pane set accepts:
| Option | Effect |
|---|---|
--floating true | Float the pane. A tiled pane lifts off centred on the tile it leaves, at the default float size of 42% of the canvas, unless you also pass a rect. A pane that already floats stays where it is. |
--floating false | Return the pane to the tiling, at the end of the tiling order. |
--fullscreen true|false | Make the pane fullscreen, or restore it. A workspace has at most one fullscreen pane, so making one fullscreen restores any other. |
--rect X,Y,W,H | Place a floating pane, in canvas cells. X may be negative. |
--rect-fraction X,Y,W,H | Place a floating pane, as fractions of the canvas. |
--split-ratio R | Dwindle only: set the pane's share of the split that directly holds it. |
--width-ratio R | Scrollable only: set the pane's column width as a fraction of the viewport. |
layout set --master-ratio R sets the master pane's share of a Master workspace. Combine it with the layout name (layout set --workspace 2 master --master-ratio 0.6) or pass it alone for a workspace that is already Master.
Each ratio works only with the layout that uses it. Using a ratio with another layout, on a floating pane, or on a lone Dwindle tile fails with unsupported. Ratios run from 0.2 to 0.8, the same limits as dragging a divider; a value outside that range fails with invalid-argument. A ratio cannot be combined with --floating or a rect in one request; float or re-tile the pane first. Sizes change at once instead of animating.
A rect places a floating pane, so it needs a pane that already floats or --floating true. Rects are clamped the same way a dragged float is: part of the pane may leave the canvas, but a margin always stays on screen to grab. The float lands on whole cells.
pane move --workspace N puts the pane at the end of workspace N: last in its tiling order when tiled, at the same rect when floating. A pinned pane is unpinned. A fullscreen pane stays fullscreen and restores any fullscreen pane already in N. The view does not follow the pane. If the pane had focus, focus moves to another pane in the workspace it left, as if the pane had closed. Moving a pane to the workspace it is already in is changed: false.
pane swap --with ID exchanges two tiled panes in one workspace: each takes the other's tile and position in the tiling order. Anything else, including a floating pane or panes in different workspaces, fails with invalid-argument.
pane reveal scrolls the pane's Scrollable strip so the pane is in view, without focusing it. The strip scrolls just far enough, as when you focus a partly hidden column. It changes only this UI's view, so it needs no layout control, creates no revision, and --if-revision does not apply. On a workspace that is not on screen, the pane is shown when you switch to it. The reply is { "changed": bool, "workspace": N }; changed is false when the pane was already in view, floats, or sits in a layout that shows every tile. Focus is unchanged, so the strip scrolls back to the focused pane when it next scrolls to follow focus, such as when you type into it or move focus.
rozi pane reveal --target 7pane close ends the pane's process and removes it from the layout. The request is the confirmation, so [confirm] is not consulted. It replies with the closed id, the new revision, committed, and the workspace the pane left. A session endpoint can also close a pane its layout does not place; workspace is then absent and no revision is written. Closing a pane that does not exist fails with pane-not-found.
Every other write replies with the same shape from both endpoints:
{ "changed": true, "revision": 19, "committed": false, "workspace": { "index": 1, "…": "…" } }workspace is the affected workspace in the shape layout get reports it. revision is the revision the layout has with the change applied. A session endpoint commits the change itself, so committed is always true there. A UI answers before the server confirms its change; if the server rejects it, the UI takes the server's layout back.
--if-revision N refuses the write with conflict unless the layout is still at revision N. Read the revision with layout get, decide, then write with --if-revision, so that a change someone made in the meantime is not overwritten. Each successful write's reply gives the revision to pass to the next one.
A write needs the same layout authority as a person rearranging panes:
- A UI must hold layout control. A follower fails with
not-controller, and a read-only UI withread-only. - A session endpoint fails with
not-controllerwhile any client holds layout control, and withunavailablefor a session that has panes but no layout (the same case in whichsplitis refused). - A scratch pane is client-local and has no shared layout, so
pane setrefuses it withunsupported. - A pane the layout does not place fails with
pane-not-found.
Watching the layout
A UI's subscribe stream raises layout-changed whenever the server accepts a layout revision and this UI has received it:
{"event":"layout-changed","data":{"revision":"19","author":"self"}}author is self for this UI's own change, client for another client's, and server for a change made through a session endpoint. The event fires only for accepted revisions: not for a change the server rejects, not for animation frames, and not before the server confirms. A script driving a detached session subscribes to the session itself (see Session subscriptions); there author is client or server, with the writing client's ID in author_client (0 for the server).
Sending keys and capturing output
send-keys recognizes tmux-style names including C-c, M-x, Enter, Escape, Space, Tab, BSpace, arrows, Home, End, PgUp, PgDn, and F1 through F12. Unknown tokens are sent as literal text. --literal makes every token literal. -- ends option parsing. --target must come before the first key; the wait and --capture options may come anywhere before --.
rozi send-keys C-c
rozi send-keys 'echo hi' Enter
rozi send-keys --literal C-c
rozi send-keys -- -n hellocapture-pane returns the visible grid by default, without the spaces that pad each row to the pane's width. --scrollback N returns the last N retained lines, --scrollback full returns all retained lines, and --last-output returns the output of the most recent command, using shell integration. A full-scrollback reply can exceed 1 MiB; the CLI accepts replies of that size.
--render picks the form of the capture:
--render | Result |
|---|---|
text (default) | Plain text. |
ansi | The visible grid as text with SGR color and style sequences. Every row keeps the pane's width and ends with a reset; there is no cursor movement or screen clearing, so cat shows it in place. |
png | An image of the visible grid, in the pane's theme colors, with the cursor drawn and any images the program displayed. |
spans | The visible grid as JSON: each row's runs of text with their colors and attributes, the cursor, and any images. See Read colors and styles as JSON. |
ansi, png, and spans cover the visible screen only. Combining them with --scrollback or --last-output fails rather than dropping the styling.
Images a program displayed with the Kitty graphics protocol, such as kitty icat output, are included. A PNG draws their pixels, scaled into the cells they occupy. Text has no pixels, so ansi shows each such cell as a ▀ half block in the image's colors, and text shows the ▀ characters alone, marking where an image is.
--output FILE writes the capture itself to FILE and prints nothing, for any --render; for spans, that is the frame's JSON. It cannot be combined with --format. Without --output, a PNG goes to stdout as raw bytes, and rozi refuses to write it to a terminal. --format json still returns the JSON envelope, with the image base64-encoded.
rozi capture-pane --target 3 --render png --output pane.png
rozi capture-pane --target 3 --render png > pane.png
rozi capture-pane --target 3 --render ansi --format text | less -R--scale 2 or --scale 3 draws a PNG two or three times larger, with sharper text and images, for screenshots people will look at: a 120x36 UI is 960x576 pixels at scale 1 and 1920x1152 at scale 2. The default, scale 1, suits agents, because vision models shrink large images anyway. --scale works with --render png only.
A PNG uses the theme colors a UI gave the pane; a session whose panes no UI has ever shown renders with default terminal colors. Text uses installed fonts, including CJK, color emoji, and Nerd Font symbols when a font on that machine has them. With --session, the session server renders the image with the fonts installed where it runs. A session reply must fit in 8 MiB, so a larger capture fails with message-too-large; the UI endpoint has no such limit.
Wait for output
A send can wait for the program to answer, and a capture can wait for the screen to show something, so a script never has to guess how long to sleep:
rozi send-keys --target 3 'cargo test' Enter --wait-for 'test result:' --timeout 10m --capture text
rozi capture-pane --target 3 --wait-for '$ ' --timeout 5s
rozi capture-pane --target 3 --settle 500ms --timeout 30s --render png --output pane.png| Option | Waits until |
|---|---|
--wait-for TEXT | TEXT appears within one row of the visible screen. It is matched literally, including spaces at the end of a row. |
--settle DURATION | The visible screen has not changed for DURATION. |
--timeout DURATION | Required with either option, counted from when the request arrives. At most one hour. |
Durations are written 500ms, 30s, or 2m; a bare number is seconds. With both --wait-for and --settle, rozi waits for the text first, then for the screen to stay unchanged.
The two commands wait differently:
capture-panewaits on the screen as it is. Text that is already showing answers at once, and the settle period starts when the request arrives.send-textandsend-keyswait for the answer to their own input. Only output that arrives after the input counts, so a prompt or a previous result already on screen does not satisfy--wait-for, even after it scrolls up. The settle period starts once the session has confirmed the input and rozi has taken the screen as it stood right after it, not when the request arrives. Over a remote attachment that can be measurably later than the write itself.--capture text|ansi|png|spansreturns the screen once the wait resolves, with--scale,--output, and--formatas forcapture-pane. Without--capture, the reply only says the wait resolved.
"Changed" means the characters, colors, or styles on screen changed. A program that redraws the same screen, moves only the cursor, or changes only its title counts as settled. A screen scrolled back into history still waits on the live screen.
A wait that has not resolved by its timeout fails with timeout, even if the text shows up or the settle period ends a moment later. A pane that exits or closes first fails with pane-not-running; output it printed just before exiting still counts. Unless the pane is gone, the reply carries the capture as the screen stood at the end, which the CLI prints (or writes to --output) before exiting 1.
Waits work against a UI, a session, and a remote session. A remote wait keeps its SSH connection open until it resolves.
Read colors and styles as JSON
--render spans returns the visible grid as a JSON frame, for a script or an agent that needs to know what is red, bold, or selected without looking at an image. Each row is a handful of runs rather than one entry per cell, so a frame stays small: a shell pane is about a kilobyte, and a 120x36 UI with one shell about 11 KB.
rozi capture-pane --target 3 --render spans --format text | jq '.rows[0]'
rozi capture-pane --target 3 --render spans --output pane.json
rozi capture-ui --render spans --format text--format text, or a terminal, prints the frame itself on one line; --format json wraps it in the reply as data.frame. A frame looks like this, shortened:
{
"format": "rozi-spans",
"version": 2,
"width": 40,
"height": 3,
"palette": {"foreground": "#cdd6f4", "background": "#1e1e2e", "ansi": ["#45475a", "#f38ba8", "…"]},
"cursor": {"x": 2, "y": 1, "visible": true, "shape": "block", "blinking": true},
"rows": [
[{"x": 0, "width": 2, "text": "$ "}, {"x": 2, "width": 5, "text": "error", "fg": "red", "bold": true}],
[{"x": 0, "width": 2, "text": "$ "}],
[]
]
}- Rows and runs.
rowshas one entry per row, and each run coverswidthcolumns from columnx. A wide character counts two columns, so usexandwidthto find a column rather than counting characters. Blank cells in default colors at the end of a row are left out, and a blank row is[]. - Only what differs. A run lists only the colors and attributes that differ from the default:
fg,bg,underline_color,bold,dim,italic,underline(single,double,curly,dotted, ordashed),reverse, andstrikethrough. - Colors. A pane's colors stay symbolic rather than being resolved: an ANSI name such as
redorbright-black, a 256-color index, or#rrggbb. The 16 ANSI colors are always names, however the program asked for them.palettesays what the names, and the default foreground and background, look like in this capture, with the same theme colors a PNG uses.capture-uicolors are mostly#rrggbb, because the UI resolves them as it draws. - Cursor. Its position, whether it is shown, its shape (
block,hollow-block,underline, orbar), whether it blinks, and its color when the program set one. A pane's cursor is a blinking block until its program asks for another. - Images. Each image a program displayed lists the cells it is laid out over and its size in pixels.
fill_cell_box: truemeans its pixels fill those cells, preserving terminal tile coverage even when a screenshot uses a different font. The cells under an image hold▀half blocks in its colors, so the runs there are a coarse copy of the picture, not text. When something covers part of an image,visiblelists, row by row, the column ranges still showing it.--image-pixelsadds each image's pixels as a base64 PNG, with the covered parts transparent, so it shows no more than apngcapture does. A session reply with a large image can then exceed 8 MiB.
The frame has its own version, which changes only when a field changes meaning or is removed. New fields can appear at any time, so ignore fields you do not know. --scale does not apply, and --image-pixels works with --render spans only. The protocol reference lists every field.
Capturing the whole UI

The image above is rozi capture-ui --render png --scale 2. capture-ui captures what the UI is showing: the bar, pane borders and titles, overlays and toasts, and every visible pane, at the size of the terminal rozi runs in. It takes the same --render, --scale, --image-pixels, --output, and --format options as capture-pane, with the same rules.
rozi capture-ui --render png --output ui.png
rozi capture-ui --render ansi --format text | less -R
rozi capture-ui --format json | jq -r .data.textThe capture is the next frame the UI draws. rozi draws it immediately for the request, so an idle UI answers too, and simultaneous requests share that frame. Colors match the screen: the theme's background and text colors fill cells a program left at its terminal defaults. Images in panes are included as for capture-pane, and anything drawn over one, such as an overlay or a floating pane, covers it in the capture as it does on screen. A dimmed backdrop behind a modal does not dim the image parts that remain visible.
capture-ui needs a UI; --session is refused because a session server draws nothing. Capture its panes one at a time with capture-pane instead. The reply reports the frame's width and height in cells alongside the capture.
To save a PNG by hand instead, use the Screenshot pane and Screenshot UI commands; see Take a screenshot.
Actions, status, and notifications
run-action accepts:
- built-in action IDs, such as
toggle-float - IDs from
[[commands]], such asbranches - extension command IDs, such as
git-tools.branches
Destructive actions honor [confirm].
status reports a short free-form value for a pane. working, blocked, done, error, and idle have built-in presentation. A status is a generic activity: it appears in Activity, but does not make the pane an agent. In a pane where rozi detects an agent, it sets that agent's state. An agent integration reports through agents report instead. Values are limited to 64 characters and reasons to 256, after display-text sanitization. --target may come before or after the value, and is the only way to name a pane from a script that is not running inside one. The update is queued to the session server, so a successful reply does not guarantee that every client has drawn it yet.
Use notify for failures, and for successful results that would otherwise go unseen:
rozi notify "tests failed" --title Build --level error
rozi --session work notify "background build finished"A notification sent to a session is stored there and raises session-notification. Every attached UI presents live notifications. A late attachment receives the retained inbox, which holds at most 64 notifications and 256 KiB of message and title text while that server runs. A single message and title together may contain at most 8192 bytes. Restarting the server clears this inbox.
Read state belongs to each UI. Displaying a notification in one UI does not hide it from another. Reconnecting to the same session instance avoids repeating notifications already displayed by that UI; starting a new UI presents the retained inbox again. Session subscription snapshots include notifications, with each item's id, level, title, message, and event seq.
show-command and hide-command work only from an extension's own command or service, for a command its manifest declares. See Show a command only when it applies.
Call an action
invoke calls an action an extension deployed to a session. It prints the result on a terminal; piped, it prints the JSON envelope with the result in data.result. run-action runs a command the way a person would and answers nothing; invoke is for a caller that needs the result. actions list --format json shows what a session offers, with each action's schemas.
rozi --session work invoke git-tools.run-tests --args-json '{"package":"rozi"}' --timeout 2mWithout --args-json the arguments are {}, and --args-json - reads them from standard input, which suits arguments too long for a command line. --timeout can shorten the action's deadline but not lengthen it. Inside a session's pane, invoke, actions list, and invocation without --session go to that session, whether or not a UI is attached. From a script on the UI's machine they go to the session on screen. Extension processes started by a UI cannot call actions through it: deploy the caller as a session service.
A read-only client can list calls but cannot start or cancel them. Closing the connection cancels the call, including a call made through a UI.
| Error code | Meaning |
|---|---|
action-not-found | No action of that ID is deployed in this session. |
invalid-arguments | The arguments do not match the action's schema, or exceed 512 KiB. |
invalid-result | The action did not print one JSON value matching its schema within 512 KiB. |
action-failed | The process could not start, exited with another status, or left a process holding stdout. Read the extension's log. |
action-busy | A singleton action is already running. |
invocation-limit | The session is already running 8 calls. |
invocation-depth | Actions are already nested 8 deep. |
invocation-timeout | The call reached its deadline. |
invocation-cancelled | The call was cancelled, a redeploy changed its extension, it was undeployed, or the session was renamed. |
invocation-not-found | invocation cancel named a call that is not running. |
Subscriptions
subscribe streams events as line-delimited JSON (NDJSON) until the endpoint closes:
rozi subscribe pane-exited pane-status-changed |
jq -r 'select(.event == "pane-status-changed") | [.data.pane, .data.status] | @tsv'When run against a UI endpoint, every event has event and data; event fields are under data. See Hooks for the event names and fields.
Session subscriptions
A script or service can subscribe to a session, local or remote, with no UI attached:
rozi --session dev subscribe pane-spawned pane-exited
rozi --remote host --session dev subscribe --snapshot
rozi --session dev subscribe --after 4a2b:42Every line has a type:
type | Meaning |
|---|---|
snapshot | The session's current state, sent first with --snapshot: session_instance, session, seq, controller, client_count, layout_rev, layout, and panes. The next event has the sequence number after its seq. Nothing is lost or repeated between them. |
event | One event: session_instance, seq, kind, and data, with the same fields except the client's own focused and self_controller. |
lagged | The subscriber fell too far behind. The stream ends here; resume from the last seq you read. |
ended | The session stopped cleanly. The stream ends here. |
A session raises pane-spawned, pane-exited, pane-status-changed, pane-title-changed, pane-cwd-changed, agent-state-changed, foreground-changed, bell, layout-changed, session-renamed, controller-changed, client-joined, and client-left. Naming any other event is refused, because it describes one UI and only that UI can raise it. A pane's pane-spawned comes before any other event about it, and pane-exited after every other one, so treat a pane's agents as gone once it has exited. Panes a session restores when it starts are in --snapshot, and raise no pane-spawned.
--after INSTANCE:SEQ replays the retained events after that point and carries on live. The session keeps its most recent 1024 events, up to 1 MiB. A cursor it cannot honour, because the event after it is gone, the sequence number is ahead of the session, or the session has restarted under a new instance, fails with cursor-expired and never falls back to a snapshot. As with other commands, the failure is on stderr, and redirected stdout also gets the failure document, whose data holds current_instance, oldest_seq, and latest_seq. Start again with --snapshot.
The exit status says whether the stream did what it promised, so a supervisor can act on it without reading the records:
| Exit status | How the stream stopped |
|---|---|
0 | The session stopped cleanly (ended), or whatever was reading the output went away. |
1 | The subscriber fell behind (lagged), or the subscription was refused. |
2 | The stream was cut off: the session crashed, the connection was lost, or it could not be reached. |
A service with the default restart = "on-failure" therefore restarts after 1 or 2 and stays stopped after 0. A plain restart carries on live and skips what it missed, so a subscriber that cannot miss events saves the last seq it read and restarts with --after. In a pipeline such as rozi --session dev subscribe | jq ., use set -o pipefail to see this status.
Pickers
In plain mode, pick reads one label per input line and prints the selected label:
branch=$(git branch --format='%(refname:short)' | rozi pick --title Branch) || exit 0
git switch -- "$branch"Selection exits 0, cancellation exits 1, and a lost connection exits 2.
Use --json for stable row IDs, descriptions, groups, disabled or active rows, custom actions, prompts, empty-list text, tabs, and live updates. The first input line is picker metadata and may contain initial rows. Each later line replaces the complete row set.
{
"title": "Branches",
"rows": [
{ "id": "main", "label": "main", "active": true },
{ "id": "old", "label": "old", "disabled": "protected" }
]
}{"title":"Branches","rows":[{"id":"main","label":"main","active":true},{"id":"old","label":"old","disabled":"protected"}]}
JSON mode prints selection, cancellation, and action objects. An action without close: true keeps the picker open so the producer can send refreshed rows. empty is the text shown for an empty row list while the filter is empty; a filter that matches nothing always says No matches. prompt may be a title string or an object with title, placeholder, value, from_query, and masked; from_query starts the prompt with the text typed in the search field.
Declare tabs to show several related lists in one picker, with tabs below the search field. Each tab keeps its own rows, filter, and highlight; Tab/Shift+Tab or Right/Left switch between them. Row snapshots name their tab, and JSON mode prints {"tab":"…"} on each switch so the producer can load a tab when it is first shown:
{
"title": "Git",
"tabs": [
{ "id": "branches", "label": "Branches" },
{ "id": "worktrees", "label": "Worktrees" }
],
"rows": [{ "id": "main", "label": "main" }]
}
{
"tab": "worktrees",
"rows": [{ "id": "/src/rozi-review", "label": "rozi-review" }]
}{"title":"Git","tabs":[{"id":"branches","label":"Branches"},{"id":"worktrees","label":"Worktrees"}],"rows":[{"id":"main","label":"main"}]}
{"tab":"worktrees","rows":[{"id":"/src/rozi-review","label":"rozi-review"}]}
Selections and actions from a tabbed picker carry the tab: {"selected":"main","tab":"branches"}.
A list longer than the picker's 512 rows can follow the search field: with "report_query":true, JSON mode prints {"query":"…","query_seq":N} each time the text changes, and the producer sends the rows that match it, echoing query_seq so rozi can drop an answer to text that has since changed. See Picker protocol.
Prompts
prompt asks for one line of text, with no list to choose from, and prints what was entered:
url=$(rozi prompt --title 'Open URL' --placeholder 'https://example.com') || exit 0Enter submits and exits 0, Esc exits 1, and a lost connection exits 2. The text is printed as entered, even when empty. --value fills the field to start with, and --masked hides what is typed on screen; the printed text is still plain. --json prints the reply object instead: {"input":"…"} or {"cancelled":true}. A prompt from another host names that host in its title. See Prompt protocol.
Published activity
publish keeps a two-way stream open. Write complete row snapshots to stdin:
{"rows":[{"id":"job-1","title":"Run tests","status":"working","active":true}]}Read activation requests from stdout:
{"activate":"job-1"}An empty row list or a closed stream withdraws the rows. Use stable IDs. A process with ROZI_PANE publishes for that pane. A supervised service has no pane, so rozi uses the focused live pane when the stream opens. A stream opened by an extension closes when rozi reloads its configuration or disables the extension.
Published rows are activities by default: they appear in Activity, but not in the Agents view or rozi agents, and they do not keep the host awake. A program whose rows are agents, such as a coding-agent client listing its sessions, declares it:
rozi publish --kind agentAn agent publisher's rows are the pane's agents, and rozi derives a detected agent's displayed state from them instead of from its screen. An activity publisher never changes the pane's agent.
Several publishers can share a pane, and each owns only its own rows. One caller that runs more than one publisher in a pane gives each a --name; a publisher restarted with the same name replaces its own earlier stream. See Several publishers in one pane.
See Published activity protocol and Sidebar.
Sidebar streams
sidebar publish keeps a two-way stream open to a sidebar stream tab. Write complete snapshots of your rows to stdin:
{
"rows": [
{ "id": "api", "label": "api", "tone": "working" },
{ "id": "api/eu", "parent": "api", "label": "eu-west-1" }
]
}{"rows":[{"id":"api","label":"api","tone":"working"},{"id":"api/eu","parent":"api","label":"eu-west-1"}]}
Read what the user does on stdout:
{"visible":true}
{"activate":"api/eu"}An extension process names its own tab by its local name; any other program names a stream tab from config.toml. Several programs can publish to one tab, each owning only its own rows, and a program that exits takes its rows with it. See the Sidebar stream protocol for every field, actions, and events.
Drive a detached session
--session <NAME> reaches the session server directly. Nothing has to be attached, and nothing becomes attached: the request is answered and the connection closes, so the session's client count and layout control are untouched and a script cannot make an empty session look occupied.
#!/bin/sh
# Start a build in a session nobody is looking at, then read the result back.
pane=$(rozi --session dev split 'cargo test' | jq -r '.data.id')
until rozi --session dev list-panes --format json |
jq -e --argjson p "$pane" '.data[] | select(.id == $p and (.status | startswith("exited")))' \
>/dev/null; do
sleep 2
done
rozi --session dev capture-pane --target "$pane" --scrollback full --format textThe session must already exist. Start one in the background with rozi sessions new dev --detach, or leave a detached rozi dev running.
rozi --session dev subscribe streams the session's events with no UI attached (see Session subscriptions). Focus and workspace events describe one UI, so only that UI's own subscribe reports them. agents wait waits inside the server, without polling.
Extensions and detached sessions
When ROZI_EXTENSION is set, the CLI marks every request as coming from that extension, stamped with a token the running UI renews on each config reload. The UI checks that token, so leftover processes from a disabled or reloaded extension stop being obeyed.
A session server cannot make that check, so it refuses such requests:
a session server cannot check whether extension `git-tools` is still active, and will not act on
its behalf; reach a running rozi instead, or clear ROZI_EXTENSION when the caller is not the
extensionAn extension that wants to drive a session should go through a running UI. A person typing in a pane that an extension opened inherits ROZI_EXTENSION and hits the same refusal; run env -u ROZI_EXTENSION rozi --session … to make the request as yourself.
A placed extension process reaches the UI that placed it through its runtime, wherever it runs. Its requests carry a credential that ties them to the host and generation it was started for, and are limited to what a placed process may do: commands that would run something on the client fail with not-permitted, and commands about a session outside the process's host fail with out-of-scope.
Session lifecycle
These commands manage sessions rather than control a UI:
rozi dev
rozi sessions attach dev
rozi sessions attach dev --read-only
rozi sessions new dev
rozi sessions new review --profile dev
rozi sessions new api --cwd ~/src/api
rozi sessions new scratch --detach
rozi sessions new tests --detach --no-resurrect
rozi sessions list
rozi sessions restart dev
rozi sessions upgrade dev
rozi sessions kill devsessions list, sessions new --detach, sessions restart, sessions upgrade, and sessions kill also take --remote:
rozi sessions list --remote workbox
rozi sessions new scratch --detach --remote workbox
rozi sessions restart dev --remote workbox
rozi sessions upgrade dev --remote workbox
rozi sessions kill dev --remote workboxTo control a session on another machine, see Sessions on another machine. Git checkouts have their own rozi worktrees namespace; see Use worktrees from the command line.
See Sessions and Remote sessions.