---
name: live-ui-generation
version: 0.5.0
description: Enables live UI generation by click-inspecting DOM elements or capturing screenshots, then applying code fixes with a generating animation. Use for "inspect element", "live UI generation", "live gen", "fix this element", "update this component", "change this UI", "edit this part of the page", "make this look different", or when visually selecting DOM elements for modification. Also handles teardown — use for "stop frontloop", "remove frontloop", "uninstall live-ui-generation", or "stop the servers". Does not apply to general code generation without element inspection.
---

# Live UI Generation

## Overview

Closes the loop between browser and Claude via three modes:

- **Element inspect:** Hover ⋞ FAB → transforms to ✛ Inspect → click element → describe fix. Orange shimmer appears, and the TASK payload arrives in the shell.
- **Screenshot capture:** Hover ⋞ FAB → click [] Screenshot → drag to select region → describe fix. TASK payload includes `screenshotPath`.
- **Undo:** Hover ⋞ FAB → click ↩ Undo (red when history exists). Sends UNDO task; Claude reverts the last change and reloads.

```
Browser ──WS──▶ ws-task-server (stdout) ──▶ Claude reads TASK, applies change
  ▲                                                        │
  └──────────── HTTP POST /complete/<id> ◀────────────────┘

Browser ──WS──▶ ws-task-server (stdout) ──▶ Claude reads UNDO, reverts change
  (fire-and-forget — no COMPLETE response)
```

**FAB:** Fixed bottom-right. Hover transforms the FAB from ⋞ to ✛ (Inspect), reveals two dial items: **↩ Undo** (grey/red) and **[] Screenshot** (indigo), plus an **"Inspect" label** next to the FAB. Hides entirely while any operation is active; Escape restores. Only active in dev mode — guarded by `window.__FRONTLOOP_ENABLED__` set from the framework's env variable. **Clicking the FAB itself starts inspect mode** — the click is never wasted.

**Console error capture:** Both the inspect and screenshot prompt panels include a "Send console errors" checkbox. When checked, captured `console.error` calls are included in the TASK payload. A live `[N errors]` counter appears next to the checkbox.

**Undo stack:** Task IDs stored in `sessionStorage` (`__frontloop_undo_stack__`). Survives page reloads. Button turns red when non-empty, grey when empty.

## Architecture

| Component | Location | Role |
|-----------|----------|------|
| WS + HTTP server | `<repo-root>/ws-task-server.<server-ext>` | WS 7332, HTTP completion + reload + screenshot on 7333 |
| Client bundle | `<frontend>/public/frontloop-bundle.js` (copied from `reference/live-ui-bundle.js`) | Self-contained ESM bundle: FAB, WebSocket, console error capture. Guards on `window.__FRONTLOOP_ENABLED__` — no-op in production. |

- **`<repo-root>`** — `git rev-parse --show-toplevel`
- **`<frontend>`** — dir with `src/` + `package.json` with a dev server script
- **`<server-ext>`** — `.js` if `package.json` has no `"type": "module"`, otherwise `.cjs` (the server uses CommonJS `require()`)

## Monitoring

The **Monitor tool** spawns a persistent background process. `ws-task-server.<server-ext>` emits `TASK: {...}` lines to stdout — each line is a real-time notification. **Use Monitor, not Bash**; Bash is ephemeral and cannot host a persistent WebSocket server reliably.

> **Sandbox note:** If Claude Code is running inside its network sandbox, `Monitor` inherits the same restrictions and the server will fail to bind with `EPERM: operation not permitted`. See Step 3 for the sandbox-safe fallback.

## Setup

Claude handles steps 1–5. **Step 6 (dev server) must be started by the user.**

### Step 1 — Resolve repo paths
Identify `<repo-root>` and `<frontend>` using the rules above.

### Step 2 — ws-task-server
Copy [reference/ws-task-server.js](reference/ws-task-server.js) verbatim to `<repo-root>/ws-task-server.<server-ext>`. Use bash to copy — do not reconstruct from memory. Check `package.json` for `"type": "module"` first to resolve `<server-ext>` (`.cjs` if present, `.js` otherwise).

Check whether `ws` is available before starting the server:
```sh
ls <frontend>/node_modules/ws 2>/dev/null && echo "ws found" || echo "ws missing"
```
If missing, install it as a dev dependency in the frontend:
```sh
cd <frontend> && npm install --save-dev ws
```

### Step 3 — Start server with Monitor

**If the server is already running** (`lsof -i :7332 | grep LISTEN` shows a process), skip launching a new server and go straight to starting the Monitor — do not wait for the user to ask. Always start the Monitor before handing back to the user.

