Fix 5 bugs from real-world live mode testing

1. Skill reference: poll should run as background task with no timeout.
   Changed "blocking poll loop" to "background task, no timeout" so the
   agent keeps the main conversation free for other work.

2. Resume restores selectedAction from localStorage: the bar was showing
   "Freeform" after page reload even when the user picked "Bolder". Also
   improved selectedElement targeting to prefer the visible variant's
   content over the wrapper parent.

3. Discard no longer shows "Applying variant...": accept shows the
   saving→confirmed flow, but discard now dismisses immediately and
   cleans up the DOM. Different intent, different UX.

4. Picker works after discard: cleanup() now removes the variant wrapper
   from the live DOM and restores the original element. Previously the
   stale wrapper with data-impeccable-variant attributes confused the
   picker's isPickable/own checks.

5. Stop live mode: added "Stopping Live Mode" section to the skill
   reference. The user can say "stop live mode" in the conversation, and
   the agent proceeds to cleanup (remove script tag, stop server).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-13 10:04:31 -07:00
co-authored by Claude Opus 4.6
parent 90d5158331
commit 52b050bb7e
25 changed files with 602 additions and 169 deletions
+14 -4
View File
@@ -3,7 +3,6 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Prerequisites
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
- The impeccable CLI installed (`npm i -g impeccable`)
## Start the Server
@@ -49,12 +48,12 @@ If browser automation tools are available, also navigate to the page so the user
## Enter the Poll Loop
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
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: node {{scripts_path}}/live-poll.mjs
Read the JSON output. Dispatch based on the "type" field:
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
@@ -170,6 +169,17 @@ The event contains: `{id}`.
node {{scripts_path}}/live-poll.mjs --reply SESSION_ID done
```
## 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 `exit` after 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:
@@ -1076,9 +1076,8 @@
if (!currentSessionId) return;
sendEvent({ type: 'discard', id: currentSessionId });
markSessionHandled();
state = 'SAVING';
updateBarContent('saving');
// Wait for "done" WS message to show confirmation and dismiss
// Discard dismisses immediately (no "Applying" state, the agent just cleans up)
cleanup();
}
// ---------------------------------------------------------------------------
@@ -1135,6 +1134,26 @@
}
function cleanup() {
// Remove any leftover variant wrapper from the live DOM.
// After discard, the agent cleans the source, but on dev servers without
// HMR the DOM still has the old wrapper, which confuses the picker.
if (currentSessionId) {
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (wrapper) {
// Restore the original element into the DOM
const orig = wrapper.querySelector('[data-impeccable-variant="original"]');
if (orig) {
const content = orig.firstElementChild;
if (content) {
wrapper.parentElement.replaceChild(content, wrapper);
} else {
wrapper.remove();
}
} else {
wrapper.remove();
}
}
}
hideBar();
hideHighlight();
stopScrollTracking();
@@ -1199,14 +1218,21 @@
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
// Restore visible variant from localStorage if available, else default to 1
// Restore state from localStorage if available
const saved = loadSession();
visibleVariant = (saved && saved.id === sessionId && saved.visible > 0 && saved.visible <= arrivedVariants)
? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved && saved.id === sessionId) {
visibleVariant = (saved.visible > 0 && saved.visible <= arrivedVariants) ? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
} else {
visibleVariant = arrivedVariants > 0 ? 1 : 0;
}
// Find the visible variant's content element for highlight positioning
// Find the visible variant's content element for highlight positioning.
// Try the visible variant first, fall back to the original's content.
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
selectedElement = visEl || wrapper.parentElement;
const origEl = wrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
selectedElement = visEl || origEl || wrapper.parentElement;
// Set display state BEFORE starting observer (avoid triggering it)
if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);
+14 -4
View File
@@ -3,7 +3,6 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Prerequisites
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
- The impeccable CLI installed (`npm i -g impeccable`)
## Start the Server
@@ -49,12 +48,12 @@ If browser automation tools are available, also navigate to the page so the user
## Enter the Poll Loop
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
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: node {{scripts_path}}/live-poll.mjs
Read the JSON output. Dispatch based on the "type" field:
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
@@ -170,6 +169,17 @@ The event contains: `{id}`.
node {{scripts_path}}/live-poll.mjs --reply SESSION_ID done
```
## 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 `exit` after 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:
@@ -1076,9 +1076,8 @@
if (!currentSessionId) return;
sendEvent({ type: 'discard', id: currentSessionId });
markSessionHandled();
state = 'SAVING';
updateBarContent('saving');
// Wait for "done" WS message to show confirmation and dismiss
// Discard dismisses immediately (no "Applying" state, the agent just cleans up)
cleanup();
}
// ---------------------------------------------------------------------------
@@ -1135,6 +1134,26 @@
}
function cleanup() {
// Remove any leftover variant wrapper from the live DOM.
// After discard, the agent cleans the source, but on dev servers without
// HMR the DOM still has the old wrapper, which confuses the picker.
if (currentSessionId) {
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (wrapper) {
// Restore the original element into the DOM
const orig = wrapper.querySelector('[data-impeccable-variant="original"]');
if (orig) {
const content = orig.firstElementChild;
if (content) {
wrapper.parentElement.replaceChild(content, wrapper);
} else {
wrapper.remove();
}
} else {
wrapper.remove();
}
}
}
hideBar();
hideHighlight();
stopScrollTracking();
@@ -1199,14 +1218,21 @@
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
// Restore visible variant from localStorage if available, else default to 1
// Restore state from localStorage if available
const saved = loadSession();
visibleVariant = (saved && saved.id === sessionId && saved.visible > 0 && saved.visible <= arrivedVariants)
? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved && saved.id === sessionId) {
visibleVariant = (saved.visible > 0 && saved.visible <= arrivedVariants) ? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
} else {
visibleVariant = arrivedVariants > 0 ? 1 : 0;
}
// Find the visible variant's content element for highlight positioning
// Find the visible variant's content element for highlight positioning.
// Try the visible variant first, fall back to the original's content.
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
selectedElement = visEl || wrapper.parentElement;
const origEl = wrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
selectedElement = visEl || origEl || wrapper.parentElement;
// Set display state BEFORE starting observer (avoid triggering it)
if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);
+14 -4
View File
@@ -3,7 +3,6 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Prerequisites
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
- The impeccable CLI installed (`npm i -g impeccable`)
## Start the Server
@@ -49,12 +48,12 @@ If browser automation tools are available, also navigate to the page so the user
## Enter the Poll Loop
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
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: node {{scripts_path}}/live-poll.mjs
Read the JSON output. Dispatch based on the "type" field:
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
@@ -170,6 +169,17 @@ The event contains: `{id}`.
node {{scripts_path}}/live-poll.mjs --reply SESSION_ID done
```
## 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 `exit` after 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:
@@ -1076,9 +1076,8 @@
if (!currentSessionId) return;
sendEvent({ type: 'discard', id: currentSessionId });
markSessionHandled();
state = 'SAVING';
updateBarContent('saving');
// Wait for "done" WS message to show confirmation and dismiss
// Discard dismisses immediately (no "Applying" state, the agent just cleans up)
cleanup();
}
// ---------------------------------------------------------------------------
@@ -1135,6 +1134,26 @@
}
function cleanup() {
// Remove any leftover variant wrapper from the live DOM.
// After discard, the agent cleans the source, but on dev servers without
// HMR the DOM still has the old wrapper, which confuses the picker.
if (currentSessionId) {
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (wrapper) {
// Restore the original element into the DOM
const orig = wrapper.querySelector('[data-impeccable-variant="original"]');
if (orig) {
const content = orig.firstElementChild;
if (content) {
wrapper.parentElement.replaceChild(content, wrapper);
} else {
wrapper.remove();
}
} else {
wrapper.remove();
}
}
}
hideBar();
hideHighlight();
stopScrollTracking();
@@ -1199,14 +1218,21 @@
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
// Restore visible variant from localStorage if available, else default to 1
// Restore state from localStorage if available
const saved = loadSession();
visibleVariant = (saved && saved.id === sessionId && saved.visible > 0 && saved.visible <= arrivedVariants)
? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved && saved.id === sessionId) {
visibleVariant = (saved.visible > 0 && saved.visible <= arrivedVariants) ? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
} else {
visibleVariant = arrivedVariants > 0 ? 1 : 0;
}
// Find the visible variant's content element for highlight positioning
// Find the visible variant's content element for highlight positioning.
// Try the visible variant first, fall back to the original's content.
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
selectedElement = visEl || wrapper.parentElement;
const origEl = wrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
selectedElement = visEl || origEl || wrapper.parentElement;
// Set display state BEFORE starting observer (avoid triggering it)
if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);
+14 -4
View File
@@ -3,7 +3,6 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Prerequisites
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
- The impeccable CLI installed (`npm i -g impeccable`)
## Start the Server
@@ -49,12 +48,12 @@ If browser automation tools are available, also navigate to the page so the user
## Enter the Poll Loop
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
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: node {{scripts_path}}/live-poll.mjs
Read the JSON output. Dispatch based on the "type" field:
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
@@ -170,6 +169,17 @@ The event contains: `{id}`.
node {{scripts_path}}/live-poll.mjs --reply SESSION_ID done
```
## 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 `exit` after 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:
@@ -1076,9 +1076,8 @@
if (!currentSessionId) return;
sendEvent({ type: 'discard', id: currentSessionId });
markSessionHandled();
state = 'SAVING';
updateBarContent('saving');
// Wait for "done" WS message to show confirmation and dismiss
// Discard dismisses immediately (no "Applying" state, the agent just cleans up)
cleanup();
}
// ---------------------------------------------------------------------------
@@ -1135,6 +1134,26 @@
}
function cleanup() {
// Remove any leftover variant wrapper from the live DOM.
// After discard, the agent cleans the source, but on dev servers without
// HMR the DOM still has the old wrapper, which confuses the picker.
if (currentSessionId) {
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (wrapper) {
// Restore the original element into the DOM
const orig = wrapper.querySelector('[data-impeccable-variant="original"]');
if (orig) {
const content = orig.firstElementChild;
if (content) {
wrapper.parentElement.replaceChild(content, wrapper);
} else {
wrapper.remove();
}
} else {
wrapper.remove();
}
}
}
hideBar();
hideHighlight();
stopScrollTracking();
@@ -1199,14 +1218,21 @@
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
// Restore visible variant from localStorage if available, else default to 1
// Restore state from localStorage if available
const saved = loadSession();
visibleVariant = (saved && saved.id === sessionId && saved.visible > 0 && saved.visible <= arrivedVariants)
? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved && saved.id === sessionId) {
visibleVariant = (saved.visible > 0 && saved.visible <= arrivedVariants) ? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
} else {
visibleVariant = arrivedVariants > 0 ? 1 : 0;
}
// Find the visible variant's content element for highlight positioning
// Find the visible variant's content element for highlight positioning.
// Try the visible variant first, fall back to the original's content.
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
selectedElement = visEl || wrapper.parentElement;
const origEl = wrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
selectedElement = visEl || origEl || wrapper.parentElement;
// Set display state BEFORE starting observer (avoid triggering it)
if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);
+14 -4
View File
@@ -3,7 +3,6 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Prerequisites
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
- The impeccable CLI installed (`npm i -g impeccable`)
## Start the Server
@@ -49,12 +48,12 @@ If browser automation tools are available, also navigate to the page so the user
## Enter the Poll Loop
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
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: node {{scripts_path}}/live-poll.mjs
Read the JSON output. Dispatch based on the "type" field:
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
@@ -170,6 +169,17 @@ The event contains: `{id}`.
node {{scripts_path}}/live-poll.mjs --reply SESSION_ID done
```
## 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 `exit` after 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:
@@ -1076,9 +1076,8 @@
if (!currentSessionId) return;
sendEvent({ type: 'discard', id: currentSessionId });
markSessionHandled();
state = 'SAVING';
updateBarContent('saving');
// Wait for "done" WS message to show confirmation and dismiss
// Discard dismisses immediately (no "Applying" state, the agent just cleans up)
cleanup();
}
// ---------------------------------------------------------------------------
@@ -1135,6 +1134,26 @@
}
function cleanup() {
// Remove any leftover variant wrapper from the live DOM.
// After discard, the agent cleans the source, but on dev servers without
// HMR the DOM still has the old wrapper, which confuses the picker.
if (currentSessionId) {
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (wrapper) {
// Restore the original element into the DOM
const orig = wrapper.querySelector('[data-impeccable-variant="original"]');
if (orig) {
const content = orig.firstElementChild;
if (content) {
wrapper.parentElement.replaceChild(content, wrapper);
} else {
wrapper.remove();
}
} else {
wrapper.remove();
}
}
}
hideBar();
hideHighlight();
stopScrollTracking();
@@ -1199,14 +1218,21 @@
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
// Restore visible variant from localStorage if available, else default to 1
// Restore state from localStorage if available
const saved = loadSession();
visibleVariant = (saved && saved.id === sessionId && saved.visible > 0 && saved.visible <= arrivedVariants)
? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved && saved.id === sessionId) {
visibleVariant = (saved.visible > 0 && saved.visible <= arrivedVariants) ? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
} else {
visibleVariant = arrivedVariants > 0 ? 1 : 0;
}
// Find the visible variant's content element for highlight positioning
// Find the visible variant's content element for highlight positioning.
// Try the visible variant first, fall back to the original's content.
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
selectedElement = visEl || wrapper.parentElement;
const origEl = wrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
selectedElement = visEl || origEl || wrapper.parentElement;
// Set display state BEFORE starting observer (avoid triggering it)
if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);
+14 -4
View File
@@ -3,7 +3,6 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Prerequisites
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
- The impeccable CLI installed (`npm i -g impeccable`)
## Start the Server
@@ -49,12 +48,12 @@ If browser automation tools are available, also navigate to the page so the user
## Enter the Poll Loop
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
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: node {{scripts_path}}/live-poll.mjs
Read the JSON output. Dispatch based on the "type" field:
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
@@ -170,6 +169,17 @@ The event contains: `{id}`.
node {{scripts_path}}/live-poll.mjs --reply SESSION_ID done
```
## 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 `exit` after 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:
@@ -1076,9 +1076,8 @@
if (!currentSessionId) return;
sendEvent({ type: 'discard', id: currentSessionId });
markSessionHandled();
state = 'SAVING';
updateBarContent('saving');
// Wait for "done" WS message to show confirmation and dismiss
// Discard dismisses immediately (no "Applying" state, the agent just cleans up)
cleanup();
}
// ---------------------------------------------------------------------------
@@ -1135,6 +1134,26 @@
}
function cleanup() {
// Remove any leftover variant wrapper from the live DOM.
// After discard, the agent cleans the source, but on dev servers without
// HMR the DOM still has the old wrapper, which confuses the picker.
if (currentSessionId) {
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (wrapper) {
// Restore the original element into the DOM
const orig = wrapper.querySelector('[data-impeccable-variant="original"]');
if (orig) {
const content = orig.firstElementChild;
if (content) {
wrapper.parentElement.replaceChild(content, wrapper);
} else {
wrapper.remove();
}
} else {
wrapper.remove();
}
}
}
hideBar();
hideHighlight();
stopScrollTracking();
@@ -1199,14 +1218,21 @@
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
// Restore visible variant from localStorage if available, else default to 1
// Restore state from localStorage if available
const saved = loadSession();
visibleVariant = (saved && saved.id === sessionId && saved.visible > 0 && saved.visible <= arrivedVariants)
? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved && saved.id === sessionId) {
visibleVariant = (saved.visible > 0 && saved.visible <= arrivedVariants) ? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
} else {
visibleVariant = arrivedVariants > 0 ? 1 : 0;
}
// Find the visible variant's content element for highlight positioning
// Find the visible variant's content element for highlight positioning.
// Try the visible variant first, fall back to the original's content.
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
selectedElement = visEl || wrapper.parentElement;
const origEl = wrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
selectedElement = visEl || origEl || wrapper.parentElement;
// Set display state BEFORE starting observer (avoid triggering it)
if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);
+14 -4
View File
@@ -3,7 +3,6 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Prerequisites
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
- The impeccable CLI installed (`npm i -g impeccable`)
## Start the Server
@@ -49,12 +48,12 @@ If browser automation tools are available, also navigate to the page so the user
## Enter the Poll Loop
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
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: node {{scripts_path}}/live-poll.mjs
Read the JSON output. Dispatch based on the "type" field:
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
@@ -170,6 +169,17 @@ The event contains: `{id}`.
node {{scripts_path}}/live-poll.mjs --reply SESSION_ID done
```
## 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 `exit` after 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:
@@ -1076,9 +1076,8 @@
if (!currentSessionId) return;
sendEvent({ type: 'discard', id: currentSessionId });
markSessionHandled();
state = 'SAVING';
updateBarContent('saving');
// Wait for "done" WS message to show confirmation and dismiss
// Discard dismisses immediately (no "Applying" state, the agent just cleans up)
cleanup();
}
// ---------------------------------------------------------------------------
@@ -1135,6 +1134,26 @@
}
function cleanup() {
// Remove any leftover variant wrapper from the live DOM.
// After discard, the agent cleans the source, but on dev servers without
// HMR the DOM still has the old wrapper, which confuses the picker.
if (currentSessionId) {
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (wrapper) {
// Restore the original element into the DOM
const orig = wrapper.querySelector('[data-impeccable-variant="original"]');
if (orig) {
const content = orig.firstElementChild;
if (content) {
wrapper.parentElement.replaceChild(content, wrapper);
} else {
wrapper.remove();
}
} else {
wrapper.remove();
}
}
}
hideBar();
hideHighlight();
stopScrollTracking();
@@ -1199,14 +1218,21 @@
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
// Restore visible variant from localStorage if available, else default to 1
// Restore state from localStorage if available
const saved = loadSession();
visibleVariant = (saved && saved.id === sessionId && saved.visible > 0 && saved.visible <= arrivedVariants)
? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved && saved.id === sessionId) {
visibleVariant = (saved.visible > 0 && saved.visible <= arrivedVariants) ? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
} else {
visibleVariant = arrivedVariants > 0 ? 1 : 0;
}
// Find the visible variant's content element for highlight positioning
// Find the visible variant's content element for highlight positioning.
// Try the visible variant first, fall back to the original's content.
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
selectedElement = visEl || wrapper.parentElement;
const origEl = wrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
selectedElement = visEl || origEl || wrapper.parentElement;
// Set display state BEFORE starting observer (avoid triggering it)
if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);
+14 -4
View File
@@ -3,7 +3,6 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Prerequisites
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
- The impeccable CLI installed (`npm i -g impeccable`)
## Start the Server
@@ -49,12 +48,12 @@ If browser automation tools are available, also navigate to the page so the user
## Enter the Poll Loop
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
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: node {{scripts_path}}/live-poll.mjs
Read the JSON output. Dispatch based on the "type" field:
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
@@ -170,6 +169,17 @@ The event contains: `{id}`.
node {{scripts_path}}/live-poll.mjs --reply SESSION_ID done
```
## 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 `exit` after 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:
+34 -8
View File
@@ -1076,9 +1076,8 @@
if (!currentSessionId) return;
sendEvent({ type: 'discard', id: currentSessionId });
markSessionHandled();
state = 'SAVING';
updateBarContent('saving');
// Wait for "done" WS message to show confirmation and dismiss
// Discard dismisses immediately (no "Applying" state, the agent just cleans up)
cleanup();
}
// ---------------------------------------------------------------------------
@@ -1135,6 +1134,26 @@
}
function cleanup() {
// Remove any leftover variant wrapper from the live DOM.
// After discard, the agent cleans the source, but on dev servers without
// HMR the DOM still has the old wrapper, which confuses the picker.
if (currentSessionId) {
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (wrapper) {
// Restore the original element into the DOM
const orig = wrapper.querySelector('[data-impeccable-variant="original"]');
if (orig) {
const content = orig.firstElementChild;
if (content) {
wrapper.parentElement.replaceChild(content, wrapper);
} else {
wrapper.remove();
}
} else {
wrapper.remove();
}
}
}
hideBar();
hideHighlight();
stopScrollTracking();
@@ -1199,14 +1218,21 @@
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
// Restore visible variant from localStorage if available, else default to 1
// Restore state from localStorage if available
const saved = loadSession();
visibleVariant = (saved && saved.id === sessionId && saved.visible > 0 && saved.visible <= arrivedVariants)
? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved && saved.id === sessionId) {
visibleVariant = (saved.visible > 0 && saved.visible <= arrivedVariants) ? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
} else {
visibleVariant = arrivedVariants > 0 ? 1 : 0;
}
// Find the visible variant's content element for highlight positioning
// Find the visible variant's content element for highlight positioning.
// Try the visible variant first, fall back to the original's content.
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
selectedElement = visEl || wrapper.parentElement;
const origEl = wrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
selectedElement = visEl || origEl || wrapper.parentElement;
// Set display state BEFORE starting observer (avoid triggering it)
if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);
+14 -4
View File
@@ -3,7 +3,6 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Prerequisites
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
- The impeccable CLI installed (`npm i -g impeccable`)
## Start the Server
@@ -49,12 +48,12 @@ If browser automation tools are available, also navigate to the page so the user
## Enter the Poll Loop
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
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: node {{scripts_path}}/live-poll.mjs
Read the JSON output. Dispatch based on the "type" field:
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
@@ -170,6 +169,17 @@ The event contains: `{id}`.
node {{scripts_path}}/live-poll.mjs --reply SESSION_ID done
```
## 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 `exit` after 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:
@@ -1076,9 +1076,8 @@
if (!currentSessionId) return;
sendEvent({ type: 'discard', id: currentSessionId });
markSessionHandled();
state = 'SAVING';
updateBarContent('saving');
// Wait for "done" WS message to show confirmation and dismiss
// Discard dismisses immediately (no "Applying" state, the agent just cleans up)
cleanup();
}
// ---------------------------------------------------------------------------
@@ -1135,6 +1134,26 @@
}
function cleanup() {
// Remove any leftover variant wrapper from the live DOM.
// After discard, the agent cleans the source, but on dev servers without
// HMR the DOM still has the old wrapper, which confuses the picker.
if (currentSessionId) {
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (wrapper) {
// Restore the original element into the DOM
const orig = wrapper.querySelector('[data-impeccable-variant="original"]');
if (orig) {
const content = orig.firstElementChild;
if (content) {
wrapper.parentElement.replaceChild(content, wrapper);
} else {
wrapper.remove();
}
} else {
wrapper.remove();
}
}
}
hideBar();
hideHighlight();
stopScrollTracking();
@@ -1199,14 +1218,21 @@
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
// Restore visible variant from localStorage if available, else default to 1
// Restore state from localStorage if available
const saved = loadSession();
visibleVariant = (saved && saved.id === sessionId && saved.visible > 0 && saved.visible <= arrivedVariants)
? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved && saved.id === sessionId) {
visibleVariant = (saved.visible > 0 && saved.visible <= arrivedVariants) ? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
} else {
visibleVariant = arrivedVariants > 0 ? 1 : 0;
}
// Find the visible variant's content element for highlight positioning
// Find the visible variant's content element for highlight positioning.
// Try the visible variant first, fall back to the original's content.
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
selectedElement = visEl || wrapper.parentElement;
const origEl = wrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
selectedElement = visEl || origEl || wrapper.parentElement;
// Set display state BEFORE starting observer (avoid triggering it)
if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);
+14 -4
View File
@@ -3,7 +3,6 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Prerequisites
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
- The impeccable CLI installed (`npm i -g impeccable`)
## Start the Server
@@ -49,12 +48,12 @@ If browser automation tools are available, also navigate to the page so the user
## Enter the Poll Loop
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
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: node {{scripts_path}}/live-poll.mjs
Read the JSON output. Dispatch based on the "type" field:
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
@@ -170,6 +169,17 @@ The event contains: `{id}`.
node {{scripts_path}}/live-poll.mjs --reply SESSION_ID done
```
## 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 `exit` after 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:
@@ -1076,9 +1076,8 @@
if (!currentSessionId) return;
sendEvent({ type: 'discard', id: currentSessionId });
markSessionHandled();
state = 'SAVING';
updateBarContent('saving');
// Wait for "done" WS message to show confirmation and dismiss
// Discard dismisses immediately (no "Applying" state, the agent just cleans up)
cleanup();
}
// ---------------------------------------------------------------------------
@@ -1135,6 +1134,26 @@
}
function cleanup() {
// Remove any leftover variant wrapper from the live DOM.
// After discard, the agent cleans the source, but on dev servers without
// HMR the DOM still has the old wrapper, which confuses the picker.
if (currentSessionId) {
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (wrapper) {
// Restore the original element into the DOM
const orig = wrapper.querySelector('[data-impeccable-variant="original"]');
if (orig) {
const content = orig.firstElementChild;
if (content) {
wrapper.parentElement.replaceChild(content, wrapper);
} else {
wrapper.remove();
}
} else {
wrapper.remove();
}
}
}
hideBar();
hideHighlight();
stopScrollTracking();
@@ -1199,14 +1218,21 @@
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
// Restore visible variant from localStorage if available, else default to 1
// Restore state from localStorage if available
const saved = loadSession();
visibleVariant = (saved && saved.id === sessionId && saved.visible > 0 && saved.visible <= arrivedVariants)
? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved && saved.id === sessionId) {
visibleVariant = (saved.visible > 0 && saved.visible <= arrivedVariants) ? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
} else {
visibleVariant = arrivedVariants > 0 ? 1 : 0;
}
// Find the visible variant's content element for highlight positioning
// Find the visible variant's content element for highlight positioning.
// Try the visible variant first, fall back to the original's content.
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
selectedElement = visEl || wrapper.parentElement;
const origEl = wrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
selectedElement = visEl || origEl || wrapper.parentElement;
// Set display state BEFORE starting observer (avoid triggering it)
if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);
+14 -4
View File
@@ -3,7 +3,6 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Prerequisites
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
- The impeccable CLI installed (`npm i -g impeccable`)
## Start the Server
@@ -49,12 +48,12 @@ If browser automation tools are available, also navigate to the page so the user
## Enter the Poll Loop
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
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: node {{scripts_path}}/live-poll.mjs
Read the JSON output. Dispatch based on the "type" field:
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
@@ -170,6 +169,17 @@ The event contains: `{id}`.
node {{scripts_path}}/live-poll.mjs --reply SESSION_ID done
```
## 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 `exit` after 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:
@@ -1076,9 +1076,8 @@
if (!currentSessionId) return;
sendEvent({ type: 'discard', id: currentSessionId });
markSessionHandled();
state = 'SAVING';
updateBarContent('saving');
// Wait for "done" WS message to show confirmation and dismiss
// Discard dismisses immediately (no "Applying" state, the agent just cleans up)
cleanup();
}
// ---------------------------------------------------------------------------
@@ -1135,6 +1134,26 @@
}
function cleanup() {
// Remove any leftover variant wrapper from the live DOM.
// After discard, the agent cleans the source, but on dev servers without
// HMR the DOM still has the old wrapper, which confuses the picker.
if (currentSessionId) {
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (wrapper) {
// Restore the original element into the DOM
const orig = wrapper.querySelector('[data-impeccable-variant="original"]');
if (orig) {
const content = orig.firstElementChild;
if (content) {
wrapper.parentElement.replaceChild(content, wrapper);
} else {
wrapper.remove();
}
} else {
wrapper.remove();
}
}
}
hideBar();
hideHighlight();
stopScrollTracking();
@@ -1199,14 +1218,21 @@
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
// Restore visible variant from localStorage if available, else default to 1
// Restore state from localStorage if available
const saved = loadSession();
visibleVariant = (saved && saved.id === sessionId && saved.visible > 0 && saved.visible <= arrivedVariants)
? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved && saved.id === sessionId) {
visibleVariant = (saved.visible > 0 && saved.visible <= arrivedVariants) ? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
} else {
visibleVariant = arrivedVariants > 0 ? 1 : 0;
}
// Find the visible variant's content element for highlight positioning
// Find the visible variant's content element for highlight positioning.
// Try the visible variant first, fall back to the original's content.
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
selectedElement = visEl || wrapper.parentElement;
const origEl = wrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
selectedElement = visEl || origEl || wrapper.parentElement;
// Set display state BEFORE starting observer (avoid triggering it)
if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);
+26 -25
View File
@@ -85,40 +85,40 @@
<div class="hero-combined-container">
<!-- Left: Title & Info -->
<div class="hero-combined-left">
<h1 class="hero-title-combined">Impeccable</h1>
<p class="hero-tagline-combined">Design fluency for AI harnesses</p>
<h1 class="hero-title-combined">Impeccable</h1>
<p class="hero-tagline-combined">Design fluency for AI harnesses</p>
<p class="hero-hook-text hero-hook-text--full">Great design prompts require design vocabulary. Most people don't have it. Impeccable teaches your AI deep design knowledge and gives you 22 commands to steer the result.</p>
<p class="hero-hook-text hero-hook-text--short">Impeccable teaches your AI real design and gives you 22 commands to steer the result.</p>
<p class="hero-hook-text hero-hook-text--full">Great design prompts require design vocabulary. Most people don't have it. Impeccable teaches your AI deep design knowledge and gives you 22 commands to steer the result.</p>
<p class="hero-hook-text hero-hook-text--short">Impeccable teaches your AI real design and gives you 22 commands to steer the result.</p>
<div class="hero-included-box">
<div class="hero-included-box">
<span class="hero-included-title">What's included</span>
<div class="hero-included-items">
<span><em>Impeccable</em> agent skill with 22 design commands</span>
<span class="hero-included-sep">·</span>
<span>Optional CLI + Chrome extension</span>
<span><em>Impeccable</em> agent skill with 22 design commands</span>
<span class="hero-included-sep">·</span>
<span>Optional CLI + Chrome extension</span>
</div>
</div>
</div>
<div class="hero-cta-group">
<div class="hero-cta-group">
<a href="#downloads" class="hero-cta-combined">Get Started</a>
<div class="hero-logos-inline">
<span class="hero-logos-label">Works with</span>
<div class="hero-logos-row">
<span class="hero-logo-icon has-tooltip" data-tooltip="Cursor"><img src="assets/cursor-logo.png" alt="Cursor" width="24" height="24" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="Claude Code"><img src="assets/claude-logo.png" alt="Claude Code" width="24" height="24" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="Gemini CLI"><img src="assets/gemini-logo.png" alt="Gemini CLI" width="20" height="20" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="Codex CLI"><img src="assets/openai-logo.png" alt="Codex CLI" width="20" height="20" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="VS Code Copilot"><img src="assets/github-logo.png" alt="VS Code Copilot" width="20" height="20" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="Antigravity"><img src="assets/antigravity-logo.png" alt="Antigravity" width="20" height="20" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="Kiro"><img src="assets/kiro-logo.png" alt="Kiro" width="20" height="20" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="OpenCode"><img src="assets/opencode-logo.png" alt="OpenCode" width="20" height="20" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="Pi"><img src="assets/pi-logo.svg" alt="Pi" width="20" height="20" loading="lazy" style="background:#1a1a1a;border-radius:50%;padding:3px"></span>
</div>
<span class="hero-logos-label">Works with</span>
<div class="hero-logos-row">
<span class="hero-logo-icon has-tooltip" data-tooltip="Cursor"><img src="assets/cursor-logo.png" alt="Cursor" width="24" height="24" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="Claude Code"><img src="assets/claude-logo.png" alt="Claude Code" width="24" height="24" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="Gemini CLI"><img src="assets/gemini-logo.png" alt="Gemini CLI" width="20" height="20" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="Codex CLI"><img src="assets/openai-logo.png" alt="Codex CLI" width="20" height="20" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="VS Code Copilot"><img src="assets/github-logo.png" alt="VS Code Copilot" width="20" height="20" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="Antigravity"><img src="assets/antigravity-logo.png" alt="Antigravity" width="20" height="20" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="Kiro"><img src="assets/kiro-logo.png" alt="Kiro" width="20" height="20" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="OpenCode"><img src="assets/opencode-logo.png" alt="OpenCode" width="20" height="20" loading="lazy"></span>
<span class="hero-logo-icon has-tooltip" data-tooltip="Pi"><img src="assets/pi-logo.svg" alt="Pi" width="20" height="20" loading="lazy" style="background:#1a1a1a;border-radius:50%;padding:3px"></span>
</div>
</div>
</div>
<p class="hero-version-link"><a href="#changelog">v3.0: 1 skill, 22 commands</a></p>
</div>
</div>
<p class="hero-version-link"><a href="#changelog">v3.0: 1 skill, 22 commands</a></p>
</div>
<!-- Right: Before/After Demo -->
<div class="hero-combined-right">
@@ -753,5 +753,6 @@
</footer>
<script type="module" src="./app.js"></script>
</body>
</html>
+14 -4
View File
@@ -3,7 +3,6 @@ Launch interactive live variant mode: select elements in the browser, pick a des
## Prerequisites
- A running development server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser
- The impeccable CLI installed (`npm i -g impeccable`)
## Start the Server
@@ -49,12 +48,12 @@ If browser automation tools are available, also navigate to the page so the user
## Enter the Poll Loop
Run a blocking poll loop. On each iteration, wait for a browser event and respond:
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: node {{scripts_path}}/live-poll.mjs
Read the JSON output. Dispatch based on the "type" field:
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
@@ -170,6 +169,17 @@ The event contains: `{id}`.
node {{scripts_path}}/live-poll.mjs --reply SESSION_ID done
```
## 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 `exit` after 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:
@@ -1076,9 +1076,8 @@
if (!currentSessionId) return;
sendEvent({ type: 'discard', id: currentSessionId });
markSessionHandled();
state = 'SAVING';
updateBarContent('saving');
// Wait for "done" WS message to show confirmation and dismiss
// Discard dismisses immediately (no "Applying" state, the agent just cleans up)
cleanup();
}
// ---------------------------------------------------------------------------
@@ -1135,6 +1134,26 @@
}
function cleanup() {
// Remove any leftover variant wrapper from the live DOM.
// After discard, the agent cleans the source, but on dev servers without
// HMR the DOM still has the old wrapper, which confuses the picker.
if (currentSessionId) {
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (wrapper) {
// Restore the original element into the DOM
const orig = wrapper.querySelector('[data-impeccable-variant="original"]');
if (orig) {
const content = orig.firstElementChild;
if (content) {
wrapper.parentElement.replaceChild(content, wrapper);
} else {
wrapper.remove();
}
} else {
wrapper.remove();
}
}
}
hideBar();
hideHighlight();
stopScrollTracking();
@@ -1199,14 +1218,21 @@
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
// Restore visible variant from localStorage if available, else default to 1
// Restore state from localStorage if available
const saved = loadSession();
visibleVariant = (saved && saved.id === sessionId && saved.visible > 0 && saved.visible <= arrivedVariants)
? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved && saved.id === sessionId) {
visibleVariant = (saved.visible > 0 && saved.visible <= arrivedVariants) ? saved.visible : (arrivedVariants > 0 ? 1 : 0);
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
} else {
visibleVariant = arrivedVariants > 0 ? 1 : 0;
}
// Find the visible variant's content element for highlight positioning
// Find the visible variant's content element for highlight positioning.
// Try the visible variant first, fall back to the original's content.
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
selectedElement = visEl || wrapper.parentElement;
const origEl = wrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
selectedElement = visEl || origEl || wrapper.parentElement;
// Set display state BEFORE starting observer (avoid triggering it)
if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);