Skip to content

Development

npm run check type-checks the extension and the page, and npm test runs the suite. The page is vanilla TypeScript in gui/, built by Vite into dist/gui; the sidebar plugin is plain JavaScript in herdr-plugin/; prompts are plain markdown in prompts/.

Terminal window
npm install
npm run check # tsc --noEmit for the extension and for the page
npm run gui:build # builds the page into dist/gui (npm test and npm pack do this first)
npm run build:server # bundles the sb server into dist/server (npm pack does this; the build fails on a value import of pi's packages)
npm run server -- status # runs src/server/cli.ts from the checkout through tsx
npm test # node --test, against a fake Herdr and a fake intercom broker, in a temp home of its own per test process (tests/setup.ts)
npm run test:one -- tests/x.test.ts # one file, isolated the same way
npm run gui:dev # the page with hot reload against a demo net; see below
Details

The dashboard has its own file (src/dashboard.ts, budget 162 lines) and its rendering is a pure function of a snapshot, so tests drive it without a terminal. What it shows without drawing (the marks, the rows, and the subagent reading) is in src/netdata.ts, which reaches no value import of pi-tui, typebox, or pi-coding-agent so the server can load it; tests/pure-imports.test.ts walks the import graph and fails on one. The browser view’s data and server (src/net.ts, src/gui.ts) share a budget of 300 lines; buildNet turns sessions, panes, and workspaces into the JSON snapshot as a pure function (and fills the registry of readable session files), and collectNet reads Herdr and intercom and reuses the subagent linking of src/netdata.ts. The session reader (src/history.ts), the entry-to-item mapping (src/items.ts), and the stream and API glue (src/watch.ts) are tested against fixture files.

The page is vanilla TypeScript in gui/, built by Vite (a development dependency only) into dist/gui, which ships in the npm package; dist/ is not committed. The page diffs the DOM by element id, animates with CSS on transform and opacity (the map’s packets are transform keyframes along each wire’s route, routePackets in gui/headband.ts; gui/calm.ts rests the endless ones), and its reconnect logic (gui/stream.ts) is tested with a fake stream and a fake clock. The map’s chunk holds the map, the header, the router, and the alert spotting (gui/alerts.ts). The page’s messages are Paraglide’s (gui/messages/<locale>.json, compiled by npm run gui:i18n into the ignored gui/paraglide/, which check, gui:build, gui:dev, and gui:demo run first).

A page file imports its m from gui/sentences.ts, never from Paraglide: the compile step is followed by scripts/gui-sentences.mjs, which writes gui/paraglide/said/ (one module per message, so tree shaking is unchanged), and a message that a theme’s sentence pack (gui/themes/<view>/sentences.ts) names is said in the pack’s words while that theme is shown (Raven Keep’s zh-TW pack says 管家 and 鴉閣 where Standard says 塔台 and 主塔台, and 血僕 for a main and 眷屬 for agents in general where Standard says 主智慧體 and 智慧體; tests/raven-words.test.ts). A new message in a theme’s pack needs no other step: the next gui:i18n (a running gui:dev is restarted) wraps it.

The focus views (gui/views.ts coordinates gui/workspace.ts, gui/session.ts, and gui/dossier.ts), the comms log with the markdown renderer (gui/chat.ts, gui/markdown.ts), and the overlay layer’s DOM and tiles (gui/layer.ts) load on first use. The overlay rules (gui/overlay.ts for the dialog stack, createTiles in gui/layer.ts) are pure and tested without a browser (tests/overlay.test.ts); phase 2 and 3 dialogs (ask_user, robot_hand, secret_drop) and show_doc documents (gui/doc.ts, its rules tested in tests/doc-page.test.ts) plug into the same stack and tiles. Size budgets are checked by tests/imports.test.ts: the map’s chunk ≤ 15 KB (6 KB gzipped), the lazy chunks together ≤ 35 KB (12 KB), the stylesheet ≤ 22.1 KB (6.1 KB).

The sidebar plugin is plain JavaScript in herdr-plugin/ and imports nothing from src/, so the folder works wherever Herdr links or installs it. tests/netview.test.ts drives it against a fake Herdr that keeps tokens and the one active view, and against a real Unix socket. The reaper plugin in herdr-reaper/ is built the same way: its decisions (decide.mjs) are pure functions, its runs (sweep.mjs) take Herdr, git, the state store and the clock as arguments, and tests/reaper.test.ts runs them against fakes.

npm run gui:dev serves the page on a local port with hot reload, fed by a demo net with invented names that changes state every few seconds. The demo includes a session with every kind of chat entry (open any unit), and the fixture’s toolbar (bottom left) has drop stream, go silent (no messages, no error: the 45 s stall monitor), restore, to review the disconnected page, and doc arrives, which has billing-main show one more document: on its comms it opens as a tile, elsewhere its badge counts up. billing-main lists a markdown plan and an HTML review whose form posts nowhere in the demo.

To point the dev page at a live stream instead, run /boss gui and start the dev server with its URL: GUI_LIVE=http://127.0.0.1:<port>/<token>/ npm run gui:dev.

To check a checkout in real pi while your own sb server runs (one per state directory), start a scratch pi with --no-extensions, -e for pi-intercom and your provider, and -e for a two-line wrapper that calls this checkout’s createStrawBoss({ stateDir: "<scratch dir>", openUrl: async () => {} }), with PI_CODING_AGENT_DIR and HOME pointing at scratch folders and a scratch Herdr. Its /boss gui then starts a second sb server from the checkout, recorded in the scratch directory, and opens no browser.

Prompts are plain markdown in prompts/, read at session start, so an edit applies after /reload.