live-server.mjs stop now runs live-inject.mjs --remove after the HTTP server shuts down, so HTML entries do not keep loading a dead localhost live.js URL. Add stop --keep-inject to stop only the helper. Update reference/live.md cleanup steps and sync all provider skill copies. Made-with: Cursor
16 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",
"hasProduct": true,
"product": "...full PRODUCT.md contents...",
"productPath": "PRODUCT.md",
"hasDesign": true,
"design": "...full DESIGN.md contents...",
"designPath": "DESIGN.md",
"migrated": false
}
serverPort / serverToken: These belong to the small Impeccable live helper HTTP server (serves /live.js for the injected <script>, SSE, and the agent’s /poll long-poll). That port is not your framework dev server and is usually not the URL you open to view the app—unless your project is set up that way on purpose. The browser page you care about is whatever origin actually serves the HTML entry (pageFile / your dev or preview workflow): Vite/Next/Bun, static server, tunnel, LAN hostname, etc.
Keep PRODUCT.md (strategic: users, brand, principles) and DESIGN.md (visual: colors, typography, components) in mind for variant generation. DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.
If migrated is true, the loader auto-renamed legacy .impeccable.md to PRODUCT.md — mention this once to the user and suggest running /impeccable document to also generate a DESIGN.md.
After live.mjs succeeds (same turn, no waiting)
- Navigate to the URL that serves
pageFile(infer frompackage.json, docs, terminals, or an open tab). If the IDE has browser MCP (e.g. Cursor:browser_navigate), do this before the first poll. Never useserverPortfrom the JSON as that URL; it is the helper, not the app. - If there is no browser automation, say once that the user should open their dev/preview URL for this project.
- Start the Poll loop below.
Live session contract
While live mode is active, a long poll must almost always be running (or about to run again). If nothing is blocking on /poll, generate / accept / discard / exit events are not delivered to this agent; the page can stay stuck (e.g. after --reply with no follow-up poll).
- Poll command:
node {{scripts_path}}/live-poll.mjswith default arguments (the default HTTP timeout is 600000 ms; seelive-poll.mjs --help). Forbidden: passing a short--timeout=to end the turn, “probe,” or save time. Allowed: omit--timeoutentirely unless the user explicitly asked to pause or exit live. - Immediately after
live-poll.mjs --reply EVENT_ID done --file …, and immediately after accept/discard when no extra work is required: runlive-poll.mjsagain with the same long-timeout policy before other tool chatter or ending the turn. {"type":"timeout"}: No event yet—runlive-poll.mjsagain with the same policy. Do not shorten--timeout; that is not exit and not permission to drop the loop.- Harness: Cursor: run the poll in the foreground (blocking shell; not
block_until_ms: 0, not background). Background terminals and background subagents do not reliably resume this chat with poll stdout (subagents). Claude Code: poll may run in a background task with no short timeout. Other harnesses: foreground unless stdout reliably returns to this session.
Assistant chat output (keep minimal)
Live mode is latency-sensitive. Treat chat as overhead.
- Do not end the turn with a long recap (ports, token paths, duplicated context from JSON, “next steps” lists). The user needs a polling agent, not a tutorial.
- Do spend tokens on tools and edits; on failure, one or two short sentences.
- Do not paste PRODUCT/DESIGN bodies into chat; use them silently.
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.
Poll loop
Required after a successful live.mjs in the same invocation as {{command_prefix}}impeccable live. Rules and timeouts: Live session contract above.
LOOP:
node {{scripts_path}}/live-poll.mjs
Read JSON; dispatch on "type"
"generate" → Handle Generate; then --reply … done; then LOOP (same poll policy)
"accept" → Handle Accept; then LOOP
"discard" → Handle Discard; then LOOP
"exit" → break → Cleanup
"timeout" → LOOP (same poll policy; do not shorten --timeout)
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 2a: MANDATORY — Load the action's reference file
This step is non-negotiable. Before generating anything, you MUST load the reference file for event.action:
event.actionis "impeccable" (default, no sub-command chosen): use the main design principles fromSKILL.md(already loaded). Do NOT load a sub-command reference.event.actionis any other value (e.g. "bolder", "quieter", "distill", "polish", "typeset", "colorize", "layout", "adapt", "animate", "delight", "overdrive"): use Read to loadreference/<action>.mdright now. Do not proceed until it's in context.
Skipping this step is a critical failure. The sub-commands exist precisely because the generic "impeccable" prompt produces generic variants. Each action encodes a specific design discipline — ignoring the reference file means ignoring what the user asked for.
Step 2b: Plan 3+ distinctly different directions BEFORE writing any code
Before writing the first variant, write out (in your own head or as a short plan) the distinct direction each variant will take. Each direction must differ on at least ONE of these structural axes, not just superficial styling:
- Hierarchy: which element is the focal point? (title-first, number-first, image-first, quote-first)
- Layout topology: how are pieces arranged? (stacked, side-by-side, inline, grid, magazine-columns, overlay)
- Typographic system: different font pairing, different scale ratios, different case/weight strategy
- Color strategy: different palette hue, different accent placement, different contrast profile (not just "a slightly different shade of the same accent")
- Density: minimal vs. dense vs. editorial whitespace
- Tone/personality: refined/editorial vs. brutalist/raw vs. soft/pastel vs. technical/utilitarian vs. playful
- Structural decomposition: combining multiple pieces vs. splitting into more pieces vs. hiding secondary info behind progressive disclosure
Rule of thumb: if you can summarize two variants in the same one-line description (e.g. "rose accent on the title"), they are too similar. Redo one.
For action-specific rules, each variant must differ along the dimension the action names:
bolder: amplifies a DIFFERENT dimension per variant (one goes huge on scale, one on color saturation, one on structural change). Not three "slightly bigger" variants.quieter: pulls back a DIFFERENT dimension per variant (one strips color, one strips ornament, one widens the spacing).distill: removes a DIFFERENT class of excess per variant (one removes visual noise like borders/shadows, one removes redundant content, one collapses nested structure).polish: targets a DIFFERENT refinement axis per variant (one fixes spacing/alignment rhythm, one sharpens typographic hierarchy, one tunes micro-details like corner radii, focus states, optical kerning).typeset: each variant uses a DIFFERENT type pairing and a DIFFERENT scale ratio. Not three riffs on the same pairing.colorize: each variant uses a DIFFERENT hue family (not three shades of the same hue) and varies chroma/contrast strategy.layout: each variant changes structural arrangement (stacked / side-by-side / grid / asymmetric), not spacing tweaks.adapt: each variant targets a DIFFERENT context (mobile-first / tablet-columns / desktop-spread / print/low-data). Don't make three mobile layouts.animate: each variant uses a DIFFERENT motion vocabulary (cascade stagger / clip wipe / scale-and-focus / morph / parallax). Not three staggered fades.delight: each variant adds a DIFFERENT flavor of personality (unexpected micro-interaction / typographic surprise / illustrated accent / sound-or-haptic moment / easter-egg interaction). Not three button hovers.overdrive: each variant breaks a DIFFERENT convention (scale / structure / motion / input model / state transitions). Skipoverdrive.md's "propose and ask" step — live mode is non-interactive, the user will pick from the variants.
Step 2c: Apply the freeform prompt (if present)
If event.freeformPrompt is set, treat it as the user's ceiling on direction — all variants must honor it — but the variants still need to explore meaningfully different interpretations of that direction. Example: prompt "make it feel like a newspaper front page" → variant 1 = broadsheet masthead + rule-divided columns, variant 2 = tabloid headline + single dominant image, variant 3 = minimalist editorial with oversized drop cap. Not three newspapers in the same voice.
Step 2d: Generate ALL variants and write them in a SINGLE edit
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).
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).
Then live-poll.mjs again per Live session contract (default long timeout; Cursor: foreground).
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.live-poll.mjsagain (Live session contract). - 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 Thenlive-poll.mjsagain (Live session contract); 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. live-poll.mjs again (Live session contract).
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:
- Stop the live helper and remove the injected script tag (one command):
This stops the HTTP server and runs
node {{scripts_path}}/live-server.mjs stoplive-inject.mjs --removeso the HTML entry no longer loadslocalhost:…/live.js. To stop the server without editing the entry file, usestop --keep-injectand remove the tag manually when ready. (config.jsonstays so futurelive-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).
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.