**Normal path (no sandbox):**
```sh
# Via Monitor — server stdout becomes real-time TASK notifications
NODE_PATH=<frontend>/node_modules node <repo-root>/ws-task-server.<server-ext>
```

**Sandbox path** — if the Monitor attempt fails with `EPERM: operation not permitted` on port 7332, the Claude Code sandbox is blocking `listen()`. Use `Bash` with `dangerouslyDisableSandbox: true` instead:

```sh
# 1. Start server detached, log to file
NODE_PATH=<frontend>/node_modules node <repo-root>/ws-task-server.<server-ext> \
  > /tmp/ws-task-server.log 2>&1 &

# 2. Then Monitor the log file for TASK events
tail -f /tmp/ws-task-server.log | grep --line-buffered -E "TASK:|UNDO:|ERROR|Error|WS on|HTTP on"
```

The `Bash` call needs `dangerouslyDisableSandbox: true` (Claude Code will prompt for permission). The `Monitor` on the log file does **not** need sandbox disabled — it is just reading a file.

Verify either path: `lsof -i :7332 | grep LISTEN && lsof -i :7333 | grep LISTEN`. Expect `ws-task-server: WS on 7332, HTTP on 7333`.

### Step 4 — Copy bundle to public/

Locate the bundle relative to this SKILL.md:
```sh
find ~/.claude -path '*/live-ui-generation/reference/live-ui-bundle.js' 2>/dev/null | head -1
```

Copy it to the project's public folder:
```sh
cp <skill-bundle-path> <frontend>/public/frontloop-bundle.js
```

For CRA the public folder is `<frontend>/public/`. For Vite/Vue/Svelte/Angular/vanilla it is also `public/` at the frontend root. Verify the file landed at a path that will be served as `/frontloop-bundle.js` by the dev server.

### Step 5 — Inject into HTML entry point

**Use the exact inject command from [reference/wiring.md](reference/wiring.md) for the detected framework.** Do not add only the bundle `<script>` tag — the guard script (`window.__FRONTLOOP_ENABLED__`) is mandatory and must be injected in the same operation. The bundle is a no-op without it.

Both tags are plain `<script>` — no `type="module"` on either. Vite extracts inline `type="module"` scripts into proxy modules that fail to load; IIFE bundles don't need module semantics. The guard uses a runtime hostname check that works identically across all frameworks:
```html
<script>window.__FRONTLOOP_ENABLED__ = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';</script>
<script src="/frontloop-bundle.js"></script>
```

Framework → HTML file:

| Framework | HTML entry point |
|-----------|-----------------|
| Vite (React / Vue / Svelte) | `<frontend>/index.html` |
| CRA | `<frontend>/public/index.html` |
| Angular | `<frontend>/src/index.html` |
| Next.js App Router | `src/app/layout.tsx` (source file — see wiring.md) |
| Next.js Pages Router | `src/pages/_document.tsx` (source file — see wiring.md) |
| Vanilla | `<frontend>/index.html` |

After injecting, verify both lines landed:
```sh
grep '__FRONTLOOP_ENABLED__' <html-entry-point> && grep 'frontloop-bundle' <html-entry-point> && echo "both present" || echo "MISSING — check injection"
```

### Step 5.5 — Update .gitignore
Append `public/frontloop-bundle.js` to `<frontend>/.gitignore` (or `<repo-root>/.gitignore` if no frontend-level one):

```sh
grep -q 'frontloop-bundle' <frontend>/.gitignore 2>/dev/null || \
  echo 'public/frontloop-bundle.js' >> <frontend>/.gitignore
```

Also append `.rewind/`:
```sh
grep -q '\.rewind/' <frontend>/.gitignore 2>/dev/null || \
  echo '.rewind/' >> <frontend>/.gitignore
```

### Step 6 — User starts dev server
**Claude must NOT start the dev server.** Instruct the user to start their dev server.

---

## Usage Flow

### Phase A — Element inspect
1. User hovers ⋞ FAB → transforms to ✛ Inspect, two dial items (Undo, Screenshot) slide out to the left
2. User clicks ✛ FAB (or Inspect label) → FAB hides, crosshair cursor, elements highlight orange on hover
3. User clicks element → highlight pinned, prompt panel appears (bottom-right)
4. User describes fix, optionally checks **"Send console errors"**. Two radios appear: **Minimal** (default, messages only) and **Detailed** (full stack traces). A live `[N errors]` counter shows how many errors are captured.
5. User presses Enter / clicks "Send Fix Task" — or Escape to cancel
6. Orange shimmer over element, browser sends `TASK` via WebSocket with `consoleErrors` payload if enabled

