Skip to content

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

1
MacBar
Menu-bar glance and dashboard popover
StatusItemControllerStatusItemViewDashboardViewSettingsViewHotkeyCenter
2
Aggregation
One snapshot per live session; agents tagged with sessionName
SessionManagerAgentAggregatorHerdrLogic
3
Per-session watch
Command socket + long subscribe; events invalidate, they are not truth
SessionWatcherHerdrClientevents.subscribe
4
Local enrichment
Fields Herdr does not send: branch, model, conversation time
GitBranchCacheSessionTimeCacheFocusRaiserUpdateCheck

MacBarApp sets the activation policy to .accessory. No Dock icon, no main window.

Data flow ​

Events invalidate; the list is truth

Unix socket→events.subscribe→invalidate→agent.list→enrich→Aggregator→menu bar + dashboard

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 without id trigger 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:

SessionSocket
default~/.config/herdr/herdr.sock
named (e.g. work)~/.config/herdr/sessions/<name>/herdr.sock
pinned HERDR_SOCKETthat 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:

  1. agent.focus — mark that agent seen
  2. tab.focus / pane.focus — Herdr 0.9 keeps workspace/tab per client; the extra is an API socket, so agent.focus alone often does not move the attached TUI
  3. raiseHost — after the TUI has switched, activate the GUI ancestor (activateIgnoringOtherApps, not activateAllWindows)

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 TUI
  • herdr 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.

Copyright © 2026 openalon. MIT License.