Skip to content

feat(herd): port the sidebar to hqtui, as a folding tree - #546

Merged
ralyodio merged 1 commit into
herd-ui-defaultfrom
herd-hqtui
Sep 25, 2026
Merged

ralyodio merged 1 commit into
herd-ui-defaultfrom
herd-hqtui

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

are we using hqtui.com library for this tui? it looks pretty old and outdated. upgrade it to hqtui

Stacked on #545. Base it there so the crash fix is not held hostage by this; rebase onto main once #545 lands.

First, the fact

moshcode did not use hqtui at all. Zero references in src/ or bin/. The TUI is hand-rolled escape sequences throughout, so this is a port, not a version bump. @profullstack/hqtui is now a dependency at ^0.7.0 (a house package, zero runtime dependencies, TypeScript shipping ESM).

Scope: the herd sidebar, and only the herd sidebar

The hand-rolled TUI is roughly 3,500 lines across five files. This ports the one Anthony is actually looking at when he complains, and leaves the rest running exactly as it did. What porting the others would take is written up at the bottom.

src/herd-workspace.mjs is now the tmux half and only the tmux half (221 lines: which pane is which, parking and joining, title pinning). src/herd-sidebar.mjs is the new view. The right-hand pane is still a real tmux pane running a real agent, because no renderer can substitute for one: it has a real cursor, a real mouse inside the agent, and its own full-screen UI.

What the library actually buys

Three bugs came out of the hand-rolled shape, and none of them were ever about herds.

The click map was a second source of truth. sidebarRows() numbered the lines and renderSidebar() printed them, and when they drifted every click landed on the row below the pointer. hqtui's tree reports the row it drew each node on via onRow, so the map is built by the thing that draws. The tests press screen cells: screen.find("web") then screen.click(x, y), and assert the pane that gets joined in is web's.

There was no hover, so a click had to be spent selecting. That is where the double-click came from. onHoverRow plus hovered is the whole of it now.

The restore path was escape sequences only. hqtui's Terminal restores on SIGINT, SIGTERM and an uncaught error by default (installExitHandlers), so that class of damage is the library's problem rather than a thing this file has to keep remembering.

A folding tree, in place

The herd is herds containing members, which is a tree, and Anthony's standing expectation for a pane of things-containing-things is that it unfolds in place on one click with the row under the pointer lit. Verified live in a real tmux workspace:

 herd                        after one click on MAIN:     herd
 └─ MAIN (3)                                              └─ MAIN (3)
    ├─ ▸alpha         ?
    ├─  beta          ?
    └─  logs          ?
 ─ actions ──────────────
  + shell     s
  + agent     a
  ✕ stop      x
  ⊞ tile all  t
  ← detach    q
 click open  F12 bar

Clicking a herd folds it; clicking a member opens it, which means shown and given the keyboard. Nothing is ever swapped out for a different screen. The state mark is a right-aligned column with PRD 0019's ? for an inferred state, suppressed on unknown whose mark is already ?.

Actions are real buttons with onPress, so they join the Tab order and are clickable, and the shortcut sits in a column rather than two spaces after a ragged label.

The two traps, and why neither is tripped

  • collapseBorders only merges where two BORDERED siblings touch, needs gap 0 and an explicit bordered flag on any wrapper. The sidebar is one unbordered box in a 26-column pane, so there is nothing for it to merge; it is explicitly off, with the reasoning in a comment rather than left to be rediscovered.
  • A panel's title and subtitle share one border row, subtitle winning, so near-identical panels truncate differently. No panel here carries both: the heading is a heading widget inside the box, and the actions are separated by a divider rather than by a second panel.

Tests

node --test over the whole repo, on the committed tree: tests 3514 pass 3510 fail 0 skipped 4

test/herd-sidebar-click.test.mjs was rewritten around renderToScreen, which is the part worth having: it renders the real view headlessly and then presses real cells.

  • clicking the cell that says a member's name opens that member
  • one click, not two: nothing reads clicks
  • clicking a herd folds it in place, and the herd itself stays on screen
  • the row under the pointer is the row a click would take
  • the flat index a click resolves to is the row hqtui drew, for every row of a two-herd tree
  • every action is a button you can press, showing its key
  • herdSidebar itself wired to a fake App and a stand-in tmux: a click joins the pane in and hands it the keyboard; a tmux call that throws paints the reason instead of ending the sidebar; a dead member says so; enter still opens; x still stops

test/herd-workspace.test.mjs keeps the real-tmux integration test unchanged, and the row/line agreement tests are gone because that agreement is no longer something this repo has to maintain.

Verified live as well, with the real moshcode herd sidebar running in a real tmux workspace pane and clicks delivered through tmux.

What porting the rest would take

Written up here rather than guessed at in a follow-up ticket.

src/herd-ui.mjs, the plain list (348 lines). Small and nearly free: it is already a layout() + render() + parseInput() triple that maps onto a table with onSelectRow/onHoverRow, and it already has the alt-screen and restore dance hqtui would take over. The one real question is the hand-off: the list gives the terminal to a real tmux attach and takes it back, so the app has to stop() and be rebuilt around the attach rather than merely redraw. Half a day, and it deletes the last hand-written SGR mouse parser in the repo.