### Phase A2 — Screenshot capture
1. User hovers ⋞ FAB → clicks [] Screenshot → FAB hides, crosshair cursor
2. User drags → dashed indigo selection rectangle
3. User releases → best enclosing container found, screenshot captured as PNG
4. Panel opens with thumbnail preview, selector, textarea, and same **"Send console errors"** checkbox + radios
5. User sends → PNG saved to `/tmp/liveui-screenshot-<id>.png`. TASK dispatched with `screenshotPath` and optional `consoleErrors`
6. Indigo shimmer over region

### Phase A3 — Undo
1. User hovers ⋞ FAB → clicks ↩ Undo (only active when stack is non-empty)
2. Browser pops last task ID, sends `UNDO: {"id":"<new>","type":"undo","targetTaskId":"<last>"}` via WebSocket
3. Monitor notifies Claude with `type: "undo"` task
4. Claude reverts the code change → sends `POST /reload`

### Phase B — TASK arrives
Check `type` field:
- **`"dom-fix"`** → Phase C
- **`"undo"`** → Phase D (`targetTaskId` identifies the fix to revert)

If `screenshotPath` present, `Read` the image file before planning.

If `consoleErrors` present (full payload in stderr, slim summary in stdout), review the errors before applying a fix — they may explain the problem the user is reporting (e.g. React deprecation warnings causing blank UI areas). The `consoleErrors` field has:
- `mode`: `"minimal"` (messages only) or `"detailed"` (full stack traces)
- `errors`: array of `{ message, stack, timestamp }` objects (full errors only in stderr)
- Slim stdout summary: `count` (number of errors)

### Phase C — Respond and complete

**Always send `POST /complete/<taskId>` at the end of every `dom-fix` task — even if the prompt was a question, a clarification, or required no code change.** The shimmer stays on screen until completion is received; leaving it unsent strands the UI.

1. Identify the source file from payload: use `element.sourceLocation` (exact file + line) when present — it's the most reliable signal. Fall back to `element.componentHierarchy` for React components, `page` + `pageTitle` for route components, or `container.title` for card identity.
2. If a code change is needed: find and edit the source file.
3. If a code change was made — run: `npx tsc --noEmit 2>&1 | head -40` — zero new errors required. **Important:** `tsc` does not catch JSX syntax errors (missing `>` etc.) — those surface as bundler errors. So run build to identify those.
4. `curl -s -X POST http://localhost:7333/complete/<taskId>` — always, regardless of whether code changed.
5. If a code change was made and HMR seems not working: `curl -s -X POST http://localhost:7333/reload`

### Phase D — Undo
Undo is **session-aware**: only revert changes made in the current session.

1. If the change was made this session: revert that edit, run type check, `POST /reload`
2. If unknown (prior session): inspect `git diff`, tell user which file was changed, ask which to revert

No `POST /complete` for undo — fire-and-forget.

### Timing
- Target: TASK → change → completion in **under 60s** for simple changes
- Shimmer auto-clears after **180s** if no completion received

---

## Payload Schema

### dom-fix

| Field | Type | Req | Description |
|-------|------|-----|-------------|
| `id` | string | yes | 8-char alphanumeric task ID |
| `type` | string | yes | `"dom-fix"` |
| `prompt` | string | yes | Free-text fix description |
| `element.selector` | string | yes | CSS selector |
| `element.tag` | string | yes | Lowercase tag name |
| `element.html` | string | yes | `outerHTML` (truncated to 2000 chars) |
| `element.text` | string | yes | `textContent` (trimmed to 500 chars) |
| `element.styles` | object | yes | Computed style properties |
| `element.componentHierarchy` | string | no | React fiber chain e.g. `"th (CurrentTabPane > Table)"`. `null` on non-React. |
| `element.sourceLocation` | string | no | Source file + line of the clicked element, e.g. `"components/layout/ActivitySidebar.tsx:30"`. Path relative to `src/`. `null` when not available (see edge cases). Present in slim stdout when non-null. |
| `element.columnIndex` | number | no | 0-based column index when `<th>` clicked |
| `container.*` | varied | no | Card context: `selector`, `tag`, `html`, `text`, `title`, `value`, `description`, `dataKeys` |
| `pageTitle` | string | yes | `document.title` |
| `page` | string | yes | `window.location.href` |
| `screenshotPath` | string | no | Absolute path to saved PNG. Use `Read` to view. |
| `consoleErrors` | object | no | Present when user checked "Send console errors". Contains `mode`: `"minimal"` — messages only, or `"detailed"` — full stack traces. `errors`: array of `{message, stack, timestamp}`. Slimmed in stdout to `{mode, count}`; full errors in stderr. |

