Skip to content

feat: add herdr as an alternative multiplexer backend - #304

Open
rubenfiszel wants to merge 4 commits into
mainfrom
feat/herdr-backend
Open

rubenfiszel wants to merge 4 commits into
mainfrom
feat/herdr-backend

Conversation

@rubenfiszel

@rubenfiszel rubenfiszel commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Adds herdr as an alternative to tmux for backing a project's panes, plus a webmux multiplexer command to move an existing project between the two. Default stays tmux; projects that don't opt in are unaffected.

webmux multiplexer            # → tmux
webmux multiplexer herdr      # move every open worktree onto herdr

Verified end to end

Against live herdr 0.8.0 / protocol 19, in isolated named sessions:

Worktree lifecycle on herdr — webmux add produced one workspace, one tab, 2 panes matching the profile, with pane.process_info confirming claude in the agent pane and zsh in the shell pane. list → open, close → tab removed, open → recreated, and a second worktree added a second tab to the same workspace.

Switching, both directions — on a throwaway repo:

Closing sw-a → Switching config to herdr → Reopening sw-a
Switched tmux → herdr (1/1 reopened)

tmux:  wm-sw-a gone          herdr: wm-sw-a panes=2

…and webmux multiplexer tmux put it back exactly.

Switching is a rebuild, not a handoff

A pane cannot be moved between multiplexers — the PTY belongs to whichever server spawned it, and neither exposes a way to transfer one. So the switch closes each open worktree on the outgoing multiplexer, flips the config, and re-opens on the incoming one.

Survives: which worktrees were open, and agent conversations — reopening relaunches in resume mode (claude --continue).
Does not: scrollback, in-flight agent turns, and every running process. Profile command panes (dev servers, watchers) restart from scratch, which is expensive on a large project.

The ordering is load-bearing. Closing happens strictly before the config flips, and a close failure aborts the switch with the config untouched. Otherwise a half-switched project strands windows in a multiplexer webmux can no longer reach — unrecoverable except by hand. Two tests pin this: persist must come after the last close, and a failed close must produce no persist and no reopen.

The choice is written to .webmux.local.yaml — which multiplexer is running is a per-machine fact, not something a project commits for everyone.

What changed

SessionGateway (backend/src/adapters/session-gateway.ts) — the old TmuxGateway, renamed and made multiplexer-agnostic. Two cleanups came with it:

  • Async, since herdr round-trips a unix socket. Nearly every call site was already in an async method.
  • setWindowOption is gone from the interface — session-service no longer pokes pane-base-index / automatic-rename / allow-rename; BunTmuxGateway.createWindow applies them itself. A new focusWindow replaces the CLI's hand-rolled tmux select-window.

HerdrGateway (backend/src/adapters/herdr.ts) — maps session→workspace, window→tab, pane→pane, joined by label rather than id, because webmux persists names and herdr ids don't survive a restart.

multiplexer-switch-service.ts — the switch orchestration, with IO injected so the ordering guarantees are unit-testable.

herdr 0.8.0 quirks this works around

The socket API accepts more parameters than it honours. Each was found by running it, and three only surfaced once more than one tab existed:

  1. One request per connection. Reusing a socket silently hangs every later call. Only events.subscribe is long-lived.
  2. herdr never self-daemonizes. ping reports detached_server_daemon: false; everything else returns server_not_running. Unlike tmux start-server, ensureServer() spawns and detaches herdr server itself, then polls.
  3. pane.list ignores tab_id / tab / workspace_id and returns every pane in the session. The adapter filters by each pane's own tab_id — without it, pane index 0 of a worktree resolves into a different tab.
  4. pane.split ignores pane_id and splits whatever is focused. The adapter focuses the anchor first and restores prior focus, preserving tmux's detached split-window -d behaviour for parked panes.
  5. pane.swap needs source_pane_id/target_pane_id — a shape the CLI doesn't expose, so the adapter uses the socket directly. This is what lets the existing parked-pane tab system work unchanged.

pane.send_text and pane.read do honour pane_id. EXPECTED_PROTOCOL is pinned and a mismatch logs a warning, since herdr is pre-1.0.

Not supported in herdr mode: the web terminal

A real limitation, not an oversight. The browser terminal attaches a tmux grouped session (new-session -t + window-size latest) so each tab gets an independently-sized view of a shared window. herdr has no equivalent: no method accepts rows/cols (panes live on a server-side virtual screen), and pane.read is a snapshot, not an incremental stream. Attaching now returns an explicit error pointing at your own terminal. Everything else — worktree lifecycle, dashboard, git state, PRs, Linear, conversations — is unaffected.

Reviewer notes

  • Deliberate scope call: Settings displays the active multiplexer (with an explanation under herdr), but switching is CLI-only. A browser button that switches you to herdr would immediately break the browser terminal you clicked it in, and needs a mid-flight server runtime rebuild that drops live sockets. Flagging rather than silently skipping AGENTS.md §3 — happy to add the endpoint plus a ProjectManager.reload(prefix) if wanted.
  • bin/ is not covered by any tsconfig and has never been typechecked. I checked it with an ad-hoc config while working: 10 pre-existing errors, unchanged by this PR (verified by stashing). Worth a follow-up; out of scope here.
  • The parked-pane/swap tab machinery is untouched and works on herdr as-is.

Tests

778 pass, 0 fail across backend + bin + api-contract (738 baseline + 40 new: 24 HerdrGateway tests against a stub socket reproducing the quirks above, 8 switch-ordering tests, 4 CLI tests, 4 misc). Backend typecheck and svelte-check clean. Frontend's 35 test failures are the pre-existing localStorage/jsdom baseline — identical with these changes stashed.

Docs: docs/herdr.md.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
webmux b0b508a Commit Preview URL

Branch Preview URL
Aug 11 2026, 07:33 AM

rubenfiszel and others added 3 commits August 6, 2026 21:36
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…SION

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rubenfiszel
rubenfiszel marked this pull request as ready for review August 17, 2026 16:38
@PheonBest

Copy link
Copy Markdown

Hello, I would like to know more about the limitation you're referring to:

"Not supported in herdr mode: the web terminal"

If the shown terminals are snapshots only, what does it mean practically? Your webmux become a mux without the web?

pun intended

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants