Skip to content

feat(herd): a one-line mosh prompt under the session - #354

Merged
ralyodio merged 1 commit into
mainfrom
herd-bar
Aug 9, 2026
Merged

feat(herd): a one-line mosh prompt under the session#354
ralyodio merged 1 commit into
mainfrom
herd-bar

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Run /herd start claude --agent, get into it, and you are looking at one agent with no visible way to leave. This adds the row that fixes it — and because it takes input, it is also how you start the second agent without leaving the first.

┌ herd ──────┬─ api ─────────────────────────────┐
│ MAIN       │  claude                           │
│ ▸ ! api    │  Do you want to proceed?          │
│   · work   │  ❯ 1. Yes                         │
│ ACTIONS    │    2. No                          │
│   + shell  │                                   │
│   ✕ stop   │                                   │
│ enter ▸ …  │                                   │
│ F12 ▸ …    │                                   │
├────────────┴───────────────────────────────────┤
│ mosh ▸ ps · start claude · show <n> · detach   │
└────────────────────────────────────────────────┘

Why a prompt and not a status line

A status line was the small version: one row of text that never goes away, so the way out is always on screen. But a row you can only read answers "how do I get out" and nothing else — you still have to leave to start anything. Making the row take input means the escape hatch and the command line are the same thing, and it is the surface that already exists: every moshcode herd verb works in it.

mosh ▸ start claude          # another agent, now on screen
mosh ▸ show api              # put a different member up
mosh ▸ ps                    # roster over the session, then out of the way

F12 reaches it from anywhere, including from inside an agent that has taken the keyboard — the binding lives in tmux's root table, so it is claimed before the pane's application ever sees it. It switches the client first, so it is also a way out of a member you attached to directly, not only of the workspace. Esc goes back to the session. Output grows the bar over the content while you read it, then collapses to one row.

attach in the bar means show: the real attach would be a tmux client inside the client already drawing the pane, which tmux refuses, and in a workspace the word means "put it in the content pane" anyway.

Clicking the member already on screen — or pressing Enter — now hands it the keyboard. Without that the workspace could only be watched.

Two bugs the probe caught

Both from driving a live tmux instead of reasoning about it:

  • join-pane -b -t <bar> fails: "create pane failed: pane too small." -b splits the bar's cell, and the bar is one row. The fix drops select-layout main-vertical for a plain split of the sidebar — which is better regardless, because main-vertical owns every pane in the window, so it dragged the footer into the right-hand column and handed it an equal share. Splitting the sidebar touches only the region above the footer.

  • bind-key -n F12 cmd1 ";" cmd2 as separate argv elements binds cmd1 and runs cmd2 once, right then. That silently produced a key that switched sessions and did nothing else:

    bind-key -T root F12   switch-client -t herd          # what I wrote
    bind-key -T root F12   switch-client -t herd \; select-pane -t "%3"   # what works
    

Also fixed: contentPane() excluded only itself, which was fine with two panes and wrong with three — a swap would have parked the bar into a session named after it. It now excludes by title, like everything else here.

Verification

Geometry holds across four swaps, footer full width and exactly one row:

   herd     w=26  h=28 top=0  left=0
   beta     w=73  h=28 top=0  left=27
   mosh-bar w=100 h=1  top=29 left=0

Live end-to-end against the real herd bar process:

bar renders → "mosh ▸ ps · start claude · show <n> · kill <n> · detach · help"
typed 'ps' → "mosh ▸ ps"
after Enter, bar height → mosh-bar:3
after Esc,   bar height → mosh-bar:1
focus after Esc → alpha:1

1513 tests pass, 0 fail. 15 new for the bar (line editor, command resolution, one-row prompt at narrow widths, role classification) plus the integration test now asserts the footer survives every swap at height 1 and full width, that Enter reaches the session, and that the F12 binding carries both commands — the exact thing that was silently broken.

🤖 Generated with Claude Code

The workspace put a real agent in the content pane, which is the point of
it — but a real agent takes the keyboard. Click into claude and every
sidebar key stops working, and with tmux's status line off there is
nothing on screen that says how to get back out. You are looking at one
agent with no visible way to leave it.

A status line would have been the small fix: one row of text that never
goes away. But a row you can only read answers "how do I get out" and
nothing else — you still cannot start a second agent without leaving the
first. So the row takes input. It is the same surface as the CLI, which
makes the escape hatch and the command line one thing rather than two.

  mosh ▸ ps · start claude · show <n> · kill <n> · detach · help

F12 reaches it from anywhere, including from inside an agent that has
taken the keyboard: the binding lives in tmux's root table, so it is
claimed before the pane's application ever sees it. It switches the
client first, so it is also a way out of a member you attached to
directly. Esc goes back to the session; Enter on an empty line does too.
Output grows the bar over the content while you read it, then it
collapses back to one row.

`attach` in the bar means `show`. The real attach would be a tmux client
inside the client already drawing the pane, which tmux refuses, and in a
workspace the word means "put it in the content pane" anyway.

Two things this turned up, both caught by probing a live tmux rather than
by reasoning about it:

- `join-pane -b -t <bar>` splits the BAR's cell, and the bar is one row:
  "create pane failed: pane too small". Dropping `select-layout
  main-vertical` for a plain split of the sidebar fixes it and is better
  anyway — main-vertical owns every pane in the window, so it dragged the
  footer into the right-hand column and gave it an equal share. Splitting
  the sidebar touches only the region above the footer, so the bar spans
  the full width and needs no correction after a swap.
- `bind-key -n F12 cmd1 ";" cmd2` as separate argv elements binds cmd1
  and runs cmd2 once, immediately. That produced a key which switched
  sessions and did nothing else. The chain has to arrive as one string.

`contentPane()` excluded only itself, which was fine with two panes and
wrong with three — a swap would have parked the bar. It now excludes by
title, like everything else here.

Also: clicking the member already on screen, or pressing Enter, hands it
the keyboard — without that the workspace can only be watched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread src/herd-workspace.mjs
// command itself, so tmux binds the first command and runs the second once,
// now. That silently produced a key that switched sessions and did nothing
// else — the binding has to arrive as a single command sequence.
tmux(["bind-key", "-n", BAR_KEY, `switch-client -t ${WORKSPACE} ; select-pane -t ${paneId}`], { runner });
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

94 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 44 | LOW: 48

Severity Rule Location
HIGH manifest-typosquat apps/pwa/package.json:19
HIGH js-ssrf-outbound-request apps/pwa/public/sw.js:45
MEDIUM tls-verification-disabled apps/pwa/src/lib/moshpit-gateway.mjs:299
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:61
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:75
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:101
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:265
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:269
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:314
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:499
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:675
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:677
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:736
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:782
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:852
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:955
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1063
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1199
MEDIUM js-unescaped-html-sink apps/pwa/src/routes/moshpit.mjs:1419
MEDIUM js-dynamic-code-execution apps/pwa/test/apikey-mask.test.mjs:129
MEDIUM sql-template-interpolation apps/pwa/test/credits-webhook-event-match.test.mjs:111
MEDIUM sql-template-interpolation apps/pwa/test/credits-webhook-event-match.test.mjs:131
MEDIUM sql-template-interpolation apps/pwa/test/moshpit-terms.test.mjs:192
MEDIUM sql-string-concatenation src/cli-schema.mjs:124
MEDIUM tls-verification-disabled src/dns.mjs:741
MEDIUM sql-template-interpolation src/dns.mjs:2549
MEDIUM sql-template-interpolation src/herd-workspace.mjs:112
MEDIUM sql-template-interpolation src/selfupdate.mjs:166
MEDIUM sql-template-interpolation src/selfupdate.mjs:170
MEDIUM sql-template-interpolation src/selfupdate.mjs:208
MEDIUM sql-template-interpolation src/selfupdate.mjs:209
MEDIUM insecure-temp-file test/dns-disable-restore.test.mjs:93
MEDIUM insecure-temp-file test/dns-disable-restore.test.mjs:310
MEDIUM insecure-temp-file test/plugins.test.mjs:152
MEDIUM insecure-temp-file test/pty.test.mjs:28
MEDIUM insecure-temp-file test/pty.test.mjs:31
MEDIUM insecure-temp-file test/pty.test.mjs:40
MEDIUM insecure-temp-file test/pty.test.mjs:42
MEDIUM insecure-temp-file test/pty.test.mjs:47
MEDIUM insecure-temp-file test/pty.test.mjs:48
MEDIUM insecure-temp-file test/pty.test.mjs:49
MEDIUM insecure-temp-file test/tabs.test.mjs:8
MEDIUM insecure-temp-file test/tabs.test.mjs:13
MEDIUM insecure-temp-file test/tabs.test.mjs:14
MEDIUM insecure-temp-file test/tabs.test.mjs:22
MEDIUM insecure-temp-file test/trust.test.mjs:240
LOW secret-generic-credential apps/pwa/test/apikey-bearer-scheme.test.mjs:30
LOW secret-generic-credential apps/pwa/test/apikey-mask.test.mjs:38
LOW secret-generic-credential apps/pwa/test/apikey-reveal.test.mjs:35
LOW secret-generic-credential apps/pwa/test/approvals-context.test.mjs:28

…and 44 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 677c54f into main Aug 9, 2026
4 checks passed
@ralyodio
ralyodio deleted the herd-bar branch August 9, 2026 23:51
@ralyodio ralyodio mentioned this pull request Aug 9, 2026
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