**`sourceLocation` source by React version:**
- React 19: always populated — React captures `fiber._debugStack` (an `Error`) natively at fiber creation; Vite dev URLs in the stack already contain the original `.tsx` path.
- React 16–18 + `@vitejs/plugin-react` or CRA: populated via `fiber._debugSource` (set by Babel's `@babel/plugin-transform-react-jsx-development`).
- React 16–18 + esbuild/SWC only: `null` — no source injection in the JSX transform.
- Non-React (Angular, Vue, Svelte): always `null`.

### undo

| Field | Type | Req | Description |
|-------|------|-----|-------------|
| `id` | string | yes | 8-char alphanumeric ID |
| `type` | string | yes | `"undo"` |
| `targetTaskId` | string | yes | `id` of the `dom-fix` task being undone |

---

## Teardown

Run this when the user asks to stop, uninstall, or remove Frontloop from their project.

### Detect install version first

```sh
ls <frontend>/src/mocks/dom-inspector.ts 2>/dev/null && echo "legacy" || echo "current"
```

- **`legacy`** (v0.1.0–0.3.0): TS source files in `src/mocks/` + provider wiring. Use [reference/teardown-legacy.md](reference/teardown-legacy.md) instead of the steps below.
- **`current`** (v0.4.0+): bundle in `public/` + HTML script tag injection. Continue with steps below.

### Step T1 — Kill the running servers
```sh
lsof -ti :7332,:7333 | xargs kill 2>/dev/null; echo "servers stopped"
```

### Step T2 — Remove the server file and log
```sh
rm -f <repo-root>/ws-task-server.js <repo-root>/ws-task-server.cjs
rm -f /tmp/ws-task-server.log
```

### Step T3 — Remove the bundle
```sh
rm -f <frontend>/public/frontloop-bundle.js
```

### Step T4 — Remove the script tag injection

Three things were injected — the comment, the guard script, and the bundle script. All three must go.

For **Vite** (`<frontend>/index.html`), **CRA** (`<frontend>/public/index.html`), **Angular** (`<frontend>/src/index.html`), and **Vanilla** (`<frontend>/index.html`), run the appropriate command:

```sh
# Vite
sed -i '' '/frontloop dev guard/d; /frontloop-bundle/d; /__FRONTLOOP_ENABLED__/d' <frontend>/index.html

# CRA
sed -i '' '/frontloop dev guard/d; /frontloop-bundle/d; /__FRONTLOOP_ENABLED__/d' <frontend>/public/index.html

# Angular
sed -i '' '/frontloop dev guard/d; /frontloop-bundle/d; /__FRONTLOOP_ENABLED__/d' <frontend>/src/index.html
```

For **Angular**, also remove the `{ "glob": "**/*", "input": "public", "output": "/" }` asset entry from `angular.json` (`build` and `test` targets) before running the sed command above.

For **Next.js**, remove the entire conditional `<Script>` block from `layout.tsx` / `_document.tsx` — see [reference/wiring.md](reference/wiring.md) for the exact block that was added.

Verify nothing remains:
```sh
grep -r '__FRONTLOOP_ENABLED__\|frontloop-bundle' <frontend>/index.html <frontend>/public/index.html <frontend>/src/index.html 2>/dev/null && echo "still present" || echo "clean"
```

### Step T5 — Clean up .gitignore
Remove the lines added during setup:
```
public/frontloop-bundle.js
.rewind/
```

### Step T6 — Confirm
```sh
lsof -i :7332 | grep LISTEN || echo "7332 clear"
lsof -i :7333 | grep LISTEN || echo "7333 clear"
ls <repo-root>/ws-task-server.js <repo-root>/ws-task-server.cjs 2>/dev/null && echo "server file still present — recheck T2" || echo "server file clean"
ls <frontend>/public/frontloop-bundle.js 2>/dev/null && echo "bundle still present — recheck T3" || echo "bundle clean"
```
Tell the user: Frontloop has been removed. Their source files are untouched — all edits made by Frontloop remain in place and in git history.

---

## Edge Cases

| Situation | Action |
|-----------|--------|
| TypeScript errors in modified files | Fix before completion. If fix requires infra changes, tell user + still send completion |
| Concurrent tasks | Complete task 1 fully before starting task 2 |
| Server not running (`lsof -i :7332` empty) | Restart via Monitor (Step 3). Shimmer auto-removes after 180s |
| `EPERM: operation not permitted` on port 7332/7333 | Claude Code sandbox is blocking `listen()`. Start via `Bash` with `dangerouslyDisableSandbox: true` + redirect stdout to `/tmp/ws-task-server.log`, then Monitor the log file (see Step 3 sandbox path) |
| Completion returns 404 | WS connection already closed (tab nav/timeout). Shimmer already gone — no action needed |
| Screenshot upload fails (no `screenshotPath`) | CORS issue — server must set `Access-Control-Allow-Origin: *` and handle `OPTIONS`. Reference file handles this. |
| `element.sourceLocation` is null | Expected when: React 18 with esbuild-only JSX (no Babel), production builds, or non-React frameworks (Angular/Vue). Gracefully omitted from slim stdout — fall back to `componentHierarchy`. |
| FAB doesn't appear | Check `window.__FRONTLOOP_ENABLED__` in browser console — must be `true`. Verify the guard script is injected before the bundle script. |
| FAB appears in production | Guard expression evaluated to `true` in prod. Verify the framework env var (e.g. `import.meta.env.DEV` should be `false` in Vite prod builds). |
| `Failed to load module script: … MIME type of "text/html"` | Either the bundle file is missing from `public/` (dev server returned its SPA fallback), or a `type="module"` guard script was used and Vite extracted it into a proxy module that failed to load. Fix: (1) verify `<frontend>/public/frontloop-bundle.js` exists; (2) ensure neither the guard nor the bundle script tag has `type="module"`. |
| Guard or bundle script tag has `type="module"` | Remove it from both tags. Vite extracts inline `type="module"` scripts into proxy modules (`index.html?html-proxy&index=N.js`) — if that proxy fails, `window.__FRONTLOOP_ENABLED__` is never set and the bundle is a no-op. IIFE bundles don't need module semantics. |
| `window.__FRONTLOOP_ENABLED__` is `false` in dev | Hostname check failed — dev server is not on localhost or 127.0.0.1 (e.g. remote dev box, custom domain). Open browser console and check `window.location.hostname`. If needed, manually set `window.__FRONTLOOP_ENABLED__ = true` in the guard script for that hostname. |
| Bundle loads but FAB doesn't appear | Check `window.__FRONTLOOP_ENABLED__` in the browser console — must be `true`. Verify the guard script is present and comes before the bundle script. |
| `value.replace is not a function` | `cssEscape` receives a non-string value. Guard with `typeof value !== "string"` |
| HMR doesn't reflect change | `POST /reload` forces full page refresh |
| Duplicate FAB in DOM | Bundle loaded twice — check for duplicate script tags in HTML |
| CSS `:hover` dial too wide | Use JS mouseenter/mouseleave (in reference file). CSS `:hover` on flex container includes invisible children in hit area |
| `for...of` over NodeList fails | Use `.forEach()` — `--downlevelIteration` may not be enabled |
| Undo button stays grey after reload | `saveUndoStack()` missing after a `push()`, or `updateUndoBtnState()` not called |
| UNDO task for unknown `targetTaskId` | Prior session — use `git diff` to identify recent edits and offer to revert |

---

## Common Mistakes

| Mistake | Fix |
|---------|------|
| Wrong `taskId` in curl | Copy `id` verbatim from TASK JSON — 8 alphanumeric chars |
| Skipping type check | Always run `npx tsc --noEmit` before `POST /complete` |
| Starting dev server yourself | Never — instruct user to run it |
| Server `MODULE_NOT_FOUND` | `NODE_PATH` must point to `<frontend>/node_modules`, not repo root |
| `EPERM` when starting via Monitor | Sandbox blocks port binding. Use `Bash` + `dangerouslyDisableSandbox: true` + log file + Monitor log (Step 3 sandbox path) |
| Sending `POST /complete` for undo | Undo is fire-and-forget — only send `POST /reload` |
| Not sending `POST /complete` after a question/clarification | Completion is always required for `dom-fix` tasks — even when no code changed. Omitting it leaves the shimmer on screen indefinitely. |
| `type="module"` on guard or bundle script | Remove it — Vite extracts inline module scripts into proxy modules that fail to load; IIFE bundles don't need module semantics |
| Guard script after bundle script | `window.__FRONTLOOP_ENABLED__` must be set before the bundle loads — guard script must come first |
| Injecting bundle into `src/` files | Only patch the HTML entry point — no TypeScript/JSX source files (except Next.js layout) |
