A phone-first web mirror of your herdr session.
herdview reflects the agents already running in your herdr session into a mobile-friendly web UI — see each agent's state, read its transcript as a chat, send input, answer multiple-choice prompts, and drive menus. It never spawns a new agent on its own: it reads and steers the sessions you already have, so you're not creating throwaway "remote-control" sessions just to check in from your phone.
It's a herdr plugin: a small Go binary that drives the herdr CLI / socket (the documented plugin API) and serves an embedded web UI. No Node, no Python, no runtime for users to install.
herdr plugin install Orchard-Robotics/herdviewThat's it — no separate start step. Installing downloads the binary and
starts the server, and it re-ensures itself whenever you focus a pane, so it's
up and reachable at http://<this-host>:8848 (by tailnet name/IP — see below).
The install step (scripts/fetch.sh) downloads the prebuilt binary for your
OS/arch from this repo's latest GitHub Release, verifies its
SHA-256, and places it at ./herdview. So the install machine needs network
access and curl (or wget) — but no Go toolchain, and no auth (the repo is
public). Binaries are never committed to git; CI builds and publishes them on
each tagged release.
To run it in a visible pane instead (its lifetime = the server's):
herdr plugin pane open --plugin orchard.herdview --entrypoint server.
Updating is just a reinstall: the launcher is version-aware, so herdr plugin install … again detects a running older build and auto-swaps in the new one
(no kill, no reboot) — a plain reinstall upgrades a whole fleet.
⚠️ Reboot caveat: the server runs as a detached background process, not a system service. It survives your herdr session, but not a machine reboot — after a reboot it comes back the next time you interact with a herdr pane (thepane.focusedevent re-starts it), or immediately on a reinstall. A boot-persistent systemd service is on the roadmap.
herdview works in any browser. If you use the Moshi
phone app, its in-app detection needs the moshi-hook daemon running on the
host. herdview does not install third-party software for you: if moshi-hook
is already present, the install step just ensures its daemon is up; otherwise it
prints a pointer. To link the app to a host, pair once (token from the app):
moshi-hook pair --token <token> --store file.
- A live grid of every agent, blocked agents sorted to the top ("N need you"), each with a state pill, working directory, and git branch. Multiple herdr sessions are aggregated — pick a session, then its agents.
- Tap an agent for its transcript as chat bubbles (with markdown, tables,
and fenced code), a compose box (Shift+Enter to send), tappable multiple-choice
answers for
AskUserQuestion/permission prompts, a task checklist, and a side list of any artifact links the agent produced. - ⑂ diff — a colored view of the agent's uncommitted changes (working diff
--statsummary + untracked files), rendered on the phone.
- When an agent is blocked wanting to Edit/Write a file, the approval card shows the proposed change as a diff (old→new), so you approve knowing what it'll do — not just the filename.
- Rich blocks — agents can emit fenced blocks that render inline as live UI:
herdview-card(titled card + progress bars),herdview-chart(bar/line), andhtml-widget(arbitrary HTML in a sandboxed, network-blocked iframe). Toggle rendering off (show raw) via ⚙ → Render rich blocks. See "Rich blocks" below. - When you switch tabs, the browser-tab title badges the count of agents that
need you —
(N) herdview— and clears when you return.
By default herdview binds 0.0.0.0:8848 (all interfaces), so on a machine on
your tailnet you just browse http://<host>:8848 — e.g. http://solo:8848 by its
MagicDNS name, or its 100.x tailnet IP. No env, no port-forward. (A terminal
app's web preview of localhost:8848 works too.)
Override the bind with HERDVIEW_ADDR (e.g. 127.0.0.1:8848 to force loopback).
The host allowlist auto-accepts loopback, this box's hostname, and private/tailnet
IPs; add others with HERDVIEW_ALLOW_HOSTS=host1,host2 (or * to disable the check).
herdr can load a working directory directly, no build/publish needed:
go build -o herdview ./cmd/herdview # requires Go 1.22+ (build-time only)
herdr plugin link /path/to/herdview # register this dir as a plugin
herdr plugin pane open --plugin orchard.herdview --entrypoint serverRun the tests (Go unit + Playwright browser e2e) with sh scripts/test.sh.
Steering bugs (approvals, menu navigation) are only debuggable if you know
exactly what herdview typed into a pane and when. Set HERDVIEW_DEBUG_KEYS
to turn on a keystroke log:
HERDVIEW_DEBUG_KEYS=1 herdview --addr 0.0.0.0:8848 # → <stateDir>/keys.log
HERDVIEW_DEBUG_KEYS=/tmp/keys.log herdview … # → an explicit pathEvery send-text/send-keys is logged with a millisecond timestamp, pane,
session, client IP, and payload — so "one tap = one keystroke" is verifiable at
a glance. Add HERDVIEW_DEBUG_KEYS_PROMPT=1 to also snapshot the on-screen
selector just before each send (what the keystroke was answering); this costs one
extra herdr read per send, so enable it only while chasing a prompt-correlation
bug. Tail the log from a browser/phone at /api/debug/keys (404 when
disabled). Unset = off, zero overhead. Don't enable on a shared deploy — the log
records text typed to agents.
| Layer | Mechanism |
|---|---|
| Read | herdr agent list (grid) + herdr pane read (per-agent output) via $HERDR_BIN_PATH |
| Render | embedded mobile web UI (web/, compiled into the binary via go:embed) |
| Steer | herdr pane send-text + Enter (message), herdr pane send-keys (menus) into the existing pane |
| Route | Purpose |
|---|---|
GET /api/version |
the running build's version (used by --detach to auto-upgrade) |
GET /api/agents |
live agent grid across all sessions (state, cwd, branch, session) |
GET /api/pane/read?pane=ID&session=S |
recent output for one pane (text) |
GET /api/pane/transcript?pane=ID&session=S |
structured conversation (chat bubbles); 404 → fall back to read |
GET /api/pane/choices?pane=ID&session=S |
parsed multiple-choice prompt, if the pane is sitting on one |
GET /api/pane/tasks?pane=ID&session=S |
parsed task checklist, if present |
GET /api/pane/diff?pane=ID&session=S |
the agent repo's uncommitted working diff (+ --stat, untracked) |
GET /api/pane/image?pane=ID&session=S&path=P |
a raster image the agent referenced in a herdview-image block (path must appear in the pane's transcript) |
POST /api/pane/send?pane=ID&session=S |
{text} → type + Enter into the pane |
POST /api/pane/key?pane=ID&session=S |
{keys:[...]} → raw keystrokes (menus) |
GET /api/debug/keys |
tail of the dev keystroke log (404 unless HERDVIEW_DEBUG_KEYS is set) |
Agents produce rich output by writing a fenced code block in their normal message; herdview upgrades it to a live element in the chat bubble (no artifact, no new tab):
```herdview-card— JSON{title, status, rows:[{label,value}], progress:[{label,value,max}]}```herdview-chart— JSON{type:"bar", data:[{label,value}]}or{type:"line", points:[…]}```html-widget— raw HTML/SVG/canvas, rendered in a sandboxed iframe (sandbox="allow-scripts", CSPdefault-src 'none'→ no network, no page access), auto-sized to its content.```herdview-image— an embedded plot/image. JSON{path}(herdview loads the file out-of-band via/api/pane/image, keeping big images out of the transcript) or a small inline{src:"data:image/…"}. External URLs and SVG are refused; only a path the agent referenced in that message is served.```diff— a colorized diff (green adds, red deletes, dimmed hunk/file headers).- Callouts —
> [!NOTE] / [!TIP] / [!IMPORTANT] / [!WARNING] / [!CAUTION]render as colored admonition boxes. ```mermaid— Mermaid diagrams → SVG (securityLevel: strict, adopted via DOMParser, no innerHTML). The ~3.5 MB Mermaid build is vendored and lazy-loaded — fetched only the first time a diagram appears.
JSON blocks become DOM nodes; malformed input falls back to a plain code block, so nothing is lost. Turn rendering off (view raw) with ⚙ → Render rich blocks.
Teaching agents to use them: the herdview-blocks skill (in this repo at
.claude/skills/herdview-blocks/) documents the formats and when to use them. It
ships with the plugin — herdr plugin install clones the whole repo, so the skill
lands at ~/.config/herdr/plugins/github/orchard.herdview-*/.claude/skills/herdview-blocks/.
It is not auto-loaded; point your project's CLAUDE.md at it (glob that path,
the hash suffix changes per install) so agents load it when herdview is present. The
skill keys off HERDR_ENV, so it only kicks in inside a herdr session.
Drop this into your repo's CLAUDE.md:
## herdview rich output (when your session is mirrored)
When running in a herdr session (`HERDR_ENV` is set) with the **herdview** plugin
installed, prefer herdview's **rich blocks** for visual output — they render inline
in the phone/desktop mirror instead of as a wall of text or a heavy artifact.
- The format guide isn't auto-discovered, so **read it once when it's relevant**
(summarizing results/metrics, a comparison, progress, or a small widget). Find it
via glob — check the github-install path and your local checkout, if any (the
hash suffix changes per install):
```
ls ~/.config/herdr/plugins/github/orchard.herdview-*/.claude/skills/herdview-blocks/SKILL.md \
/path/to/herdview/.claude/skills/herdview-blocks/SKILL.md 2>/dev/null | head -1
```
Read that file and follow it. If the glob doesn't resolve, herdview isn't
installed → skip this and write normally.
- It documents several fences: ` ```herdview-card ` (titled card + progress bars),
` ```herdview-chart ` (bar/line), ` ```herdview-image ` (an embedded plot/image
by file path), ` ```html-widget ` (arbitrary HTML in a sandboxed iframe), plus
` ```diff `, ` ```mermaid `, and `> [!NOTE]` callouts. Keep them compact; they
render inline in a chat bubble.
- To turn this off: delete this section, or toggle it off in the viewer
(⚙ → "Render rich blocks").herdview steers terminals, so treat the port as sensitive.
⚠️ It binds all interfaces (0.0.0.0) by default so it's reachable over your tailnet with zero config. That means anyone who can reach:8848on any network the host is attached to — and there is no login — can read transcripts and drive your agents. Only run it on a machine whose network is gated (a tailnet with ACLs, a trusted LAN, or behind a firewall). On an untrusted network, setHERDVIEW_ADDR=127.0.0.1:8848to bind loopback-only and reach it via an SSH/mosh port-forward instead.
- The Host + Origin allowlist blocks browser DNS-rebinding / cross-site (CSRF) POSTs: it accepts loopback, this box's hostname, and private/tailnet IPs, and rejects arbitrary public domains. It is not authentication — it doesn't stop a direct attacker on a network that can already reach the port.
- No user login. The allowlist and the network gating are the whole story — size your deployment accordingly.
The chat view renders Claude's own JSONL transcript instead of the terminal.
herdr exposes each pane's PID, and Claude writes <config>/sessions/<pid>.json
(with sessionId + cwd) for every running session — so herdview resolves
pane → PID → session → transcript with no Claude hook and no config edits.
It walks the process tree, so it still works while the agent is mid tool-run.
<config> is each pane process's own CLAUDE_CONFIG_DIR (default ~/.claude),
so it works on shared accounts where developers isolate Claude with per-user
config dirs (e.g. ~/.claude-alice).
If it can't resolve a pane, it falls back to the raw terminal read. (A legacy
herdview hook that writes an explicit pane→transcript map is still honored as a
fallback, but is not required.)
Binaries are distributed via GitHub Releases, built by CI:
git tag v0.3.0 && git push origin v0.3.0.github/workflows/release.yml cross-compiles all four platforms
(herdview_{linux,macos}_{amd64,arm64}), writes SHA256SUMS, and publishes the
Release. scripts/fetch.sh pulls from /releases/latest/, so the newest release
is what new installs receive. Build the artifacts by hand with sh scripts/build.sh.
- Live agent grid with state
- Tap into an agent: recent output + send a message + menu keys
- Structured JSONL transcript (chat bubbles) — resolved hook-free via
~/.claude/sessions/<pid>.json; no config; falls back to terminal text - Multi-session view — aggregate every running herdr session into one grid with a session tier, fanning out over each session's socket
- Multiple-choice answering, task checklist, artifact links, off-tab badge
- Auto-start —
--detachbackground launcher started at install and re-ensured onpane.focused; reachable over the tailnet by default, no manual step - Approve/deny buttons refined from
herdr agent explain's matched blocker - Boot-persistent service (systemd user unit) so it survives a reboot unattended
MIT