Metadata-Version: 2.3
Name: devpanel
Version: 0.5.0
Summary: Chrome DevTools panel with terminal and browser context injection for Claude Code
Author: Fredrik Angelsen
Author-email: Fredrik Angelsen <fredrikangelsen@gmail.com>
Requires-Dist: aiohttp>=3.13.5
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# 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 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
  ├── 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**

```bash
# 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**

```bash
# 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](#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, 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

```bash
# 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:

```bash
# .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:
   ```json
   {"mcpServers":{"devpanel":{"type":"http","url":"http://127.0.0.1:$DEVPANEL_PORT/mcp?session=$DEVPANEL_SESSION"}}}
   ```

2. **`--settings`** — registers the UserPromptSubmit hook (drain fallback):
   ```json
   {"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.
