BlackBeltTechnology/pi-agent-dashboard: Real-time web dashboard for pi coding-agent sessions. Multi-session view, live chat mirroring, integrated terminal, diff viewer, pi-flows execution, and mobile-first remote control via mDNS or zrok tunnel. Coex
One browser tab to command an army of pi agents. Spawn parallel sessions, watch reasoning live, attach OpenSpec changes, ship work — from your laptop or phone.
macOS requirement: macOS 13 (Ventura) or newer — Intel and Apple Silicon both supported. macOS 12 (Monterey) and older are not supported (Electron 44 dropped macOS 12); users on those versions keep their currently installed version and are not offered the newer update.
On first launch a setup wizard walks you through mode selection (standalone vs. power-user), API key / OAuth sign-in, and recommended extensions. The standalone mode bundles Node.js and auto-installs pi + dashboard + openspec into ~/.pi-dashboard/ — no terminal, npm, or Node.js required.
Picking the right macOS DMG: run uname -m in Terminal — arm64 means Apple Silicon (M1/M2/M3/M4), x86_64 means Intel. Or open Apple menu → About This Mac and read the chip name. Download the matching DMG; if you grab the wrong one macOS will refuse to launch the app with a "cannot be opened" error.
First-run unblocking (unsigned binaries):
macOS — the DMGs are not yet notarized. Either right-click PI-Dashboard.app → Open the first time, or clear all extended attributes from Terminal:
xattr -cr /Applications/PI-Dashboard.app
Use -cr (clear, recursive) rather than -d com.apple.quarantine — it's idempotent and won't print No such xattr: com.apple.quarantine when the attribute isn't there. That message is harmless; it just means quarantine was never set or already cleared.
Windows — SmartScreen warns on first launch. Click More info → Run anyway, or right-click the downloaded .exe / .zip → Properties → tick Unblock → OK before running. For ZIPs, unblock the archive before extracting.
Note: A future release will rename the macOS DMGs to PI-Dashboard-darwin-arm64-.dmg and PI-Dashboard-darwin-x64-.dmg (previously a single PI Dashboard.dmg was produced and silently overwrote one arch on each release). Direct download links pointing at the unsuffixed filename will 404 from that release onward; please link to the Releases page instead. See OpenSpec change fix-darwin-dmg-arch-collision.
B — pi package (recommended for CLI users)
If you don't have pi yet, you can install the dashboard directly via npm — pi/openspec/tsx ship as regular npm dependencies, so a single install brings everything in:
6. Start (foreground first time so you can see any errors)
pi-dashboard start
7. From the browser
start http://localhost:8000
C++ build tools are typically not required — node-pty ships a Windows x64 prebuild. Install Visual Studio Build Tools only if the prebuild fails to load. See docs/installation-windows.md for more detail (offline / tarball / nvm-windows caveats).
C — From source (contributors)
Before your first change, read CONTRIBUTING.md — features and bugfixes go through a spec-first pipeline (explore → plan → build → ship), not a plain branch-and-PR flow.
git clone https://github.com/BlackBeltTechnology/pi-agent-dashboard.git
cd pi-agent-dashboard
corepack enable # activates the pinned pnpm (packageManager field)
pnpm install
pnpm run build # one-time client build
pi install /path/to/pi-agent-dashboard # global
or: pi install -l /path/to/pi-agent-dashboard # project-local only
Use the local checkout as the global pi-dashboard command
By default, pi-dashboard on your PATH refers to whatever copy was installed globally (via npm i -g or the Electron bundle). To make it point at your working tree instead — so every edit is live and bridge auto-start uses your changes — link the workspace:
pnpm run link:local # symlinks pi-dashboard on PATH to packages/server/bin/pi-dashboard.mjs
pi-dashboard status
pnpm run unlink:local # restore (removes the global symlink)
The link survives across shells. Every invocation — including pi's bridge auto-spawn — runs packages/server/src/cli.ts via jiti, so you don't need to rebuild the server on edits. The client still requires pnpm run build (or pnpm run dev for HMR).
Windows note: symlink creation needs an admin shell or Windows Developer Mode enabled. Everything else works the same as POSIX.
To try the extension in a single pi session without registering it:
pi -e /path/to/pi-agent-dashboard/packages/extension/src/bridge.ts
Remove with pi remove /path/to/pi-agent-dashboard. Alternatively, add the package path directly to ~/.pi/agent/settings.json (global) or .pi/settings.json (project) under "packages": [...].
D — Docker (self-contained, no host install)
Run the whole ecosystem — server, pi agent, code-server, zrok, tmux, terminals — in one container. No host install of pi, Node, or any tool.
cd docker
cp .env.example .env # add ANTHROPIC_API_KEY (optional)
docker compose up -d --build
open http://localhost:8000
State persists in a named volume; API keys seed into auth.json on first run (or add providers later via the UI). Mount host projects at their identical absolute paths with PI_WORKSPACES="/abs/a:/abs/b" ./up.sh (auto-pinned on first run). The Electron desktop app can attach to a Docker-hosted server via the wizard's Remote mode — no local install. Full guide, volume performance profiles, dev mode, and external-gateway config: docker/README.md. For a disposable, fully isolated instance for manual browser QA (can't collide with a running host dashboard): docker/TESTING.md.
E — Claude Code / Claude Desktop plugin
Start the local dashboard on demand and drive pi sessions from Claude Code. Global install only with explicit consent. See packages/claude-plugin/README.md.
Features
Sessions & chat
Real-time session mirroring — all active pi sessions with live streaming messages
Bidirectional interaction — send prompts and commands from the browser
Session statistics — token counts, costs, model info, thinking level, context usage bar
Elapsed time tracking — live ticking counters on running operations, final duration on completed tool calls and reasoning blocks
Session spawning — launch new pi sessions from the dashboard (headless by default, or via tmux)
On-demand session loading — browse historical sessions with lazy-loaded content from pi session files
Force kill escalation — two-click Stop button; first click sends soft abort, second force-kills (SIGTERM → SIGKILL). Session preserved as "ended" for resume/fork.
Workspace & UI
Workspace management — organize sessions by project folder with pinned directories and drag-to-reorder
Command autocomplete — / prefix triggers a filtering dropdown
Mobile-friendly — responsive layout with swipe drawer, touch targets, and mobile action menus
Markdown preview — rendered markdown views with search, mermaid diagrams, syntax highlighting, and safe handling for raw HTML ref attributes
Local-image inlining + LaTeX math in chat — agents can reference local screenshots inline as !alt or !alt and they render in chat (the bridge inlines bytes via a streaming-safe pi-asset: channel — each unique image's bytes ride exactly once per session, no matter how many streaming chunks repeat the token). Math expressions — inline $x = \beta$ and display $$\sum_i^n i$$ (block-level) — are typeset via KaTeX. PNG / JPEG / GIF / WebP / SVG / AVIF / BMP supported with a 5 MB-per-image, 20 MB-per-message cap; oversized or unreadable references render as a visible placeholder rather than a broken-image glyph. The dashboard server adds zero new HTTP routes.
PromptBus architecture — unified prompt routing with adapters (TUI, dashboard, custom). Interactive dialogs (confirm/select/input/editor/multiselect) survive page refresh and server restart. Multiselect uses the bus-routed browser path exclusively (the dashboard MultiselectRenderer dialog) since pi 0.70's RPC mode has no working terminal-overlay primitive. First-response-wins semantics with cross-adapter dismissal.
Extension UI System (Phase 1) — extensions can declare slash-command-triggered modal UIs as data, without authoring React or importing an SDK. Listen on pi.events.on("ui:list-modules", probe) and push descriptors into probe.modules; the dashboard renders table / grid / form views with row actions, optional confirm-dialog gates, and MDI icons. Modules survive reconnect via the server-side cache; in pure-pi mode descriptors stay dormant. See openspec/specs/extension-ui-system/spec.md for the protocol contract.
pi-flows integration — live flow execution dashboard with agent cards, detail views, flow graph, summary, abort/auto controls. Launch flows and design new ones with Flow Architect, all from the browser. Fork decisions and subagent dialogs forwarded via PromptBus.
OpenSpec integration — browse specs, view archive history, manage changes, create new changes from the sidebar
Browser-based provider auth — sign in to Anthropic, OpenAI Codex, GitHub Copilot, Gemini CLI, and Antigravity from Settings. Enter API keys for other providers. Credentials saved to ~/.pi/agent/auth.json and live-synced to running sessions.
Custom LLM providers — add OpenAI-compatible, Anthropic-compatible, or Google Generative AI endpoints (Settings → Providers → LLM Providers). Test button verifies the base URL + API key before saving. Adding / editing / removing takes effect live in every running session — no restart.
Package management — browse, install, update, remove, and move pi packages between global and project scopes from a single rich-row UI used in both Settings and Pi Resources. Install dialog exposes a Local/Global radio when launched from a per-folder context. Search the npm registry for pi-package extensions/skills/themes; install from npm or git URL. Active sessions auto-reload after changes.
MCP endpoint — POST /mcp exposes dashboard sessions to MCP clients (Claude Desktop, Cursor) once the server runs. Every request needs a bearer credential, including localhost. Local pi sessions get it automatically: the bridge registers pi-dashboard with pi's built-in MCP (pi.registerMcpServer), no mcp.json write. External clients authenticate with a paired-device token. Requires pi ≥ 1.0.0.
Dev tools
Integrated terminal — full browser-based terminal emulator (xterm.js + node-pty) with ANSI colors, scrollback, and keep-alive
Diff viewer — side-by-side and unified diff views with file tree navigation.
Editor integration — open files in VS Code, Cursor, etc. directly from tool call cards
Networking & distribution
Network discovery — mDNS-based auto-discovery of other dashboard servers on the local network
Zrok tunnel — optional persistent public URL via reserved shares (see Configuration → Tunnel)
Chinese UI
PI Dashboard now includes a lightweight Simplified Chinese interface for the core operator workflow: onboarding, the session sidebar, chat composer, connection banners, Settings, provider setup, and package management.
The language selector lives in Settings → General → Interface. English remains the default for existing users, and the selection is saved in the browser. Deployments that want to start in Chinese can build the web client with:
VITE_PI_DASHBOARD_DEFAULT_LANGUAGE=zh-CN pnpm --filter @blackbelt-technology/pi-dashboard-web run build
This keeps plugin-provided dynamic content, package names, model names, and command output unchanged while making the main dashboard usable for Chinese-speaking operators out of the box.
Prerequisites
Only needed for Quickstart paths B and C. The Electron app (path A) bundles everything in standalone mode.
| Requirement | Why | Install |
|-------------|-----|---------|
| pi | The AI coding agent the dashboard monitors | npm i -g @mariozechner/pi-coding-agent |
| Node.js ≥ 22.19.0 | Server runtime. Node 22.0.0–22.18.x and 24.1.0–24.2.x refused (affected by nodejs/node#58515, crashes Fastify at startup). Cap < 27 for tested range. | nodejs.org |
| C++ build tools | Required by node-pty native addon for the integrated terminal | Xcode CLI Tools (macOS) / build-essential (Linux) |
Optional:
| Tool | Purpose | When needed |
|------|---------|-------------|
| tmux | Spawn new pi sessions in a tmux window | When spawnStrategy is "tmux" |
| zrok | Public tunnel with persistent URLs | When tunnel.enabled is true (default) |
Configuration
Config file:~/.pi/dashboard/config.json (auto-created with defaults on first run)
Tool overrides (machine-local):~/.pi/dashboard/tool-overrides.json — see Tool overrides
Settings UI: click the ⚙ gear icon in the sidebar header to edit all fields from the browser
| CLI flag | Env var | Config key | Default | Description |
|----------|---------|------------|---------|-------------|
| --port | PI_DASHBOARD_PORT | port | 8000 | HTTP + browser WebSocket port |
| --pi-port | PI_DASHBOARD_PI_PORT | piPort | 9999 | Pi extension WebSocket port |
| --dev | — | — | false | Development mode (proxy to Vite) |
| --no-tunnel | — | tunnel.enabled | true | Disable zrok tunnel |
| — | — | network.trustMode | decided at first boot | LAN admission. "pair": network match authenticates nothing, devices pair. "legacy": listed networks admitted without sign-in. See LAN access |
| — | — | autoStart | true | Bridge auto-starts server if not running |
| — | — | autoShutdown | false | Server shuts down when idle |
| — | — | shutdownIdleSeconds | 300 | Seconds idle before auto-shutdown |
| — | — | spawnStrategy | "headless" | Session spawn mode: "headless" or "tmux" |
| — | — | reattachPlacement | "always" | After a dashboard restart, where re-registering bridges land in folder lists. "always" (top), "streaming-only" (only mid-completion), "preserve" (legacy: keep prior drag order) |
| — | — | devBuildOnReload | false | Rebuild client + restart server on /reload |
| — | — | askUserPromptTimeoutSeconds | 300 | ask_user prompt timeout in seconds. ≤ 0 (e.g. -1) = wait indefinitely |
| — | PI_CODING_AGENT_SESSION_DIR | piSessionsDir | ~/.pi/agent/sessions | Pi sessions root the dashboard scans. See Pi sessions directory |
| — | — | push.enabled | false | Server push notifications (opt-in) |
| — | — | push.coalesceWindowMs | 30000 | Webhook per-(session, token) coalescing window. Clamped 5000–300000 |
| — | — | push.webPush.contactEmail | — | VAPID mailto: subject. Web Push disabled without it |
| — | — | push.fcm.serviceAccountPath | — | Path to Google service-account JSON (FCM) |
Push notifications: manage devices and webhooks under Settings ▸ Sessions ▸ Push notifications; details in "Push Notifications" in docs/architecture.md.
The bridge also honours PI_DASHBOARD_URL=ws://host:port to point at a remote server instead of localhost. For a remote server, port is the pi gateway port (default 9999), not the web UI port.
A remote bridge (one with PI_DASHBOARD_TOKEN set) also makes HTTP calls to the dashboard (ticket mint, identity check, trust query). It sends those to the dashboard's HTTP origin, resolved as:
PI_DASHBOARD_HTTP_URL (e.g. https://dash.example) when set. Only its origin is used. It applies to the host named in PI_DASHBOARD_URL; an invalid value (not http(s), or with credentials) is refused, never ignored.
Plaintext ws://host:: http://host:, where the HTTP port is PI_DASHBOARD_PORT → DASHBOARD_PORT → 8000. The bridge machine's own ~/.pi/dashboard/config.json is not read. If this machine also runs its own dashboard on a custom port, set PI_DASHBOARD_HTTP_URL instead.
wss://host[:port] or a port-less ws://host: the endpoint's own origin (a TLS proxy in front is assumed to route /api).
Breaking (narrow): a plaintext reverse proxy serving both /api and the bridge path on one explicit non-default ws://host: now needs PI_DASHBOARD_HTTP_URL (or PI_DASHBOARD_PORT=). The bridge log prints the resolved http origin (); bridge ticket wrong-origin means that origin is not the dashboard's HTTP listener.
Pi sessions directory
The dashboard is a separate process from pi, so it resolves one sessions root to scan. When pi's sessions live somewhere other than the default, point the dashboard at them via the following precedence (first match wins; blank values fall through, leading ~/ expands against $HOME):
PI_CODING_AGENT_SESSION_DIR — pi's session-dir env var, if inherited by the dashboard process.
PI_CODING_AGENT_DIR + /sessions — pi's agent-dir relocation env var.
~/.pi/agent/sessions — built-in default.
Omit piSessionsDir (or leave it blank) to fall through to the env vars and default. Per-invocation --session-dir flags on other pi processes are not observable to the dashboard and are out of scope.
| Key | Required | Description |
|-----|----------|-------------|
| auth.secret | No | JWT signing secret (auto-generated if omitted) |
| auth.providers | Yes | Map of provider → { clientId, clientSecret, issuerUrl? } |
| auth.allowedUsers | No | Allowlist: usernames, emails, or *@domain wildcards. Public providers (github, google, or an oidc issuer on a public IdP such as accounts.google.com / github.com / login.microsoftonline.com /common) refuse login when both this and auth.groupTiers are empty or unset. Providers with an issuer of your own (keycloak, a private oidc) allow every account at operate when empty, with a startup warning |
| auth.groupTiers | No | Config-file only. Map of IdP group → tier (observe / control / operate) for OIDC providers. Setting a non-empty valid map lifts the open-enrolment refusal on a public provider |
Supported providers:github, google, keycloak, oidc (generic OIDC with issuerUrl).
Callback URL: register https:///auth/callback/ in your OAuth provider settings. The tunnel URL is stable across restarts (reserved shares are auto-created).
Security note:/api/spawn-failures is reachable to any caller on deployments without auth; entries contain cwd paths. Enable auth before exposing via tunnel.
Approving a device pairing
A new device asks to pair; the dashboard shows a "A device wants to connect" dialog. Approving needs a host credential, so a plain localhost tab can be refused. The dialog lists what to do; the universal path is a terminal on the machine that runs the server:
pi-dashboard pair list # pending requests (never shows the code)
pi-dashboard pair approve [<pendingId>] # prompts for the 8-digit code shown on the device
pi-dashboard pair deny <pendingId>
pi-dashboard open [--url <origin>] # or authorize this browser tab, then press Approve again
Docker: docker exec pi-dashboard pair approve. The CLI must run as the same OS user as the server. Per-setup table: "How do I approve a pending device pairing request?" in docs/faq.md.
Local access (requireLocalProof)
Default requireLocalProof: true. A caller on the loopback address with no forwarding header (bare loopback) reads observe-tier REST and /v1/* only. Every other REST route and WebSocket needs local proof: a pi_dash_local cookie, the X-Pi-Local-Proof header from the CLI or bridge, the desktop app, or a signed-in/paired principal. Refusal: 403 network_not_allowed with reason: "local_proof_required".
Browser tab refused: run pi-dashboard open (opens the dashboard already authorized). Forwarded loopback (ssh -L, VS Code port forward, WSL): run pi-dashboard open --print on the server, then open the URL through a forward on the same port (the cookie is bound to the port).
Scripts on this host: pi-dashboard api [--data ] (mints proof per request). Raw curl to control routes is refused.
Opt-out: "requireLocalProof": false in config.json (live, no restart). Opting out re-exposes the control plane to every container on the host (Docker Desktop host.docker.internal, --network host, ssh -R, socat all look like bare loopback). Keep it on unless the host is single-user and no container or relay reaches 127.0.0.1.
Upgrade: the first start rotates the local token and auth.secret once, so everyone signs in again. Stop every dashboard instance before upgrading.
legacy — trustedNetworks / auth.bypassHosts entries admitted without sign-in, full access.
First boot writes the mode once (network.trustModeOrigin: "upgrade"): pair iff every entry loopback-only (127.0.0.0/8, ::1), else legacy (hostnames and match-all → legacy). Never re-derived from the list.
Switch: Settings → Security → Network access (live, no restart). Rollback: "Trust whole networks" or network.trustMode: "legacy".
In legacy, Settings → Access shows the count of devices reaching the dashboard by network trust only (/api/healthtrustPosture.networkOnly; counts, no IPs).
The dashboard auto-connects a zrok tunnel on start when tunnel.enabled is true. Install with brew install zrok (macOS) and run zrok enable to enrol — the dashboard reads zrok's own config (~/.zrok2/environment.json), no keys are stored in the dashboard. Reserved shares provide persistent URLs across restarts.
Shell output (!, !!, exec templates) reaches every tunnel viewer; only credential-vault values are masked. Do not print secrets in shell commands on a tunneled session — see FAQ.
OpenSpec background polling
Tune how often the server polls known directories for OpenSpec updates (openspec block):
| Key | Default | Range | Description |
|-----|---------|-------|-------------|
| pollIntervalSeconds | 30 | 5–3600 | How often each known directory is polled |
| maxConcurrentSpawns | 3 | 1–16 | Cap on concurrent openspec CLI invocations |
| changeDetection | "mtime" | "mtime" \| "always" | mtime skips unchanged proposals; always polls unconditionally |
| jitterSeconds | 5 | 0–60 | Per-directory phase offset so polls don't align on the same tick |
Live-reconfigurable via Settings → Advanced → "Background polling (OpenSpec)" or PUT /api/config — no server restart needed. See docs/architecture.md for the cost model.
Tool overrides
The dashboard resolves every external tool it calls (pi, pi-coding-agent, openspec, npm, node, tsx, git, zrok, pi-dashboard) through a single ToolRegistry. Each tool has an ordered strategy chain (override → managed install → bare-import / npm-global → PATH search), and every resolution records a diagnostic trail.
Inspecting and overriding — Settings → General → Tools shows every resolved tool, its source, and the trail. You can set a per-tool override path, rescan individually or all at once, and export the full diagnostic report.
The file is deliberately separate from config.json so machine-specific paths don't follow a dotfiles sync. Invalid overrides (path doesn't exist) are recorded in the trail and the registry falls through to the next strategy automatically.
MCP servers
Enable the MCP Client plugin (mcp-client) in Settings → Plugins to manage the MCP servers your pi sessions use. It uses pi's built-in MCP config (pi ≥ 1.0.0) — no pi-mcp-adapter.
Global surface — /settings/plugins/mcp-client: every server in pi's two layers with its provenance (Pi global, Pi folder), enable/disable, a per-server editor, and live state from pi mcp list --json. Writes land only in ~/.pi/agent/mcp.json ($PI_CODING_AGENT_DIR honoured).
Per folder — /folder//mcp, reachable from the folder pill: the effective view for that directory plus folder-layer overrides. A project entry replaces the global entry of the same name. Writes land only in /.pi/mcp.json, and only when the folder is trusted.
Both layers parse as strict JSON: an mcp.json with comments or trailing commas is skipped whole by pi, shown as unparseable, and never written over. Secret values inherited from a layer you cannot write are redacted server-side and never reach the browser. Adapter-only keys (e.g. disabled) are ignored by pi — the editor offers a one-click Convert to enabled: false.
Using the model proxy
The dashboard exposes an OpenAI-compatible HTTP proxy on the same port as the dashboard UI (/v1/...). Any LLM client that accepts a custom base_url can use it.
# Example: point an OpenAI-compatible client at the dashboard
export OPENAI_BASE_URL=http://localhost:8000/v1
export OPENAI_API_KEY=pi-proxy-<your-proxy-key>
Setup: open Settings → API Proxy in the dashboard UI, enable the proxy, and create an API key.
Endpoints:
GET /v1/models — list available models (requires models:list scope or all)
POST /v1/chat/completions — OpenAI chat completions, streaming + non-streaming
POST /v1/messages — Anthropic messages, streaming + non-streaming
Auth: proxy API keys only (pi-proxy- prefix). Dashboard JWT is never accepted on /v1/.
Usage
Auto-start (default)
The bridge extension automatically starts the dashboard server when pi launches if it's not already running. Disable with "autoStart": false in ~/.pi/dashboard/config.json.
In the Electron app, if the initial launch attempts fail (or the server is stopped externally), the loading page exposes a Start server button, an Open Doctor link, and a collapsible Server log panel showing the last 20 lines of ~/.pi/dashboard/server.log. The system tray menu also includes a Start server / Restart server item that reflects current server state. All entry points share a single idempotent launch routine in the Electron main process.
Debugging the Electron app via CDP. The desktop app accepts an opt-in --debug-cdp[=] CLI flag (and equivalent PI_DEBUG_CDP env var) that exposes Chromium's Chrome DevTools Protocol on a loopback-only port (default 9222) so agent-browser, Playwright, Chrome DevTools, or any CDP client can attach to the installed shell — not just a separate browser pointed at http://localhost:8000. The flag wins if both flag and env are set. CDP is off by default, never binds beyond 127.0.0.1, and logs a one-line [debug-cdp] warning to stderr when active. Single-instance contract: CDP must be enabled at first-instance launch; passing the flag to an already-running instance cannot enable CDP retroactively and only logs a warning — fully quit and relaunch. From this repo, cd packages/electron && pnpm run dev:cdp launches the dev Electron with CDP on. The bundled universal browser skill (shipped by the bridge extension) contains a worked-example recipe for attaching agent-browser to the Pi Dashboard app.
Doctor diagnostics. Help → Doctor (or the loading-page link) opens a styled BrowserWindow (doctor.html) that runs the same checks the Electron app already performed — grouped into sections (Runtime, Pi, Server, Bundles, Diagnostics) with status pills, paths, and per-row suggestion callouts; toolbar offers Re-run, Copy as Markdown / Plain, Open server log, Open doctor log, Run setup wizard. The web client exposes the portable subset at Settings → Diagnostics, which fetches /api/doctor and renders the same sections (Electron-only rows omitted). Both surfaces share packages/shared/src/doctor-core.ts, so a check defined once shows up everywhere.
Daemon mode
pi-dashboard start # background daemon (production)
pi-dashboard start --dev # dev mode (proxy to Vite, fallback to production build)
pi-dashboard stop # stop daemon (announced shutdown first, so terminal pi sessions do not auto-start a replacement; SIGTERM fallback); also kills port holders THIS HOME owns (lock sidecar / health identity). For a server the open desktop app started, prints a note: the app shows its reconnect page
pi-dashboard stop --force # DANGEROUS: kills EVERY listener on the ports - another HOME's/user's dashboard, the Electron server, or an unrelated service. Only to recover an orphaned listener nothing else can attribute; restart ignores it
pi-dashboard restart # restart (production)
pi-dashboard restart --dev # restart in dev mode
pi-dashboard status # daemon status for the resolved port; says when the desktop app started it
Daemon stdout/stderr is logged to ~/.pi/dashboard/server.log (append mode with timestamped headers per start).
Manual server start
npx tsx packages/server/src/cli.ts
npx tsx packages/server/src/cli.ts --port 8000 --pi-port 9999
npx tsx packages/server/src/cli.ts --dev # proxy to Vite dev server
Graceful restart via API
A bare curl to a control or operate route is refused (403 local_proof_required): requireLocalProof is on by
default, so a local caller must prove it is on this machine. pi-dashboard api mints that proof for each request.
# Restart in the same mode
pi-dashboard api POST /api/restart
Switch to dev / production
pi-dashboard api POST /api/restart --data '{"dev":true}'
pi-dashboard api POST /api/restart --data '{"dev":false}'
The restart endpoint waits for the old server to exit, starts the new one, and verifies health. It works identically on Windows, macOS, and Linux (no sh / lsof / curl dependency).
Dev mode with production fallback
When started with --dev, the server proxies client requests to the Vite dev server for HMR. If Vite is not running, it automatically falls back to serving the production build from dist/client/:
pi-dashboard start --devalways works — no 502 errors
Start/stop Vite independently without restarting the dashboard
Start Vite later and refresh the browser to get HMR
Dev build on reload
Set "devBuildOnReload": true in config.json for a one-command full-stack refresh:
/reload → build client → stop server → reload extension → auto-start fresh server
Blocks pi for ~2–5s during the build. The server shutdown affects all connected sessions — they auto-reconnect when one restarts the server.
Session spawning
Headless (default) — runs pi as a background process with no terminal attached. Interaction through the web UI.
tmux — runs pi inside a tmux session named pi-dashboard, each spawned session as a new window:
tmux attach -t pi-dashboard # attach
tmux list-windows -t pi-dashboard # list windows
inside tmux: Ctrl-b n / p / w # next / prev / picker
Switch with "spawnStrategy": "tmux" in ~/.pi/dashboard/config.json.
Slash commands (/dashboard:*) from a pi session
The bundled pi-dashboard skill ships a /dashboard:* slash-command namespace
for driving the dashboard from any pi session without hand-writing curl.
Two classes:
LLM-free (read-only, zero token cost) — the command body runs as bash and
renders directly in chat; the LLM is never invoked. A footer
"ℹ ran locally — LLM not invoked" appears beneath the output. Examples:
/dashboard:session-list # table of every session
/dashboard:session-info abc123 # all fields for a session (by id-prefix)
/dashboard:server-health # pid + uptime
/dashboard:git-branches # branches in the current dir
LLM-bound (mutations / judgment) — the command expands into a user message
the agent interprets, then calls the matching REST endpoint. Examples:
/dashboard:session-tell abc123 please run the tests
/dashboard:session-spawn # new session in the current dir
/dashboard:session-abort-all # asks scope before aborting
Bash-style history recall and per-session draft persistence:
| Key | Action |
|-----|--------|
| Enter | Send the prompt |
| Shift+Enter | Insert a newline |
| ArrowUp | Recall previous user prompt (caret on first line, no dropdown open). Repeat to walk back |
| ArrowDown | Walk forward through history (caret on last line). Past the newest entry, restores the in-progress draft |
| Escape | Restore in-progress draft and exit history mode; also cancels pending prompt / dismisses dropdown |
| Tab / Enter in dropdown | Accept the highlighted /command or @file suggestion |
Drafts (typed-but-unsent text) are persisted per session in localStorage under chat-draft: and survive navigation (Settings, OpenSpec preview, diffs, …) and full page reloads. Drafts never leak between sessions.
Recommended extensions
The dashboard integrates tightly with a small, curated set of pi extensions — for custom tool rendering and the Flow dashboard. The Electron wizard installs them in one go; the Packages tab and a top-of-page banner keep them discoverable afterwards.
Claude subscription requests. Anthropic bills subscription requests as extra usage when pi's docs prompt names pi packages. Turn on Settings ▸ Providers ▸ Claude subscription compat (global preference claudeSubscriptionCompat, default off; needs the Flows plugin).
Upgrade note.pi-anthropic-messages is no longer required. After turning on the compat switch, you may uninstall pi-anthropic-messages: pi remove npm:@blackbelt-technology/pi-anthropic-messages.
Authoritative source: packages/shared/src/recommended-extensions.ts. Descriptions, versions, and installed-state are enriched live via GET /api/packages/recommended (offline fallback).
Authoring a dashboard plugin
The dashboard's UI is composed of named slots that plugins claim with React components. To create a new plugin, install the scaffolding skill:
npm i -g @blackbelt-technology/pi-dashboard-plugin-skill
Then, from any pi session:
/skill dashboard-plugin-scaffold
The skill has two modes:
new — scaffold a fresh packages/-plugin/ inside this monorepo. Pick which of the 10 React slots to claim (session-card-badge, content-view, settings-section, tool-renderer, …); the skill renders package.json (with pi-dashboard-plugin manifest), src/client.tsx with stubs, optional src/server/index.ts, optional src/bridge/index.ts, configSchema.json, and tests.
augment — retrofit an existing pi-extension project on disk. The skill greps for TUI surface (ctx.ui.*, pi.registerTool, …), drives the agent through a canonical mapping table, asks per-callsite what to port, then injects a manifest field into package.json and adds src/dashboard/. Purely additive — your existing TUI keeps working in pure-pi sessions.
Installing and trusting third-party dashboard plugins
A dashboard plugin published as a pi package (keyword pi-dashboard-plugin) installs like any pi package:
pi install npm:<package> --scope global
Through the dashboard (Packages tab, or POST /api/packages/install with scope: "global"): the plugin appears live in the Plugins tab, no restart.
From a terminal pi install: the dashboard is not told, so the plugin appears after a dashboard restart.
Local scope (pi install -l, project .pi/settings.json): detected for active sessions' folders and listed as local-detected, never loaded. Install it globally to enable it.
Server code of a plugin that arrives as a pi package (pi-package source) is default-deny: it stays server-untrusted until a local operator grants server trust in the Plugins tab (POST /api/plugins/:id/trust {"server": true}, localhost only). Plugins from other sources run under the existing pi extension trust.
Client UI of a prebuilt plugin (manifest clientBundle) is loaded at runtime only after a local operator trusts that exact build: the grant pins the bundle's content hash (auth.trustedPlugins). A rebuilt bundle needs a fresh grant ("Trust this version").
If pi launches but the dashboard never becomes reachable, inspect the launch log:
cat ~/.pi/dashboard/server.log # Linux / macOS
type %USERPROFILE%\.pi\dashboard\server.log # Windows
The log is append-mode with timestamped headers per start attempt, so previous crashes are preserved. Common issues:
ERR_UNSUPPORTED_ESM_URL_SCHEME on Windows — fully fixed in 0.4.0+. The 0.2.10 release wrapped the --import loader position as a file:// URL, but the entry-script position stayed a raw Windows path — which crashed Node on non-C: drives (A:\, B:\, …) because the drive-letter heuristic has gaps there. 0.4.0 routes all four server-spawn call sites through a single spawnNodeScript / toFileUrl helper that wraps both positions unconditionally, and a repo-level lint test prevents regression. Upgrade the package.
Cannot find pi's TypeScript loader — pi is not installed globally. Run npm install -g @mariozechner/pi-coding-agent.
Fastify crash at startup — you're on Node 22.0.0–22.18.x or 24.1.0–24.2.x which are affected by nodejs/node#58515. Upgrade to 22.19+ or 24.3+.
Port already in use
Windows:netstat -ano | findstr :8000 then taskkill /F /PID
Unix:lsof -t -i :8000 | xargs kill
Or change port in ~/.pi/dashboard/config.json.
UI is empty or stuck after switching servers
Since the safe-server-switch release, switching servers via the header dropdown is transactional: the UI verifies the target through a short-lived staging WebSocket (5 s timeout) before clearing state or writing localStorage. If the target is unreachable, nothing changes — a toast appears and you stay on the previous server.
If the currently-active server drops for more than 3 s, a yellow “Disconnected from \. Retrying…” banner appears at the top with a Switch server button — use it to pick a reachable server.
You should no longer need to manually localStorage.removeItem("pi-dashboard-last-server") to recover from a bad switch. If you still get stuck, please file an issue.
Windows: sessions die when the dashboard restarts
Since the consolidate-windows-spawn-and-platform-handlers release, pi sessions on Windows survive dashboard restart, matching macOS/Linux behaviour. Previously, killing the dashboard process (Task Manager, Ctrl+C, /api/restart, crash) terminated every running pi session because the children were in the server's libuv kill-on-close Job Object. The fix uses detached: true so children are excluded from the parent's job.
If you previously relied on "closing the dashboard cleans everything up," use the per-session Force Kill action instead (or POST /api/session/:id/force-kill).
Windows Terminal tab doesn't appear
Install Windows Terminal (wt.exe) for tabbed interactive sessions — the dashboard prefers it over WSL tmux / headless when available. Windows 11 ships with it; on Windows 10 install from the Microsoft Store.
If wt.exe is on PATH but launching does nothing, check Settings → Apps → Advanced app settings → App execution aliases. If the "wt" alias is disabled, wt.exe is found but can't be executed. Enable the alias or uninstall/reinstall Windows Terminal.
Sessions don't group under my pinned folder
Since v0.3+, session grouping uses OS-aware path equality (platform/paths.ts). Sessions group correctly under a pinned folder even across trailing-separator, separator-style, or case differences (on Windows and macOS).
If you still see two entries for what should be one folder, the paths are likely on different Windows drives (A:\Foo and B:\Foo are different filesystems and never merge) — that's correct behaviour. If the paths really are the same filesystem, file an issue with both the pinned path (Settings → Tools → Export diagnostics) and the session cwd from /api/sessions.
Tool not found (pi / openspec / npm / …)
Open Settings → General → Tools, click the chevron next to the failing tool to see the