Architecture
Two Swift targets: HerdrCore (protocol, discovery, aggregation, raising the host) and MacBar (NSStatusItem + SwiftUI popover). Herdr.js is still the behavior oracle for status order, counts, and socket paths.
Layers
From socket to menu bar
MacBarApp sets the activation policy to .accessory. No Dock icon, no main window.
Data flow
Events invalidate; the list is truth
Each discovered socket gets a SessionWatcher:
- Command socket (closed after one response):
agent.list,agent.focus,tab.focus,pane.focus - Subscribe socket (long-lived):
events.subscribe. Lines withoutidtrigger a refresh
Refresh path: agent.list → GitBranchCache.enrich (read .git/HEAD, never spawn git) → SessionTimeCache.enrich (Claude jsonl) → SessionSnapshot. Failure reconnects with exponential backoff (1s → 30s).
AgentAggregator merges online snapshots only. When a session drops, its agents leave the combined list.
Discovery
SessionDiscovery scans:
| Session | Socket |
|---|---|
default | ~/.config/herdr/herdr.sock |
named (e.g. work) | ~/.config/herdr/sessions/<name>/herdr.sock |
pinned HERDR_SOCKET | that path, named demo |
SessionManager watches ~/.config/herdr and sessions/ (DispatchSource file events). That is filesystem invalidation, not agent.list polling. If the config directory does not exist yet, retry with a 2s backoff until it does.
Agent identity is (sessionName, pane_id). Agent.id renders as sessionName::paneId. The same pane token can live in default and work at once.
Focus raiser
FocusRaiser is isolated from protocol logic. A row click is:
agent.focus— mark that agent seentab.focus/pane.focus— Herdr 0.9 keeps workspace/tab per client; the extra is an API socket, soagent.focusalone often does not move the attached TUIraiseHost— after the TUI has switched, activate the GUI ancestor (activateIgnoringOtherApps, notactivateAllWindows)
A named session-chip arrow skips 1–2 and only calls raiseHost for that session, so the TUI keeps its last selected pane.
It looks for the TUI client:
herdr(no args) = default TUIherdr session attach <name>= named session- not
herdr server(typically ppid 1, holding the listening socket)
Then it walks parents to a GUI ancestor with activationPolicy == .regular (skipping Xcode and herdr-bar itself), unhide + activate. On macOS 14+ it yieldActivation first. Host PIDs are cached off the click path via sysctl (KERN_PROC / KERN_PROCARGS2) — no lsof / ps spawn.