Live browser dashboard for tmux sessions. Every tmux window becomes a card; clicking a card opens a live xterm.js-style modal bridged over WebSocket. Parses Claude Code agent panes to surface branch, PR, CI status, spinner, recap, and pending input — so a wall of running agents is glanceable instead of a tab fight.
Kanban layout — NeedsStrip with Broadcast (THI-66), preset chip-bar + Save filter (THI-98), layout switcher (THI-59/60), pin badge on api-server, inline y n quick actions on the waiting agent (THI-97).
- Three layouts — Kanban (sessions as columns), Grid (sessions as rows of responsive cards), List (dense tabular rows). One-click switcher in the subhead, plus a suggestion chip that nudges you toward the densest fit for the current view.
- Claude Code detection — branch + PR chip, CI rollup, spinner with elapsed, recap of last assistant line, and a "waiting on you" badge when an agent prompts
- Live terminal modal powered by xterm.js with WebSocket auto-reconnect, buffer paste + image paste
- Live activity timers —
38s/2mlabels tick every second, not every poll - ⌘K command palette with broadcast mode — send keys to one pane, or fan out to every pending agent in one shot
- Pinned windows + saved filter presets — pin a card to keep it visible across filter changes; save the current filter as a named preset and swap between curated workspaces with one click
- Pane history search — full-text grep across every pane's capture buffer with surrounding context, click to jump
- Session templates — declare a workspace in
~/.switchboard/templates/<name>.yml(windows, cwd, cmd, variable substitution) and bootstrap a whole session in one click - Per-kind quick actions on cards — inline
y/n/Ctrl-C/Continuefor agents,Restartfor servers,Pause/Resumefor logs - Browser notifications + favicon dot when any agent flips to waiting
- Clickable file paths → IDE and PR chips → GitHub inside terminal output
- ✨ Auto-rename — Anthropic Haiku suggests names for every window in a session, preview/edit/skip per row before applying
- Claude usage pill — rolling-window token totals + plan-% scraping
- Drag to reorder columns and tiles within a column
- Four themes — dark / light / high-contrast / phosphor — every interactive surface clears WCAG AA contrast
- First-run tour, replayable from Settings
# 1. Install backend deps (uses uv)
cd backend && uv sync && cd ..
# 2. Install frontend deps
cd frontend && npm install && cd ..
# 3. (one-time, per clone) Enable the pre-push hook
git config core.hooksPath scripts/hooks
# 4. Run both servers (backend :8765, frontend :5173)
./scripts/dev.sh
# 5. (optional) Seed a multi-session / multi-window tmux server for local testing
./scripts/seed-tmux.shOpen http://localhost:5173.
The pre-push hook runs the same gates as CI (ruff format/check, ty, pytest, tsc, vitest, vite build, WCAG contrast audit) before any git push. Skip with git push --no-verify for emergencies.
All env vars are prefixed SWITCHBOARD_ and read from the launch environment or a backend/.env file (pydantic-settings).
| Variable | Default | Purpose |
|---|---|---|
SWITCHBOARD_HOST |
127.0.0.1 |
Bind address. Anything off-loopback flips on auth automatically. |
SWITCHBOARD_PORT |
8765 |
Backend port. The frontend dev server defaults to 5173. |
SWITCHBOARD_AUTH_REQUIRED |
(auto) | Force auth on/off — overrides the loopback heuristic. |
SWITCHBOARD_ANTHROPIC_API_KEY |
(none) | Enables ✨ auto-rename. Falls back to the standard ANTHROPIC_API_KEY. |
SWITCHBOARD_ANTHROPIC_MODEL |
claude-haiku-4-5 |
Model used by auto-rename. |
SWITCHBOARD_ANTHROPIC_CAPTURE_LINES |
80 |
How much pane scrollback to feed the model per window. |
SWITCHBOARD_CLAUDE_PROJECTS_DIR |
~/.claude/projects |
Where Claude Code writes JSONL session logs, sourced for the usage pill. |
SWITCHBOARD_USAGE_SCRAPE_ENABLED |
true |
Toggle the periodic claude /usage scrape (plan-% meters). |
SWITCHBOARD_IDE_CMD |
code |
Editor binary launched for clicked file paths. Must be on the GUI allowlist (code-insiders, cursor, subl, idea, pycharm, webstorm, rubymine, goland, rider, clion, phpstorm). |
SWITCHBOARD_PANE_CAPTURE_LINES |
200 |
Lines fetched per capture-pane for parser + previews. |
SWITCHBOARD_PASTE_IMAGE_MAX_BYTES |
10 MiB |
Cap on /api/paste-image uploads. |
Switchboard can read your panes and inject keystrokes — treat the endpoint like a shell.
Loopback mode (default). Bound to 127.0.0.1, so only processes on your machine can reach it. No token required — zero friction. Two protections still apply: the Host header must match a loopback allowlist (defeats DNS-rebinding from a malicious web page), and mutating requests need a double-submit CSRF cookie+header.
Exposed mode. If you bind to a non-loopback host (SWITCHBOARD_HOST=0.0.0.0, a LAN IP, etc.) auth turns on automatically. A random token is generated on first run and stored at ~/.switchboard/token (mode 0600). On startup the console prints a bootstrap URL — http://host:port/?token=… — open it once and the backend swaps the token for an HttpOnly session cookie. API clients can alternatively send Authorization: Bearer <token>.
Override the auto-detection with SWITCHBOARD_AUTH_REQUIRED=true|false. Rotate the token via POST /api/auth/regenerate (this invalidates existing session cookies).
If you find a way to bypass the loopback Host check or the exposed-mode auth flow, please don't open a public issue. Report it privately via GitHub Security Advisories or email [email protected].
Include:
- a clear description and the conditions to trigger it,
- the Switchboard commit SHA and your OS / Python / Node versions,
- a PoC if you have one.
You'll get an acknowledgement within a few days. Once a fix lands, the advisory is published and credits the reporter.
flowchart LR
Browser["<b>Browser</b><br/>React 18 · Vite<br/>xterm.js"]
subgraph Backend["<b>Backend</b> — FastAPI · uvicorn"]
direction TB
State["state poller<br/>(single-flight)"]
Stream["pane WS stream<br/>(pipe-pane → FIFO)"]
Parser["claude_parser<br/>(agents · recap · prompts)"]
Usage["claude_usage<br/>(JSONL + /usage scrape)"]
Auth["auth + CSRF middleware"]
end
Tmux["tmux server<br/>(libtmux)"]
Panes["agent / shell panes"]
Anthropic["Anthropic SDK<br/>(claude-haiku-4-5)"]
Files["~/.claude/projects/*.jsonl"]
Browser <-->|HTTPS + WebSocket| Backend
Backend -->|capture-pane · send-keys · pipe-pane| Tmux
Tmux ---|hosts| Panes
Usage -.->|reads| Files
Backend -.->|opt-in: ✨ auto-rename| Anthropic
- Frontend — single-page React app served by Vite in dev, a static bundle behind any reverse proxy in prod. Live state polled at an adaptive interval (faster while a modal is open or an agent is waiting); pane bytes stream over a per-pane WebSocket.
- Backend — FastAPI app. State is a single-flight
tmux ls/list-windows/list-panespass parsed into typed Pydantic models. Pane streaming usestmux pipe-paneinto a FIFO and forwards bytes to the WS; reconnects re-attach to the same FIFO without losing scrollback. - Agent detection —
services/claude_parser.pyreads the last ~200 lines of each pane, finds spinner glyphs, the⏺/●recap marker, prompt boxes, and free-form open questions likeShould I commit?. No Claude binary required. - Auto-rename —
services/anthropic_client.pyis lazy-imported so the server boots without a key./api/auto-rename/*returns503when disabled, and the frontend hides the ✨ button.
backend/ FastAPI + libtmux service (uv-managed)
src/switchboard/
routers/ HTTP + WS endpoints
services/ tmux, claude_parser, claude_usage, anthropic_client, pane_stream
config.py / schemas.py / main.py / security.py / auth.py
tests/ pytest (~280 tests)
frontend/ React 18 + TypeScript + Vite SPA
src/
components/ WindowCard, Kanban, GridView, ListView, TerminalModal, CommandPalette, TemplatesModal, SearchModal, AutoRenameModal, NeedsStrip, …
components/docs/ in-app docs diagrams (callouts)
lib/ settings, filter, status, suggestLayout, usePins, usePresets, xtermThemes, xtermStreamRewriter, …
api/ client + polling
vitest (~488 tests)
scripts/
dev.sh launches both servers in tmux panes
seed-tmux.sh populates a multi-session tmux server for local UI work
hooks/ pre-push hook (CI gates locally)
a11y/contrast_audit.py per-theme WCAG audit, also wired into CI
screenshots/capture.ts README capture script (Playwright + tsx)
docs/
screenshots/ README hero shots
# Backend
cd backend
uv run pytest # ~280 tests
uv run ruff format --check . && uv run ruff check . && uvx ty check
# Frontend
cd frontend
npm run typecheck && npm run test && npm run build
# WCAG contrast audit
python3 scripts/a11y/contrast_audit.py --check-sync
python3 scripts/a11y/contrast_audit.py --rule focus --rule hover --rule disabled --rule selection --quiet
# Regenerate README screenshots (requires both dev servers running on localhost)
cd frontend && npx tsx ../scripts/screenshots/capture.tsCI runs the same gates on every push (.github/workflows/ci.yml).
Issues and PRs welcome. A few practical notes:
- One-time setup:
git config core.hooksPath scripts/hooksenables the pre-push hook that runs the same gates as CI (ruff format/check, ty, pytest, tsc, vitest, vite build, WCAG audit). Catch failures locally instead of round-tripping through CI. - Tests first — every backend module has a
tests/test_*.py; frontend libs and components have colocated.test.ts(x). Add coverage for new behavior; refactors stay green or earn a test. - Commit style — Conventional Commits (
type(scope): subject). Recent history usesfeat,fix,docs,refactor,test,style,sec. - Worktree workflow —
scripts/wt -b <branch>creates an isolated worktree under.worktrees/(gitignored). Handy when bouncing between branches without disrupting a running dev server. - One PR per logical change, targeting
main. Merge commits keep per-commit history readable — squash isn't used.
MIT. See LICENSE.





