Skip to main content

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. 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 JSjs(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 + watchersconsole() 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 SSE
  • GET /stack (hook drain) — returns markdown for prompt injection
  • MCP drain tool — 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

  1. Open Chrome DevTools on any page
  2. Click the DevPanel tab
  3. A terminal opens with your shell — DEVPANEL_PORT, DEVPANEL_SESSION, DEVPANEL_SPILL_DIR, and DEVPANEL_REPLD_SOCKET env vars are set
  4. Your shell wrapper (see Shell setup) passes --mcp-config, --settings, and --dangerously-load-development-channels to claude
  5. Run claude — it connects to the MCP server and registers for channel notifications
  6. Select elements, capture network/console, take screenshots — context flows to Claude automatically
  7. 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:

  1. --mcp-config — connects to the devpanel MCP server:

    {"mcpServers":{"devpanel":{"type":"http","url":"http://127.0.0.1:$DEVPANEL_PORT/mcp?session=$DEVPANEL_SESSION"}}}
    
  2. --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"}]}]}}
    
  3. --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_PORT and $DEVPANEL_SESSION into the --mcp-config URL at call time
  • Keep the $DEVPANEL_PORT in 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

devpanel-0.11.0.tar.gz (543.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

devpanel-0.11.0-py3-none-any.whl (552.8 kB view details)

Uploaded Python 3

File details

Details for the file devpanel-0.11.0.tar.gz.

File metadata

  • Download URL: devpanel-0.11.0.tar.gz
  • Upload date:
  • Size: 543.7 kB
  • Tags: Source
  • Uploaded using 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}

File hashes

Hashes for devpanel-0.11.0.tar.gz
Algorithm Hash digest
SHA256 550b733d5b2c8bdd4f310e058617354da34ee2531c3ca3b2759bd1e477c5c84b
MD5 dc6edbe09a5404ef08059061442367a3
BLAKE2b-256 089c511d543102ead1d4132c4b253997ff8ea8090f0c725e94f52e8a8fad672b

See more details on using hashes here.

File details

Details for the file devpanel-0.11.0-py3-none-any.whl.

File metadata

  • Download URL: devpanel-0.11.0-py3-none-any.whl
  • Upload date:
  • Size: 552.8 kB
  • Tags: Python 3
  • Uploaded using 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}

File hashes

Hashes for devpanel-0.11.0-py3-none-any.whl
Algorithm Hash digest
SHA256 91ef11fa42ef7a1462dce1a3e1952fd458ae78f1891cea1ef27815f17a8f21d7
MD5 5bd38fa53ab198635cef50af6a9c5608
BLAKE2b-256 e22eb84842fd6a6f5d64f1b42d00b331428afec583ea8e358b5fd9cbade1555d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page