mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 14:16:28 +03:00
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:
co-authored by
Claude Opus 4.6
parent
90d5158331
commit
52b050bb7e
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user