From 544c7ebebbc1db65a2949642d367edc446ae559b Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Thu, 2 Apr 2026 23:59:54 +0300 Subject: feat: add tabs to socket api --- README.md | 12 +- SKILL.md | 51 +++++-- SOCKET_API.md | 158 +++++++++++++++++++++- src/api/mod.rs | 16 +++ src/api/schema.rs | 106 +++++++++++++++ src/app/mod.rs | 367 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/cli.rs | 180 ++++++++++++++++++++++++- src/main.rs | 2 + tests/api_ping.rs | 113 +++++++++++++++- tests/cli_wrapper.rs | 49 +++++++ 10 files changed, 1031 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 4f94ab27..7a336f60 100644 --- a/README.md +++ b/README.md @@ -79,11 +79,12 @@ herdr is not just a passive manager for humans watching agents. it gives agents - [`SKILL.md`](./SKILL.md) — the reusable agent skill. use this if you want an agent inside herdr to learn the workflow quickly through the existing cli surface. - [`SOCKET_API.md`](./SOCKET_API.md) — the direct integration doc. use this if you want the low-level socket protocol, event subscriptions, or the cli wrapper reference that sits on top of it. -those two paths meet at the same control surface. the built-in `herdr workspace ...`, `herdr pane ...`, and `herdr wait ...` commands all talk to the same local socket api. +those two paths meet at the same control surface. the built-in `herdr workspace ...`, `herdr tab ...`, `herdr pane ...`, and `herdr wait ...` commands all talk to the same local socket api. that means agents running inside herdr can do useful orchestration work themselves: - create new workspaces for parallel tasks +- create and focus tabs inside a workspace - split panes for servers, logs, tests, or scratch work - spawn other agents in sibling panes - read pane output or wait for output matches @@ -243,6 +244,8 @@ for all keybindings, onboarding, notification, sound, ui options, and environmen herdr saves your workspaces, tabs, pane layouts, pane working directories, and focused tab/pane on exit. when you restart, everything is restored. sessions are stored at `~/.config/herdr/session.json`. +through the socket api, tabs are first-class too now. raw integrations and cli wrappers can list/create/focus/rename/close tabs directly while pane ids stay workspace-scoped. + use `--no-session` to start fresh. ## how agent detection works @@ -296,7 +299,7 @@ for direct integration details, use the docs instead of reverse-engineering the - [`SKILL.md`](./SKILL.md) — reusable agent skill for agents already running inside herdr - [`SOCKET_API.md`](./SOCKET_API.md) — canonical socket protocol + cli wrapper reference -`SOCKET_API.md` now covers transport, request/response envelopes, workspace and pane methods, subscription behavior, and the `herdr workspace`, `herdr pane`, and `herdr wait` commands that wrap the same socket surface. +`SOCKET_API.md` now covers transport, request/response envelopes, workspace, tab, and pane methods, subscription behavior, and the `herdr workspace`, `herdr tab`, `herdr pane`, and `herdr wait` commands that wrap the same socket surface. ## what's coming @@ -315,12 +318,13 @@ there will be rough edges. if you hit one, [open an issue](https://github.com/og ## cli wrappers -herdr's workspace, pane, and wait commands are documented in [`SOCKET_API.md`](./SOCKET_API.md) together with the socket methods they wrap. +herdr's workspace, tab, pane, and wait commands are documented in [`SOCKET_API.md`](./SOCKET_API.md) together with the socket methods they wrap. workspace ids are compact public ids like `1`, `2`, `3`. +tab ids are compact public ids like `1:1`, `1:2`, `2:1`. pane ids are compact public ids like `1-1`, `1-2`, `2-1`. -even with tabs enabled, pane ids remain workspace-scoped public ids rather than `workspace-tab-pane` triples. they are positional within the current live session, so numbering compacts when workspaces or panes are closed. +even with tabs enabled, pane ids remain workspace-scoped public ids rather than `workspace-tab-pane` triples. both tab ids and pane ids are positional within the current live session, so numbering compacts when tabs, workspaces, or panes are closed. ## building from source diff --git a/SKILL.md b/SKILL.md index 5032bf0e..78d1a3d5 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,30 +1,33 @@ --- name: herdr -description: "Control herdr from inside it. Split panes, spawn agents, read output, wait for state changes, and manage workspaces — all via CLI commands that talk to the running herdr instance over a local unix socket. Use when running inside herdr (HERDR_ENV=1)." +description: "Control herdr from inside it. Manage workspaces and tabs, split panes, spawn agents, read output, and wait for state changes — all via CLI commands that talk to the running herdr instance over a local unix socket. Use when running inside herdr (HERDR_ENV=1)." --- # herdr — agent skill -you are running inside herdr, a terminal-native agent multiplexer. herdr gives you workspaces and panes — each pane is a real terminal with its own shell, agent, server, or log stream — and you can control all of it from the cli. +you are running inside herdr, a terminal-native agent multiplexer. herdr gives you workspaces, tabs, and panes — each pane is a real terminal with its own shell, agent, server, or log stream — and you can control all of it from the cli. this means you can: - see what other panes and agents are doing +- create tabs for separate subcontexts inside one workspace - split panes and run commands in them - start servers, watch logs, and run tests in sibling panes - wait for specific output before continuing - wait for another agent to finish - spawn more agent instances -the `herdr` binary is available in your PATH. its workspace, pane, and wait commands talk to the running herdr instance over a local unix socket. +the `herdr` binary is available in your PATH. its workspace, tab, pane, and wait commands talk to the running herdr instance over a local unix socket. if you need the raw protocol or full api reference, read [`SOCKET_API.md`](./SOCKET_API.md). ## concepts -**workspaces** are project contexts. each workspace has one or more panes. +**workspaces** are project contexts. each workspace has one or more tabs. -**panes** are terminal splits inside a workspace. each pane runs its own process — a shell, an agent, a server, anything. +**tabs** are subcontexts inside a workspace. each tab has one or more panes. + +**panes** are terminal splits inside a tab. each pane runs its own process — a shell, an agent, a server, anything. **agent state** is detected automatically by herdr. each pane can be: @@ -33,9 +36,9 @@ if you need the raw protocol or full api reference, read [`SOCKET_API.md`](./SOC - `blocked` — agent needs input or approval - `unknown` — no recognized agent, or just a shell -**ids** — workspace ids look like `1`, `2`. pane ids look like `1-1`, `1-2`, `2-1`. these are compact public ids for the current live session. +**ids** — workspace ids look like `1`, `2`. tab ids look like `1:1`, `1:2`, `2:1`. pane ids look like `1-1`, `1-2`, `2-1`. these are compact public ids for the current live session. -important: ids can compact when panes or workspaces are closed. do not treat them as durable ids. +important: ids can compact when tabs, panes, or workspaces are closed. do not treat them as durable ids. ## discover yourself @@ -53,6 +56,38 @@ list workspaces: herdr workspace list ``` +## tab management + +list tabs in the current workspace: + +```bash +herdr tab list --workspace 1 +``` + +create a new tab: + +```bash +herdr tab create --workspace 1 +``` + +rename it: + +```bash +herdr tab rename 1:2 "logs" +``` + +focus it: + +```bash +herdr tab focus 1:2 +``` + +close it: + +```bash +herdr tab close 1:2 +``` + ## read another pane see what is on another pane's screen: @@ -221,7 +256,7 @@ herdr pane read 1-1 --source recent --lines 100 ## notes -- `workspace list`, `pane list`, `workspace create`, `pane split`, `wait output`, and `wait agent-state` print json on success. +- `workspace list`, `workspace create`, `tab list`, `tab create`, `tab get`, `tab focus`, `tab rename`, `tab close`, `pane list`, `pane get`, `pane split`, `wait output`, and `wait agent-state` print json on success. - `pane read` prints text, not json. - `pane send-text`, `pane send-keys`, and `pane run` print nothing on success. - parse ids from `workspace create` and `pane split` responses when you need the new ids. do not guess. diff --git a/SOCKET_API.md b/SOCKET_API.md index cff166c5..53fa9d52 100644 --- a/SOCKET_API.md +++ b/SOCKET_API.md @@ -9,7 +9,7 @@ if you are teaching an agent that is already running inside herdr, start with [` 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 workspace ...`, `herdr pane ...`, `herdr wait ...`. best for shell scripts and simple orchestration. +- **cli wrappers** — `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. @@ -87,7 +87,12 @@ that means: these are compact public ids for the current live session. they are **not durable database ids**. if a workspace or pane closes, higher numbers compact down. -herdr now supports tabs inside workspaces in the ui, but pane ids remain workspace-scoped public ids like `1-2` rather than `workspace-tab-pane` triples. tabs are not exposed as first-class socket api objects yet. +tabs are first-class socket api objects now. + +- tab ids look like `1:1`, `1:2`, `2:1` +- first number = workspace number +- second number = tab number within that workspace +- pane ids still stay workspace-scoped like `1-2` rather than becoming `workspace-tab-pane` triples ## core objects @@ -100,6 +105,22 @@ herdr now supports tabs inside workspaces in the ui, but pane ids remain workspa "label": "herdr", "focused": true, "pane_count": 1, + "tab_count": 1, + "active_tab_id": "1:1", + "agent_state": "unknown" +} +``` + +`tab_info` responses contain objects like: + +```json +{ + "tab_id": "1:1", + "workspace_id": "1", + "number": 1, + "label": "1", + "focused": true, + "pane_count": 1, "agent_state": "unknown" } ``` @@ -110,6 +131,7 @@ herdr now supports tabs inside workspaces in the ui, but pane ids remain workspa { "pane_id": "1-1", "workspace_id": "1", + "tab_id": "1:1", "focused": true, "cwd": "/home/can/Projects/herdr", "agent": "pi", @@ -124,6 +146,7 @@ herdr now supports tabs inside workspaces in the ui, but pane ids remain workspa { "pane_id": "1-1", "workspace_id": "1", + "tab_id": "1:1", "source": "recent", "text": "...", "revision": 0, @@ -149,6 +172,12 @@ agent states are: | `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.read` | read pane output | `pane_read` | @@ -218,6 +247,8 @@ example response: "label": "herdr", "focused": true, "pane_count": 1, + "tab_count": 1, + "active_tab_id": "1:1", "agent_state": "unknown" } } @@ -270,6 +301,104 @@ returns: } ``` +## 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 focuses by default unless you pass `--no-focus` + +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` @@ -345,6 +474,7 @@ example response: "read": { "pane_id": "1-1", "workspace_id": "1", + "tab_id": "1:1", "source": "recent", "text": "...", "revision": 0, @@ -464,6 +594,7 @@ example success response: "read": { "pane_id": "1-1", "workspace_id": "1", + "tab_id": "1:1", "source": "recent", "text": "...server ready...", "revision": 0, @@ -497,6 +628,10 @@ base lifecycle subscriptions: - `workspace.created` - `workspace.closed` - `workspace.focused` +- `tab.created` +- `tab.closed` +- `tab.focused` +- `tab.renamed` - `pane.created` - `pane.closed` - `pane.focused` @@ -533,6 +668,10 @@ request: "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" }, @@ -555,6 +694,8 @@ example pushed event: "label": "herdr", "focused": true, "pane_count": 1, + "tab_count": 1, + "active_tab_id": "1:1", "agent_state": "unknown" } } @@ -604,6 +745,7 @@ example pushed `pane.output_matched` event: "read": { "pane_id": "1-1", "workspace_id": "1", + "tab_id": "1:1", "source": "recent", "text": "...server ready...", "revision": 0, @@ -644,6 +786,17 @@ herdr workspace rename