Skip to content

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.

sh
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 viewing

Diagnostic 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.
EndpointSelected byServes
A running UI--socket, ROZI_SOCKET, or discoveryEvery command.
A named session server--session <NAME> or --hereThe commands a server can answer without a screen.
sh
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 test

Both 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:

bash
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:

sh
rozi context --format json
rozi --here agents list --format json
rozi --here capture-pane
rozi --here split --argv cargo test

For 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:

  1. --socket PATH
  2. ROZI_SOCKET, while that UI is running
  3. From a session pane, the only UI showing the pane's session
  4. 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.

PlatformEndpoint named by ROZI_SOCKET
LinuxUnix-domain socket under $XDG_RUNTIME_DIR/rozi, /run/user/<uid>/rozi, or a private per-user temporary directory
macOSUnix-domain socket in rozi's private runtime directory
WindowsDiscovery 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:

text
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:

sh
rozi --session dev status working --target "$ROZI_PANE"

Target a pane explicitly when a script drives a pane it created:

sh
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" Enter

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

CommandPurpose--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 IDScroll 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 JSONGo 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] TEXTValidate, 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] -- TEXTAsk another agent without waiting; prints the request.yes
agents ask TARGET --wait --timeout DUR [--format text|json] -- TEXTAsk, then wait for the answer as agents await does.yes
agents reply ID --stdinAnswer a request sent to the calling agent with the text on stdin.yes
agents reply ID --laterKeep 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 IDCancel 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 TEXTLabel the current moment of the UI recording.no
record stop --uiStop 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
publishPublish 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 describePrint the API versions and capabilities of the installed binary.—
api schemaPrint the JSON Schema of the installed binary's control API.—
api commandsList 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:

json
{
  "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 statusMeaning
0The command succeeded.
1The command was invalid or refused, the reply had ok: false, a wait timed out, or a capture could not be written.
2No 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:

bash
cargo run --features schema-gen --bin rozi-api-schema

Two 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.json keeps its name while x-rozi-schema-version counts 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.

OptionEffect
--cwd DIRStart the pane in DIR.
--title TEXTSet the pane title.
--workspace 1-9Open the pane in that workspace.
--target IDSplit pane ID, in its workspace. Cannot go with --workspace.
--split-ratio RGive the new pane this share of the split it opens. Dwindle only.
--focusFocus the new pane.
--keep-openKeep the pane after its process exits.
--size COLSxROWSStart the pane at this terminal size, 1 to 1000 cells per side.
--format text|jsonPrint 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.

sh
rozi split --cwd "/repo with spaces" --title Tests --keep-open 'cargo test'
rozi split --workspace 3 --focus --argv cargo test -- --nocapture

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

sh
# 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.log

rozi 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:

sh
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 capture

The 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.
  • --focus is 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.

sh
rozi layout get --format json
rozi --session dev layout get --workspace 2 --format json

The report keeps two questions apart:

  • How the session is arranged. workspaces describes 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. client and each pane's view_rect describe 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.

json
{
  "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 }
  }
}
FieldMeaning
revisionThe 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.
canvasThe shared canvas that rect is measured against: the pane area of the client that last controlled the layout.
workspacesEvery 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.
layoutdwindle, master, grid, columns, rows, scrollable, or monocle.
master_ratioMaster workspaces only: the master pane's share of the width.
orderThe pane's position in the tiling order that every layout except Dwindle arranges panes in. Null for a floating pane.
pinnedWhether the pane is pinned. A pinned pane floats, is never fullscreen, and moves to each workspace the controlling client switches to.
rectWhere 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_fractionThe 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_ratioDwindle workspaces only: the pane's share of the split that directly holds it. Absent for a floating pane or a lone tile.
width_ratioScrollable workspaces only: the pane's column width as a fraction of the viewport.
view_rectWhere 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_panesSession endpoint only: panes the server runs that no layout places yet.
client.controllerWhether this UI holds layout control. A UI without a shared session controls its own layout.
client.committedFalse 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 rect can extend past the right edge and rect_fraction can exceed 1.0. The shared rect always starts the strip at its first column; view_rect shows 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: rect is where the pane returns to afterwards. view_rect covers the screen while it is fullscreen.
  • Followers: a client that follows another centres the controller's canvas in its own window, so its view_rect values 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.

sh
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 18

Every 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:

OptionEffect
--floating trueFloat 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 falseReturn the pane to the tiling, at the end of the tiling order.
--fullscreen true|falseMake 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,HPlace a floating pane, in canvas cells. X may be negative.
--rect-fraction X,Y,W,HPlace a floating pane, as fractions of the canvas.
--split-ratio RDwindle only: set the pane's share of the split that directly holds it.
--width-ratio RScrollable 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.

