Metadata-Version: 2.3
Name: devpanel
Version: 0.4.2
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 (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. 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. `network()` MCP tool also supports filters (`url`, `method`, `status`, `since`).

**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 outside of a `click`/`type`/`navigate` MCP call.

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. Routing: browser events → most-recently-active McpSession; `defer`/`every` replies → originating McpSession only.
- **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)                       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 / selectAndScreenshot
  ├── network.onNavigated ───────────────────── POST /channel
  ├── 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= → push context (spills large data to disk)
  ├── GET  /stack?session= → drain (advances watermark) / ?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  /controls       → dev endpoint (port, pid, stack 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 reduced to 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

```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`, `DEVPANEL_SESSION`, `DEVPANEL_SPILL_DIR`, and `DEVPANEL_REPLD_SOCKET` 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 env vars on the PTY: `DEVPANEL_PORT`, `DEVPANEL_SESSION`, `DEVPANEL_SPILL_DIR`, `DEVPANEL_REPLD_SOCKET`. 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 DEVPANEL_SPILL_DIR DEVPANEL_REPLD_SOCKET"
```

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 the most-recently-active McpSession for that PTY. Claude Code receives it instantly via the SSE stream and renders `<channel source="devpanel">...</channel>` in the conversation. Stale McpSessions (no POST/GET activity for 5 min) are reaped automatically. `defer`/`every` replies route to the originating session only.

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

### 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 (HAR, screenshots) spills to disk. The agent uses Read/Grep on the file paths. With channel routing live, the hook usually returns empty — the items are already in the conversation.
