Files
pbakaus_impeccable/.github/skills/impeccable/scripts/live-browser.js
T
Paul BakausandClaude Opus 4.6 830fe8e5fc Instant accept/discard for live mode, SSE heartbeats, background server startup
Accept and discard in live variant mode are now handled by a deterministic
script (live-accept.mjs) that runs inside the poller before returning to
the agent. The browser updates the DOM instantly on click (fire-and-forget)
so the user is never blocked waiting for LLM-driven file cleanup.

Key changes:
- New live-accept.mjs: deterministic accept/discard file operations
- Poller auto-runs accept script for accept/discard events (_acceptResult)
- Browser handleAccept() now commits DOM change instantly, no SAVING state
- CSS+HTML colocated in one write (style tag inside variant wrapper)
- SSE heartbeat every 30s prevents silent connection drops
- Poll timeout increased from 2min to 10min
- EventSource onopen resets retry counter for reliable reconnection
- Server --background flag for clean single-command startup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:06:03 -07:00

1511 lines
56 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Impeccable Live Variant Mode — Browser Script
*
* Injected into the user's page via <script src="http://localhost:PORT/live.js">.
* The server prepends window.__IMPECCABLE_TOKEN__ and window.__IMPECCABLE_PORT__
* before this code.
*
* UI: a single floating bar that morphs between three states —
* configure (pick action + go), generating (progressive dots), and cycling
* (prev/next + accept/discard). Feels like Spotlight, not a modal.
*/
(function () {
'use strict';
if (typeof window === 'undefined') return;
// Guard against double-init. Bun's HTML loader may process the <script> tag
// and create a bundled copy alongside the external load, or HMR may re-execute.
// Check BEFORE reading token/port to catch all cases.
if (window.__IMPECCABLE_LIVE_INIT__) return;
window.__IMPECCABLE_LIVE_INIT__ = true;
const TOKEN = window.__IMPECCABLE_TOKEN__;
const PORT = window.__IMPECCABLE_PORT__;
if (!TOKEN || !PORT) {
window.__IMPECCABLE_LIVE_INIT__ = false; // reset so the real load can init
return;
}
// ---------------------------------------------------------------------------
// Design tokens
// ---------------------------------------------------------------------------
const C = {
brand: 'oklch(55% 0.25 350)',
brandHov: 'oklch(48% 0.25 350)',
brandSoft: 'oklch(55% 0.25 350 / 0.12)',
ink: 'oklch(15% 0.01 350)',
ash: 'oklch(55% 0 0)',
paper: 'oklch(98% 0.005 350 / 0.92)',
paperSolid:'oklch(98% 0.005 350)',
mist: 'oklch(90% 0.01 350 / 0.6)',
white: 'oklch(99% 0 0)',
};
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 };
const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint
const PREFIX = 'impeccable-live';
const SKIP_TAGS = new Set([
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
]);
const ACTIONS = [
{ value: 'impeccable', label: 'Freeform' },
{ value: 'bolder', label: 'Bolder' },
{ value: 'quieter', label: 'Quieter' },
{ value: 'distill', label: 'Distill' },
{ value: 'polish', label: 'Polish' },
{ value: 'typeset', label: 'Typeset' },
{ value: 'colorize', label: 'Colorize' },
{ value: 'layout', label: 'Layout' },
{ value: 'adapt', label: 'Adapt' },
{ value: 'animate', label: 'Animate' },
{ value: 'delight', label: 'Delight' },
{ value: 'overdrive', label: 'Overdrive' },
];
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
let state = 'IDLE';
let hoveredElement = null;
let selectedElement = null;
let currentSessionId = null;
let expectedVariants = 0;
let arrivedVariants = 0;
let visibleVariant = 0;
let variantObserver = null;
let hasProjectContext = false;
let selectedAction = 'impeccable';
let selectedCount = 3;
// UI refs
let highlightEl = null;
let tooltipEl = null;
let barEl = null;
let pickerEl = null;
let toastEl = null;
let scrollRaf = null;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function own(el) {
return el && (el.id?.startsWith(PREFIX) || el.closest?.('[id^="' + PREFIX + '"]'));
}
function pickable(el) {
if (!el || el.nodeType !== 1) return false;
if (SKIP_TAGS.has(el.tagName.toLowerCase())) return false;
if (own(el)) return false;
const r = el.getBoundingClientRect();
return r.width >= 20 && r.height >= 20;
}
function desc(el) {
if (!el) return '';
let s = el.tagName.toLowerCase();
if (el.id) s += '#' + el.id;
else if (el.classList.length) s += '.' + [...el.classList].slice(0, 2).join('.');
return s;
}
function id8() { return crypto.randomUUID().replace(/-/g, '').slice(0, 8); }
// ---------------------------------------------------------------------------
// Highlight overlay
// ---------------------------------------------------------------------------
function initHighlight() {
highlightEl = document.createElement('div');
highlightEl.id = PREFIX + '-highlight';
Object.assign(highlightEl.style, {
position: 'fixed', top: '0', left: '0', width: '0', height: '0',
border: '2px solid ' + C.brand, borderRadius: '3px',
pointerEvents: 'none', zIndex: Z.highlight, boxSizing: 'border-box',
// No transition on position/size: avoids layout-property animation detection
// AND gives instant cursor tracking (no lag)
transition: 'opacity 0.15s ease',
display: 'none', opacity: '0',
});
document.body.appendChild(highlightEl);
tooltipEl = document.createElement('div');
tooltipEl.id = PREFIX + '-tooltip';
Object.assign(tooltipEl.style, {
position: 'fixed',
background: C.ink, color: C.white,
fontFamily: MONO, fontSize: '10px', fontWeight: '500',
padding: '2px 6px', borderRadius: '3px',
zIndex: Z.highlight + 1, pointerEvents: 'none',
whiteSpace: 'nowrap', display: 'none',
letterSpacing: '0.02em',
});
document.body.appendChild(tooltipEl);
}
function showHighlight(el) {
if (!el || !highlightEl) return;
const r = el.getBoundingClientRect();
Object.assign(highlightEl.style, {
top: (r.top - 2) + 'px', left: (r.left - 2) + 'px',
width: (r.width + 4) + 'px', height: (r.height + 4) + 'px',
display: 'block', opacity: '1',
});
tooltipEl.textContent = desc(el);
const tipTop = r.top - 20;
Object.assign(tooltipEl.style, {
top: (tipTop < 4 ? r.bottom + 4 : tipTop) + 'px',
left: Math.max(4, r.left) + 'px', display: 'block',
});
}
function hideHighlight() {
if (highlightEl) { highlightEl.style.opacity = '0'; highlightEl.style.display = 'none'; }
if (tooltipEl) tooltipEl.style.display = 'none';
}
// ---------------------------------------------------------------------------
// Element context extraction
// ---------------------------------------------------------------------------
function extractContext(el) {
const cs = getComputedStyle(el);
const r = el.getBoundingClientRect();
const props = {};
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule.style) for (let i = 0; i < rule.style.length; i++) {
const p = rule.style[i];
if (p.startsWith('--') && !props[p]) {
const v = cs.getPropertyValue(p).trim();
if (v) props[p] = v;
}
}
}
} catch { /* cross-origin */ }
}
return {
tagName: el.tagName.toLowerCase(), id: el.id || null,
classes: [...el.classList],
textContent: (el.textContent || '').slice(0, 500),
outerHTML: el.outerHTML.slice(0, 10000),
computedStyles: {
'font-family': cs.fontFamily, 'font-size': cs.fontSize,
'font-weight': cs.fontWeight, 'line-height': cs.lineHeight,
'color': cs.color, 'background': cs.background,
'background-color': cs.backgroundColor,
'padding': cs.padding, 'margin': cs.margin,
'display': cs.display, 'position': cs.position,
'gap': cs.gap, 'border-radius': cs.borderRadius,
'box-shadow': cs.boxShadow,
},
cssCustomProperties: props,
parentContext: el.parentElement
? '<' + el.parentElement.tagName.toLowerCase()
+ (el.parentElement.id ? ' id="' + el.parentElement.id + '"' : '')
+ (el.parentElement.className ? ' class="' + el.parentElement.className + '"' : '')
+ '>'
: null,
boundingRect: { width: Math.round(r.width), height: Math.round(r.height) },
};
}
// ---------------------------------------------------------------------------
// The Bar — one floating element, three modes
// ---------------------------------------------------------------------------
function initBar() {
barEl = document.createElement('div');
barEl.id = PREFIX + '-bar';
Object.assign(barEl.style, {
position: 'fixed', zIndex: Z.bar,
display: 'none', opacity: '0',
transform: 'translateY(6px)',
transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
// Visual
background: C.paper,
backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)',
border: '1px solid ' + C.mist,
borderRadius: '10px',
boxShadow: '0 4px 20px oklch(0% 0 0 / 0.08), 0 1px 3px oklch(0% 0 0 / 0.06)',
fontFamily: FONT, fontSize: '13px', color: C.ink,
padding: '6px',
maxWidth: '460px', minWidth: '320px',
});
document.body.appendChild(barEl);
}
function positionBar() {
if (!barEl || !selectedElement) return;
const r = selectedElement.getBoundingClientRect();
const barH = barEl.offsetHeight || 44;
const barW = barEl.offsetWidth || 380;
let top = r.bottom + 8;
let left = r.left + (r.width - barW) / 2;
// Keep in viewport
if (top + barH + 8 > window.innerHeight) top = r.top - barH - 8;
if (top < 8) top = 8;
if (left < 8) left = 8;
if (left + barW > window.innerWidth - 8) left = window.innerWidth - barW - 8;
Object.assign(barEl.style, { top: top + 'px', left: left + 'px' });
}
function showBar(mode) {
barEl.innerHTML = '';
if (mode === 'configure') barEl.appendChild(buildConfigureRow());
else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
barEl.style.display = 'block';
positionBar();
requestAnimationFrame(() => {
barEl.style.opacity = '1';
barEl.style.transform = 'translateY(0)';
});
}
function hideBar() {
if (!barEl) return;
barEl.style.opacity = '0';
barEl.style.transform = 'translateY(6px)';
setTimeout(() => { if (barEl) barEl.style.display = 'none'; }, 250);
hideActionPicker();
}
function updateBarContent(mode) {
if (!barEl || barEl.style.display === 'none') return;
barEl.innerHTML = '';
// Reset bar styling
barEl.style.background = C.paper;
barEl.style.border = '1px solid ' + C.mist;
if (mode === 'configure') barEl.appendChild(buildConfigureRow());
else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
else if (mode === 'saving') barEl.appendChild(buildSavingRow());
else if (mode === 'confirmed') {
barEl.appendChild(buildConfirmedRow());
barEl.style.background = 'oklch(95% 0.05 145)';
barEl.style.border = '1px solid oklch(75% 0.12 145 / 0.4)';
}
}
// --- Configure row ---
function buildConfigureRow() {
const row = el('div', {
display: 'flex', alignItems: 'center', gap: '4px',
});
// Action pill
const pill = el('button', {
display: 'inline-flex', alignItems: 'center', gap: '4px',
padding: '5px 10px', borderRadius: '6px',
background: C.ink, color: C.white,
fontFamily: FONT, fontSize: '12px', fontWeight: '500',
border: 'none', cursor: 'pointer',
transition: 'background 0.12s ease, transform 0.1s ease',
whiteSpace: 'nowrap', flexShrink: '0',
});
pill.textContent = actionLabel() + ' \u25BE';
pill.addEventListener('mouseenter', () => pill.style.background = C.brandHov);
pill.addEventListener('mouseleave', () => pill.style.background = C.ink);
pill.addEventListener('mousedown', () => pill.style.transform = 'scale(0.97)');
pill.addEventListener('mouseup', () => pill.style.transform = 'scale(1)');
pill.addEventListener('click', (e) => { e.stopPropagation(); toggleActionPicker(); });
row.appendChild(pill);
// Freeform input
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
input.placeholder = selectedAction === 'impeccable' ? 'describe what you want...' : 'refine further (optional)...';
Object.assign(input.style, {
flex: '1', minWidth: '0',
padding: '5px 8px', borderRadius: '6px',
border: '1px solid transparent', background: 'transparent',
fontFamily: FONT, fontSize: '12px', color: C.ink,
outline: 'none',
transition: 'border-color 0.15s ease, background 0.15s ease',
});
input.addEventListener('focus', () => {
input.style.borderColor = C.mist;
input.style.background = C.white;
});
input.addEventListener('blur', () => {
input.style.borderColor = 'transparent';
input.style.background = 'transparent';
});
input.addEventListener('keydown', (e) => {
e.stopPropagation(); // Don't trigger element picker keyboard nav
if (e.key === 'Enter') { e.preventDefault(); handleGo(); }
if (e.key === 'Escape') { e.preventDefault(); input.blur(); hideBar(); state = 'PICKING'; }
});
row.appendChild(input);
// Variant count toggle
const count = el('button', {
padding: '4px 6px', borderRadius: '5px',
border: '1px solid ' + C.mist, background: 'transparent',
fontFamily: MONO, fontSize: '11px', fontWeight: '600',
color: C.ash, cursor: 'pointer',
transition: 'color 0.12s ease, border-color 0.12s ease',
flexShrink: '0', whiteSpace: 'nowrap',
});
count.textContent = '\u00D7' + selectedCount;
count.title = 'Variants: click to change';
count.addEventListener('mouseenter', () => { count.style.color = C.ink; count.style.borderColor = C.ink; });
count.addEventListener('mouseleave', () => { count.style.color = C.ash; count.style.borderColor = C.mist; });
count.addEventListener('click', (e) => {
e.stopPropagation();
selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
count.textContent = '\u00D7' + selectedCount;
});
row.appendChild(count);
// Go button
const go = el('button', {
padding: '5px 12px', borderRadius: '6px',
border: 'none', background: C.brand, color: C.white,
fontFamily: FONT, fontSize: '12px', fontWeight: '600',
cursor: 'pointer',
transition: 'background 0.12s ease, transform 0.1s ease',
flexShrink: '0', whiteSpace: 'nowrap',
});
go.textContent = 'Go \u2192';
go.addEventListener('mouseenter', () => go.style.background = C.brandHov);
go.addEventListener('mouseleave', () => go.style.background = C.brand);
go.addEventListener('mousedown', () => go.style.transform = 'scale(0.97)');
go.addEventListener('mouseup', () => go.style.transform = 'scale(1)');
go.addEventListener('click', (e) => { e.stopPropagation(); handleGo(); });
row.appendChild(go);
// Auto-focus input after a beat
setTimeout(() => input.focus(), 60);
return row;
}
// --- Generating row ---
function buildGeneratingRow() {
const row = el('div', {
display: 'flex', alignItems: 'center', gap: '8px',
padding: '2px 4px',
});
// Action label
const label = el('span', {
fontWeight: '600', fontSize: '12px', color: C.ink,
flexShrink: '0', whiteSpace: 'nowrap',
});
label.textContent = actionLabel();
row.appendChild(label);
// Dots
row.appendChild(buildDots(false));
// Status
const status = el('span', {
fontSize: '11px', color: C.ash, whiteSpace: 'nowrap',
marginLeft: 'auto',
});
status.textContent = arrivedVariants < expectedVariants
? 'Generating ' + (arrivedVariants + 1) + ' of ' + expectedVariants + '...'
: 'Done';
row.appendChild(status);
return row;
}
// --- Cycling row ---
function buildCyclingRow() {
const row = el('div', {
display: 'flex', alignItems: 'center', gap: '6px',
padding: '1px 2px',
});
// Prev
const prev = navBtn('\u2190');
prev.addEventListener('click', (e) => { e.stopPropagation(); cycleVariant(-1); });
if (visibleVariant <= 1) prev.style.opacity = '0.3';
row.appendChild(prev);
// Dots (clickable)
row.appendChild(buildDots(true));
// Counter
const counter = el('span', {
fontFamily: MONO, fontSize: '11px', fontWeight: '500',
color: C.ash, minWidth: '24px', textAlign: 'center',
});
counter.textContent = visibleVariant + '/' + arrivedVariants;
row.appendChild(counter);
// Next
const next = navBtn('\u2192');
next.addEventListener('click', (e) => { e.stopPropagation(); cycleVariant(1); });
if (visibleVariant >= arrivedVariants) next.style.opacity = '0.3';
row.appendChild(next);
// Spacer
row.appendChild(el('div', { flex: '1' }));
// Accept
const accept = el('button', {
padding: '4px 10px', borderRadius: '5px',
border: 'none', background: C.ink, color: C.white,
fontFamily: FONT, fontSize: '11px', fontWeight: '600',
cursor: 'pointer', transition: 'background 0.12s ease',
whiteSpace: 'nowrap',
});
accept.textContent = '\u2713 Accept';
accept.addEventListener('mouseenter', () => accept.style.background = C.brand);
accept.addEventListener('mouseleave', () => accept.style.background = C.ink);
accept.addEventListener('click', (e) => { e.stopPropagation(); handleAccept(); });
if (arrivedVariants === 0) { accept.style.opacity = '0.3'; accept.style.pointerEvents = 'none'; }
row.appendChild(accept);
// Discard
const discard = el('button', {
padding: '4px 6px', borderRadius: '5px',
border: '1px solid ' + C.mist, background: 'transparent',
fontFamily: FONT, fontSize: '11px', color: C.ash,
cursor: 'pointer', transition: 'color 0.12s ease, border-color 0.12s ease',
});
discard.textContent = '\u2715';
discard.title = 'Discard all variants';
discard.addEventListener('mouseenter', () => { discard.style.color = C.ink; discard.style.borderColor = C.ink; });
discard.addEventListener('mouseleave', () => { discard.style.color = C.ash; discard.style.borderColor = C.mist; });
discard.addEventListener('click', (e) => { e.stopPropagation(); handleDiscard(); });
row.appendChild(discard);
return row;
}
// --- Shared UI builders ---
// --- Saving row (waiting for agent to process accept/discard) ---
function buildSavingRow() {
const row = el('div', {
display: 'flex', alignItems: 'center', gap: '8px',
padding: '2px 8px',
});
const spinner = el('div', {
width: '14px', height: '14px', borderRadius: '50%',
border: '2px solid ' + C.mist,
borderTopColor: C.brand,
animation: 'impeccable-spin 0.6s linear infinite',
flexShrink: '0',
});
row.appendChild(spinner);
const label = el('span', {
fontSize: '12px', color: C.ash, fontWeight: '500',
});
label.textContent = 'Applying variant...';
row.appendChild(label);
// Inject the keyframes if not already present
if (!document.getElementById(PREFIX + '-keyframes')) {
const style = document.createElement('style');
style.id = PREFIX + '-keyframes';
style.textContent = '@keyframes impeccable-spin { to { transform: rotate(360deg); } }';
document.head.appendChild(style);
}
return row;
}
// --- Confirmed row (green success, auto-dismisses) ---
function buildConfirmedRow() {
const row = el('div', {
display: 'flex', alignItems: 'center', gap: '8px',
padding: '2px 8px',
});
const check = el('span', {
fontSize: '15px', lineHeight: '1', flexShrink: '0',
color: 'oklch(45% 0.15 145)',
});
check.textContent = '\u2713';
row.appendChild(check);
const label = el('span', {
fontSize: '12px', color: 'oklch(35% 0.1 145)', fontWeight: '600',
});
label.textContent = 'Variant applied';
row.appendChild(label);
return row;
}
// --- Shared UI builders ---
function buildDots(clickable) {
const container = el('div', {
display: 'flex', alignItems: 'center', gap: '4px',
});
for (let i = 1; i <= expectedVariants; i++) {
const arrived = i <= arrivedVariants;
const active = i === visibleVariant;
const dot = el('div', {
width: '7px', height: '7px', borderRadius: '50%',
background: active ? C.brand : (arrived ? C.ash : 'transparent'),
border: '1.5px solid ' + (arrived ? C.brand : C.mist),
transition: 'all 0.2s ' + EASE,
cursor: (clickable && arrived) ? 'pointer' : 'default',
transform: arrived ? 'scale(1)' : 'scale(0.6)',
opacity: arrived ? '1' : '0.4',
});
if (clickable && arrived) {
const idx = i;
dot.addEventListener('click', (e) => {
e.stopPropagation();
visibleVariant = idx;
showVariantInDOM(currentSessionId, idx);
updateSelectedElement();
updateBarContent('cycling');
});
}
container.appendChild(dot);
}
return container;
}
function navBtn(text) {
const b = el('button', {
width: '26px', height: '26px', borderRadius: '5px',
border: '1px solid ' + C.mist, background: 'transparent',
color: C.ink, fontFamily: FONT, fontSize: '13px',
cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'border-color 0.12s ease, background 0.12s ease',
padding: '0', lineHeight: '1',
});
b.textContent = text;
b.addEventListener('mouseenter', () => { b.style.borderColor = C.ink; });
b.addEventListener('mouseleave', () => { b.style.borderColor = C.mist; });
return b;
}
function actionLabel() {
const a = ACTIONS.find(a => a.value === selectedAction);
return a ? a.label : 'Freeform';
}
function el(tag, styles) {
const e = document.createElement(tag);
if (styles) Object.assign(e.style, styles);
return e;
}
// ---------------------------------------------------------------------------
// Action picker popover
// ---------------------------------------------------------------------------
function initActionPicker() {
pickerEl = document.createElement('div');
pickerEl.id = PREFIX + '-picker';
Object.assign(pickerEl.style, {
position: 'fixed', zIndex: Z.picker,
display: 'none', opacity: '0',
transform: 'scale(0.96) translateY(4px)',
transformOrigin: 'bottom left',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: C.paperSolid,
border: '1px solid ' + C.mist,
borderRadius: '10px',
boxShadow: '0 8px 30px oklch(0% 0 0 / 0.10), 0 2px 6px oklch(0% 0 0 / 0.06)',
padding: '6px',
fontFamily: FONT,
});
// Build the chip grid
const grid = el('div', {
display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '3px',
});
ACTIONS.forEach(action => {
const chip = el('button', {
padding: '6px 8px', borderRadius: '6px',
border: 'none',
background: action.value === selectedAction ? C.brandSoft : 'transparent',
color: action.value === selectedAction ? C.brand : C.ink,
fontFamily: FONT, fontSize: '11px', fontWeight: '500',
cursor: 'pointer',
transition: 'background 0.1s ease, color 0.1s ease',
textAlign: 'center', whiteSpace: 'nowrap',
});
chip.textContent = action.label;
chip.dataset.action = action.value;
chip.addEventListener('mouseenter', () => {
if (action.value !== selectedAction) chip.style.background = C.brandSoft;
});
chip.addEventListener('mouseleave', () => {
chip.style.background = action.value === selectedAction ? C.brandSoft : 'transparent';
});
chip.addEventListener('click', (e) => {
e.stopPropagation();
selectedAction = action.value;
hideActionPicker();
updateBarContent('configure');
});
grid.appendChild(chip);
});
pickerEl.appendChild(grid);
document.body.appendChild(pickerEl);
}
function toggleActionPicker() {
if (pickerEl.style.display !== 'none') { hideActionPicker(); return; }
// Rebuild chips to reflect current selection
pickerEl.querySelectorAll('button').forEach(chip => {
const isActive = chip.dataset.action === selectedAction;
chip.style.background = isActive ? C.brandSoft : 'transparent';
chip.style.color = isActive ? C.brand : C.ink;
});
// Position above the bar
const barRect = barEl.getBoundingClientRect();
const pickerH = 90; // approximate
let top = barRect.top - pickerH - 6;
if (top < 8) top = barRect.bottom + 6;
Object.assign(pickerEl.style, {
top: top + 'px', left: barRect.left + 'px',
display: 'block',
});
requestAnimationFrame(() => {
pickerEl.style.opacity = '1';
pickerEl.style.transform = 'scale(1) translateY(0)';
});
}
function hideActionPicker() {
if (!pickerEl) return;
pickerEl.style.opacity = '0';
pickerEl.style.transform = 'scale(0.96) translateY(4px)';
setTimeout(() => { if (pickerEl) pickerEl.style.display = 'none'; }, 180);
}
// ---------------------------------------------------------------------------
// Variant cycling in DOM
// ---------------------------------------------------------------------------
function showVariantInDOM(sessionId, num) {
const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!wrapper) return;
for (const child of wrapper.children) {
const v = child.dataset ? child.dataset.impeccableVariant : null;
if (!v) continue;
child.style.display = (v === String(num)) ? '' : 'none';
}
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*/
function injectVariantsFromSource(filePath, sessionId) {
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
// Parse the raw source HTML
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.error('[impeccable] Variant wrapper not found in source file.');
return;
}
// Find the original element in the live DOM.
// The original is inside the wrapper in the source. We find the
// corresponding element in the live DOM by matching the first child's
// tag + classes from the original snapshot.
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const tag = origContent.tagName.toLowerCase();
const cls = origContent.className;
let liveEl = null;
if (origContent.id) {
liveEl = document.getElementById(origContent.id);
} else if (cls) {
// Find by tag + exact class match
const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
for (const c of candidates) {
if (c.className === cls && !own(c)) { liveEl = c; break; }
}
}
if (!liveEl) {
console.error('[impeccable] Could not find original element in live DOM.');
return;
}
// Replace the live element with the full wrapper from source
const wrapper = srcWrapper.cloneNode(true);
liveEl.parentElement.replaceChild(wrapper, liveEl);
// Update state: count variants, show the first one
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants);
visibleVariant = 1;
showVariantInDOM(sessionId, 1);
// Update selectedElement to the visible variant's content
const visEl = wrapper.querySelector('[data-impeccable-variant="1"] > :first-child');
selectedElement = visEl || wrapper.parentElement;
state = 'CYCLING';
updateBarContent('cycling');
saveSession();
console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.');
})
.catch(err => {
console.error('[impeccable] Failed to fetch source:', err);
showToast('Could not load variants. Try refreshing the page.', 5000);
});
}
function cycleVariant(dir) {
const next = visibleVariant + dir;
if (next < 1 || next > arrivedVariants) return;
visibleVariant = next;
showVariantInDOM(currentSessionId, next);
updateSelectedElement();
updateBarContent('cycling');
saveSession();
}
function updateSelectedElement() {
if (!currentSessionId) return;
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (!wrapper) return;
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
if (visEl) selectedElement = visEl;
}
// ---------------------------------------------------------------------------
// MutationObserver for progressive variant reveal
// ---------------------------------------------------------------------------
function startVariantObserver(sessionId) {
let updating = false; // re-entrancy guard
const obs = new MutationObserver((mutations) => {
if (updating) return;
// Only react to mutations that add nodes with data-impeccable-variant,
// or mutations inside the variant wrapper. Ignore our own bar/UI changes.
let dominated = false;
for (const m of mutations) {
if (m.target.closest?.('[data-impeccable-variants]')) { dominated = true; break; }
for (const n of m.addedNodes) {
if (n.nodeType === 1 && (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant)) {
dominated = true; break;
}
}
if (dominated) break;
}
if (!dominated) return;
const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!wrapper) return;
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
const count = variants.length;
// Nothing new
if (count <= arrivedVariants) return;
updating = true;
arrivedVariants = count;
if (visibleVariant === 0 && arrivedVariants > 0) {
visibleVariant = 1;
showVariantInDOM(sessionId, 1);
}
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
if (expected > 0) expectedVariants = expected;
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
state = 'CYCLING';
updateBarContent('cycling');
} else if (state === 'GENERATING') {
updateBarContent('generating');
}
saveSession();
updating = false;
});
obs.observe(document.body, { childList: true, subtree: true });
return obs;
}
// ---------------------------------------------------------------------------
// Bar scroll tracking
// ---------------------------------------------------------------------------
function startScrollTracking() {
function tick() {
if (state === 'CONFIGURING' || state === 'GENERATING' || state === 'CYCLING') {
positionBar();
showHighlight(selectedElement);
}
scrollRaf = requestAnimationFrame(tick);
}
scrollRaf = requestAnimationFrame(tick);
}
function stopScrollTracking() {
if (scrollRaf) { cancelAnimationFrame(scrollRaf); scrollRaf = null; }
}
// ---------------------------------------------------------------------------
// SSE (server→browser) + fetch POST (browser→server)
// Zero-dependency replacement for WebSocket.
// ---------------------------------------------------------------------------
let evtSource = null;
let sseRetries = 0;
const SSE_MAX_RETRIES = 20; // generous: heartbeats keep the connection alive, so retries mean real trouble
function connectSSE() {
evtSource = new EventSource('http://localhost:' + PORT + '/events?token=' + TOKEN);
evtSource.onopen = () => {
sseRetries = 0; // reset on successful (re)connect
};
evtSource.onmessage = (e) => {
sseRetries = 0; // reset on any successful message
let msg; try { msg = JSON.parse(e.data); } catch { return; }
switch (msg.type) {
case 'connected':
hasProjectContext = !!msg.hasProjectContext;
if (!hasProjectContext) showToast('No .impeccable.md found. Variants will be brand-agnostic.', 6000);
console.log('[impeccable] Live mode connected.');
if (state === 'IDLE') state = 'PICKING';
break;
case 'done':
// Generate completion: handle no-HMR fallback
if (arrivedVariants === 0 && expectedVariants > 0 && msg.file) {
console.log('[impeccable] No HMR detected. Fetching variants from source file...');
injectVariantsFromSource(msg.file, currentSessionId);
return;
}
if (state === 'GENERATING') {
state = 'CYCLING';
updateBarContent('cycling');
}
break;
case 'error':
console.error('[impeccable] Error:', msg.message);
showToast('Error: ' + msg.message, 5000);
hideBar();
state = 'PICKING';
break;
}
};
evtSource.onerror = () => {
sseRetries++;
if (sseRetries <= SSE_MAX_RETRIES) {
console.log('[impeccable] SSE connection lost. Retry ' + sseRetries + '/' + SSE_MAX_RETRIES + '...');
return; // EventSource auto-reconnects
}
// Server is gone. Clean up gracefully.
console.log('[impeccable] Live server unreachable. Cleaning up UI.');
evtSource.close();
evtSource = null;
handleServerLost();
};
}
/** Server died or became unreachable. Reset UI to a clean state. */
function handleServerLost() {
if (state === 'GENERATING' || state === 'CYCLING' || state === 'SAVING') {
showToast('Live server disconnected. Session ended.', 5000);
}
hideBar();
hideHighlight();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
clearSession();
selectedElement = null;
currentSessionId = null;
selectedAction = 'impeccable';
state = 'IDLE';
}
function sendEvent(msg) {
msg.token = TOKEN;
fetch('http://localhost:' + PORT + '/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(msg),
}).catch(err => console.error('[impeccable] Failed to send event:', err));
}
// ---------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------
function handleMouseMove(e) {
if (state !== 'PICKING' || !pickActive) return;
const target = document.elementFromPoint(e.clientX, e.clientY);
if (!target || !pickable(target) || target === hoveredElement) return;
hoveredElement = target;
showHighlight(target);
}
function handleClick(e) {
// Close action picker on any outside click
if (pickerEl?.style.display !== 'none' && !own(e.target)) {
hideActionPicker();
}
if (state !== 'PICKING' || !pickActive) return;
if (own(e.target)) return;
if (!hoveredElement || !pickable(hoveredElement)) return;
e.preventDefault();
e.stopPropagation();
selectedElement = hoveredElement;
state = 'CONFIGURING';
showHighlight(selectedElement);
showBar('configure');
startScrollTracking();
}
function handleKeyDown(e) {
if (e.key === 'Escape') {
e.preventDefault();
if (pickerEl?.style.display !== 'none') { hideActionPicker(); return; }
if (state === 'CONFIGURING') { hideBar(); stopScrollTracking(); state = 'PICKING'; return; }
if (state === 'CYCLING') { handleDiscard(); return; }
if (state === 'SAVING' || state === 'CONFIRMED') return; // don't interrupt
if (state === 'PICKING') { hideHighlight(); state = 'IDLE'; return; }
}
if (state === 'PICKING' && hoveredElement) {
let next = null;
if (e.key === 'ArrowDown' && !e.shiftKey) {
next = hoveredElement.nextElementSibling;
while (next && !pickable(next)) next = next.nextElementSibling;
} else if (e.key === 'ArrowUp' && !e.shiftKey) {
next = hoveredElement.previousElementSibling;
while (next && !pickable(next)) next = next.previousElementSibling;
} else if (e.key === 'ArrowUp' && e.shiftKey) {
next = hoveredElement.parentElement;
if (next && !pickable(next)) next = null;
} else if (e.key === 'ArrowDown' && e.shiftKey) {
next = hoveredElement.firstElementChild;
while (next && !pickable(next)) next = next.nextElementSibling;
} else if (e.key === 'Enter') {
e.preventDefault();
selectedElement = hoveredElement;
state = 'CONFIGURING';
showHighlight(selectedElement);
showBar('configure');
startScrollTracking();
return;
}
if (next) {
e.preventDefault();
hoveredElement = next;
showHighlight(next);
next.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
return;
}
if (state === 'CYCLING') {
if (e.key === 'ArrowLeft') { e.preventDefault(); cycleVariant(-1); }
if (e.key === 'ArrowRight') { e.preventDefault(); cycleVariant(1); }
if (e.key === 'Enter') { e.preventDefault(); handleAccept(); }
}
}
function handleGo() {
if (!selectedElement || state !== 'CONFIGURING') return;
const input = document.getElementById(PREFIX + '-input');
const prompt = input ? input.value.trim() : '';
currentSessionId = id8();
expectedVariants = selectedCount;
arrivedVariants = 0;
visibleVariant = 0;
sendEvent({
type: 'generate', id: currentSessionId,
action: selectedAction,
freeformPrompt: prompt || undefined,
count: selectedCount,
pageUrl: location.pathname,
element: extractContext(selectedElement),
});
state = 'GENERATING';
showBar('generating');
saveSession();
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
}
function handleAccept() {
if (!currentSessionId || arrivedVariants === 0) return;
sendEvent({ type: 'accept', id: currentSessionId, variantId: String(visibleVariant) });
markSessionHandled();
// Instantly commit the accepted variant in the DOM (fire-and-forget)
var wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (wrapper) {
var accepted = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"]');
if (accepted && accepted.firstElementChild) {
var parent = wrapper.parentElement;
if (parent) parent.replaceChild(accepted.firstElementChild.cloneNode(true), wrapper);
}
}
state = 'CONFIRMED';
updateBarContent('confirmed');
setTimeout(function() {
hideBar();
hideHighlight();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
clearSession();
selectedElement = null;
currentSessionId = null;
selectedAction = 'impeccable';
state = 'PICKING';
}, 1800);
}
function handleDiscard() {
if (!currentSessionId) return;
sendEvent({ type: 'discard', id: currentSessionId });
markSessionHandled();
// Instant DOM restore + fire-and-forget (script handles file cleanup)
cleanup();
}
// ---------------------------------------------------------------------------
// Session persistence via localStorage
// ---------------------------------------------------------------------------
// Survives page reloads, browser close/reopen, HMR, and accidental refreshes.
const LS_KEY = PREFIX + '-session';
function saveSession() {
if (!currentSessionId) return;
try {
localStorage.setItem(LS_KEY, JSON.stringify({
id: currentSessionId,
state: state,
action: selectedAction,
count: selectedCount,
expected: expectedVariants,
arrived: arrivedVariants,
visible: visibleVariant,
}));
} catch { /* quota exceeded or private mode */ }
}
function loadSession() {
try {
const raw = localStorage.getItem(LS_KEY);
return raw ? JSON.parse(raw) : null;
} catch { return null; }
}
function clearSession() {
try { localStorage.removeItem(LS_KEY); } catch {}
}
/** Mark session as handled (accepted/discarded). The agent will clean up
* the source, but until it does the wrapper is still in the HTML. This
* prevents resumeSession from picking it up again after reload. */
function markSessionHandled() {
if (!currentSessionId) return;
try {
localStorage.setItem(LS_KEY + '-handled', currentSessionId);
} catch {}
}
function isSessionHandled(id) {
try {
return localStorage.getItem(LS_KEY + '-handled') === id;
} catch { return false; }
}
function clearHandled() {
try { localStorage.removeItem(LS_KEY + '-handled'); } catch {}
}
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();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
clearSession();
selectedElement = null;
currentSessionId = null;
selectedAction = 'impeccable';
state = 'PICKING';
}
// ---------------------------------------------------------------------------
// Toast
// ---------------------------------------------------------------------------
function showToast(message, duration) {
if (toastEl) toastEl.remove();
toastEl = el('div', {
position: 'fixed', bottom: '16px', left: '50%',
transform: 'translateX(-50%) translateY(8px)',
background: C.ink, color: C.white,
fontFamily: FONT, fontSize: '12px',
padding: '8px 16px', borderRadius: '8px',
zIndex: Z.toast, opacity: '0',
transition: 'opacity 0.25s ' + EASE + ', transform 0.25s ' + EASE,
pointerEvents: 'none', maxWidth: '420px', textAlign: 'center',
});
toastEl.id = PREFIX + '-toast';
toastEl.textContent = message;
document.body.appendChild(toastEl);
requestAnimationFrame(() => {
toastEl.style.opacity = '1';
toastEl.style.transform = 'translateX(-50%) translateY(0)';
});
setTimeout(() => {
if (toastEl) {
toastEl.style.opacity = '0';
toastEl.style.transform = 'translateX(-50%) translateY(8px)';
setTimeout(() => { if (toastEl) { toastEl.remove(); toastEl = null; } }, 250);
}
}, duration);
}
// ---------------------------------------------------------------------------
// Init
// ---------------------------------------------------------------------------
// Resume an active variant session after HMR/page reload.
// If a [data-impeccable-variants] wrapper exists in the DOM, the agent wrote
// variants before HMR fired. Pick up where we left off.
function resumeSession() {
const wrapper = document.querySelector('[data-impeccable-variants]');
if (!wrapper) { clearSession(); clearHandled(); return false; }
const sessionId = wrapper.dataset.impeccableVariants;
// Don't resume if this session was already accepted/discarded
if (isSessionHandled(sessionId)) return false;
currentSessionId = sessionId;
expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || '0');
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
// Restore state from localStorage if available
const saved = loadSession();
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.
// Try the visible variant first, fall back to the original's content.
const visEl = wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"] > :first-child');
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);
state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING';
showBar(state === 'CYCLING' ? 'cycling' : 'generating');
startScrollTracking();
saveSession();
// Start observing for more variants AFTER initial setup
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
return true;
}
// ---------------------------------------------------------------------------
// Global bar (always visible at bottom)
// ---------------------------------------------------------------------------
let globalBarEl = null;
let detectActive = false;
let pickActive = true;
let detectCount = 0;
let detectScriptLoaded = false;
function initGlobalBar() {
globalBarEl = el('div', {
position: 'fixed', bottom: '14px', left: '50%',
transform: 'translateX(-50%) translateY(20px)',
zIndex: Z.bar + 5,
display: 'flex', alignItems: 'center',
padding: '4px 5px', gap: '2px',
background: C.paper,
backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)',
border: '1px solid ' + C.mist,
borderRadius: '10px',
boxShadow: '0 4px 20px oklch(0% 0 0 / 0.08), 0 1px 3px oklch(0% 0 0 / 0.06)',
fontFamily: FONT, fontSize: '12px',
opacity: '0',
transition: 'opacity 0.3s ' + EASE + ', transform 0.3s ' + EASE,
});
globalBarEl.id = PREFIX + '-global-bar';
// Brand
const brand = el('span', {
fontWeight: '600', fontSize: '11px', letterSpacing: '0.02em',
color: C.brand, fontFamily: MONO,
padding: '0 6px 0 4px',
});
brand.textContent = 'Impeccable';
globalBarEl.appendChild(brand);
// Detect toggle: eye icon + label + integrated badge
const detectBtn = el('button', {
display: 'inline-flex', alignItems: 'center', gap: '5px',
padding: '5px 10px', borderRadius: '7px',
border: 'none', background: 'transparent',
color: C.ash, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500',
cursor: 'pointer', transition: 'all 0.15s ease', whiteSpace: 'nowrap',
});
detectBtn.id = PREFIX + '-detect-toggle';
// Eye icon (SVG)
detectBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
const detectLabel = el('span', {});
detectLabel.textContent = 'Detect';
detectBtn.appendChild(detectLabel);
// Badge is inside the button
const detectBadge = el('span', {
fontSize: '10px', fontWeight: '600',
padding: '0px 5px', borderRadius: '7px', lineHeight: '16px',
background: C.brand, color: C.white,
display: 'none', fontFamily: MONO,
});
detectBadge.id = PREFIX + '-detect-badge';
detectBtn.appendChild(detectBadge);
detectBtn.addEventListener('click', () => toggleDetect());
detectBtn.addEventListener('mouseenter', () => { if (!detectActive) detectBtn.style.color = C.ink; });
detectBtn.addEventListener('mouseleave', () => { if (!detectActive) detectBtn.style.color = C.ash; });
globalBarEl.appendChild(detectBtn);
// Pick toggle: crosshair icon + label
const pickBtn = el('button', {
display: 'inline-flex', alignItems: 'center', gap: '5px',
padding: '5px 10px', borderRadius: '7px',
border: 'none', background: C.brandSoft,
color: C.brand, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500',
cursor: 'pointer', transition: 'all 0.15s ease', whiteSpace: 'nowrap',
});
pickBtn.id = PREFIX + '-pick-toggle';
pickBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/></svg>';
const pickLabel = el('span', {});
pickLabel.textContent = 'Pick';
pickBtn.appendChild(pickLabel);
pickBtn.addEventListener('click', () => togglePick());
globalBarEl.appendChild(pickBtn);
// Exit (subtle × on the right)
const exitBtn = el('button', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: '24px', height: '24px', borderRadius: '6px',
border: 'none', background: 'transparent',
color: C.mist, fontFamily: FONT, fontSize: '16px', lineHeight: '1',
cursor: 'pointer', transition: 'color 0.12s ease, background 0.12s ease',
marginLeft: '2px',
});
exitBtn.textContent = '\u00D7';
exitBtn.title = 'Exit live mode';
exitBtn.addEventListener('mouseenter', () => { exitBtn.style.color = C.ink; exitBtn.style.background = 'oklch(90% 0 0 / 0.5)'; });
exitBtn.addEventListener('mouseleave', () => { exitBtn.style.color = C.mist; exitBtn.style.background = 'transparent'; });
exitBtn.addEventListener('click', () => { sendEvent({ type: 'exit' }); teardown(); });
globalBarEl.appendChild(exitBtn);
document.body.appendChild(globalBarEl);
requestAnimationFrame(() => {
globalBarEl.style.opacity = '1';
globalBarEl.style.transform = 'translateX(-50%) translateY(0)';
});
// Listen for detection results AND ready signal
window.addEventListener('message', onDetectMessage);
}
function updateGlobalBarState() {
const detectToggle = document.getElementById(PREFIX + '-detect-toggle');
const detectBadge = document.getElementById(PREFIX + '-detect-badge');
const pickToggle = document.getElementById(PREFIX + '-pick-toggle');
if (detectToggle) {
detectToggle.style.background = detectActive ? C.brandSoft : 'transparent';
detectToggle.style.color = detectActive ? C.brand : C.ash;
}
if (detectBadge) {
detectBadge.style.display = (detectActive && detectCount > 0) ? 'inline' : 'none';
detectBadge.textContent = detectCount;
}
if (pickToggle) {
pickToggle.style.background = pickActive ? C.brandSoft : 'transparent';
pickToggle.style.color = pickActive ? C.brand : C.ash;
}
// When pick is active, make detect overlays click-through so the picker works
document.querySelectorAll('.impeccable-overlay').forEach(o => {
o.style.pointerEvents = pickActive ? 'none' : '';
});
}
let detectReady = false; // true once detect script posts 'impeccable-ready'
let detectPendingScan = false; // scan requested before script was ready
function toggleDetect() {
detectActive = !detectActive;
updateGlobalBarState();
if (detectActive) {
if (!detectScriptLoaded) {
detectPendingScan = true;
loadDetectScript();
} else if (detectReady) {
window.postMessage({ source: 'impeccable-command', action: 'scan' }, '*');
} else {
detectPendingScan = true;
}
} else {
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
detectCount = 0;
updateGlobalBarState();
}
}
function togglePick() {
pickActive = !pickActive;
updateGlobalBarState();
if (!pickActive) {
hideHighlight();
if (state === 'PICKING') state = 'IDLE';
} else {
if (state === 'IDLE') state = 'PICKING';
}
}
function loadDetectScript() {
if (detectScriptLoaded) return;
detectScriptLoaded = true;
const s = document.createElement('script');
s.src = 'http://localhost:' + PORT + '/detect.js';
s.dataset.impeccableExtension = 'true';
document.head.appendChild(s);
}
function onDetectMessage(e) {
if (!e.data || typeof e.data.source !== 'string') return;
// Detection script is loaded and ready
if (e.data.source === 'impeccable-ready') {
detectReady = true;
if (detectPendingScan && detectActive) {
detectPendingScan = false;
window.postMessage({ source: 'impeccable-command', action: 'scan' }, '*');
}
}
// Scan results arrived
if (e.data.source === 'impeccable-results') {
detectCount = e.data.count || 0;
updateGlobalBarState();
}
}
/** Full teardown: remove all UI, disconnect SSE, clean up. */
function teardown() {
cleanup();
hideBar();
if (globalBarEl) {
globalBarEl.style.transform = 'translateY(100%)';
setTimeout(() => { if (globalBarEl) globalBarEl.remove(); globalBarEl = null; }, 300);
}
if (highlightEl) { highlightEl.remove(); highlightEl = null; }
if (tooltipEl) { tooltipEl.remove(); tooltipEl = null; }
if (barEl) { barEl.remove(); barEl = null; }
if (pickerEl) { pickerEl.remove(); pickerEl = null; }
if (evtSource) { evtSource.close(); evtSource = null; }
document.removeEventListener('mousemove', handleMouseMove, true);
document.removeEventListener('click', handleClick, true);
document.removeEventListener('keydown', handleKeyDown, true);
window.removeEventListener('message', onDetectMessage);
// Remove detection overlays
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
state = 'IDLE';
window.__IMPECCABLE_LIVE_INIT__ = false;
console.log('[impeccable] Live mode exited.');
}
// ---------------------------------------------------------------------------
// Init
// ---------------------------------------------------------------------------
function init() {
initHighlight();
initBar();
initActionPicker();
initGlobalBar();
document.addEventListener('mousemove', handleMouseMove, true);
document.addEventListener('click', handleClick, true);
document.addEventListener('keydown', handleKeyDown, true);
connectSSE();
// Check for an active session to resume (variant wrapper already in DOM after HMR)
if (!resumeSession()) {
console.log('[impeccable] Live variant mode ready. Hover over elements to pick one.');
} else {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();