Previously, starting live mode required ~5-6 sequential bash calls: read .impeccable.md, start server, check config, read reference, inject tag, verify. The new live.mjs does all of this in a single command (~340ms cold, ~90ms when reusing a running server) and returns everything the agent needs in one JSON blob. Workflow is now: 1. node live.mjs # start + inject + load context (1 bash call) 2. navigate browser # optional MCP call 3. node live-poll.mjs # enter poll loop Reference doc collapsed to a single "Start Live Mode" section with the one-command path plus a first-time config creation fallback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9.1 KiB
Launch interactive live variant mode: select elements in the browser, pick a design action, and get AI-generated HTML+CSS variants hot-swapped via the dev server's HMR.
Prerequisites
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
Start Live Mode (one command)
The live.mjs entry point does everything in a single call: checks config, starts (or reuses) the server, injects the script tag, loads .impeccable.md context.
node {{scripts_path}}/live.mjs
Happy path
Output JSON:
{
"ok": true,
"serverPort": 8400,
"serverToken": "...",
"pageFile": "public/index.html",
"hasContext": true,
"context": "...full .impeccable.md contents..."
}
Keep the context in mind for variant generation. If browser automation tools are available, navigate to the page so the user can see it. Then proceed directly to the poll loop — no other setup steps needed.
First-time setup (config missing)
If live.mjs outputs {"ok": false, "error": "config_missing", "configPath": "..."}, this project has never used live mode before. Create the config at the reported path based on the project's framework:
| Framework | file |
insertBefore |
commentSyntax |
|---|---|---|---|
| Plain HTML | index.html |
</body> |
html |
| Vite / React | index.html |
</body> |
html |
| Next.js (App Router) | app/layout.tsx |
</body> |
jsx |
| Next.js (Pages) | pages/_document.tsx |
</body> |
jsx |
| Nuxt | app.vue |
</body> |
html |
| Svelte / SvelteKit | src/app.html |
</body> |
html |
| Astro | the root layout .astro file |
</body> |
html |
| Static site with a non-root HTML file | e.g. public/index.html |
</body> |
html |
Use insertAfter instead of insertBefore if the anchor should be matched after a specific line. Example:
{
"file": "public/index.html",
"insertBefore": "</body>",
"commentSyntax": "html"
}
Then re-run node {{scripts_path}}/live.mjs to proceed.
Enter the Poll Loop
Run the poll as a background task if your harness supports it (Claude Code does). This keeps the main conversation free for other work while waiting for browser events. Do NOT set a timeout: the poll should wait indefinitely until the user acts.
LOOP:
Run (background, no timeout): node {{scripts_path}}/live-poll.mjs
When the task completes, read the JSON output. Dispatch based on the "type" field:
TYPE "generate":
→ See "Handle Generate" below
TYPE "accept":
→ See "Handle Accept" below
TYPE "discard":
→ See "Handle Discard" below
TYPE "exit":
→ Break the loop
TYPE "timeout":
→ Continue (re-poll)
END LOOP
Handle Generate
The event contains: {id, action, freeformPrompt, count, pageUrl, element}.
Speed matters. The user is watching a spinner. Minimize tool calls by using the wrap helper and writing all variants in a single edit.
Step 1: Wrap the element (one CLI call)
Use the wrap helper to find the element and create the variant container:
node {{scripts_path}}/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div"
Pass the element's id (event.element.id), classes (event.element.classes joined with commas), and tag name. The command searches in priority order: ID match first, then class names, then tag+class combo. If event.pageUrl hints at the file (e.g., / is usually index.html), pass --file PATH to skip the search.
The command outputs JSON with the file path and the insert line:
{"file": "public/index.html", "insertLine": 93, "commentSyntax": {"open": "<!--", "close": "-->"}}
If wrap fails, fall back to manual grep + edit.
Step 2: Generate all variants and write them in a SINGLE edit
-
Load the design command's reference file. If
event.actionis "bolder", loadreference/bolder.md. If "impeccable" (the default), use the main design principles from this skill without loading a sub-command reference. -
Generate ALL variants at once. For each variant, create a complete HTML replacement of the original element. Consider the element's context (computed styles, parent structure, CSS custom properties from
event.element). -
Diversify across variants. Each variant should take a distinctly different approach. For "bolder", one might focus on type weight, another on color saturation, another on spatial scale, another on structural change. Do NOT generate N variations on the same idea.
-
If a freeform prompt was provided (
event.freeformPrompt), use it as additional guidance for all variants. -
Write CSS + HTML together in a SINGLE edit at the insert line reported by
wrap. Colocate any scoped CSS inside the variant wrapper as a<style>tag.<style>tags work anywhere in the document in all modern browsers, and this ensures CSS and HTML arrive atomically (no flash of unstyled content).
<!-- Variants: insert below this line -->
<style data-impeccable-css="SESSION_ID">
@scope ([data-impeccable-variant="1"]) { ... }
@scope ([data-impeccable-variant="2"]) { ... }
</style>
<div data-impeccable-variant="1">
<!-- variant 1: full element replacement -->
</div>
<div data-impeccable-variant="2" style="display: none">
<!-- variant 2: full element replacement -->
</div>
<div data-impeccable-variant="3" style="display: none">
<!-- variant 3: full element replacement -->
</div>
The first variant should NOT have style="display: none" (it should be visible by default). All others should. If variants only use inline styles and no scoped CSS, omit the <style> tag entirely.
IMPORTANT: Write CSS and all variants in ONE edit call. The browser's MutationObserver picks up everything at once.
Step 3: Signal completion
Include --file so the browser can fetch variants directly if the dev server lacks HMR:
node {{scripts_path}}/live-poll.mjs --reply EVENT_ID done --file RELATIVE_PATH
The file path should be relative to the project root (e.g., public/index.html, src/App.tsx).
Handle Accept
The event contains: {id, variantId, _acceptResult}.
The poll script already ran live-accept.mjs to handle the file operation deterministically. The browser has already updated the DOM visually (the user is unblocked).
Check _acceptResult:
- If
handledis true andcarbonizeis false: no work needed. Re-poll immediately. - If
handledis true andcarbonizeis true: the accepted variant has an inline<style>block marked withimpeccable-carbonize-start/impeccable-carbonize-endcomments. Spawn a background agent to:- Find the carbonize markers in the file
- Move the CSS rules into the project's proper stylesheet(s)
- Rewrite
@scopeselectors to use the element's real classes instead of[data-impeccable-variant] - Remove any helper classes/attributes (e.g.
data-impeccable-variant) from the accepted HTML - Delete the carbonize markers and inline
<style>block Then re-poll immediately (do not wait for the background agent).
- If
handledis false: fall back to manual cleanup (read file, find markers, edit).
Handle Discard
The event contains: {id, _acceptResult}.
The poll script already ran live-accept.mjs to restore the original and remove all variant markers. The browser has already updated the DOM visually. No work needed. Re-poll immediately.
Stopping Live Mode
The user can stop live mode in several ways:
- Saying "stop live mode" or "exit live" in the conversation
- Closing the browser tab (the SSE connection drops, poll returns
exitafter 8s) - The browser's exit button (when the global bar is implemented)
When the user asks to stop, or the poll returns exit, proceed to Cleanup below.
If the poll is still running as a background task, kill it and proceed directly to cleanup.
Cleanup (on exit)
When the loop ends:
- Remove the injected script tag:
(The config.json stays so future
node {{scripts_path}}/live-inject.mjs --removelive-inject.mjs --port PORTcalls are instant.) - Remove any leftover variant wrappers (search for
impeccable-variants-startmarkers and clean up). - Remove any leftover carbonize blocks (search for
impeccable-carbonize-startmarkers and clean up). - Stop the server:
node {{scripts_path}}/live-server.mjs stop
Variant Generation Guidelines
- Each variant must be a complete element replacement, not a CSS-only patch. Rewrite the entire element with the design transformation applied.
- Use
@scopefor CSS isolation. This is supported in Chrome 118+, Firefox 128+, Safari 17.4+, which covers all modern dev browsers. - Follow the design principles from this skill (typography, color, spatial design, etc.) and the
.impeccable.mdproject context if available. - If no
.impeccable.mdexists, generate brand-agnostic variants. The live UI will show a warning to the user. - Non-interactive mode: do NOT ask the user for clarification during generation. If context is missing, proceed with reasonable defaults.