Chrome DevTools panel with terminal and browser context injection for Claude Code
Project description
devpanel
Chrome DevTools panel with an embedded terminal and browser context injection for Claude Code.
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. In instant mode (default), items are flushed to the agent immediately. In staging mode, items accumulate 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
├── commands.ts dispatch ────────────────────── POST /stack
│ eval / screenshot / network / console / tree / click / type / navigate
├── actions.svelte.ts (Actions class)
│ 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)
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)
Claude Code (in PTY terminal)
├── MCP client ──POST/GET──→ /mcp (tools + SSE channel notifications)
└── UserPromptSubmit hook ──→ GET /stack (drain fallback for non-channel clients)
The service worker is NMH-relay-only — it spawns the daemon via sendNativeMessage and returns the port to the panel. All CDP work runs in the panel via chrome.debugger.
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, staging toggle, channel mute, 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 |
| Screenshot | Capture viewport to queue |
| Screenshot element… | Pick element, capture clipped screenshot |
| Channels: mute/unmute | Toggle automatic channel notifications (nav, errors, watchers) |
| Stage mode: ON/OFF | Toggle instant vs staging delivery |
| Push staged to agent | Flush queued items via channel (visible in staging mode) |
| Eval JS… | Run JavaScript in the inspected page |
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.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file devpanel-0.5.0.tar.gz.
File metadata
- Download URL: devpanel-0.5.0.tar.gz
- Upload date:
- Size: 381.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7573cba47c518bd59064cb7ac985dca089193e540ec710d0ddce62fed2b9a4c0
|
|
| MD5 |
1f5dd6279cb59faeea0354ed7728eb13
|
|
| BLAKE2b-256 |
da92ca8bbb3629b0b28ff7737e48a683d6640fbf2b95d532d95ceab53dee3b5c
|
File details
Details for the file devpanel-0.5.0-py3-none-any.whl.
File metadata
- Download URL: devpanel-0.5.0-py3-none-any.whl
- Upload date:
- Size: 387.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
110f1b3a2ae1888d4101f0c53609350aa778d418aba37c73e592306d0c1d6030
|
|
| MD5 |
d118892486991b89d539421feeb5673d
|
|
| BLAKE2b-256 |
a1da7b4357d477ef01e6d123f62e0fd2b11cdf79fc36e1a38868529488829855
|