YapUI — HTML Live Preview + Feedback
Turns "open this HTML" into a live loop: the user views the page in their browser and gives feedback by typing, talking, recording the screen, screenshotting, or clicking to pick an element — and a resident pre-warmed agent (booted by the relay itself) picks it up instantly, fixes it, and replies in the browser. They never have to come back to the terminal, and nothing polls: the browser is fed over SSE, the agent over stdin.
The backend relay, the injected widget, and the resident agent live next to this file under relay/ (relay/server.js, relay/widget.js, relay/agent.js). This skill folder is referred to below as <SKILL_DIR> (e.g. ~/.claude/skills/yapui).
When to use
Any request to see / preview / open / serve / render an HTML file in the browser — a mockup, prototype, report, component, or page. Prefer this over open file.html.
Launch (do these)
Target HTML = $HTML (absolute path).
- Free port — try 8765, then 8766, 8780, 8790 (
lsof -iTCP:$p -sTCP:LISTEN -t empty = free).
- Workdir for feedback artifacts — default
"$(dirname "$HTML")/.yapui" (safe to gitignore/delete).
- Start the relay (background,
run_in_background: true):PORT=<port> HTML_FILE="$HTML" WORKDIR="<workdir>" node "<SKILL_DIR>/relay/server.js"
The relay immediately spawns and pre-warms a resident headless claude agent (primed by reading the HTML), so the first note already hits a hot agent.
- Wait, then open:
curl -s --retry 30 --retry-delay 1 --retry-connrefused -o /dev/null "http://localhost:<port>/"
open -a "Google Chrome" "http://localhost:<port>/" # macOS; Linux: xdg-open; Windows: start
- Check the mode:
curl -s http://localhost:<port>/agent
"state":"ready" or "booting" → instant mode (default). Do NOT arm a watcher — the resident agent owns feedback and a watcher would double-process it. You're done.
"state":"off" (no claude on PATH, or YAP_AGENT=off) → watcher fallback — arm the watcher below.
- Tell the user it's live and how to give feedback in the browser: the feedback panel opens already expanded (if they close it, the Feedback button bottom-left — or
f — brings it back) → type · 🎙 Talk · 🎬 Record · 📸 Snap · 🎯 Pick. In instant mode, fixes are handled instantly by the live agent — they'll see each card flip 🔴→🟠 (with a live "✏️ editing…" ticker)→✅ and a reply toast, then the page auto-refreshes. In watcher fallback (/agent said off, or dead later), set expectations instead: their notes land with your main session, and you apply each fix and flip the cards yourself — same loop, slower. Mic / screen-share prompts are normal; artifacts are stored locally in the workdir, and what they send is applied by Claude on their own account either way (docs/guide/privacy.md is the full picture of what leaves the machine).
Instant mode (default) — how it works, and your role
- The relay pipes each note (with element/cursor/voice context, any screenshot, and for recordings a relay-pre-extracted frame sheet) straight into the resident agent's stdin; the card flips to ⛏️ working in ~40ms; tool activity streams onto the card; the agent's final message becomes the in-browser reply;
done flips the card and all-green auto-refreshes the page.
- Notes sent while the agent is busy queue honestly ("⏳ agent is finishing the previous fix…") and dispatch the moment it frees up. The agent keeps session context, so "make that bigger too" works across notes.
- If the agent can't action a note it replies
NEEDS-YOU: … → the card flips 🙋 and blocks auto-refresh; the user answers by sending another note.
- Your role in the terminal: nothing. Don't watch files, don't flip statuses. Stay available for direct questions. If the user asks you to change the page directly, just edit $HTML — the agent re-reads it on its next note.
- Env knobs (set on the relay):
YAP_AGENT=off (disable), YAP_AGENT_MODEL (default sonnet — it also benchmarked faster end-to-end than haiku here), YAP_CLAUDE_BIN, YAP_AGENT_RECYCLE (turns before recycling, default 30), YAP_AGENT_TIMEOUT (hung-turn kill, default 240s). The agent runs --permission-mode acceptEdits limited to Read,Edit,Write,MultiEdit,Grep,Glob in the HTML's directory — it has no shell; recording frame sheets are pre-extracted by the relay.
- If the agent dies 3× it flips remaining cards back to 🔴 and
/agent reports "dead" → treat as watcher fallback from then on.
Watcher fallback (only when /agent says off / dead)
Arm the watcher (background, run_in_background: true) — marker-based so it never skips a note:
FB="<workdir>/feedback.jsonl"; MARK="<workdir>/.fb-processed"
[ -f "$MARK" ] || { wc -l < "$FB" 2>/dev/null | tr -d ' ' > "$MARK" 2>/dev/null || echo 0 > "$MARK"; }
c=0; while [ $c -lt 5400 ]; do
now=$(wc -l < "$FB" 2>/dev/null | tr -d ' '); now=${now:-0}
seen=$(cat "$MARK" 2>/dev/null | tr -d ' '); seen=${seen:-0}
[ "${now:-0}" -gt "${seen:-0}" ] && { echo "NEW_FEEDBACK seen=$seen now=$now"; exit 0; }
c=$((c+1)); sleep 1
done; echo WATCH_IDLE_TIMEOUT
When the watcher fires (you get a task-notification):
- Read
<workdir>/feedback.md — newest at the bottom. Besides the note + page/context, an entry may carry pointing at / pointing timeline lines (where the user's cursor was during the message) — use them to resolve "make this bigger" / "move that" to the actual element, especially in 🎙 (talk) notes where they point while speaking. A talk note also gets a what you said (timeline) on the same clock — line up spoken word with cursor position to know what "this" was. It may also reference a picked element, a recording, or a screenshot.
- Look at any attached media:
- Screenshot (
<workdir>/screenshots/*.png) → Read it.
- Recording (
<workdir>/recordings/*.webm) → see the motion via frames:
ffmpeg -y -i <clip> -vf "fps=4,scale=400:-1,tile=8x8" -frames:v 1 /tmp/sheet.png → Read it; for a fast transition zoom in: -ss <t> -t <dur> -vf "fps=18,scale=560:-1,tile=6x5".
- Picked element →
element.selector / data-* / text point you straight at the DOM node in the HTML source.
- Flip the card to working, then apply the fix. Each note is a queue card whose id is its
taskId:
node "<SKILL_DIR>/relay/flip-status.js" "<workdir>" <taskId> working
Then edit the HTML. The relay re-reads the file each load, so the user just refreshes to see HTML changes — no restart. (Restart the relay only if you edit relay/server.js or relay/agent.js; widget edits self-reload.)
- Verify when it matters by rendering with headless Chrome / Playwright (
chromium.launch({channel:'chrome'}), playwright-core avoids a browser download) against http://localhost:<port>/ and Reading the screenshot.
- Reply in the browser — append one line to
<workdir>/claude-replies.jsonl:node -e 'const fs=require("fs");fs.appendFileSync(process.argv[1],JSON.stringify({ts:new Date().toISOString(),text:process.argv[2]})+"\n")' \
"<workdir>/claude-replies.jsonl" "Fixed X — refresh to see."
node "<SKILL_DIR>/relay/flip-status.js" "<workdir>" <taskId> done # or: needs-you (a question you can't action)
The relay pushes replies and flips to the browser instantly over SSE.
- Advance the marker to the watcher's reported
now, then re-arm:
echo <now> > "<workdir>/.fb-processed" (the now from NEW_FEEDBACK seen=X now=Y) — not a fresh wc -l. A note that arrived while you were working sits above now, so the re-armed watcher fires for it; re-counting the file here would mark it seen and silently drop it. Then start the watcher loop again.
Keep a short terminal note too, but the in-browser reply is the primary channel.
Browser states the user sees
- Task queue (bottom-right by default) — each note becomes a card: 🔴 queued → 🟠 ⛏️ working (with a live activity line: "✏️ editing index.html…") → ✅ done (🙋 needs-you). All cards green → the page auto-refreshes; ✕ removes a card. A 🖥 line under the header shows what their cursor is over. Drag it by its header to move it anywhere (the spot sticks across reloads); the ▾ in its header collapses it to a status pill. The Feedback panel and button drag the same way.
- ⚡ Claude is ready — instant fixes (header, idle) — resident agent hot. Watcher fallback shows 👀 Claude is watching instead.
- ⟳ Claude is working on it… (top-center) + the Feedback button glows — the instant they send.
- Claude: … — refresh to see (top-center) — on reply, then back to ⚡/👀.
Notes
- All live updates (cards, ticker, replies, agent state) push over SSE (
/events); the widget falls back to polling only if the stream drops. HTML edits → refresh (or auto-refresh on all-green). Widget edits self-reload the open page; relay/server.js / relay/agent.js edits need a relay restart (the page then self-reloads too). The feedback panel opens expanded by default, remembers a deliberate collapse, and keeps an unsent draft across reloads.
- Artifacts live under
<workdir>: feedback.md, feedback.jsonl, recordings/, screenshots/, claude-replies.jsonl, tasks.jsonl, .fb-processed.
- Requirements: Node + the
claude CLI on PATH for instant mode (falls back to watcher mode without it), a Chromium-based browser for voice/recording, ffmpeg to read recordings, internet for the screenshot lib (html2canvas via CDN).
Stop
Kill the relay (lsof -ti:<port> | xargs kill) — it takes the resident agent down with it. In watcher fallback, also stop the watcher background task.
1---2name: yapui3description: View, preview, open, render, or serve any HTML file/page/mockup/prototype in the browser with a live two-way feedback loop. Use this whenever the user wants to look at HTML in a browser, see a rendered page, preview a mockup or prototype, or iterate on an HTML UI — instead of a bare `open file.html` (file:// blocks the mic + screen capture and gives no feedback channel). It serves the HTML from a local relay, injects a feedback widget (typed notes, voice dictation, screen recording, screenshots, and click-to-pick element selection), and boots a resident pre-warmed Claude agent that picks up each note the instant it is sent (~0s pickup), applies the fix, streams live "what I'm doing" status to the page, and replies in the browser. Trigger for ANY request to view / preview / serve / open / render HTML.4---56# YapUI — HTML Live Preview + Feedback78Turns "open this HTML" into a live loop: the user views the page in their browser and gives feedback by **typing, talking, recording the screen, screenshotting, or clicking to pick an element** — and a **resident pre-warmed agent** (booted by the relay itself) picks it up instantly, fixes it, and replies in the browser. They never have to come back to the terminal, and nothing polls: the browser is fed over SSE, the agent over stdin.910The backend relay, the injected widget, and the resident agent live next to this file under `relay/` (`relay/server.js`, `relay/widget.js`, `relay/agent.js`). This skill folder is referred to below as `<SKILL_DIR>` (e.g. `~/.claude/skills/yapui`).1112## When to use13Any request to **see / preview / open / serve / render** an HTML file in the browser — a mockup, prototype, report, component, or page. Prefer this over `open file.html`.1415## Launch (do these)1617Target HTML = **$HTML** (absolute path).18191. **Free port** — try 8765, then 8766, 8780, 8790 (`lsof -iTCP:$p -sTCP:LISTEN -t` empty = free).202. **Workdir** for feedback artifacts — default `"$(dirname "$HTML")/.yapui"` (safe to gitignore/delete).213. **Start the relay (background, `run_in_background: true`):**22 ```sh23 PORT=<port> HTML_FILE="$HTML" WORKDIR="<workdir>" node "<SKILL_DIR>/relay/server.js"24 ```25 The relay immediately spawns and **pre-warms a resident headless `claude` agent** (primed by reading the HTML), so the first note already hits a hot agent.264. **Wait, then open:**27 ```sh28 curl -s --retry 30 --retry-delay 1 --retry-connrefused -o /dev/null "http://localhost:<port>/"29 open -a "Google Chrome" "http://localhost:<port>/" # macOS; Linux: xdg-open; Windows: start30 ```315. **Check the mode:** `curl -s http://localhost:<port>/agent`32 - `"state":"ready"` or `"booting"` → **instant mode** (default). Do **NOT** arm a watcher — the resident agent owns feedback and a watcher would double-process it. You're done.33 - `"state":"off"` (no `claude` on PATH, or `YAP_AGENT=off`) → **watcher fallback** — arm the watcher below.346. **Tell the user** it's live and how to give feedback in the browser: the feedback panel opens already expanded (if they close it, the **Feedback** button bottom-left — or `f` — brings it back) → **type · 🎙 Talk · 🎬 Record · 📸 Snap · 🎯 Pick**. In **instant mode**, fixes are handled instantly by the live agent — they'll see each card flip 🔴→🟠 (with a live "✏️ editing…" ticker)→✅ and a reply toast, then the page auto-refreshes. In **watcher fallback** (`/agent` said `off`, or `dead` later), set expectations instead: their notes land with your main session, and you apply each fix and flip the cards yourself — same loop, slower. Mic / screen-share prompts are normal; artifacts are stored locally in the workdir, and what they send is applied by Claude on their own account either way (`docs/guide/privacy.md` is the full picture of what leaves the machine).3536## Instant mode (default) — how it works, and your role3738- The relay pipes each note (with element/cursor/voice context, any screenshot, and for recordings a relay-pre-extracted frame sheet) straight into the resident agent's stdin; the card flips to ⛏️ working in ~40ms; tool activity streams onto the card; the agent's final message becomes the in-browser reply; `done` flips the card and all-green auto-refreshes the page.39- Notes sent while the agent is busy queue honestly ("⏳ agent is finishing the previous fix…") and dispatch the moment it frees up. The agent keeps session context, so "make **that** bigger too" works across notes.40- If the agent can't action a note it replies `NEEDS-YOU: …` → the card flips 🙋 and blocks auto-refresh; the user answers by sending another note.41- **Your role in the terminal: nothing.** Don't watch files, don't flip statuses. Stay available for direct questions. If the user asks you to change the page directly, just edit $HTML — the agent re-reads it on its next note.42- Env knobs (set on the relay): `YAP_AGENT=off` (disable), `YAP_AGENT_MODEL` (default `sonnet` — it also benchmarked faster end-to-end than `haiku` here), `YAP_CLAUDE_BIN`, `YAP_AGENT_RECYCLE` (turns before recycling, default 30), `YAP_AGENT_TIMEOUT` (hung-turn kill, default 240s). The agent runs `--permission-mode acceptEdits` limited to `Read,Edit,Write,MultiEdit,Grep,Glob` in the HTML's directory — it has no shell; recording frame sheets are pre-extracted by the relay.43- If the agent dies 3× it flips remaining cards back to 🔴 and `/agent` reports `"dead"` → treat as watcher fallback from then on.4445## Watcher fallback (only when `/agent` says `off` / `dead`)4647**Arm the watcher (background, `run_in_background: true`)** — marker-based so it never skips a note:48```sh49FB="<workdir>/feedback.jsonl"; MARK="<workdir>/.fb-processed"50[ -f "$MARK" ] || { wc -l < "$FB" 2>/dev/null | tr -d ' ' > "$MARK" 2>/dev/null || echo 0 > "$MARK"; }51c=0; while [ $c -lt 5400 ]; do52 now=$(wc -l < "$FB" 2>/dev/null | tr -d ' '); now=${now:-0}53 seen=$(cat "$MARK" 2>/dev/null | tr -d ' '); seen=${seen:-0}54 [ "${now:-0}" -gt "${seen:-0}" ] && { echo "NEW_FEEDBACK seen=$seen now=$now"; exit 0; }55 c=$((c+1)); sleep 156done; echo WATCH_IDLE_TIMEOUT57```5859**When the watcher fires (you get a task-notification):**60611. **Read `<workdir>/feedback.md`** — newest at the bottom. Besides the note + page/context, an entry may carry **`pointing at` / `pointing timeline`** lines (where the user's cursor was *during* the message) — use them to resolve "make **this** bigger" / "move **that**" to the actual element, especially in **🎙 (talk)** notes where they point while speaking. A talk note also gets a **`what you said (timeline)`** on the *same clock* — line up spoken word with cursor position to know what "this" was. It may also reference a picked element, a recording, or a screenshot.622. **Look at any attached media:**63 - Screenshot (`<workdir>/screenshots/*.png`) → Read it.64 - Recording (`<workdir>/recordings/*.webm`) → see the motion via frames:65 `ffmpeg -y -i <clip> -vf "fps=4,scale=400:-1,tile=8x8" -frames:v 1 /tmp/sheet.png` → Read it; for a fast transition zoom in: `-ss <t> -t <dur> -vf "fps=18,scale=560:-1,tile=6x5"`.66 - Picked element → `element.selector` / `data-*` / text point you straight at the DOM node in the HTML source.673. **Flip the card to working, then apply the fix.** Each note is a queue card whose id is its `taskId`:68 `node "<SKILL_DIR>/relay/flip-status.js" "<workdir>" <taskId> working`69 Then edit the HTML. The relay re-reads the file each load, so the user just **refreshes** to see HTML changes — no restart. (Restart the relay only if you edit `relay/server.js` or `relay/agent.js`; widget edits self-reload.)704. **Verify when it matters** by rendering with headless Chrome / Playwright (`chromium.launch({channel:'chrome'})`, `playwright-core` avoids a browser download) against `http://localhost:<port>/` and Reading the screenshot.715. **Reply in the browser** — append one line to `<workdir>/claude-replies.jsonl`:72 ```sh73 node -e 'const fs=require("fs");fs.appendFileSync(process.argv[1],JSON.stringify({ts:new Date().toISOString(),text:process.argv[2]})+"\n")' \74 "<workdir>/claude-replies.jsonl" "Fixed X — refresh to see."75 node "<SKILL_DIR>/relay/flip-status.js" "<workdir>" <taskId> done # or: needs-you (a question you can't action)76 ```77 The relay pushes replies and flips to the browser instantly over SSE.786. **Advance the marker to the watcher's reported `now`, then re-arm:**79 `echo <now> > "<workdir>/.fb-processed"` (the `now` from `NEW_FEEDBACK seen=X now=Y`) — **not** a fresh `wc -l`. A note that arrived while you were working sits *above* `now`, so the re-armed watcher fires for it; re-counting the file here would mark it seen and silently drop it. Then start the watcher loop again.8081Keep a short terminal note too, but the in-browser reply is the primary channel.8283## Browser states the user sees84- **Task queue (bottom-right by default)** — each note becomes a card: 🔴 queued → 🟠 ⛏️ working (with a live activity line: "✏️ editing index.html…") → ✅ done (🙋 needs-you). All cards green → the page auto-refreshes; ✕ removes a card. A 🖥 line under the header shows what their cursor is over. Drag it by its header to move it anywhere (the spot sticks across reloads); the **▾** in its header collapses it to a status pill. The Feedback panel and button drag the same way.85- **⚡ Claude is ready — instant fixes** (header, idle) — resident agent hot. Watcher fallback shows **👀 Claude is watching** instead.86- **⟳ Claude is working on it…** (top-center) + the Feedback button glows — the instant they send.87- **Claude: … — refresh to see** (top-center) — on reply, then back to ⚡/👀.8889## Notes90- All live updates (cards, ticker, replies, agent state) push over **SSE** (`/events`); the widget falls back to polling only if the stream drops. HTML edits → refresh (or auto-refresh on all-green). **Widget edits self-reload the open page**; `relay/server.js` / `relay/agent.js` edits need a relay restart (the page then self-reloads too). The feedback panel opens **expanded by default**, remembers a deliberate collapse, and keeps an unsent draft across reloads.91- Artifacts live under `<workdir>`: `feedback.md`, `feedback.jsonl`, `recordings/`, `screenshots/`, `claude-replies.jsonl`, `tasks.jsonl`, `.fb-processed`.92- Requirements: Node + the `claude` CLI on PATH for instant mode (falls back to watcher mode without it), a Chromium-based browser for voice/recording, `ffmpeg` to read recordings, internet for the screenshot lib (html2canvas via CDN).9394## Stop95Kill the relay (`lsof -ti:<port> | xargs kill`) — it takes the resident agent down with it. In watcher fallback, also stop the watcher background task.