devpanel
Chrome DevTools panel with an embedded terminal and browser context injection for Claude Code. Supports three target types: local Chrome tabs (CDP), ADB remote phones (CDP over WebSocket), and iOS Safari (WebKit Inspector Protocol via safarictl).
What it does
Opens a terminal inside Chrome DevTools. Claude Code runs in that terminal connected to a local MCP server. When you select an element, capture network/console, or take a screenshot in DevTools, it goes into a delivery queue. By default, items are flushed to the agent immediately. When muted, items accumulate and channels are silenced until you explicitly push them.
Select elements — click Select (or Ctrl+K → Select element), Chrome's native crosshair picker activates. A modal describes the gesture; outside-click cancels. Pick an element, type an annotation in the prompt, repeat.
Capture network — snapshot recent requests from devtools.network.getHAR(). Full HAR spills to a JSON file the agent can grep. network() MCP tool also supports filters (url, method, status, since).
Capture console — snapshot recent console messages (log/warn/error + exceptions). Spills to JSON. Errors are also auto-pushed as channel notifications (unless channels are muted).
Screenshot — viewport via the toolbar / screenshot() MCP tool, or element-clipped via Ctrl+K → "Screenshot element…" / screenshot(selector="…"). Element clips use captureBeyondViewport: true so off-screen targets work without scrolling. PNG spills to disk.
Eval JS — js(code) MCP tool runs in the inspected page (sync via inspectedWindow.eval, async via Runtime.evaluate({awaitPromise: true}) when await is present). defer=true returns a task_id immediately and pushes the result via channel. every=N runs on a timer until cancelled. Inside eval, sendChannel(msg) pushes ad-hoc channel notifications.
Console + watchers — console() returns recent log/warn/error entries; errors are also auto-pushed as channels. watch(url_pattern) registers a network observer; matching requests push channel notifications until cancelled.
Page navigation events — auto-pushed as channel notifications when the page navigates to a new URL. Same-URL navigations (HMR reconnects, sleep/wake reloads) are suppressed. Suppressed during click/type/navigate MCP calls (tool result already has the URL).
Delivery model
The stack is a delivery queue. Items are pushed by the panel (POST /stack) and removed on delivery. Two modes, toggled via Ctrl+K command palette:
- Instant (default) — auto-flushes after each capture. Items appear briefly in the queue then clear. The agent sees each item immediately via channel notification.
- Staging — items accumulate. StackChip shows pending count. Flush explicitly via Ctrl+K → "Push staged to agent", or let the UserPromptSubmit hook drain them on your next prompt.
Three delivery paths, all of which remove items from the queue:
POST /stack/flush— batch channel push via SSEGET /stack(hook drain) — returns markdown for prompt injection- MCP
draintool — returns markdown in tool result
Direct channel pushes (nav events, console errors, watch hits, eval results) bypass the queue — they're ephemeral notifications. These can be muted via Ctrl+K → "Channels: mute" for quiet browsing.
Architecture
Chrome DevTools (DevPanel tab) PTY daemon (Python)
├── Terminal (xterm.js) ──ws ─────────────────── /ws (attach/detach)
├── cdp.ts ── chrome.debugger ─── all CDP work
│ ├── ensureAttached / detach (with onDetach reset)
│ ├── startInspect (Overlay.setInspectMode)
│ ├── captureScreenshot (clip + captureBeyondViewport)
│ ├── captureTree (Accessibility.getFullAXTree)
│ ├── resolveSelector / focusElement / dispatchClick / dispatchType
│ ├── settle + networkDelta
│ └── viewport emulation (setViewport / clearViewport)
├── target.ts ── Target adapter (LocalTarget / RemoteTarget / SafariTarget)
│ screenshot / network / console / eval / inspect / capabilities
├── commands.ts dispatch ────────────────────── POST /stack
│ eval / screenshot / network / console / tree / click / type / navigate / viewport
├── actions.svelte.ts (Actions class) ── delegates to active Target
│ toggleSelect / captureScreenshot / captureNetwork / captureConsole / selectAndScreenshot
├── network.onNavigated ───────────────────── POST /channel (dedupe: same URL suppressed)
├── CommandPalette (Ctrl+K) / SelectionDialog / StackChip / ThemeToggle
└── Service Worker ── NMH-relay only (sendNativeMessage to spawn daemon)
ADB remote target (physical phone):
devpanel adb ─── adb forward ───→ POST /adb (store CDP port)
Panel boot ─── tabId===null ────→ WS &remote=1 (daemon auto-attaches CDP)
Daemon ─── CDP WebSocket ───────→ chrome_devtools_remote (phone)
├── tree/screenshot/click/type/navigate (MCP tools via daemon CDP)
└── /inspect/start + /inspect/stop (remote element picker)
iOS Safari target (iPhone via USB):
safarictl (Mac) ─── pymobiledevice3 ───→ WebKit Inspector Protocol (phone)
Panel boot ─── <meta name="safarictl"> ──→ POST /safari (register URL)
Daemon ─── HTTP ───→ safarictl API
├── eval/screenshot/tree/click/type/viewport (MCP tools via HTTP)
└── No issues/watch (WebKit lacks Audits domain + no event streaming yet)
PTY daemon (7 modules, no circular imports):
session.py ← PtySession, McpSession, spill, formatters, constants
ax_tree.py ← pure AX tree formatter (role classification → text)
js_templates.py ← shared JS for selector resolution (text=, role=, label=)
cdp_remote.py ← ADB/CDP helpers (screenshot, selector resolve, settle)
safari.py ← safarictl HTTP client (eval, tree, click, type, screenshot)
mcp.py ← MCP dispatch + 16 tools (imports all above)
server.py ← HTTP handlers + lifecycle (imports session + mcp)
PTY daemon endpoints
├── /ws?tab=N → terminal I/O (per-mount uuid)
├── POST /stack?session= → stage context item (spills large data to disk)
├── POST /stack/flush?session= → deliver all queued items via channel
├── GET /stack?session= → drain (clears queue) / ?peek=true (raw JSON)
├── POST /channel?session= → ad-hoc channel push
├── POST /mcp?session= → MCP JSON-RPC
├── GET /mcp?session= → SSE stream (channel notifications)
├── DELETE /mcp?session= → terminate MCP session
├── GET /spill/{name} → serve spill files (screenshots, HAR)
├── GET /controls → dev endpoint (port, pid, queue size, MCP sessions, uptime)
├── POST /adb → register ADB-forwarded CDP port
├── POST /safari → register safarictl API URL for iOS target
├── POST /remote → attach daemon CDP WS to phone target
├── POST /inspect/start → remote element picker (Overlay.setInspectMode)
├── POST /inspect/stop → cancel in-progress remote picker
└── POST /screenshot → remote screenshot via daemon CDP or ADB screencap
Claude Code (in PTY terminal)
├── MCP client ──POST/GET──→ /mcp (tools + SSE channel notifications)
└── UserPromptSubmit hook ──→ GET /stack (drain fallback for non-channel clients)
Three target paths. Local tabs: panel does CDP via chrome.debugger. Remote targets (ADB phones): daemon does CDP via a direct WebSocket to the ADB-forwarded port. iOS Safari: daemon calls safarictl HTTP API which speaks WebKit Inspector Protocol to the phone via pymobiledevice3. MCP tools dispatch via pty.safari_url / pty.cdp_ws / panel command. The service worker is NMH-relay-only — it spawns the daemon and returns the port.
Install
Two steps — Chrome assigns the extension ID on load, and the NMH manifest needs that ID.
Step 1: Load the extension
# Production
uv tool install devpanel
devpanel install # prints the extension path
# Dev (source checkout — enables repld introspection)
uv sync --dev
uv run devpanel install --dev # prints the extension path
Go to chrome://extensions → Enable Developer Mode → Load unpacked → point to the printed path. Copy the extension ID shown on the card.
Step 2: Register the NMH manifest with the ID
# Production
devpanel install --extension-id=PASTE_ID_HERE
# Dev
uv run devpanel install --extension-id=PASTE_ID_HERE --dev
The --dev flag stores the project root in ~/.config/devpanel/config.json so the NMH launcher spawns the daemon inside repld for live introspection. Without --dev, the daemon spawns directly via devpanel start.
Usage
- Open Chrome DevTools on any page
- Click the DevPanel tab
- A terminal opens with your shell —
DEVPANEL_PORT,DEVPANEL_SESSION,DEVPANEL_SPILL_DIR, andDEVPANEL_REPLD_SOCKETenv vars are set - Your shell wrapper (see Shell setup) passes
--mcp-config,--settings, and--dangerously-load-development-channelstoclaude - Run
claude— it connects to the MCP server and registers for channel notifications - Select elements, capture network/console, take screenshots — context flows to Claude automatically
- Ctrl+K opens the command palette: all capture actions, mute toggle, flush
Command palette (Ctrl+K)
| Command | What it does |
|---|---|
| Select element | Activate crosshair picker, push to queue |
| Capture network | Snapshot recent HAR entries to queue |
| Capture console | Snapshot recent console messages to queue |
| Capture issues | Browser-detected page issues (deprecations, CSP, forms) |
| Screenshot | Capture viewport to queue |
| Screenshot element… | Pick element, capture clipped screenshot |
| Device screenshot | Full physical screen via ADB (remote targets only) |
| Reload iOS page | Re-dump HTML from iOS device (Safari targets only) |
| Detect safarictl | Scan page for <meta name="safarictl"> and register target |
| Mute / Unmute | Silence channels and hold captures for manual push |
| Push queued to agent | Flush queued items (visible when muted with items) |
| Set project directory… | Map hostname → local path (for terminal cwd) |
| Renderer: WebGL/canvas | Toggle terminal renderer (canvas default, WebGL opt-in) |
| Eval JS… | Run JavaScript in the inspected page |
| Restart terminal | Kill PTY and start fresh |
Dev
# Extension
cd extension-src
npm install
npm run build # required — hot reload doesn't work with CRXJS service workers
npm run check # svelte-check
npm run lint # prettier + eslint
npm run format # prettier --write
# Daemon (Python)
uv sync --dev # installs aiohttp + repld (for live introspection)
uv run devpanel start # standalone daemon for testing
ruff format src/ # format
ruff check src/ # lint
# Then: curl localhost:PORT/controls, curl POST/GET /stack, /mcp
# Full build (extension + Python wheel)
make build
Live introspection via repld
When the NMH spawns the daemon, it goes through repld so you can inspect daemon state from your main Claude Code session:
# .mcp.json registers repld bridge (gitignored — socket path is per-user)
{"mcpServers": {"repld": {"type": "stdio", "command": "uv",
"args": ["run", "repld", "bridge", "--socket", "/run/user/$UID/devpanel/repld.sock"]}}}
Then in your Claude Code session: mcp__repld__exec("list(_sessions.keys())"), exec("[s.stack for s in _sessions.values()]"), push test channel notifications, etc.
Shell setup
DevPanel does not write any shell config files. The daemon sets env vars on the PTY: DEVPANEL_PORT, DEVPANEL_SESSION, DEVPANEL_SPILL_DIR, DEVPANEL_REPLD_SOCKET. Your shell needs to wrap claude with three flags when those vars are present:
What Claude Code needs:
-
--mcp-config— connects to the devpanel MCP server:{"mcpServers":{"devpanel":{"type":"http","url":"http://127.0.0.1:$DEVPANEL_PORT/mcp?session=$DEVPANEL_SESSION"}}}
-
--settings— registers the UserPromptSubmit hook (drain fallback):{"hooks":{"UserPromptSubmit":[{"matcher":"","hooks":[{"type":"command","command":"curl -s http://127.0.0.1:$DEVPANEL_PORT/stack?session=$DEVPANEL_SESSION"}]}]}}
-
--dangerously-load-development-channels server:devpanel— enables real-time channel notifications
How to wire it up:
Create a shell wrapper that only activates inside DevPanel PTYs (when DEVPANEL_PORT is set). The wrapper should:
- Pass all three flags to
claude - Expand
$DEVPANEL_PORTand$DEVPANEL_SESSIONinto the--mcp-configURL at call time - Keep the
$DEVPANEL_PORTin the hook command literal — Claude's hook executor expands it at fire time - Forward all other arguments (
$@/$argv)
If you use tmux inside the DevPanel terminal, ensure env vars propagate:
set -ga update-environment " DEVPANEL_PORT DEVPANEL_SESSION DEVPANEL_SPILL_DIR DEVPANEL_REPLD_SOCKET"
Drain format
When the hook fires (or the agent calls drain), it sees:
## Browser Context
### Selected elements
- `div.central-textlogo` — this is the logo
- `nav.central-featured` — language links
### Network
- 8 requests, 1 errors → /run/user/1000/devpanel/network-def.json
### Console
- 15 entries, 3 errors/warnings → /run/user/1000/devpanel/console-abc.json
### Screenshots
- /run/user/1000/devpanel/screenshot-ghi.png
Large data (HAR, console, screenshots) spills to disk. The agent uses Read/Grep on the file paths.
Metadata
Release files for devpanel 0.11.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| devpanel-0.11.0.tar.gz | 543.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| devpanel-0.11.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.1 MB
Release files / devpanel-0.11.0.tar.gz
| Download URL | devpanel-0.11.0.tar.gz |
|---|---|
| Size | 543.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
550b733d5b2c8bdd4f310e058617354da34ee2531c3ca3b2759bd1e477c5c84b
|
|
BLAKE2b-256 checksum How to use checksums |
089c511d543102ead1d4132c4b253997ff8ea8090f0c725e94f52e8a8fad672b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"EndeavourOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / devpanel-0.11.0-py3-none-any.whl
| Download URL | devpanel-0.11.0-py3-none-any.whl |
|---|---|
| Size | 552.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
91ef11fa42ef7a1462dce1a3e1952fd458ae78f1891cea1ef27815f17a8f21d7
|
|
BLAKE2b-256 checksum How to use checksums |
e22eb84842fd6a6f5d64f1b42d00b331428afec583ea8e358b5fd9cbade1555d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"EndeavourOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|