sh
rozi pane reveal --target 7

pane 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:

json
{ "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 with read-only.
  • A session endpoint fails with not-controller while any client holds layout control, and with unavailable for a session that has panes but no layout (the same case in which split is refused).
  • A scratch pane is client-local and has no shared layout, so pane set refuses it with unsupported.
  • 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:

json
{"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 --.

sh
rozi send-keys C-c
rozi send-keys 'echo hi' Enter
rozi send-keys --literal C-c
rozi send-keys -- -n hello

capture-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:

--renderResult
text (default)Plain text.
ansiThe 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.
pngAn image of the visible grid, in the pane's theme colors, with the cursor drawn and any images the program displayed.
spansThe 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.

sh
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:

sh
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
OptionWaits until
--wait-for TEXTTEXT appears within one row of the visible screen. It is matched literally, including spaces at the end of a row.
--settle DURATIONThe visible screen has not changed for DURATION.
--timeout DURATIONRequired 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-pane waits on the screen as it is. Text that is already showing answers at once, and the settle period starts when the request arrives.
  • send-text and send-keys wait 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|spans returns the screen once the wait resolves, with --scale, --output, and --format as for capture-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.

sh
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:

json
{
  "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. rows has one entry per row, and each run covers width columns from column x. A wide character counts two columns, so use x and width to 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, or dashed), reverse, and strikethrough.
  • Colors. A pane's colors stay symbolic rather than being resolved: an ANSI name such as red or bright-black, a 256-color index, or #rrggbb. The 16 ANSI colors are always names, however the program asked for them. palette says what the names, and the default foreground and background, look like in this capture, with the same theme colors a PNG uses. capture-ui colors are mostly #rrggbb, because the UI resolves them as it draws.
  • Cursor. Its position, whether it is shown, its shape (block, hollow-block, underline, or bar), 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: true means 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, visible lists, row by row, the column ranges still showing it. --image-pixels adds each image's pixels as a base64 PNG, with the covered parts transparent, so it shows no more than a png capture 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 ​

A rozi window captured with capture-ui: Neovim editing a Rust file, a shell showing the rozi logo with icat, and a shell that ran cargo run

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.

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

The 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 as branches
  • 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:

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

sh
rozi --session work invoke git-tools.run-tests --args-json '{"package":"rozi"}' --timeout 2m

Without --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 codeMeaning
action-not-foundNo action of that ID is deployed in this session.
invalid-argumentsThe arguments do not match the action's schema, or exceed 512 KiB.
invalid-resultThe action did not print one JSON value matching its schema within 512 KiB.
action-failedThe process could not start, exited with another status, or left a process holding stdout. Read the extension's log.
action-busyA singleton action is already running.
invocation-limitThe session is already running 8 calls.
invocation-depthActions are already nested 8 deep.
invocation-timeoutThe call reached its deadline.
invocation-cancelledThe call was cancelled, a redeploy changed its extension, it was undeployed, or the session was renamed.
invocation-not-foundinvocation cancel named a call that is not running.

Subscriptions ​

subscribe streams events as line-delimited JSON (NDJSON) until the endpoint closes:

sh
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:

sh
rozi --session dev subscribe pane-spawned pane-exited
rozi --remote host --session dev subscribe --snapshot
rozi --session dev subscribe --after 4a2b:42

Every line has a type:

typeMeaning
snapshotThe 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.
eventOne event: session_instance, seq, kind, and data, with the same fields except the client's own focused and self_controller.
laggedThe subscriber fell too far behind. The stream ends here; resume from the last seq you read.
endedThe 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 statusHow the stream stopped
0The session stopped cleanly (ended), or whatever was reading the output went away.
1The subscriber fell behind (lagged), or the subscription was refused.
2The 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:

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

json · sent as one line
{
  "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:

json · one message per line
{
  "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:

sh
url=$(rozi prompt --title 'Open URL' --placeholder 'https://example.com') || exit 0

Enter 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:

json
{"rows":[{"id":"job-1","title":"Run tests","status":"working","active":true}]}

Read activation requests from stdout:

json
{"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:

sh
rozi publish --kind agent

An 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 publish keeps a two-way stream open to a sidebar stream tab. Write complete snapshots of your rows to stdin:

json · sent as one line
{
  "rows": [
    { "id": "api", "label": "api", "tone": "working" },
    { "id": "api/eu", "parent": "api", "label": "eu-west-1" }
  ]
}

Read what the user does on stdout:

json
{"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.

sh
#!/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 text

The 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:

text
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
extension

An 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:

sh
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 dev

sessions list, sessions new --detach, sessions restart, sessions upgrade, and sessions kill also take --remote:

sh
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 workbox

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

MPL-2.0