From 52b050bb7e43bc2a022cd94e8413bc41973cbb57 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 13 Apr 2026 10:04:31 -0700 Subject: [PATCH] Fix 5 bugs from real-world live mode testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .agents/skills/impeccable/reference/live.md | 18 +++++-- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++--- .claude/skills/impeccable/reference/live.md | 18 +++++-- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++--- .codex/skills/impeccable/reference/live.md | 18 +++++-- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++--- .cursor/skills/impeccable/reference/live.md | 18 +++++-- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++--- .gemini/skills/impeccable/reference/live.md | 18 +++++-- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++--- .kiro/skills/impeccable/reference/live.md | 18 +++++-- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++--- .opencode/skills/impeccable/reference/live.md | 18 +++++-- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++--- .pi/skills/impeccable/reference/live.md | 18 +++++-- .pi/skills/impeccable/scripts/live-browser.js | 42 ++++++++++++--- .rovodev/skills/impeccable/reference/live.md | 18 +++++-- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++--- .trae-cn/skills/impeccable/reference/live.md | 18 +++++-- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++--- .trae/skills/impeccable/reference/live.md | 18 +++++-- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++--- public/index.html | 51 ++++++++++--------- source/skills/impeccable/reference/live.md | 18 +++++-- .../skills/impeccable/scripts/live-browser.js | 42 ++++++++++++--- 25 files changed, 602 insertions(+), 169 deletions(-) diff --git a/.agents/skills/impeccable/reference/live.md b/.agents/skills/impeccable/reference/live.md index ba0e9b0c5..34227e9fa 100644 --- a/.agents/skills/impeccable/reference/live.md +++ b/.agents/skills/impeccable/reference/live.md @@ -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: diff --git a/.agents/skills/impeccable/scripts/live-browser.js b/.agents/skills/impeccable/scripts/live-browser.js index 72534661b..c03dbdc48 100644 --- a/.agents/skills/impeccable/scripts/live-browser.js +++ b/.agents/skills/impeccable/scripts/live-browser.js @@ -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); diff --git a/.claude/skills/impeccable/reference/live.md b/.claude/skills/impeccable/reference/live.md index ba0e9b0c5..34227e9fa 100644 --- a/.claude/skills/impeccable/reference/live.md +++ b/.claude/skills/impeccable/reference/live.md @@ -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: diff --git a/.claude/skills/impeccable/scripts/live-browser.js b/.claude/skills/impeccable/scripts/live-browser.js index 72534661b..c03dbdc48 100644 --- a/.claude/skills/impeccable/scripts/live-browser.js +++ b/.claude/skills/impeccable/scripts/live-browser.js @@ -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); diff --git a/.codex/skills/impeccable/reference/live.md b/.codex/skills/impeccable/reference/live.md index ba0e9b0c5..34227e9fa 100644 --- a/.codex/skills/impeccable/reference/live.md +++ b/.codex/skills/impeccable/reference/live.md @@ -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: diff --git a/.codex/skills/impeccable/scripts/live-browser.js b/.codex/skills/impeccable/scripts/live-browser.js index 72534661b..c03dbdc48 100644 --- a/.codex/skills/impeccable/scripts/live-browser.js +++ b/.codex/skills/impeccable/scripts/live-browser.js @@ -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); diff --git a/.cursor/skills/impeccable/reference/live.md b/.cursor/skills/impeccable/reference/live.md index ba0e9b0c5..34227e9fa 100644 --- a/.cursor/skills/impeccable/reference/live.md +++ b/.cursor/skills/impeccable/reference/live.md @@ -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: diff --git a/.cursor/skills/impeccable/scripts/live-browser.js b/.cursor/skills/impeccable/scripts/live-browser.js index 72534661b..c03dbdc48 100644 --- a/.cursor/skills/impeccable/scripts/live-browser.js +++ b/.cursor/skills/impeccable/scripts/live-browser.js @@ -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); diff --git a/.gemini/skills/impeccable/reference/live.md b/.gemini/skills/impeccable/reference/live.md index ba0e9b0c5..34227e9fa 100644 --- a/.gemini/skills/impeccable/reference/live.md +++ b/.gemini/skills/impeccable/reference/live.md @@ -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: diff --git a/.gemini/skills/impeccable/scripts/live-browser.js b/.gemini/skills/impeccable/scripts/live-browser.js index 72534661b..c03dbdc48 100644 --- a/.gemini/skills/impeccable/scripts/live-browser.js +++ b/.gemini/skills/impeccable/scripts/live-browser.js @@ -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); diff --git a/.kiro/skills/impeccable/reference/live.md b/.kiro/skills/impeccable/reference/live.md index ba0e9b0c5..34227e9fa 100644 --- a/.kiro/skills/impeccable/reference/live.md +++ b/.kiro/skills/impeccable/reference/live.md @@ -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: diff --git a/.kiro/skills/impeccable/scripts/live-browser.js b/.kiro/skills/impeccable/scripts/live-browser.js index 72534661b..c03dbdc48 100644 --- a/.kiro/skills/impeccable/scripts/live-browser.js +++ b/.kiro/skills/impeccable/scripts/live-browser.js @@ -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); diff --git a/.opencode/skills/impeccable/reference/live.md b/.opencode/skills/impeccable/reference/live.md index ba0e9b0c5..34227e9fa 100644 --- a/.opencode/skills/impeccable/reference/live.md +++ b/.opencode/skills/impeccable/reference/live.md @@ -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: diff --git a/.opencode/skills/impeccable/scripts/live-browser.js b/.opencode/skills/impeccable/scripts/live-browser.js index 72534661b..c03dbdc48 100644 --- a/.opencode/skills/impeccable/scripts/live-browser.js +++ b/.opencode/skills/impeccable/scripts/live-browser.js @@ -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); diff --git a/.pi/skills/impeccable/reference/live.md b/.pi/skills/impeccable/reference/live.md index ba0e9b0c5..34227e9fa 100644 --- a/.pi/skills/impeccable/reference/live.md +++ b/.pi/skills/impeccable/reference/live.md @@ -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: diff --git a/.pi/skills/impeccable/scripts/live-browser.js b/.pi/skills/impeccable/scripts/live-browser.js index 72534661b..c03dbdc48 100644 --- a/.pi/skills/impeccable/scripts/live-browser.js +++ b/.pi/skills/impeccable/scripts/live-browser.js @@ -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); diff --git a/.rovodev/skills/impeccable/reference/live.md b/.rovodev/skills/impeccable/reference/live.md index ba0e9b0c5..34227e9fa 100644 --- a/.rovodev/skills/impeccable/reference/live.md +++ b/.rovodev/skills/impeccable/reference/live.md @@ -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: diff --git a/.rovodev/skills/impeccable/scripts/live-browser.js b/.rovodev/skills/impeccable/scripts/live-browser.js index 72534661b..c03dbdc48 100644 --- a/.rovodev/skills/impeccable/scripts/live-browser.js +++ b/.rovodev/skills/impeccable/scripts/live-browser.js @@ -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); diff --git a/.trae-cn/skills/impeccable/reference/live.md b/.trae-cn/skills/impeccable/reference/live.md index ba0e9b0c5..34227e9fa 100644 --- a/.trae-cn/skills/impeccable/reference/live.md +++ b/.trae-cn/skills/impeccable/reference/live.md @@ -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: diff --git a/.trae-cn/skills/impeccable/scripts/live-browser.js b/.trae-cn/skills/impeccable/scripts/live-browser.js index 72534661b..c03dbdc48 100644 --- a/.trae-cn/skills/impeccable/scripts/live-browser.js +++ b/.trae-cn/skills/impeccable/scripts/live-browser.js @@ -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); diff --git a/.trae/skills/impeccable/reference/live.md b/.trae/skills/impeccable/reference/live.md index ba0e9b0c5..34227e9fa 100644 --- a/.trae/skills/impeccable/reference/live.md +++ b/.trae/skills/impeccable/reference/live.md @@ -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: diff --git a/.trae/skills/impeccable/scripts/live-browser.js b/.trae/skills/impeccable/scripts/live-browser.js index 72534661b..c03dbdc48 100644 --- a/.trae/skills/impeccable/scripts/live-browser.js +++ b/.trae/skills/impeccable/scripts/live-browser.js @@ -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); diff --git a/public/index.html b/public/index.html index 82c3f8385..9d47a9845 100644 --- a/public/index.html +++ b/public/index.html @@ -85,40 +85,40 @@
-

