{"id":51251941,"url":"https://github.com/mrshu/muxboard","last_synced_at":"2026-06-29T07:32:18.346Z","repository":{"id":366236164,"uuid":"1275556470","full_name":"mrshu/muxboard","owner":"mrshu","description":"Stream Deck+ plugin that surfaces cmux coding-agent panes needing attention (Claude Code, Codex, Pi) on the 8 keys, with CodexBar quota/limits on the LCD strip.","archived":false,"fork":false,"pushed_at":"2026-06-28T07:37:22.000Z","size":1194,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-28T09:15:48.701Z","etag":null,"topics":["ai-agents","claude-code","cmux","codex","codexbar","coding-agents","dashboard","developer-tools","elgato","elgato-stream-deck","llm","macos","productivity","stream-deck","stream-deck-plugin","streamdeck","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mrshu.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-20T21:11:19.000Z","updated_at":"2026-06-28T07:37:26.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mrshu/muxboard","commit_stats":null,"previous_names":["mrshu/muxboard"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/mrshu/muxboard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrshu%2Fmuxboard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrshu%2Fmuxboard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrshu%2Fmuxboard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrshu%2Fmuxboard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mrshu","download_url":"https://codeload.github.com/mrshu/muxboard/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrshu%2Fmuxboard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34918101,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-29T02:00:05.398Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agents","claude-code","cmux","codex","codexbar","coding-agents","dashboard","developer-tools","elgato","elgato-stream-deck","llm","macos","productivity","stream-deck","stream-deck-plugin","streamdeck","typescript"],"created_at":"2026-06-29T07:32:17.657Z","updated_at":"2026-06-29T07:32:18.336Z","avatar_url":"https://github.com/mrshu.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Muxboard: a Stream Deck+ dashboard for cmux AI coding agents\n\n\u003e Monitor your [cmux](https://cmux.io) AI coding agents (Claude Code, Codex, Pi)\n\u003e from an Elgato Stream Deck+: which agents need attention show on the keys, and\n\u003e your CodexBar usage limits show on the LCD.\n\n![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)\n![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey.svg)\n![Stream Deck+](https://img.shields.io/badge/device-Stream%20Deck%2B-black.svg)\n![Node.js ≥ 20](https://img.shields.io/badge/node-%E2%89%A5%2020-43853d.svg)\n\n![Muxboard dashboard](docs/images/dashboard.png)\n\nMuxboard turns the 8 keys of an Elgato Stream Deck+ into a queue of\n[cmux](https://cmux.io) panes whose coding agents (Claude Code, Codex, Pi, or any\nother) have finished, failed, gotten blocked, or are waiting for your input. The\nLCD touch strip shows CodexBar usage: session and weekly quota with pace, plus\nspend and tokens per provider.\n\nThe newest attention item is key 1 (top-left); the queue fills left-to-right,\ntop-to-bottom:\n\n```\n1 2 3 4      key 1 = newest attention item\n5 6 7 8      key 8 = 8th newest\n```\n\nPress a key to bring cmux to the foreground and jump straight to that\nworkspace/surface. Empty slots render muted. When cmux or CodexBar is\nunreachable, the display degrades gracefully and the rest keeps working:\n\n![Offline state](docs/images/dashboard-offline.png)\n\n## Install\n\nmacOS, with Node.js, the [Elgato Stream Deck app](https://www.elgato.com/stream-deck),\nand [cmux](https://cmux.io) installed:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/mrshu/muxboard/main/scripts/setup.sh | bash\n```\n\nIt downloads the latest packaged plugin from\n[Releases](https://github.com/mrshu/muxboard/releases/latest), installs it and the\n8-key + 4-dial profile, and checks that cmux automation mode is enabled (see\n[Requirements](#requirements)). Then open the Stream Deck app and pick the Muxboard\nprofile. To build from source instead, see [Quick start](#quick-start).\n\n## How it works\n\n| Surface | Shows | Source |\n| --- | --- | --- |\n| 8 keys | Attention queue: agent glyph, status, repo, age | `cmux list-notifications --json` |\n| LCD strip (4×200×100) | One CodexBar provider per segment: session + weekly quota with pace, spend + tokens | `codexbar serve` HTTP |\n| 4 dials | Scroll, filter, quota view, refresh | local state |\n\n\u003e Required cmux setting. cmux's control socket rejects processes outside a\n\u003e cmux session by default (`socketControlMode: cmuxOnly`), and the Stream Deck\n\u003e app launches plugins outside any session. Set cmux Settings → Automation →\n\u003e Socket Control Mode → Automation (then fully quit and relaunch cmux) so the\n\u003e plugin is accepted. Without it, the keys stay on the muted \"cmux offline\"\n\u003e state. See [Requirements](#requirements).\n\n- Keys are assigned to physical slots newest-first, with the panes that need you\n  pinned ahead of those actively working. Agent, status, and age are fused from\n  several cmux signals (no terminal scraping); see\n  [How a pane's state is derived](#how-a-panes-state-is-derived).\n- Tapping a key runs `cmux open-notification --id \u003cuuid\u003e`, which focuses the\n  workspace + surface and marks the row read (it does not dismiss it).\n- Long-pressing a key (hold ~0.6s) runs `cmux dismiss-notification --id \u003cuuid\u003e`,\n  removing it from the queue (\"seen it, nothing further\") without switching to\n  cmux. It fires while you're still holding (you get a ✓); releasing does nothing\n  more. The key clears on the next poll.\n- The LCD shows one segment per CodexBar provider, auto-discovered from CodexBar\n  rather than a hardcoded list. Each segment carries the provider name in\n  CodexBar's brand color, the session and weekly gauges with their reset times,\n  and a footer with today's spend and tokens, so all your providers are visible\n  at once.\n- Each gauge also carries a calm **pace** marker, comparing how much quota you've\n  used against how far through the window the clock is: a faded same-hue\n  extension toward where you \"should\" be when you're under the clock (in reserve,\n  banking headroom), or a coral cap past it when you're over (in deficit). By\n  default the row number is percent remaining; rotating dial 3 flips it to the\n  signed pace delta (`+12%` green = reserve, `−8%` coral = deficit).\n\n### Dials (Stream Deck+)\n\n| Dial | Rotate | Press |\n| --- | --- | --- |\n| 1 | Scroll the queue (when \u003e 8 items) | Jump to newest item |\n| 2 | Cycle filter: all → claude → codex → pi | Reset filter to all |\n| 3 | Toggle the quota number: remaining% ↔ pace (reserve/deficit) | Open CodexBar `/usage` |\n| 4 | Rotate the LCD provider window (when \u003e 4 providers) | Force refresh both polls |\n\n## Requirements\n\n- Node.js ≥ 20 (developed on v26).\n- cmux on your `PATH` (or set `cmuxBin`). Verified against cmux 0.64.16. To\n  enable automation: cmux Settings → Automation → Socket Control Mode →\n  Automation (or add `\"automation\": { \"socketControlMode\": \"automation\" }` to\n  `~/.config/cmux/cmux.json`), then fully quit and relaunch cmux. This is\n  required for the keys to work; without it cmux rejects the plugin. Verify with\n  `cmux capabilities | grep access_mode` (should read `\"automation\"`).\n- cmux agent hooks, for accurate live working/waiting state. The state and age\n  on each key come from cmux's agent event stream, which cmux emits from agent\n  hooks (injected automatically by cmux's Claude wrapper, for other agents run\n  `cmux hooks setup`). They are not required to launch, but without them the keys\n  fall back to the notification feed plus process CPU and can read stale (a wrong\n  \"waiting\" or a frozen age) until the agent session restarts. If a long-running\n  session stops emitting them, restart it (or `cmux hooks setup`) to resume.\n  Check the feed is live: `cmux events --limit 5` should show recent\n  `agent.hook.*` rows while an agent works. See\n  [How a pane's state is derived](#how-a-panes-state-is-derived).\n- CodexBar for the LCD: run `codexbar serve --port 17777`. Muxboard defaults to\n  17777 (keeping CodexBar's own default 8080 free). Optional; the keys work\n  without it.\n- Stream Deck+ hardware and the free\n  [Elgato Stream Deck desktop app](https://www.elgato.com/stream-deck) to run the\n  plugin on the device. The app is what launches the plugin process.\n\n\u003e You can review the full visuals and verify all transforms without the\n\u003e hardware or the desktop app. See _Headless preview_ below.\n\n## Quick start\n\n```bash\nnpm install\nnpm test          # unit tests over the core transforms\nnpm run validate  # prints the 8-key layout + LCD summary, asserts acceptance\nnpm run preview   # renders out/dashboard.png (+ dashboard-offline.png)\n```\n\n### Headless preview\n\n`npm run preview` rasterizes the exact key + LCD SVGs from the test fixtures to\n`out/*.png` via `@resvg/resvg-js`, so you can see precisely what the device will\nshow with no Stream Deck+ and no desktop app.\n\n### Run on the device\n\n1. Enable cmux automation mode (see [Requirements](#requirements)) and relaunch\n   cmux.\n2. Install the Elgato Stream Deck desktop app.\n3. Build + link the plugin and start CodexBar:\n   ```bash\n   npm run dev\n   ```\n4. Install the device profile (places all 8 keys + 4 dials, no dragging):\n   ```bash\n   # quit the Stream Deck app first\n   npm run install-profile\n   # reopen the Stream Deck app, then pick the \"Muxboard\" profile from the\n   # profile dropdown at the top of the window\n   ```\n   Keys read cmux directly; the LCD reads CodexBar.\n\n\u003e Why a separate install step? Elgato's profile _importer_ rejects\n\u003e programmatically-built `.streamDeckProfile` files (\"content corrupted\") on\n\u003e recent macOS builds, so the plugin can't auto-apply a bundled profile.\n\u003e `install-profile` sidesteps the importer by writing the profile straight into\n\u003e the app's profile store (ProfilesV3) in its native format, keyed to your\n\u003e connected Stream Deck+. See [Architecture](#the-device-profile).\n\n## The cmux notification contract\n\nMuxboard is driven entirely by cmux notifications: agents make a pane \"need\nattention\" by emitting one. cmux already does this for built-in agents; for\ncustom agents, emit a notification (e.g. from an agent hook) shaped like the\nrows returned by `cmux list-notifications --json`:\n\n```json\n{\n  \"id\": \"015D0B50-...\",           // uuid; used as the focus/open key\n  \"title\": \"Claude Code\",         // agent → claude | codex | pi | unknown\n  \"subtitle\": \"\",\n  \"body\": \"Claude is waiting for your input\",   // status (see mapping)\n  \"is_read\": true,\n  \"workspace_id\": \"6ECA42AE-...\", // required\n  \"surface_id\": \"4F5A8945-...\",   // focused on press\n  \"tab_title\": \"RCJ Scoreboard\",  // shown as the repo/short name\n  \"created_at\": \"2026-06-20T11:59:46Z\"  // sort key, newest-first\n}\n```\n\nAgent is detected from the running process: Muxboard reads cmux's\n`top --processes` `coding_agents` (matched to the workspace by PID), so a codex\nCLI in a pane named `fieldtheory-cli` is still identified as codex. If the\nprocess can't be resolved, it falls back to matching the title/tab name\n(`claude`/`codex`/`pi`), then the optional `agentAliases` override, else\n`unknown`.\n\nStatus is mapped from `body`, strongest signal first:\n\n| Status | Body contains (any) | Treatment |\n| --- | --- | --- |\n| `failed` | fail, failed, error, crashed, exception | strongest (red border) |\n| `blocked` | permission, approve, blocked, denied, confirm | strong (amber) |\n| `waiting` | waiting, awaiting, input, ready for, your turn | strong (yellow) |\n| `finished` | done, finished, complete, completed | normal (teal) |\n| `waiting` | waiting, input, and anything else (a notification means the pane wants you) | strong (yellow) |\n\nNotes:\n\n- Rows missing `id` or `workspace_id` are dropped (never fatal).\n- Read notifications stay on the board, demoted. cmux flips `is_read` when you\n  merely *see* a notification — not when you resolve it — and muxboard's own\n  `open-notification` marks it read too, so a read row can still need you.\n  Dropping read rows would hide genuine attention, so instead a read\n  `failed`/`blocked` is demoted to `waiting`: the key stays but loses the urgent\n  badge and the front-pin. Live `Needs` status re-flags genuine attention.\n  Notifications are collapsed to one per workspace (newest wins), so each repo\n  occupies a single key showing its current state.\n- An explicit \"clear notifications\" in cmux is honored live: the `cmux events`\n  stream emits `notification.clear_requested` (carrying `--tab=\u003cworkspace\u003e`), and\n  any key that fired at or before that clear is dropped immediately, ahead of the\n  next poll — even one that was still unread. A later prompt (a re-ask) survives,\n  since its timestamp is past the clear.\n- To emit one yourself: `cmux notify --title \"Codex CLI\" --body \"Task failed: ...\"`\n  (run inside the target workspace, or pass `--workspace`).\n\n## Orca support\n\nMuxboard also surfaces [Orca](https://onorca.dev) worktrees alongside cmux\npanes on the same keys. It polls `orca worktree ps --json` and derives the key\nfrom the worktree's primary agent **`state`**, not the worktree `status` — the\nlatter is just a terminal-liveness flag (PTY alive → `active`) that does not\nroll up the agent lifecycle, so a finished or question-blocked agent still\nleaves the worktree `active`. By agent state: `waiting`/`blocked` (a permission\nprompt or an AskUserQuestion) shows as a needs-input key; `working` as a working\nkey that sinks to the end; `done` as finished (or failed when interrupted), but\nonly while the worktree is **unread** so an already-seen result doesn't linger.\nEach key carries a small badge — the Orca mark or a cmux monogram — so you can\ntell the two apart.\n\nOrca is **auto-detected**: the poller starts only when an Orca runtime is\nreachable (`orca status`), so cmux-only users see no change. Set\n`enableOrca: true|false` in the plugin's global settings to force it on or off,\nand `orcaBin`/`orcaPollMs` to tune the binary path and cadence.\n\nPressing an Orca key brings Orca forward and jumps to the worktree's most\nrecent terminal (`orca terminal focus`). Orca has no dismiss primitive, so a\nlong-press focuses the worktree too (which clears its unread in Orca).\n\n## How a pane's state is derived\n\nA key shows a status (working, waiting, permission, or failed) and an age.\nNeither comes from a single cmux field; cmux's notifications, title spinner, and\nagent state each tell a partial, often-stale story. Muxboard fuses several\nsignals so a key reflects what is actually true, which in practice is frequently\nmore accurate than any one cmux surface on its own. Each signal is best-effort\nand degrades to the next when unavailable.\n\n1. Queue membership and the reason come from `cmux list-notifications`. A\n   notification puts a pane on a key; the reason (`failed`, `blocked`, `waiting`,\n   `finished`) is mapped from structured fields, never by scraping the free-form\n   body (see the table above). cmux flips `is_read` when you see a notification,\n   not when you resolve it, so a read (`is_read: true`) permission/failure is\n   demoted to `waiting` — the key stays visible but loses its urgent badge —\n   rather than dropped, which would hide things that still need you. An explicit\n   \"clear notifications\" in cmux is honored live via the event stream\n   (`notification.clear_requested`), removing the key at once.\n\n2. Activity (working vs waiting) comes from the `cmux events` stream. Muxboard\n   prefers cmux's own computed verdict (`set_status`: `Running`, `Idle`, `Needs`),\n   the same state that drives cmux's UI. For workspaces cmux doesn't publish a\n   status for, it derives state from raw agent hooks (`UserPromptSubmit` and\n   `PreToolUse` → working; `Stop` and `SessionEnd` → idle; `Notification` and\n   `AskUserQuestion` → needs). A working pane shows `● working` and sinks below\n   the panes still waiting on you, since it no longer needs you. The title spinner\n   glyph is the fallback when the stream is unavailable.\n\n3. Age is the time since the current state began (the transition `occurred_at`),\n   so a key reads \"working for 2m\" or \"waiting since 09:31\" rather than the age of\n   a stale, lingering notification. It falls back to the notification `created_at`.\n\n4. A busy command counts as working, from `cmux top`. An agent can finish its turn\n   and return to waiting while a command it launched keeps running, so a workspace\n   whose process CPU is at or above `busyCpuPercent` is treated as working even\n   after the agent yields, with a short hysteresis window so a bursty command\n   doesn't flicker. An explicit \"needs you\" still wins over busy, so permission\n   prompts stay visible.\n\nGrid priority, front to back: failed, then permission, then needs-input (cmux's\n\"Needs\" status, shown as a prominent `◆ NEEDS YOU` badge), then plain waiting,\nthen actively-working last. The newest item is key 1. Actively-working panes are\nlisted even without a notification; they land at the very end, so the panes that\nneed you always stay up front, and pressing one focuses its workspace.\n\nKnown limitation: a Claude agent waiting on its own background subagent does that\nwork in-process, where cmux reports no spinner, no `set_status`, and low CPU, so\nthe pane reads `waiting`. The only ground truth is the agent's own terminal\nscreen, which Muxboard deliberately does not scrape. That narrow case (an agent\nblocked on its own background task) is the one state no cmux signal exposes.\n\n## CodexBar contract\n\nMuxboard polls `codexbar serve` (default `http://127.0.0.1:17777`). It queries\neach provider individually (`/usage?provider=all` returns nothing) and handles\nboth payload shapes CodexBar emits:\n\n- Codex exposes `primary`/`secondary` windows at the top level.\n- Claude and others nest them under `usage`.\n\nEach window provides `usedPercent`, `resetsAt`, `windowMinutes`, and a\n`resetDescription`; `primary` is the session (5h) and `secondary` the weekly (7d)\nwindow. The pace marker/number is derived locally from `resetsAt` + `windowMinutes`\n(elapsed-vs-used); windows with no time bounds (e.g. an \"Unlimited\" weekly) show\nno pace. Today's spend and token count come from `/cost?provider=\u003cp\u003e` (a daily\nseries; amounts are treated as USD since CodexBar emits no currency code). A\nprovider that returns an `{ error }` object (e.g. an expired token) is shown as\nunavailable. Data older than 2× the poll interval is flagged `STALE`.\n\n## Configuration\n\nStored in the plugin's global settings; all fields have safe defaults\n(`src/config.ts`):\n\n| Field | Default | Notes |\n| --- | --- | --- |\n| `cmuxBin` | `\"cmux\"` | Binary path or name (spawned directly) |\n| `codexbarBaseUrl` | `\"http://127.0.0.1:17777\"` | `codexbar serve --port 17777` base URL |\n| `codexbarProviders` | `[]` | Optional allow-list/order; empty = auto-discover all |\n| `cmuxPollMs` | `1500` | cmux poll interval |\n| `codexbarPollMs` | `45000` | CodexBar poll interval |\n| `agentAliases` | `{}` | Manual override (name substring → agent); process detection is primary |\n| `busyCpuPercent` | `40` | Workspace CPU% (from `cmux top`) at/above which a running command counts as \"working\" |\n\n## Architecture\n\n```\n  Stream Deck+ plugin ── spawns ──► cmux CLI ──► cmux socket (automation mode)\n        └── TCP ──► codexbar serve (LCD usage)\n\nsrc/\n  plugin.ts          entry: connect, load config, start services\n  runtime.ts         shared store/clients/services + macOS foregrounding\n  config.ts          defaults + defensive resolveConfig()\n  core/              dependency-free, unit-tested, no SDK import\n    types.ts\n    cmux/            client (CLI wrapper), normalize (agent/reason), sort,\n                     eventStatus (live state from the event stream + CPU)\n    codexbar/        client (HTTP), normalize (dual-shape + error + cost)\n    render/          palette, format, keyRender (SVG), lcdRender (SVG)\n    services/        store, cmux/codexbar poll loops, cmuxEvents (event stream)\n  actions/           attentionKey (8 keys), dialStrip (4 dials): thin SDK glue\nscripts/             preview / validate / gen-icons / install-profile / dev.sh\ntest/                fixtures + node:test suite\ncom.mrshu.muxboard.sdPlugin/   manifest, layouts, imgs, built bin\n```\n\n### Why automation mode is required\n\ncmux's control socket does an ancestry check: under the default\n`socketControlMode: cmuxOnly` it only accepts processes spawned inside a cmux\nsession. The Stream Deck app launches plugins via launchd, outside any session,\nso a direct `cmux` call is rejected with \"broken pipe\". Setting\n`socketControlMode: automation` removes the ancestry check for local processes of\nthe same user, which is what lets the plugin spawn cmux directly. This is the\napproach the [gonzaloserrano/streamdeck-cmux](https://github.com/gonzaloserrano/streamdeck-cmux)\nplugin also uses. (Note: on some builds and macOS versions the mode reportedly\ndoesn't take effect; see upstream issues\n[#1864](https://github.com/manaflow-ai/cmux/issues/1864) /\n[#3282](https://github.com/manaflow-ai/cmux/issues/3282), and verify with\n`cmux capabilities | grep access_mode`.)\n\n### The device profile\n\n`scripts/install-profile.mjs` writes a Muxboard profile straight into the Stream\nDeck app's `ProfilesV3` store (the app's own V3 format, keyed to the connected\nStream Deck+'s device id), placing the Attention Slot action on all 8 keys and\nthe Muxboard Dial on all 4 dials. Run it with the app closed\n(`npm run install-profile`); the app picks it up on next launch and you select it\nfrom the profile dropdown.\n\nThis deliberately bypasses the app's profile importer, which rejects\nprogrammatically-built `.streamDeckProfile` archives as \"content corrupted\" on\nrecent macOS builds (confirmed across clean/stored zips and deterministic UUIDs).\nElgato's only supported way to produce an importable profile is to build it in the\napp UI and _Export_ it, so we skip import entirely and write the store format the\napp itself uses.\n\nRendering is SVG-first: Stream Deck's `setImage` accepts SVG data-URIs, so keys\nand LCD segments are plain strings, with no native canvas dependency and fully\ntestable. Each action caches the last SVG per instance to debounce redundant\ndraws (anti-flicker). Polls never overlap, and last-good data is retained on\nfailure so a transient outage never blanks the display.\n\n## Testing\n\n```bash\nnpm test        # 30 unit tests: normalization, slotting, dual-shape codexbar,\n                # SVG structure, store dial machines, service offline retention\nnpm run validate\nnpm run typecheck\n```\n\n## Troubleshooting\n\n- Plugin won't start or crash-loops on first install. The Stream Deck app runs\n  Node plugins with its own managed Node.js runtime, downloaded on demand. If\n  it's missing (`NodeJS/manifest.json not found` in\n  `~/Library/Logs/ElgatoStreamDeck/StreamDeck.log`), fully quit and relaunch the\n  Stream Deck app so it fetches the runtime, then restart the plugin.\n- `require is not defined` / exit code 1. The bundle must be CommonJS with a\n  `.cjs` extension (this repo's `package.json` is `\"type\":\"module\"`). `npm run\n  build` already emits `bin/plugin.cjs`; the manifest's `CodePath` points at it.\n- Changed the manifest? Re-link. A plugin restart does not re-read the manifest.\n  Run `npx streamdeck link com.mrshu.muxboard.sdPlugin` again (or restart the\n  Stream Deck app) after editing it.\n- LCD shows \"CodexBar off\". Ensure `codexbar serve --port 17777` is running and\n  that `codexbarBaseUrl` matches the port.\n- Keys are blank or show \"cmux offline\". cmux is rejecting the plugin. Confirm\n  `cmux capabilities | grep access_mode` reads `\"automation\"` (not `cmuxOnly`).\n  If it still says `cmuxOnly`, the setting hasn't taken; set Socket Control Mode\n  to Automation and fully quit and relaunch cmux (a reload is not enough). The\n  plugin log (`com.mrshu.muxboard.sdPlugin/logs/`) will show `broken pipe` when\n  rejected.\n- No Muxboard keys, or the profile is missing. Run `npm run install-profile` with\n  the Stream Deck app closed, reopen it, and select the Muxboard profile from the\n  dropdown. (The app's profile importer rejects bundled profiles as \"content\n  corrupted\" on recent macOS builds, so the profile is written directly into the\n  app's store instead.)\n- A key is stuck on a stale state (wrong \"waiting\", or an age that won't move\n  even though the agent is active). cmux's agent hook feed has gone quiet for\n  that session, so Muxboard has no live signal and falls back to the last\n  notification. Confirm it: `cmux events --limit 5` shows recent UI rows but no\n  `agent.hook.*` while an agent works. See the FAQ entry below; the usual cause\n  is a PATH issue where cmux's `claude` wrapper is shadowed.\n\n## FAQ\n\n### Claude panes show stale/wrong state (or don't appear), but codex works\n\nThis is almost always a PATH problem, and it's upstream of Muxboard: cmux's\n[#5796](https://github.com/manaflow-ai/cmux/issues/5796). cmux injects Claude's\nhooks through a `claude` wrapper shim on PATH. If Claude Code's own\n`~/.local/bin/claude` (created/updated by its auto-installer) sits earlier on\nPATH, it shadows the shim, so `claude` runs the real binary and no hooks fire.\nCodex is unaffected because its hooks are a file (`~/.codex/hooks.json`), not a\nPATH shim. Diagnose:\n\n```bash\nwhich claude        # if it's ~/.local/bin/claude (not a .../cmux-cli-shims/... path), the shim is shadowed\ncmux events --limit 5   # codex emits agent.hook.* while working; Claude emits none\n```\n\nFix: make cmux's shim win on PATH by re-prepending its shim dir after your PATH\nsetup runs, then start your Claude sessions in a fresh cmux terminal (pre-existing\nsessions won't recover). Add to the end of your shell config:\n\n```fish\n# ~/.config/fish/config.fish\nfor d in $PATH\n    if string match -q '*cmux-cli-shims*' -- $d\n        set -gx PATH $d $PATH\n        break\n    end\nend\n```\n\n```bash\n# ~/.zshrc (or ~/.bashrc with the loop adapted)\nfor __d in ${(s/:/)PATH}; do\n  if [[ \"$__d\" == *cmux-cli-shims* ]]; then export PATH=\"$__d:$PATH\"; break; fi\ndone\nunset __d\n```\n\nAfter a fresh session, `which claude` should resolve to a `.../cmux-cli-shims/...`\npath. Verify hooks with `cmux events --limit 5`: you should now see\n`agent.hook.PreToolUse` while the agent works.\n\n### A pane shows \"working\" but with a stale-looking age\n\nWithout the agent-hook stream, Muxboard can't know exactly when work started, so\nthe age falls back to the last notification time. Once hooks fire (see above),\nthe age reflects the live activity. A CPU-bound command (build/test) is also\ndetected as working via `cmux top`; an agent merely waiting on remote inference\nhas no local signal.\n\n### Why is an active agent not on a key?\n\nMuxboard lists actively-working panes at the end of the queue, but only once cmux\nreports them as working (a live spinner / hook activity). A brand-new agent with\nno notification and no live \"working\" signal yet won't appear until it either\nneeds you or cmux marks it working.\n\n## Privacy \u0026 non-goals\n\n- Localhost only. The sole network call is to CodexBar on `127.0.0.1`.\n- No terminal scraping; only cmux's structured notification fields.\n- No destructive actions. Muxboard never dismisses cmux notifications, runs\n  commands inside agents, or sends approve/deny input. It reads and focuses.\n- No cloud and no database beyond plugin settings and an in-memory cache.\n\nMVP intentionally excludes: command execution, approve/deny buttons, non-Stream\nDeck+ devices.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrshu%2Fmuxboard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmrshu%2Fmuxboard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrshu%2Fmuxboard/lists"}