A live dashboard over your tmux sessions. Every window becomes a card; click
into a card to open a full live terminal (xterm.js + tmux pipe-pane over
WebSocket), send keystrokes, focus the window in tmux, or rename it. Parses
Claude Code pane status — branch, PR, CI state, recap, spinner — and surfaces
what's pending input.
Built for the "I have 30 tmux windows across 5 sessions with various Claude Code agents running, and I want a single pane of glass" workflow.
tmux(any reasonably modern version)uvfor running the single-file script- A modern browser (uses WebSockets + vanilla JS)
- Optional:
ANTHROPIC_API_KEYfor the ✨ auto-rename feature - Optional: Node 20+ if you want HMR while iterating on the frontend
uv run server.pyOpen http://127.0.0.1:8765/. Polls every 3s; the modal opens a live WebSocket bridge to the selected pane.
For hot-reload while editing static/app.js or static/styles.css:
npm install # one-time
npm run dev # then visit http://127.0.0.1:5174/npm run dev runs the FastAPI server and Vite together via
concurrently; ctrl+c stops both. Vite proxies /api/* and /ws/* to
FastAPI, so only one URL matters in the browser. There's no build step —
production still loads static/ as-is from FastAPI on :8765.
The ✨ button on each session header asks Haiku 4.5 to suggest fresh, descriptive names for every window in the session based on current pane content. Requires an Anthropic API key:
cp .env.example .env
# then edit .env and paste your keyGET /api/state— every tmux window with parsed Claude statusGET /api/pane?session=…&index=…&lines=200— capture last N lines (with ANSI escapes)POST /api/focus?session=…&index=…— switch every attached tmux client to that windowPOST /api/send?session=…&index=…— body{keys: [...], paste: "..."}; sends keystrokes / bracketed pastePOST /api/rename?session=…&index=…— body{name: "..."}; renames a windowPOST /api/auto-rename-session?session=…— Haiku-driven batch rename of every window in a sessionWS /ws/pane?session=…&index=…— live bidirectional pane stream (xterm.js powers the modal)POST /api/channel/push?pane=%N— body{content, meta?}; queues an event for the pane's channel serverPOST /api/channel/reply?pane=%N— body{message, kind?, severity?}; called by the channel server'sreplytoolPOST /api/channel/clear-unread?pane=%N— zeroes the pane's unread reply countWS /ws/channel?pane=%N— subscriber endpoint for the per-pane channel server (last-writer-wins)
Periscope can push messages into the Claude Code sessions it spawns and surface Claude's replies in its UI. This uses Claude Code's channels feature, currently in research preview.
Claude Code keeps user-level MCP servers under the mcpServers key in
~/.claude.json. Merge the periscope entry in (don't overwrite the
file — it holds lots of other Claude Code state):
jq '.mcpServers.periscope = {
"command": "uv",
"args": ["run", "--script", "/ABSOLUTE/PATH/TO/periscope/channel_server.py"],
"env": {"PERISCOPE_URL": "http://127.0.0.1:8765"}
}' ~/.claude.json > ~/.claude.json.tmp && mv ~/.claude.json.tmp ~/.claude.jsonReplace /ABSOLUTE/PATH/TO/periscope/ with your local checkout path.
After this, restart any running claude sessions you want channels in
(it's read at invocation time) or spawn fresh ones via periscope's
+ claude button.
When you click + claude in periscope, the spawned command is
claude --dangerously-load-development-channels server:periscope. Claude
launches channel_server.py as a stdio child. The server connects to
periscope's /ws/channel and bridges events both directions.
The --dangerously-load-development-channels flag is required because
channels are in research preview — bare --channels only resolves
allowlisted entries.
- Push to Claude: open a pane's modal, type a message in the
composer in the Messages section, and submit. Claude sees it on its
next turn as a
<channel source="periscope">block. - Replies from Claude: Claude can call the
replytool withkind="need_human",kind="done", orkind="info"(the default). Messages show in the modal's Messages section;need_humantriggers a pulsing red border on the pane card and fades the rest of the grid.
If a session was started outside periscope (no dev-channels flag), the push composer is disabled with a tooltip.
MIT — see LICENSE.