src/herd-tile.mjs (168 lines). Nothing to port. It has no renderer at all: it is tmux join-pane plus select-layout tiled plus key bindings, and the only thing it draws is a tmux status line. Leave it.

src/herd-bar.mjs (321 lines). This is the interesting one and the one most likely to go wrong. It is a one-row pane that resizes itself to 14 rows when it has output and back to 1 when it does not, which is exactly hqtui's inline viewport. textInput plus log would replace the hand-rolled line editing and the \x1b[2J\x1b[H repaints. The risk is that the bar also lives under a plain attach, not only in the workspace, and it drives resize-pane on its own pane while hqtui believes it owns a fixed rectangle; those two have to be reconciled before any of it is written. A day, and it should not be bundled with anything else.

src/tui.mjs, the pit shell (1,557 lines). Explicitly out of scope here, and it should stay out for a while. It contains no escape sequences at all: it is a readline REPL, and readline is doing real work for it (history, editing, completion). hqtui's textInput is not a readline replacement, and inline mode plus a prompt is a different program from what is there now. This is a rewrite of the pit's input model, not a port of its rendering, and it wants its own PRD.

src/rss-ui.mjs (553 lines). Not herd at all, but it is the other hand-rolled full-screen surface in the repo (9 escape-sequence sites) and it would port the same way the plain list does. Worth doing right after herd-ui.mjs, while the pattern is fresh.

Not done

  • Not merged, not versioned, not released, nothing posted anywhere.
  • The moshcode ps roster, the mosh bar and the tiled layout are untouched.

"are we using hqtui.com library for this tui? it looks pretty old and
outdated. upgrade it to hqtui"

moshcode did not use hqtui at all: zero references in src/ or bin/, and
the TUI is hand-rolled escape sequences throughout. So this is a port,
not a version bump. @profullstack/hqtui joins as a dependency at ^0.7.0,
a house package with zero runtime dependencies.

SCOPE. The herd sidebar, which is the surface Anthony is looking at when
he complains, and nothing else. src/herd-workspace.mjs keeps the tmux
half and only the tmux half; src/herd-sidebar.mjs is the new view. The
right-hand pane is still a real tmux pane running a real agent, because
no renderer can substitute for one.

WHAT THE LIBRARY BUYS. Three bugs came out of the hand-rolled shape and
none of them were ever about herds:

  - the click map was a second source of truth. sidebarRows() numbered
    the lines and renderSidebar() printed them, and when they drifted
    every click landed on the row below the pointer. hqtui's tree
    reports the row it drew each node on, so the map is built by the
    thing that draws it.
  - there was no hover, so a click had to be spent selecting, which is
    where the double-click came from. onHoverRow plus hovered is now
    the whole of it.
  - the restore path was escape sequences only. hqtui's Terminal
    restores on SIGINT, SIGTERM and an uncaught error by default.

A FOLDING TREE, IN PLACE. The herd is herds containing members, which is
a tree, and the expectation for a pane of things-containing-things is
that it unfolds in place on ONE click with the row under the pointer
lit. Clicking a herd folds it; clicking a member opens it, meaning shown
and given the keyboard. Nothing is swapped out for a different screen.
Actions are real buttons with onPress.

Neither known trap is tripped: collapseBorders only merges where two
bordered siblings touch and there is nothing here to merge, so it is off
with the reasoning written down; and no panel carries both a title and a
subtitle, which is where near-identical panels truncate differently.

The tests are the part worth having. renderToScreen draws the real view
headlessly and then presses real cells, so "is the thing that says api
the thing that opens api" is answered by clicking the cell that says
api, rather than by two pieces of code agreeing about a line number.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown
Contributor

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​profullstack/​hqtui@​0.7.0801009996100

View full report

@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

0 finding(s) in the 7 file(s) this pull request changes.

Nothing in the files this pull request changes.

104 pre-existing finding(s) elsewhere in the repository — **HIGH/CRITICAL**: 8 | **MEDIUM**: 85 | **LOW**: 11

Not introduced by this pull request. The full set is in the Security tab.

Severity Rule Location
HIGH js-ssrf-outbound-request apps/pwa/public/sw.js:45
HIGH tls-verification-disabled apps/pwa/src/lib/moshpit-gateway.mjs:299
HIGH sh-remote-script-execution install.sh:86
HIGH sh-remote-script-execution install.sh:90
HIGH sh-remote-script-execution install.sh:258
HIGH sh-remote-script-execution install.sh:269
HIGH sh-remote-script-execution install.sh:275
HIGH tls-verification-disabled src/dns.mjs:766
MEDIUM sql-template-interpolation apps/pwa/src/lib/moshpit-certs.mjs:44
MEDIUM sql-template-interpolation apps/pwa/src/lib/moshpit-certs.mjs:82
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:139
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:153
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:179
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:373
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:377
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:422
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:671
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:867
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:869
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:928

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

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 2b697b0 into herd-ui-default Sep 25, 2026
6 checks passed
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.

1 participant