diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-16 23:43:10 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-16 23:43:10 +0300 |
| commit | 8ee3a7bdc03f435175d9c5abecfa4e2b5e295264 (patch) | |
| tree | 82baa4deeafb8152989dfc735511b05841c1d6da /docs/next/SOCKET_API.md | |
| parent | 79be292911cea4f617c1994fc4b8fd022f85cab5 (diff) | |
docs: move next-release docs out of .pi
Diffstat (limited to 'docs/next/SOCKET_API.md')
| -rw-r--r-- | docs/next/SOCKET_API.md | 1048 |
1 files changed, 1048 insertions, 0 deletions
diff --git a/docs/next/SOCKET_API.md b/docs/next/SOCKET_API.md new file mode 100644 index 00000000..783d6e97 --- /dev/null +++ b/docs/next/SOCKET_API.md @@ -0,0 +1,1048 @@ +# herdr socket api + +herdr exposes a local unix socket api for scripts, tools, and coding agents that want to control a running herdr instance or subscribe to events. + +if you are teaching an agent that is already running inside herdr, start with [`SKILL.md`](./SKILL.md). use this document when you want the direct protocol, or when you want the cli wrapper reference for the commands that sit on top of it. + +## choose your integration layer + +there are three practical ways to integrate with herdr: + +- **agent skill** — [`SKILL.md`](./SKILL.md). best when an agent inside herdr just needs to learn the workflow quickly. +- **cli wrappers** — `herdr server stop`, `herdr workspace ...`, `herdr tab ...`, `herdr pane ...`, `herdr wait ...`. best for shell scripts and simple orchestration. +- **raw socket api** — best when you want direct request/response control or long-lived event subscriptions. + +these layers are intentionally stacked on top of the same control surface. + +important difference: `pane.run` and `wait agent-status` are **cli conveniences**, not raw socket methods. + +## transport + +- transport: unix domain socket +- encoding: newline-delimited json +- request/response: send one json request per line, read one json response per line +- subscriptions: send `events.subscribe`, receive an ack, then keep the same connection open and continue reading pushed events + +named sessions are runtime/socket namespaces, not replacements for herdr workspaces. each named session has its own server sockets and persistent runtime state while config remains global. + +socket path resolution order: + +1. explicit `herdr --session <name>`: + `$XDG_CONFIG_HOME/herdr/sessions/<name>/herdr.sock` or `$HOME/.config/herdr/sessions/<name>/herdr.sock` +2. `HERDR_SOCKET_PATH` +3. `HERDR_SESSION=<name>`: + `$XDG_CONFIG_HOME/herdr/sessions/<name>/herdr.sock` or `$HOME/.config/herdr/sessions/<name>/herdr.sock` +4. default session path: + `$XDG_CONFIG_HOME/herdr/herdr.sock` or `$HOME/.config/herdr/herdr.sock` + +this means `HERDR_SOCKET_PATH` remains an exact low-level socket override, but an explicit cli `--session <name>` still wins when a command runs inside a pane that inherited `HERDR_SOCKET_PATH`. + +session names may contain ASCII letters, numbers, `.`, `_`, and `-`. `default` is reserved for the default session. use `herdr session list`, `herdr session attach <name>`, `herdr session stop <name>`, and `herdr session delete <name>` to inspect and manage session namespaces. session commands print human-readable output by default; pass `--json` for machine-readable output. `session delete` refuses running sessions and does not delete the default session. + +## request and response envelopes + +all socket requests use this envelope: + +```json +{ + "id": "req_1", + "method": "ping", + "params": {} +} +``` + +successful responses look like: + +```json +{ + "id": "req_1", + "result": { + "type": "pong", + "version": "0.1.2", + "protocol": 2 + } +} +``` + +errors look like: + +```json +{ + "id": "req_1", + "error": { + "code": "pane_not_found", + "message": "pane 1-99 not found" + } +} +``` + +## ids and numbering + +workspace ids are opaque, stable ids like: + +- `w64e95948145ed1` +- `w64e95948146a82` + +pane ids are workspace-scoped and stable across workspace reorder: + +- `w64e95948145ed1-1` +- `w64e95948145ed1-2` +- `w64e95948146a82-1` + +that means: + +- workspace id = stable workspace identity +- pane number = compact pane number within that workspace + +workspace ids are durable for the life of the workspace and survive display reordering. pane numbers are still compact public numbers, so if a pane closes, higher pane numbers in that same workspace compact down. + +tabs are first-class socket api objects now. + +- tab ids look like `w64e95948145ed1:1`, `w64e95948145ed1:2` +- workspace id = stable workspace identity +- tab number = tab number within that workspace +- pane ids still stay workspace-scoped like `w64e95948145ed1-2` rather than becoming `workspace-tab-pane` triples + +for backward compatibility, requests also accept the older positional forms like `1`, `1:2`, and `1-2` as shorthand for the current session order. responses use the stable ids. + +## core objects + +`workspace_info` responses contain objects like: + +```json +{ + "workspace_id": "w64e95948145ed1", + "number": 1, + "label": "herdr", + "focused": true, + "pane_count": 1, + "tab_count": 1, + "active_tab_id": "w64e95948145ed1:1", + "agent_status": "unknown" +} +``` + +`tab_info` responses contain objects like: + +```json +{ + "tab_id": "w64e95948145ed1:1", + "workspace_id": "w64e95948145ed1", + "number": 1, + "label": "1", + "focused": true, + "pane_count": 1, + "agent_status": "unknown" +} +``` + +`pane_info` responses contain objects like: + +```json +{ + "pane_id": "w64e95948145ed1-1", + "workspace_id": "w64e95948145ed1", + "tab_id": "w64e95948145ed1:1", + "focused": true, + "cwd": "/home/can/Projects/herdr", + "label": "reviewer", + "agent": "pi", + "agent_status": "working", + "custom_status": "indexing", + "revision": 0 +} +``` + +`label` is an optional manual pane name set through `pane.rename`. + +`agent` is an optional display label string. + +- when herdr detects a built-in agent, this is that built-in name like `pi` or `claude` +- when a hook or plugin reports a custom agent through `pane.report_agent`, this can be any non-empty label like `hermes` +- when no agent identity is known, it is omitted + +`custom_status` is an optional hook-owned display label. It is present only while hook authority is active. It does not change `agent_status`, `done` semantics, waits, notifications, or workspace/tab rollup priority. + +`pane_read` responses contain objects like: + +```json +{ + "pane_id": "w64e95948145ed1-1", + "workspace_id": "w64e95948145ed1", + "tab_id": "w64e95948145ed1:1", + "source": "recent", + "format": "text", + "text": "...", + "revision": 0, + "truncated": false +} +``` + +`agent_status` is the public agent field: + +- `idle` +- `working` +- `blocked` +- `done` +- `unknown` + +`done` means the agent has finished, but you have not looked at that finished pane yet. + +## methods at a glance + +| method | purpose | success result type | +|---|---|---| +| `ping` | health check / version | `pong` | +| `server.stop` | gracefully stop the running background server | `ok` | +| `workspace.list` | list workspaces | `workspace_list` | +| `workspace.get` | inspect one workspace | `workspace_info` | +| `workspace.create` | create a workspace | `workspace_info` | +| `workspace.focus` | focus a workspace | `workspace_info` | +| `workspace.rename` | rename a workspace | `workspace_info` | +| `workspace.close` | close a workspace | `ok` | +| `tab.list` | list tabs, optionally filtered by workspace | `tab_list` | +| `tab.get` | inspect one tab | `tab_info` | +| `tab.create` | create a tab in a workspace | `tab_info` | +| `tab.focus` | focus a tab | `tab_info` | +| `tab.rename` | rename a tab | `tab_info` | +| `tab.close` | close a tab | `ok` | +| `pane.list` | list panes, optionally filtered by workspace | `pane_list` | +| `pane.get` | inspect one pane | `pane_info` | +| `pane.rename` | set or clear a manual pane label | `pane_info` | +| `pane.read` | read pane output | `pane_read` | +| `pane.split` | split a pane and create a sibling pane | `pane_info` | +| `pane.send_text` | send literal text without Enter | `ok` | +| `pane.send_keys` | send keypresses like `Enter` | `ok` | +| `pane.send_input` | send literal text plus keypresses in order | `ok` | +| `pane.report_agent` | report hook-authoritative agent label and state for a pane | `ok` | +| `pane.clear_agent_authority` | clear hook-authoritative agent state for a pane | `ok` | +| `pane.release_agent` | release a pane from the reported agent back to shell state | `ok` | +| `pane.close` | close a pane | `ok` | +| `pane.wait_for_output` | one-shot blocking wait for text | `output_matched` | +| `events.subscribe` | start a long-lived subscription stream | `subscription_started` ack | + +### `server.stop` + +request: + +```json +{ + "id": "req_stop", + "method": "server.stop", + "params": {} +} +``` + +returns `ok` and asks the running background server to shut down cleanly. + +this is the explicit server-level shutdown path for persistence mode. normal in-app quit actions detach the current client instead of sending this request. + +## workspace methods + +### `workspace.list` + +request: + +```json +{ + "id": "req_list", + "method": "workspace.list", + "params": {} +} +``` + +returns `workspace_list` with zero or more workspace objects. + +### `workspace.get` + +params: + +```json +{ + "workspace_id": "1" +} +``` + +returns `workspace_info` for one workspace. + +### `workspace.create` + +params: + +```json +{ + "cwd": "/home/can/Projects/herdr", + "focus": true +} +``` + +notes: + +- `cwd` is optional +- if `cwd` is omitted, herdr uses its current working directory and falls back to `/` if needed +- `focus` is optional in raw socket requests and defaults to `false` +- the cli wrapper also defaults to no focus; pass `--focus` to switch to the new workspace + +example response: + +```json +{ + "id": "req_create", + "result": { + "type": "workspace_info", + "workspace": { + "workspace_id": "1", + "number": 1, + "label": "herdr", + "focused": true, + "pane_count": 1, + "tab_count": 1, + "active_tab_id": "1:1", + "agent_status": "unknown" + } + } +} +``` + +### `workspace.focus` + +params: + +```json +{ + "workspace_id": "1" +} +``` + +returns the focused workspace as `workspace_info`. + +### `workspace.rename` + +params: + +```json +{ + "workspace_id": "1", + "label": "api" +} +``` + +returns updated `workspace_info`. + +### `workspace.close` + +params: + +```json +{ + "workspace_id": "1" +} +``` + +returns: + +```json +{ + "id": "req_close", + "result": { + "type": "ok" + } +} +``` + +## tab methods + +### `tab.list` + +request with no filter: + +```json +{ + "id": "req_tabs", + "method": "tab.list", + "params": {} +} +``` + +request filtered to one workspace: + +```json +{ + "id": "req_tabs_ws", + "method": "tab.list", + "params": { + "workspace_id": "1" + } +} +``` + +returns `tab_list`. + +### `tab.get` + +params: + +```json +{ + "tab_id": "1:2" +} +``` + +returns `tab_info`. + +### `tab.create` + +params: + +```json +{ + "workspace_id": "1", + "cwd": "/home/can/Projects/herdr", + "focus": true +} +``` + +notes: + +- `workspace_id` is optional and defaults to the active workspace +- `cwd` is optional; if omitted, herdr uses the focused pane cwd in that workspace when available +- `focus` is optional in raw socket requests and defaults to `false` +- the cli wrapper also defaults to no focus; pass `--focus` to switch to the new tab + +returns `tab_info` for the new tab. + +### `tab.focus` + +params: + +```json +{ + "tab_id": "1:2" +} +``` + +returns focused `tab_info`. + +### `tab.rename` + +params: + +```json +{ + "tab_id": "1:2", + "label": "logs" +} +``` + +returns updated `tab_info`. + +### `tab.close` + +params: + +```json +{ + "tab_id": "1:2" +} +``` + +returns `ok`. the last tab in a workspace cannot be closed. + +## pane methods + +### `pane.list` + +request with no filter: + +```json +{ + "id": "req_panes", + "method": "pane.list", + "params": {} +} +``` + +request filtered to one workspace: + +```json +{ + "id": "req_panes_ws", + "method": "pane.list", + "params": { + "workspace_id": "1" + } +} +``` + +returns `pane_list`. + +### `pane.get` + +params: + +```json +{ + "pane_id": "1-1" +} +``` + +returns `pane_info`. + +### `pane.rename` + +params: + +```json +{ + "pane_id": "1-1", + "label": "reviewer" +} +``` + +send `label: null` or omit `label` to clear the manual pane label. + +returns `pane_info`. + +### `pane.read` + +params: + +```json +{ + "pane_id": "1-1", + "source": "recent", + "lines": 80, + "format": "text", + "strip_ansi": true +} +``` + +notes: + +- `source` is required and must be `visible`, `recent`, or `recent_unwrapped` +- `lines` is optional +- current implementation defaults to `80` lines when `lines` is omitted and caps reads at `1000` +- `format` defaults to `text`; use `ansi` for a rendered VT/ANSI snapshot with styles preserved +- `strip_ansi` defaults to `true` and is kept for compatibility + +`source` meanings: + +- `visible` — current viewport +- `recent` — recent scrollback text +- `recent_unwrapped` — recent scrollback text with soft wraps joined + +example response: + +```json +{ + "id": "req_read", + "result": { + "type": "pane_read", + "read": { + "pane_id": "1-1", + "workspace_id": "1", + "tab_id": "1:1", + "source": "recent", + "format": "text", + "text": "...", + "revision": 0, + "truncated": false + } + } +} +``` + +### `pane.split` + +params: + +```json +{ + "target_pane_id": "1-1", + "direction": "right", + "focus": true +} +``` + +notes: + +- `direction` must be `right` or `down` +- `cwd` is optional +- `focus` is optional in raw socket requests and defaults to `false` +- the cli wrapper also defaults to no focus; pass `--focus` to switch to the new pane + +returns `pane_info` for the new pane. + +### `pane.send_text` + +params: + +```json +{ + "pane_id": "1-1", + "text": "bun run dev" +} +``` + +this sends literal text only. it does **not** press Enter. + +### `pane.send_keys` + +params: + +```json +{ + "pane_id": "1-1", + "keys": ["Enter"] +} +``` + +use this after `pane.send_text` when you want to submit a command. + +### `pane.send_input` + +params: + +```json +{ + "pane_id": "1-1", + "text": "bun run dev", + "keys": ["Enter"] +} +``` + +this sends text plus encoded keypresses in order within one request. when bracketed paste is enabled in the pane, the text portion is sent as a paste payload before the keys. use this when you need `text + Enter` to behave more like a real keypress sequence than `pane.send_text` with a literal trailing `\r`. + +`text` and `keys` are both optional, but at least one should usually be present. + +### `pane.report_agent` + +use this when an agent hook or plugin wants to report a semantic state directly over the socket api. + +params: + +```json +{ + "pane_id": "1-1", + "source": "custom:hermes", + "agent": "hermes", + "state": "working", + "message": "running tools", + "custom_status": "indexing" +} +``` + +notes: + +- `source` is required and identifies the reporting integration instance +- `agent` is required and may be any non-empty label string +- built-in names like `pi` are normalized to their public label form +- custom labels like `hermes` are accepted as-is +- while this authority is active, the reported `agent` and `state` override heuristic display for that pane +- process detection still owns pane liveness and fallback when hook authority is cleared or released +- `message` is optional metadata for the reporting integration +- `custom_status` is an optional short display label such as `scheduled`, `indexing`, or `stuck` +- `custom_status` is visual-only; use `state` for semantic behavior like working or blocked +- omitting `custom_status` clears any previous custom status from this reporting source + +returns `ok`. + +### `pane.clear_agent_authority` + +params: + +```json +{ + "pane_id": "1-1", + "source": "custom:hermes" +} +``` + +notes: + +- `source` is optional +- when `source` is omitted, any hook authority for that pane is cleared +- when `source` is present, only that reporting source is cleared + +returns `ok`. + +### `pane.release_agent` + +use this when the reported agent is exiting cleanly and wants herdr to drop agent identity immediately instead of waiting for fallback detection. + +params: + +```json +{ + "pane_id": "1-1", + "source": "custom:hermes", + "agent": "hermes" +} +``` + +notes: + +- `agent` uses the same non-empty label rules as `pane.report_agent` +- this clears the pane's effective agent identity immediately when the source and label match the active authority +- for built-in detected agents, herdr also applies its normal short reacquire suppression during graceful release + +returns `ok`. + +### `pane.close` + +params: + +```json +{ + "pane_id": "1-2" +} +``` + +returns `ok`. + +## waits + +### `pane.wait_for_output` + +this is the direct socket-side one-shot blocking wait. + +params: + +```json +{ + "pane_id": "1-1", + "source": "recent", + "lines": 200, + "match": { "type": "substring", "value": "ready" }, + "timeout_ms": 30000, + "strip_ansi": true +} +``` + +matcher forms: + +```json +{ "type": "substring", "value": "ready" } +``` + +```json +{ "type": "regex", "value": "server.*ready" } +``` + +notes: + +- `source` must be `visible`, `recent`, or `recent_unwrapped` +- `lines` is optional +- `timeout_ms` is optional +- `strip_ansi` defaults to `true` +- for `source = "recent"`, output matching uses unwrapped recent terminal text so soft wraps do not break matches +- `source = "recent_unwrapped"` is also available on `pane.read` when you want to inspect the same unwrapped transcript directly +- on success you get `output_matched` +- on timeout you get an error response with code `timeout` + +example success response: + +```json +{ + "id": "req_wait", + "result": { + "type": "output_matched", + "pane_id": "1-1", + "revision": 0, + "matched_line": "server ready", + "read": { + "pane_id": "1-1", + "workspace_id": "1", + "tab_id": "1:1", + "source": "recent_unwrapped", + "text": "...server ready...", + "revision": 0, + "truncated": false + } + } +} +``` + +## subscriptions + +`events.subscribe` is the long-lived pubsub entrypoint. + +you send a subscribe request once, get an ack on the same connection, and then keep reading newline-delimited json events from that same socket. + +### subscription ack + +```json +{ + "id": "sub_1", + "result": { + "type": "subscription_started" + } +} +``` + +### supported subscriptions + +base lifecycle subscriptions: + +- `workspace.created` +- `workspace.closed` +- `workspace.focused` +- `tab.created` +- `tab.closed` +- `tab.focused` +- `tab.renamed` +- `pane.created` +- `pane.closed` +- `pane.focused` +- `pane.exited` +- `pane.agent_detected` + +parameterized subscriptions: + +- `pane.output_matched` +- `pane.agent_status_changed` + +### event naming rule + +this part matters because the pushed event names are **not all shaped the same**. + +- when you subscribe to a **base lifecycle event**, the pushed `event` value uses snake_case with underscores: + - subscribe with `workspace.created` + - receive `workspace_created` +- when you subscribe to a **parameterized subscription**, the pushed `event` value keeps the dotted name: + - subscribe with `pane.output_matched` + - receive `pane.output_matched` + +examples below show both forms. + +### example: subscribe to lifecycle events + +request: + +```json +{ + "id": "sub_life", + "method": "events.subscribe", + "params": { + "subscriptions": [ + { "type": "workspace.created" }, + { "type": "workspace.focused" }, + { "type": "tab.created" }, + { "type": "tab.focused" }, + { "type": "tab.renamed" }, + { "type": "tab.closed" }, + { "type": "pane.created" }, + { "type": "pane.focused" }, + { "type": "pane.agent_detected" }, + { "type": "pane.closed" }, + { "type": "workspace.closed" } + ] + } +} +``` + +example pushed event: + +```json +{ + "event": "workspace_created", + "data": { + "workspace": { + "workspace_id": "1", + "number": 1, + "label": "herdr", + "focused": true, + "pane_count": 1, + "tab_count": 1, + "active_tab_id": "1:1", + "agent_status": "unknown" + } + } +} +``` + +### example: subscribe to output matches and agent status changes + +request: + +```json +{ + "id": "sub_1", + "method": "events.subscribe", + "params": { + "subscriptions": [ + { + "type": "pane.output_matched", + "pane_id": "1-1", + "source": "recent", + "lines": 200, + "match": { "type": "substring", "value": "ready" } + }, + { + "type": "pane.agent_status_changed", + "pane_id": "1-1", + "agent_status": "done" + } + ] + } +} +``` + +notes: + +- `pane.output_matched` supports `source`, optional `lines`, matcher config, and optional `strip_ansi` +- `pane.agent_status_changed` accepts an optional `agent_status` filter; if omitted, any status transition for that pane can match + +example pushed `pane.output_matched` event: + +```json +{ + "event": "pane.output_matched", + "data": { + "pane_id": "1-1", + "matched_line": "server ready", + "read": { + "pane_id": "1-1", + "workspace_id": "1", + "tab_id": "1:1", + "source": "recent_unwrapped", + "text": "...server ready...", + "revision": 0, + "truncated": false + } + } +} +``` + +example pushed `pane.agent_status_changed` event: + +```json +{ + "event": "pane.agent_status_changed", + "data": { + "pane_id": "1-1", + "workspace_id": "1", + "agent_status": "done", + "agent": "pi", + "custom_status": "scheduled" + } +} +``` + +`agent` in pushed events follows the same rules as `pane_info.agent`: it may be a built-in detected name, a custom hook-reported label, or omitted. `custom_status` may be included when the pane has hook-owned custom status metadata at the time of the semantic status transition. +## cli wrappers + +these commands provide the shell-facing control surface. most command groups talk to the local socket; `status client` only inspects the local executable. + +### command groups + +status commands: + +```text +herdr status +herdr status server +herdr status client +``` + +`herdr -V` and `herdr --version` print the local executable version without contacting the server. `herdr status` compares that local executable with the running server when one is reachable. + +workspace commands: + +```text +herdr workspace list +herdr workspace create [--cwd PATH] [--label TEXT] [--focus] [--no-focus] +herdr workspace get <workspace_id> +herdr workspace focus <workspace_id> +herdr workspace rename <workspace_id> <label> +herdr workspace close <workspace_id> +``` + +tab commands: + +```text +herdr tab list [--workspace <workspace_id>] +herdr tab create [--workspace <workspace_id>] [--cwd PATH] [--label TEXT] [--focus] [--no-focus] +herdr tab get <tab_id> +herdr tab focus <tab_id> +herdr tab rename <tab_id> <label> +herdr tab close <tab_id> +``` + +pane commands: + +```text +herdr pane list [--workspace <workspace_id>] +herdr pane get <pane_id> +herdr pane rename <pane_id> <label>|--clear +herdr pane read <pane_id> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi] +herdr pane split <pane_id> --direction right|down [--cwd PATH] [--focus] [--no-focus] +herdr pane close <pane_id> +herdr pane send-text <pane_id> <text> +herdr pane send-keys <pane_id> <key> [key ...] +herdr pane report-agent <pane_id> --source ID --agent LABEL --state idle|working|blocked|unknown [--message TEXT] [--custom-status TEXT] [--seq N] +herdr pane run <pane_id> <command> +``` + +wait commands: + +```text +herdr wait output <pane_id> --match <text> [--source visible|recent|recent-unwrapped] [--lines N] [--timeout MS] [--regex] [--raw] +herdr wait agent-status <pane_id> --status <idle|working|blocked|done|unknown> [--timeout MS] +``` + +### cli behavior notes + +- `status` prints local client version/protocol, running server version/protocol when reachable, socket path, compatibility, and whether a restart is needed +- `status server` prints only the running server side; if no server is reachable it exits successfully and prints `status: not running` +- `status client` prints only the local executable version/protocol and binary path without contacting the server +- `workspace create` keeps focus where it is by default; pass `--focus` to switch to the new workspace +- `workspace create` without `--label` keeps the default cwd-based workspace naming +- `workspace create --label` applies the custom workspace name immediately +- `workspace create` returns `result.workspace`, `result.tab`, and `result.root_pane` +- `tab create` keeps focus where it is by default; pass `--focus` to switch to the new tab +- `tab create` without `--label` keeps the default numbered tab naming +- `tab create --label` applies the custom tab name immediately +- `tab create` returns `result.tab` and `result.root_pane` +- `pane split` keeps focus where it is by default; pass `--focus` to switch to the new pane +- `pane read` prints **text**, not json +- `pane read --format ansi` and `pane read --ansi` print a rendered ANSI snapshot with colors/styles preserved +- `pane read --source recent-unwrapped` returns recent terminal text with soft wraps joined back together +- `pane send-text`, `pane send-keys`, `pane report-agent`, and `pane run` print nothing on success +- list/get/create/split/wait commands print json on success +- `pane run` is a convenience wrapper for `pane.send_input` with the command text followed by a real `Enter` keypress +- `wait agent-status` is a cli convenience built on top of event subscriptions +- use it when you want the same `done` / `idle` distinction the UI shows +- `--raw` is a legacy alias for ANSI formatted `pane read` output and still disables ansi stripping for `wait output` +- `wait output --source recent` matches against unwrapped recent terminal text by default, so pane width and soft wrapping do not break matches + +### cli examples + +create a workspace, split a pane, run a server, and wait for readiness: + +```bash +herdr workspace create --cwd /path/to/project --label "api server" +herdr pane split 1-1 --direction right --no-focus +herdr pane run 1-2 "npm run dev" +herdr wait output 1-2 --match "ready" --timeout 30000 +``` + +wait for another agent to finish in the same user-facing sense the UI shows: + +```bash +herdr wait agent-status 1-1 --status done --timeout 60000 +``` + +inspect another pane's output: + +```bash +herdr pane read 1-1 --source recent --lines 80 +``` + +## behavior notes and gotchas + +- `pane.send_text` sends literal text only. if you want to execute a command, follow it with `pane.send_keys` and `Enter`, use `pane.send_input` for ordered `text + keypress` input, or use cli `pane run`, which sends the text and then a real Enter key in one request. +- `pane.read` and `pane.wait_for_output` strip ansi by default. +- `pane.output_matched` subscriptions fire on transitions into a matching state; they do not repeatedly spam the same still-visible match on every poll. +- closing the socket connection ends the subscription. +- there is no separate event transport. +- the same herdr process can serve regular request/response calls and long-lived subscription connections at the same time. |