Impeccable

-

Design fluency for AI harnesses

+

Impeccable

+

Design fluency for AI harnesses

-

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.

-

Impeccable teaches your AI real design and gives you 22 commands to steer the result.

+

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.

+

Impeccable teaches your AI real design and gives you 22 commands to steer the result.

-
+
What's included
- Impeccable agent skill with 22 design commands - · - Optional CLI + Chrome extension + Impeccable agent skill with 22 design commands + · + Optional CLI + Chrome extension +
-
-
+
Get Started
- Works with -
- Cursor - Claude Code - Gemini CLI - Codex CLI - VS Code Copilot - Antigravity - Kiro - OpenCode - Pi -
+ Works with +
+ Cursor + Claude Code + Gemini CLI + Codex CLI + VS Code Copilot + Antigravity + Kiro + OpenCode + Pi +
+
+
+
-
- -
@@ -753,5 +753,6 @@ + diff --git a/source/skills/impeccable/reference/live.md b/source/skills/impeccable/reference/live.md index ba0e9b0c5..34227e9fa 100644 --- a/source/skills/impeccable/reference/live.md +++ b/source/skills/impeccable/reference/live.md @@ -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: diff --git a/source/skills/impeccable/scripts/live-browser.js b/source/skills/impeccable/scripts/live-browser.js index 72534661b..c03dbdc48 100644 --- a/source/skills/impeccable/scripts/live-browser.js +++ b/source/skills/impeccable/scripts/live-browser.js @@ -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);