mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 15:16:35 +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);
|
||||
|
||||
Reference in New Issue
Block a user