AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Discover and navigate to coding agent sessions
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
See all your coding-agent sessions at a glance, including their child agents,
and jump between the roots.
You run Claude Code or Codex in a few different projects and lose track of which
root is working, which child is waiting on you, and where each terminal lives.
Switchboard is a small daemon that discovers the interactive root processes on
your machine — no pre-naming or registration — and tells you, in real time,
what each root and its nested agent threads are doing.README 전문 보기 ↗See all your coding-agent sessions at a glance, including their child agents, and jump between the roots.
You run Claude Code or Codex in a few different projects and lose track of which root is working, which child is waiting on you, and where each terminal lives. Switchboard is a small daemon that discovers the interactive root processes on your machine — no pre-naming or registration — and tells you, in real time, what each root and its nested agent threads are doing.
It installs as a single binary, detects your environment at runtime, and degrades gracefully: it works on any Linux box with zero desktop configuration, and lights up click-to-focus when it recognizes your window manager and terminal.
Provenance. Switchboard's design and architecture are mine; the Go implementation was built by AI coding agents under my direction, growing out of a shell prototype I hand-wrote.
Everything hangs on two tiers. Switchboard never hard-fails on a missing integration — it just offers what the environment supports.
| Tier | What you get | Needs | Availability |
|---|---|---|---|
| Observe | live count, working directory, and per-session status (working / idle / waiting-on-permission) | nothing but the OS process layer | always — the floor |
| Navigate | click or keybind to focus a specific session's window + pane | a supported window manager and terminal | when both are detected; otherwise degrades to Observe |
state.json (the stable public contract) is emitted in
every tier, so any bar or script can render your sessions. The headline is the
Observe tier: runs anywhere, zero desktop config, any bar can render it.
One binary; it probes the environment and picks backends live (build tags are used only for the OS syscall layer).
| Seam | Backends | Detection |
|---|---|---|
| OS process (Observe floor) | Linux /proc + pidfd · macOS (planned) | per-OS at build |
| Window manager (Navigate) | Hyprland · sway · i3 · X11/EWMH · none | HYPRLAND_INSTANCE_SIGNATURE → SWAYSOCK → I3SOCK → DISPLAY |
| Terminal (Navigate) | wezterm · tmux · herdr · foot · alacritty · none | tmux server socket · wezterm gui sockets · a running herdr / foot / alacritty process (composes when nested) |
The daemon logs its chosen stack at startup and records it in the
capabilities block of state.json.
go install github.com/tjmisko/switchboard/cmd/...@latest
# run the daemon (foreground; see the systemd unit below for a managed service)
switchboard &
# watch your sessions live — in any terminal, even over SSH
claude-tui
claude-tui is the reference renderer: a zero-dependency live list of every
root session with its cwd, status, nested agent rows, and (if resolved)
workspace. No window manager, bar, or terminal integration required.
go install is the right way to try Switchboard. It is the wrong way to
redeploy a host that runs it as a service: it writes to ~/go/bin, which a
configured unit need not read, so the restart afterwards can succeed against the
old binary and report active either way. Use scripts/deploy from a checkout
once a host is managed — see
Deploying a release.
switchboard · 3 sessions · navigate · wm=hyprland term=wezterm
* ● permission ~/Projects/switchboard pid 4821 ws 4
├─ ● reviewer active · 18s
└─ ● metadata approval · 3m
● idle ~/Tools/other pid 5102 ws 2
○ unknown ~/scratch pid 5390
Only the root lines are navigation targets. Child agents do not own independent
terminal targets or Switchboard sessions; they appear as indented, non-focusable
rows in the TUI, and as a one-line roll-up in the Waybar hover. Unknown (grey)
root sessions are reachable with cycle next|prev but are not members of the
attention ring — nothing is waiting on them.
Prefer your own UI? Read ~/.cache/switchboard/state.json directly — see the
schema and bar integrations for
Polybar / Waybar / eww / i3blocks.
Every Waybar chip has a hover card:
ARACHNE ~/Projects/Arachne
goosebook ● working · 53m
react-canvas-refactor
up 2h20m · started 14:49 · ws 4 · pid 20805
33 agents · 3 live · 2h24m done
Line 1 is the project's full display name — the chip carries only the terse
abbreviation, so the hover is where Switchboard belongs — paired with the path
it lives at, since the name and its location answer one question together. It is
lowercased before the small-caps run: pango shrinks lowercase letters and leaves
uppercase ones at full height, so folding the case first is what makes the title
one uniform height. Line 2 names the host, then the task, then a dimmed block of
timing, workspace, process, and the subagent roll-up. Status age comes from the
additive status_since field.
The workspace is the one on this machine. For a local session that is simply
its own window's; for a remote one the daemon strips the remote desktop's
coordinates — they name a workspace on the other machine — and publishes
local_workspace, the workspace of the window actually showing that session. It
is the same number the chip is ordered by, and the only one worth acting on.
Nothing on the card may tick faster than once a minute. The tooltip travels
inside the module's JSON, so rewriting it makes Waybar re-render the module,
which dismisses an open hover — a per-second counter makes the card impossible
to read. Durations therefore use durfmt.Coarse (<1m below a minute), and the
subagent roll-up counts cumulative time for finished agents only, since live
ones would advance the sum a minute per minute each. Measured on the live bar:
the old per-agent tree emitted 1.17 lines/s, of which 93% were tooltip-only
churn; the card emits 0.07/s.
Switchboard can also remember: an append-only log of every status transition, session lifecycle, subagent fan-out, and token-usage sample, so you can see — over time — when and how hard your agents worked.
It is off by default (it records when and where you work). Turn it on with one
file, $XDG_CONFIG_HOME/switchboard/history.json:
{ "enabled": true, "detail": "minimal", "retain_days": 90, "max_bytes": 104857600 }
The log is one JSON-per-line file per local calendar day under
$XDG_STATE_HOME/switchboard/history/, local-only, with a privacy tier
(minimal omits cwd / task descriptions). Inspect and render it — no daemon
needed:
switchboard-ctl history tail # recent events
switchboard-ctl timeline # per-session swimlanes + attention stats
switchboard-ctl timeline --json # the stable contract for a future dashboard
timeline draws each session as a colored bar over time (parallel sessions
overlap) and reports the three "hours of agent attention" figures — union
wall-clock, per-session sum, and fan-out-weighted — plus subagents launched and
tokens used. See the activity-log schema and the
design plan.
Claude and Codex cost estimates use canonical usage snapshots and sourced public price catalogs. Unknown costs remain null; API-equivalent amounts are separate from vendor credits or billed estimates. See pricing and deployment compatibility for collection semantics, freshness diagnostics, and dashboard validation.
┌────────────────────────────────────────────┐
│ switchboard (daemon) │
OS process ───►│ discovery: interactive roots (Observe) │
layer │ death watch: pidfd/kqueue → drop session │
│ │
WM seam ───►│ window lifecycle + focus (Navigate) │
terminal ───►│ pane locate + focus (Navigate) │
providers ───►│ root + nested agent graph │
hooks ───►│ exact identity / fallback enrichment │
│ │
│ → ~/.cache/switchboard/state.json │
│ → $XDG_RUNTIME_DIR/switchboard.sock (RPC) │
└────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
claude-tui switchboard-ctl any bar
(reference) (focus/cycle/…) (reads state.json)
Two load-bearing invariants:
pidfd_open(2) on Linux); the session disappears the instant the
process dies, however it died (Ctrl+C, /exit, kill, OOM, shell hangup).The join from a claude PID to a focusable window is anchored on the
controlling tty, which the kernel can't lose:
root PID ──/proc──► cwd, tty, pidfd death signal
tty ──terminal seam──► pane (mux, pane id, window title)
mux+title──WM seam──► window address, workspace (opaque, backend-owned ref)
The tty match is bulletproof; the (mux, title) join to the WM window is
best-effort and returns nothing rather than guessing on a collision (it retries
next tick). A session that can't be mapped stays in the Observe tier.
cmd/
switchboard/ daemon — fans the signal sources into one store
switchboard-ctl/ CLI — list / focus / cycle / pick / hook / bottombar
claude-tui/ reference TUI renderer (subscribe → live list)
switchboard-polybar/ Polybar stream renderer — all sessions, inline actions
switchboard-waybar/ standalone/debug wrapper for the shared Waybar renderer
internal/
waybarchip/ shared all-slot Waybar rendering (live broker + debug wrapper)
osproc/ Seam 1 — OS process layer (enumerate + death watch; per-OS)
terminal/ Seam 2 — terminal locator (wezterm, tmux, auto, none, chain)
wm/ Seam 3 — window manager (hyprland, sway/i3, x11, none)
detect/ runtime backend selection + capability reporting
proc/ Linux /proc reader — pid metadata (cwd, tty, comm, state)
discovery/ 1 Hz interactive Claude/Codex root classifier
hyprland/ Hyprland IPC client (wrapped by wm/hyprland)
wezterm/ wezterm multi-mux cli client (wrapped by terminal/wezterm)
mapping/ orchestrates proc → pane → window
state/ in-memory store + atomic state.json mirror
rpc/ Unix socket: local + aggregate views, exact focus, hooks
remotestate/ bounded local-only JSONL stream + one SSH worker per host
federation/ detached aggregate view + exact route/action controller
panebind/ bounded OSC payload, live route registry, local validation
agentgraph/ provider-neutral root/child graph + status reducer
provider/ Claude and Codex graph observers
conformance/ backend-agnostic contract suites reused by every backend
testsupport/ fixtures (fake conn, fake /proc, real-child death helpers)
The portability design and phase plan live in
docs/portability-plan.md.
switchboard-ctlswitchboard-ctl list # human-friendly snapshot
switchboard-ctl --json list # raw JSON
switchboard-ctl status # one-line count
switchboard-ctl focus active # jump to the focused session
switchboard-ctl focus pid:<n> # PID, if unique across hosts
switchboard-ctl focus idx:<n> # Nth aggregate session
switchboard-ctl focus host:<host>:pid:<n> # exact host namespace
switchboard-ctl cycle next|prev # focus next/prev session, wrapping
switchboard-ctl attention # move right to the most urgent color, wrapping; only toggle one layer up when already there
switchboard-ctl pick # exact-token<TAB>label<TAB>ws<TAB>cwd (for fzf)
switchboard-ctl diagnose --observer # content-free binding/freshness/graph health
switchboard-ctl explain --pid <n> # why one root shows its status now; --json for the record
On an Observe-only stack, focus returns a clean "navigate unsupported"
message instead of failing obscurely.
mkdir -p ~/.config/systemd/user
cp systemd/switchboard.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now switchboard.service
The daemon and desktop renderer are deliberately separate. Select Waybar or Polybar per host; installing or pulling one integration never activates it on another machine. See machine-specific configuration for the renderer units, host-local drop-ins, and shared-dotfiles rules.
Force a backend (e.g. to test degradation) with the daemon flags
-wm auto|hyprland|sway|i3|x11|none and -terminal auto|wezterm|tmux|herdr|foot|alacritty|none.
The Codex observer separately accepts -codex-observer auto|off (default
auto).
Add one repeatable daemon flag per remote machine:
switchboard -remote buildbox -remote user@gpu-box
Each destination runs one hardened, noninteractive command:
ssh -n -T … <destination> switchboard-ctl remote-stream
The remote command exports only that machine's complete local snapshots and
does not expose an action channel. A transport break leaves the last complete
snapshot visible while one bounded reconnect attempt confirms whether the host
is really gone. A valid replacement resumes without a remove/add flash; a
failed confirmation removes the host and invalidates its action routes. The
remote machine needs switchboard running and a compatible switchboard-ctl
on its SSH PATH; ordinary SSH host-key checking and batch authentication must
already work.
The journal records the hidden confirmation edge as category=reconnecting;
that diagnostic is informational and does not remove rows or routes.
That PATH is the one used by a noninteractive SSH command, not necessarily
the one in an interactive terminal. A SWITCHBOARD_BIN systemd override also
selects only the daemon; it does not make the adjacent switchboard-ctl
discoverable to SSH. If the client journal repeatedly reports
destination=<host> host= category=disconnected and the command above exits 127
with switchboard-ctl: command not found, install a stable command link in a
directory on that exact noninteractive path as described in
machine-specific configuration.
If the command resolves but prints usage which omits remote-stream and exits
2, the remote switchboard-ctl predates federation; deploy the daemon and
control binary from the same current revision.
Sessions are live-namespaced by (hostname,pid). Actions also carry
started_at, Switchboard's discovery timestamp, as a best-effort stale-action
fence. It rejects PID replacement observed while the daemon stays up; because
hydrated survivors retain it, it is not a kernel birth token across daemon
downtime. list, Waybar, the TUI, cycle, attention and the picker use the
aggregate view; the transport itself stays on the original host-local
subscribe endpoint so two federating clients cannot recurse.
Session names are resolved by the machine that owns the process. This matters
for Claude: /name and /rename update a PID-keyed file on that machine, and a
federating client must not look up the same numeric PID in its own home
directory. The remote snapshot therefore carries Claude's resolved name and the
client prefers it over prompt-derived graph/title fallbacks. Codex names are
already conversation-bound on the wire; its generated display name is cleared
when an authoritative native /rename arrives, locally or remotely. Deploy the
same revision to the remote daemon/control binary and the local daemon/renderers
to enable the Claude projection.
Exact remote focus requires the WezTerm module in
integrations/wezterm/switchboard.lua.
See its adjacent README for the small setup and sole format-window-title
composition. Until its OSC binding is present and unique, a remote row remains
visible but is marked observe-only and navigation controls skip it.
For the checked-in systemd unit, set repeatable -remote <destination>
arguments through the host-local SWITCHBOARD_ARGS drop-in described in
machine-specific configuration, then reload
and restart the user unit. Do not replace the shared ExecStart just to add a
remote. Remote tmux attachment and duplicate-hostname topologies are
intentionally outside this first implementation.
Status colors come from Claude Code hooks. Without them, sessions still appear
(Observe) but show unknown status. In ~/.claude/settings.json:
{
"hooks": {
"SessionStart": [{ "hooks": [{ "type": "command", "command": "switchboard-ctl hook SessionStart", "timeout": 2 }] }],
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "switchboard-ctl hook UserPromptSubmit", "timeout": 2 }] }],
"PreToolUse": [{ "matcher": "AskUserQuestion|ExitPlanMode", "hooks": [{ "type": "command", "command": "switchboard-ctl hook PreToolUse", "timeout": 2 }] }],
"PostToolUse": [{ "hooks": [{ "type": "command", "command": "switchboard-ctl hook PostToolUse", "timeout": 2 }] }],
"PermissionRequest": [{ "hooks": [{ "type": "command", "command": "switchboard-ctl hook PermissionRequest", "timeout": 2 }] }],
"Stop": [{ "hooks": [{ "type": "command", "command": "switchboard-ctl hook Stop", "timeout": 2 }] }],
"SubagentStart": [{ "hooks": [{ "type": "command", "command": "switchboard-ctl hook SubagentStart", "timeout": 2 }] }],
"SubagentStop": [{ "hooks": [{ "type": "command", "command": "switchboard-ctl hook SubagentStop", "timeout": 2 }] }]
}
}
The forwarder is fire-and-forget; a broken hook can never corrupt state or block Claude Code.
The matcher on Claude PreToolUse is load-bearing: this hook only stages opaque
call identity and must not be broadened to arbitrary tools. PermissionRequest
remains the event that opens red; a missing or ambiguous join falls back to the
transcript latch. See the implementation and rollout record in
docs/pretooluse-hook-plan.md.
If you add your own slash commands, plugins or tools to Claude Code, see custom commands and tools for how they can show the wrong color and how to avoid it.
SubagentStart/SubagentStop are optional — they make subagent-fanout
detection real-time by triggering an immediate re-scan instead of waiting for the
next reconcile tick. Fanouts are still detected without them (the daemon scans
the authoritative subagents/ metadata directory every tick), so these two only
reduce the latency on the delegating chip and the "N agents" count. The hook is
a pure trigger; the daemon's Observer remains the single source of truth, so a
duplicated or dropped subagent hook can never miscount.
Switchboard discovers ordinary interactive OpenAI Codex TUI processes while
excluding non-interactive subcommands such as exec, mcp, and
app-server. Run Codex normally with codex; navigation remains rooted in
the OS process and its controlling tty. Codex child agents are nested graph
nodes and never become focus/cycle/pick targets of their own.
Trusted Codex hooks provide the exact conversation ID, immediate root status,
and the bounded context used for display naming. Configure
~/.codex/hooks.json with the standard command:
{
"description": "Switchboard Codex identity, naming, and fallback status",
"hooks": {
"SessionStart": [{"hooks": [{"type": "command", "command": "switchboard-ctl codex-hook SessionStart", "timeout": 2}]}],
"UserPromptSubmit": [{"hooks": [{"type": "command", "command": "switchboard-ctl codex-hook UserPromptSubmit", "timeout": 2}]}],
"PreToolUse": [{"hooks": [{"type": "command", "command": "switchboard-ctl codex-hook PreToolUse", "timeout": 2}]}],
"PostToolUse": [{"hooks": [{"type": "command", "command": "switchboard-ctl codex-hook PostToolUse", "timeout": 2}]}],
"PermissionRequest": [{"hooks": [{"type": "command", "command": "switchboard-ctl codex-hook PermissionRequest", "timeout": 2}]}],
"Stop": [{"hooks": [{"type": "command", "command": "switchboard-ctl codex-hook Stop", "timeout": 2}]}],
"SubagentStart": [{"hooks": [{"type": "command", "command": "switchboard-ctl codex-hook SubagentStart", "timeout": 2}]}],
"SubagentStop": [{"hooks": [{"type": "command", "command": "switchboard-ctl codex-hook SubagentStop", "timeout": 2}]}]
}
}
This three-level event → matcher group → handler shape follows the
official OpenAI hooks documentation.
Codex requires non-managed command hooks to be reviewed and trusted; use
/hooks after adding or changing the file. A PID is always paired with its
process-lifetime started_at, and hook identities retired by /clear cannot
rotate a chip back to an older conversation. The fallback maps
UserPromptSubmit and ordinary PreToolUse/PostToolUse to active,
an ownership-ambiguous PermissionRequest to active during a 30-second grace,
and Stop to idle. Exact progress for the same tool call cancels that grace;
an unresolved request falls back to approval attention at the deadline. A
request_user_input PreToolUse instead opens a user-input wait keyed by its
opaque tool_use_id; only the matching PostToolUse, its turn's Stop, or a
new conversation clears that wait. Unrelated tool hooks and generic app-server
snapshots cannot repaint it green. SessionStart(clear|startup|resume) is held
for 250 ms so an immediate same-thread continuation replaces the provisional
idle edge, while a standalone /clear still settles idle.
SessionStart(compact) remains active because Codex continues the model
immediately after compaction. Only content-free lifecycle metadata is used to
correlate the wait; its question, answer, and raw tool input stay out of the
daemon. The root-status hook fallback is partial and root-only. Active evidence
remains fresh for 10 minutes, confirmed or timeout-fallback approval and
user-input waits for 24 hours, and idle edges for 7 days. All outstanding
structured questions are immediately red, including nonblocking requests and
requests with an automatic-resolution timeout. A request_user_input_async
hook or a live assistant-message item with structured questions opens an async
input latch without stopping the runtime. Tool completion, turn completion,
and ordinary progress preserve that red; the next user submission clears it.
This is a provisional dismissal rule until Codex exposes exact answer
correlation for async questions. Historical message items never reopen answered
questions. See
Codex Auto-review attention ownership for
the evidence model and rollout diagnostics. A daemon restart during an
already-open hook-only interview cannot reconstruct that wait and therefore
remains unknown rather than confidently green. The two child hooks are required
for live child state on the tested
standard-CLI path, but not for discovery or topology. Matched running child
edges expire after 10 minutes without a later edge; matched completions persist
within the exact root lifetime and can be reopened by a later start. See the
.
After the first usable prompt/Stop pair in a conversation, Switchboard asks
an isolated ephemeral naming turn for a 2–5-word lowercase kebab-case label (at
most 40 characters). Both the prompt and final assistant message are trimmed to
1,000 Unicode characters, used only in memory, and never written to state,
history, diagnostics, or logs. The default model is gpt-5.6-luna; select a
different one with -codex-autoname-model. Invalid or failed output is retried
once, then replaced by a deterministic fallback.
Only the resulting display_name record is persisted. It is bound to the
exact Codex conversation and shown before the native graph nickname.
Switchboard never changes Codex's native thread name. When a later
authoritative app-server observation reports a native name different from the
baseline captured for the display record, the generated record is cleared, so
/rename wins.
In -codex-observer auto mode, the generic observer binds exclusively from an
exact hook-supplied thread ID. It may read that thread and its descendants and
consume native-name notifications, but exposes no visible-thread write path. If
observation is disabled or temporarily unavailable, hooks still drive status
and display-name generation; native-rename precedence takes effect when an
authoritative name observation next becomes available.
Automatic degradation is fail-open for discovery and fail-closed for status:
-codex-observer off never constructs the standalone app-server. Codex hooks
can still supply the partial root view, but child hooks cannot synthesize
topology; without hooks the root remains visible with unknown status and no
proven live children.Use switchboard-ctl diagnose --observer to inspect content-free binding,
snapshot freshness, completeness, node counts, observer mode, display-name
origin, and finite error categories. It never prints cwd, transcripts, thread
labels, prompts, assistant content, commands, or raw provider payloads.
diagnose is the history; switchboard-ctl explain --pid <pid> [--json] is
the present. It asks the daemon why one local root shows the status it does
now, as the one status resolver decided it: the source that decided
(provider graph, hook, transcript tail, herdr, restored state, usage limit), a
reason code (graph_authority, event_authority, transcript_authority,
terminal_authority, last_known, attention_held, descendants_live,
prior_held, usage_limit_overlay, or why the status is unknown:
binding_missing, observation_pending, observation_expired,
coverage_unsupported, identity_mismatch, unrecorded), the evidence's
observation time and freshness deadline, when the decision was last made, and
up to eight readings that lost with the rule that rejected each
(source_outranked, older_than_current, observation_expired,
identity_mismatch, coverage_unsupported, or the deciding reason). Under a
usage limit it shows
limited with the decision beneath it. The record is in memory, bound to the
process lifetime and provider session, and carries ids, enums and times only.
pidfd_open(2) (kernel 5.3+). Go 1.25 to build.codex app-server --stdio; otherwise use -codex-observer off or
the automatic degraded root-only behavior above.PATH, or foot / Alacritty). Foot and Alacritty have no
usable IPC: Switchboard reads which terminal process drives each pty from the
pty master's /proc fdinfo, and the WM raises that window. This is exact when
each window is its own process — plain foot / alacritty, as Omarchy
launches them. Under foot --server or alacritty msg create-window, windows
share one pid, so a session in a process with several windows stays
Observe-only.HERDR_SOCKET_PATH override works. With several clients attached it raises
the most recently started one. A pane of a detached server is selected but has
no window to raise. Clients attached over SSH (herdr --remote) are not
covered. herdr is also the status authority for an agent in one of its
panes: Switchboard follows herdr's per-pane agent status over its event
stream and publishes it in place of the provider's (see the herdr block in
docs/state-schema.md). Fresh Codex input/approval attention
takes precedence, keeping questions red while herdr reports work or idle.
herdr reads the screen, so its
edges trail Claude's hooks by about a second, but it also clears a declined
prompt that fires no hook. Agents herdr detects that Switchboard has no
adapter for (Pi, OpenCode, Cursor, Gemini, Copilot, …) appear as sessions
too, with herdr's status, when they run in a herdr pane. See
docs/herdr.md for how the two divide the work.Done: runtime-detecting osproc / terminal / wm seams behind a reusable
conformance contract; Hyprland + sway/i3 + X11/EWMH WM backends; wezterm + tmux + herdr + foot + Alacritty
terminal backends with per-session locator chaining; capability reporting;
claude-tui reference renderer; provider-neutral root/child agent graphs;
Codex app-server observation with bounded degradation; Claude compatibility
projection; canonical agent history/timeline output; a provider-neutral i3 +
Polybar bottom bar; the Hyprland + Waybar two-bar appliance (appendix).
Next: jump-to-focus on every Omarchy terminal — kitty, the tmux→WM-window
focus bridge, Hyprland focus edge cases, Ghostty — phased in
docs/terminal-coverage/; macOS OS backend
(libproc + kqueue); verified eww recipes.
The original Switchboard was a Hyprland + wezterm + waybar appliance. That integration still ships as a Hyprland-specific extra; the portable core above does not depend on it.
Switchboard always exposes one ordered session list. Navigation follows that
list even when no display is visible. display mode chips uses the named bottom
strip; display mode circles uses compact status circles at the right of the
existing top Waybar. display mode toggle switches between them. Both update
live and share status, focus, remote, and suspension rules.
The circles have no visible names; hover shows details and clicking focuses the exact session generation. The integration recipe binds Super+Shift+B to the mode toggle, preserving Super+Alt+Left/Right navigation and the F8 master visibility control. The top-bar task timer is removed by the configuration helper.
See display modes and the consumer contract for setup, customization, the live JSON stream, and renderer invariants.
The legacy profile runs the top bar and bottom agent strip as separate Waybar processes. This keeps visibility control simple and remains the rollback path. The split uses two config files:
~/.config/waybar/config.jsonc — the top bar only (launched by exec-once = waybar).~/.config/waybar/claude.jsonc — the bottom strip only; not launched
directly. Its lifecycle is owned by switchboard-ctl bottombar.claude.jsonc declares 10 custom/claude-N modules so each chip is a real GTK
widget with its own CSS. One switchboard-ctl display serve subscription
renders the whole slot set into atomic files under
$XDG_RUNTIME_DIR/switchboard/. Waybar's signal mode runs a one-shot shell read
only when a slot changes; no per-slot renderer or reader stays resident. The
JSON class carries status + focused + suspended + remote so
style.css paints the chip. Click = focus that slot; right-click = picker;
scroll = cycle. A complete example lives at
waybar/claude.jsonc.
Waybar's row does not wrap, so each slot abbreviates its label to fit the
monitor (internal/barlayout). The fit shares out glyph cells, not pixels —
every chip pays the same fixed overhead, so what remains is one pool the row
divides max-min fair, with the remainder handed back a cell at a time rather
than rounded off into the bar's margin. That overhead is the chip's CSS box, so
barlayout.DefaultMetrics has to mirror style.css and claude.jsonc:
ChipFixedPx = 2×padding + 2×border + 2×margin + spacing
= 2×8 + 2×1 + 2×2 + 2 = 24
Change the chip padding, margin, or the bar's spacing and you must update
DefaultMetrics to match, or the labels will be cut short (too high) or spill
off the edge of the bar (too low).
Every chip carries a border, and the border encodes three independent things at once:
Shape is location: a session on another machine gains the remote class and is
drawn as a nested pill — 3px double instead of 1px solid, the chip
outline with a second ring just inside it. Colour is status and focus, and the
remote rule sets border width and style but deliberately never colour, so the
ring simply picks up whatever hue the status and focus rules assign. An idle
remote chip therefore rings amber rather than wearing a fixed accent that fights
its own fill. The gap between a double ring's two lines is the chip's own status
fill, so the nesting spends no colour of its own; focus is a brighter hue of that
same status colour and does not change the border width.
The nesting is free only because the extra width is paid for out of the chip's padding rather than added to its box:
/* local */ padding: 4px 8px; border-width: 1px; /* 8 + 1 = 9 per side */
/* remote */ padding: 2px 6px; border-width: 3px; /* 6 + 3 = 9 */
Keep that trade balanced — .headless restates the base padding for the same
reason, since it resets the border to 1px. ChipFixedPx is a single number
for the whole row, so a chip that were genuinely wider would make the fit depend
on how many of the visible sessions happened to be remote, and every label on the
bar would re-abbreviate whenever a remote session appeared or dropped.
A chip whose claude process is job-control-stopped (Ctrl-Z) gains the
suspended class on top of its status class. Grey it out in style.css:
#custom-claude-0.suspended, #custom-claude-1.suspended, /* … through -9 */ {
opacity: 0.4;
}
Hyprland startup wiring:
exec-once = systemctl --user import-environment HYPRLAND_INSTANCE_SIGNATURE WAYLAND_DISPLAY XDG_CURRENT_DESKTOP DISPLAY
exec-once = systemctl --user start --no-block switchboard.service
exec-once = systemctl --user restart --no-block switchboard-waybar.service
bottom bar runs ⟺ chips mode AND master visible AND sessions exist
The broker follows the live aggregate stream and inotify events for mode and
master visibility. display serve owns the split bottom process only in chips
mode; circles are widgets inside the existing top Waybar. Process ownership
survives startup and termination until pidfd exit is confirmed, so switching
modes cannot orphan a bar and launch another. A three-second safety tick remains
for recovery; normal mode and session changes are event-driven.
Overridable via SWITCHBOARD_WAYBAR_MARKER and SWITCHBOARD_BOTTOM_CONFIG.
This auto-hide logic is deeply Hyprland-specific and stays an opt-in extra, not
part of the portable core.
For hosts where eliminating the second GTK process matters, Waybar can load
both objects in one generated config. scripts/merge-waybar-config injects the
per-window signal contract, and switchboard-ctl bottombar publish attaches to
that desktop-owned process without ever starting or killing it. See
docs/bars/README.md for the guarded
cutover and rollback contract.