Metadata-Version: 2.3
Name: devpanel
Version: 0.4.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, or take a screenshot in DevTools, it appears immediately in Claude's conversation as a channel notification — no need to type "look at this," no need to drain on prompt.

**Select elements** — click Select, Chrome's native crosshair picker activates. Click an element, type an annotation in the prompt, repeat. Each selection pushes a channel notification.

**Capture network** — snapshot recent requests from `devtools.network.getHAR()`. Full HAR spills to a JSON file the agent can grep.

**Screenshot** — CDP `Page.captureScreenshot` with clip from the selected element. PNG spills to disk, agent can read it.

**Page HTML** — captured automatically on first element selection. Full HTML spills to a file for grep/read.

Two delivery channels:

- **MCP channel notifications** (primary) — `notifications/claude/channel` pushed over Streamable HTTP/SSE the moment context is captured. Claude sees `<channel source="devpanel">...</channel>` in real time.
- **UserPromptSubmit hook** (fallback) — drains the stack via `curl /stack` on every prompt. Works for clients that don't enable channels.

## Architecture

```
Chrome DevTools (DevPanel tab)
  ├── Terminal (xterm.js) ──ws──→ PTY daemon (:random)
  ├── Select button ──CDP──→ Overlay.setInspectMode → selector + annotation
  ├── Network button ──→ devtools.network.getHAR()
  └── Screenshot button ──→ chrome.debugger → Page.captureScreenshot

Service Worker
  ├── Native messaging → spawn PTY daemon
  ├── CDP element picker (Overlay domain)
  └── Screenshot relay (chrome.debugger, not available in panel)

PTY daemon (Python, aiohttp)
  ├── WebSocket /ws?tab=N      → terminal I/O (per-tab session)
  ├── POST /stack?session=N    → push context (spills large data to disk)
  ├── GET /stack?session=N     → drain + clear (formatted text for hook)
  ├── GET /stack?peek=true     → raw JSON (for panel UI)
  ├── POST /mcp?session=N      → MCP JSON-RPC (initialize, tools/call)
  ├── GET /mcp?session=N       → SSE stream (channel notifications)
  ├── DELETE /mcp?session=N    → terminate MCP session
  └── GET /controls            → dev endpoint (port, pid, stack size, MCP sessions)

Claude Code (in terminal)
  ├── MCP client → /mcp (channel notifications: element selected, network captured, ...)
  └── UserPromptSubmit hook → curl /stack (drain — fallback for non-MCP clients)
```

## Install

```bash
pip install devpanel  # or: uv tool install devpanel
devpanel install --extension-id=EXTENSION_ID
```

Then load the extension in `chrome://extensions` → Load unpacked → point to the printed path.

The `--extension-id` is shown on the extensions page after loading. Re-run `devpanel install` with the correct ID.

## Usage

1. Open Chrome DevTools on any page
2. Click the **DevPanel** tab
3. A terminal opens with your shell (fish/bash/zsh) — `DEVPANEL_PORT` and `DEVPANEL_SESSION` env vars are set
4. Your fish conf.d (see [Shell setup](#shell-setup) below) wraps `claude` with `--mcp-config`, `--settings`, and `--dangerously-load-development-channels` when those env vars are present
5. Run `claude` — it connects to the MCP server at `http://127.0.0.1:$DEVPANEL_PORT/mcp` and registers for channel notifications
6. Click **Select** → pick elements → annotate → channel push fires immediately, Claude sees `<channel source="devpanel">` in the conversation
7. (Fallback) Type a prompt — hook drains any unsent context

## 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 only sets `DEVPANEL_PORT` and `DEVPANEL_SESSION` as env vars on the PTY. You configure your dotfiles to use those vars:

**`~/.config/fish/conf.d/devpanel.fish`** — guarded function that activates only inside DevPanel PTYs:

```fish
if set -q DEVPANEL_PORT
    function claude
        command claude \
            --mcp-config '{"mcpServers":{"devpanel":{"type":"http","url":"http://127.0.0.1:'$DEVPANEL_PORT'/mcp?session='$DEVPANEL_SESSION'"}}}' \
            --settings '{"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 \
            $argv
    end
end
```

**`~/.tmux.conf`** — pass env vars through tmux:

```tmux
set -ga update-environment " DEVPANEL_PORT DEVPANEL_SESSION"
```

The fish function is harmless outside DevPanel (the `if set -q` guard skips it). The single-quote breaks (`'...:'$DEVPANEL_PORT'/...'`) make fish expand the URL at function-call time; the hook's `$DEVPANEL_PORT` stays literal so Claude's hook executor expands it at hook-fire time.

## How delivery works

Two paths, both wired up by the function above:

**MCP channel notifications** — when you click Select / Network / Screenshot, the panel POSTs to `/stack`. The daemon stores the item, then pushes `notifications/claude/channel` to all initialized MCP sessions for that tab. Claude Code receives it instantly via the SSE stream and renders `<channel source="devpanel">...</channel>` in the conversation.

**UserPromptSubmit hook** — on every prompt, Claude's hook executor curls `GET /stack?session=...`. The daemon returns formatted markdown of any pending items and clears the stack. This is the fallback for clients that don't have `--dangerously-load-development-channels` enabled.

Both fire by default. With channels active, items are typically already in the conversation by the time the hook drains; the hook just sees an empty stack.

## Drain format

When the hook fires, the agent sees:

```
## Browser Context

### Page HTML
- https://www.wikipedia.org/ → /run/user/1000/devpanel/page-abc.html

### 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

### Screenshots
- /run/user/1000/devpanel/screenshot-ghi.png
```

Large data (HTML, HAR, screenshots) spills to disk. The agent uses Read/Grep on the file paths.
