Files
amare/.github/skills/impeccable/scripts/live-browser.js
2026-08-01 22:06:48 -03:00

12513 lines
489 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__;
const APP_ROOT = window.__IMPECCABLE_APP_ROOT__ || null;
if (!TOKEN || !PORT) {
window.__IMPECCABLE_LIVE_INIT__ = false; // reset so the real load can init
return;
}
//
// Design tokens
//
// Brand kinpaku (gold) is pinned to the site's neo-kinpaku tokens
// (see site/styles/kinpaku-tokens.css) so Accept / knobs / cycle-dots /
// the selection outline / the comment tag all match the site's accent,
// not a washed theme-adjusted one. These mirror the kit's picker
// colors in site/styles/kinpaku-kit.css; keep them in sync by hand.
const C = {
brand: 'oklch(84% 0.19 80.46)', // kinpaku gold
brandHov: 'oklch(86% 0.07 84)', // kinpaku-pale (hover lift)
brandSoft: 'oklch(84% 0.19 80.46 / 0.18)', // kinpaku-dim
ink: 'oklch(4% 0.004 95)', // lacquer-deep
ash: 'oklch(55% 0.018 82)', // warm muted text
paper: 'oklch(98% 0.005 95 / 0.92)', // light overlay on user pages
paperSolid:'oklch(98% 0.005 95)',
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome - mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
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 IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable';
const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style';
const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;
const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
prefix: PREFIX,
storage: localStorage,
idFactory: () => crypto.randomUUID().replace(/-/g, '').slice(0, 8),
});
if (!sessionState) {
console.error('[impeccable] live-browser-session.js was not loaded. Live mode cannot start safely.');
window.__IMPECCABLE_LIVE_INIT__ = false;
return;
}
const HIGHLIGHT_TRANSITION =
'top 140ms ' + EASE +
', left 140ms ' + EASE +
', width 140ms ' + EASE +
', height 140ms ' + EASE +
', opacity 150ms ease';
const TOOLTIP_TRANSITION =
'top 140ms ' + EASE + ', left 140ms ' + EASE + ', opacity 150ms ease';
const SKIP_TAGS = new Set([
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
]);
// Command vocabulary (values + labels + icons) comes from the canonical source,
// skill/scripts/live/vocabulary.mjs, which live-server.mjs serializes into
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
// the token/port above, so it is always present here). The icons stack above
// each chip label and recolor to C.brand when selected (strokes use
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
const ICONS = {};
const ACTIONS = VOCAB.map((c) => {
ICONS[c.value] = c.icon;
return { value: c.value, label: c.label };
});
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
const LIVE_UI_SURFACES = [
{ key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice', PREFIX + '-page-chat-send'] },
{ key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] },
{ key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] },
{ key: 'action-picker', ids: [PREFIX + '-picker'] },
{ key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] },
{ key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] },
{ key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] },
{ key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] },
{ key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] },
{ key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] },
{ key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] },
{ key: 'design-system-panel', ids: [PREFIX + '-design-host'] },
{ key: 'toasts-and-errors', ids: [PREFIX + '-toast', PREFIX + '-mount-error'] },
{ key: 'css-isolation-boundary', ids: [PREFIX + '-root'] },
];
const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))];
//
// State
//
let state = 'IDLE';
let hoveredElement = null;
let selectedElement = null;
let currentSessionId = null;
let expectedVariants = 0;
let arrivedVariants = 0;
let visibleVariant = 0;
let generationPhase = null;
// Ascending order of the agent-generation lifecycle. The visible progress bar
// must never regress: a `browser_resumed`/behind checkpoint re-broadcasts an
// earlier phase (the server regresses the snapshot phase to `generating` on a
// behind checkpoint), and without this the bar jumps backward mid-generation.
// Unranked phases always pass so we never block a phase we do not model.
//
// Every `agent_phase` name here is emitted by recordAgentPhase() in
// live-server.mjs and listed in AGENT_PHASES in live/vocabulary.mjs, which the
// event validator enforces. This file is served raw and injected as an IIFE,
// so it cannot import that list; adding a phase means adding it in both.
// `queued`, `generating`, `variants_progress`, and `variants_ready` are set
// locally by this file and never arrive over the wire.
const PHASE_RANK = {
queued: 0,
picked_up: 1,
scaffolding: 2,
scaffold_fallback: 3,
source_ready: 4,
generation_ready: 5,
generating: 5,
variants_progress: 5,
first_reviewable: 8,
second_reviewable: 11,
all_variants_ready: 12,
variants_ready: 12,
};
function shouldAdvancePhase(current, next) {
if (!next || next === current) return false;
const nextRank = PHASE_RANK[next];
const currentRank = PHASE_RANK[current];
// Only block a known-lower phase from overwriting a known-higher one.
if (nextRank === undefined || currentRank === undefined) return true;
return nextRank >= currentRank;
}
let parameterGenerationState = 'idle';
let parameterReadyAnnouncedSession = null;
let svelteComponentSession = null;
let svelteRuntimePromise = null;
let pendingSvelteComponentRetryObserver = null;
// The persistent mount-error card. A failed import/mount used to wipe local
// session state and flash a 5s toast, which destroyed the only handle the
// user had on a session the server still considered live. The card stays up
// until the variant mounts, the user retries, or a new cycle starts.
let mountErrorEl = null;
let mountErrorState = null;
let lastReportedMountFailure = null;
let currentSourceFile = null;
let currentPreviewFile = null;
let currentPreviewMode = null;
let recoveryWaitingForAnchor = false;
let pickedAnchorSnapshot = null;
let pickedAnchorViewportTop = null;
let pendingVariantAnchorRetryObserver = null;
let pendingAcceptedSession = null;
// Survives cleanupAcceptedSession on purpose: the id of an accept whose
// POST was acknowledged (intent durable, epoch fenced) but whose actual
// source promotion hasn't reported back yet. Accept is optimistic, so the
// teardown nulls pendingAcceptedSession long before live-accept.mjs runs;
// this marker is what lets the SSE 'error' branch still recognize a late
// accept failure and say the variant was not saved (issue #384). Released
// when the real accept result arrives or a new session starts.
let awaitingAcceptResult = null;
let variantObserver = null;
let variantSelectionInFlight = false;
let variantSelectionPromise = null;
let recoveringEmptyCycling = false;
let hasProjectContext = false;
let selectedAction = 'impeccable';
let selectedCount = 3;
const browserOwner = sessionState.owner;
let checkpointTimer = null;
// Scroll lock - holds window.scrollY at a fixed value while the session is
// active, so HMR DOM patches and variant swaps can't drift the page. See
// startScrollLock / stopScrollLock below.
let scrollLockObserver = null;
let scrollLockTargetY = null;
let scrollLockAnchorTop = null;
let scrollLockRaf = null;
let scrollLockAbort = null;
const SCROLL_ANCHOR_LOCK_ID = 'impeccable-scroll-anchor-lock';
const VARIANT_STATE_STYLE_ID = 'impeccable-variant-state';
const DISCARD_STATE_STYLE_ID = 'impeccable-discard-state';
// Dedicated key for scroll position - SEPARATE from LS_KEY so that
// saveSession's state updates don't clobber a carefully-captured scrollY.
// (Previously: saveSession wrote scrollY alongside state, so every call
// during resume overwrote the pre-reload value with whatever the browser
// had landed on, typically 0.)
function writeScrollY(y) { sessionState.writeScrollY(y); }
function readScrollY() { return sessionState.readScrollY(); }
function clearScrollY() { sessionState.clearScrollY(); }
// Pre-empt the browser: apply manual scroll restoration and jump to the
// saved scrollY at script-parse time. Retries on fonts.ready and load
// are essential: scrollTo(y) clamps to the current document.scrollHeight,
// which is often hundreds of pixels short of the final value until
// async-loaded fonts swap in and reflow.
try {
history.scrollRestoration = 'manual';
const savedY = readScrollY();
if (savedY != null) {
const apply = () => {
if (Math.abs(window.scrollY - savedY) > 0.5) {
window.scrollTo(0, savedY);
}
};
apply();
if (document.fonts?.ready) document.fonts.ready.then(apply).catch(() => {});
window.addEventListener('load', apply, { once: true });
}
} catch {}
// UI refs
let highlightEl = null;
let tooltipEl = null;
let barEl = null;
let barHideSeq = 0;
let pickerEl = null;
let toastEl = null;
let scrollRaf = null;
let editBadgeEl = null;
let editBadgeProxyRoot = null;
let editBadgeProxyByTarget = new Map();
//
// Helpers
//
const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({
prefix: PREFIX,
skipTags: SKIP_TAGS,
document,
});
if (!domHelpers) {
console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.');
window.__IMPECCABLE_LIVE_INIT__ = false;
return;
}
const {
own,
pickable,
desc,
rectIsUsableAnchor,
makeFrozenAnchor,
id8,
cssId,
liveUiRoot,
uiAppend,
uiAppendStyle,
uiGetById,
activeElementDeep,
defangOutsideHandlers,
} = domHelpers;
window.__IMPECCABLE_LIVE_CHROME_CORE__ = {
version: 1,
adapter: window.__IMPECCABLE_LIVE_ADAPTER__ || 'dom',
mountContract: LIVE_CHROME_MOUNT_CONTRACT,
surfaces: LIVE_UI_SURFACES,
componentIds: LIVE_UI_COMPONENT_IDS,
root: liveUiRoot,
append: uiAppend,
appendStyle: uiAppendStyle,
getById: uiGetById,
activeElementDeep,
debugState: () => ({
state,
currentSessionId,
expectedVariants,
arrivedVariants,
visibleVariant,
savedSession: loadSession(),
sourceFile: currentSourceFile,
previewFile: currentPreviewFile,
previewMode: currentPreviewMode,
barText: barEl?.textContent || null,
barConnected: !!barEl?.isConnected,
hasSvelteComponentSession: !!svelteComponentSession,
mountedSvelteVariant: svelteComponentSession?.mountedVariant || 0,
pickActive,
pendingApplyInFlight,
hoveredElement: hoveredElement ? {
tag: hoveredElement.tagName,
classes: hoveredElement.className,
pickable: pickable(hoveredElement),
} : null,
pendingSvelteComponentRetry: !!pendingSvelteComponentRetryObserver,
recoveryWaitingForAnchor,
evtSourceReadyState: evtSource ? evtSource.readyState : null,
}),
};
//
// 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',
transition: HIGHLIGHT_TRANSITION,
display: 'none', opacity: '0',
});
uiAppend(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',
transition: TOOLTIP_TRANSITION,
});
uiAppend(tooltipEl);
}
function shouldShowHighlightTagTooltip() {
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
return state !== 'CONFIGURING' && state !== 'EDITING';
}
function hideHighlightTagTooltip() {
if (!tooltipEl) return;
tooltipEl.style.opacity = '0';
tooltipEl.style.display = 'none';
}
function showHighlight(el) {
if (!el || !highlightEl) return;
if (el.hasAttribute?.('data-impeccable-insert-placeholder')) return;
const r = el.getBoundingClientRect();
const top = (r.top - 2) + 'px', left = (r.left - 2) + 'px';
const width = (r.width + 4) + 'px', height = (r.height + 4) + 'px';
const showTagTooltip = shouldShowHighlightTagTooltip();
const hiWasHidden = highlightEl.style.display === 'none' || highlightEl.style.opacity === '0';
if (hiWasHidden) {
// Snap to first target without animating from (0,0), then fade in.
highlightEl.style.transition = 'none';
Object.assign(highlightEl.style, { top, left, width, height, display: 'block' });
void highlightEl.offsetWidth;
highlightEl.style.transition = HIGHLIGHT_TRANSITION;
highlightEl.style.opacity = '1';
} else {
Object.assign(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });
}
if (!showTagTooltip) {
hideHighlightTagTooltip();
return;
}
const tipTop = r.top - 20;
const tipY = (tipTop < 4 ? r.bottom + 4 : tipTop) + 'px';
const tipX = Math.max(4, r.left) + 'px';
tooltipEl.textContent = desc(el);
if (hiWasHidden) {
tooltipEl.style.transition = 'none';
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block' });
void tooltipEl.offsetWidth;
tooltipEl.style.transition = TOOLTIP_TRANSITION;
tooltipEl.style.opacity = '1';
} else {
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });
}
}
function hideHighlight() {
if (highlightEl) { highlightEl.style.opacity = '0'; highlightEl.style.display = 'none'; }
if (tooltipEl) { tooltipEl.style.opacity = '0'; tooltipEl.style.display = 'none'; }
}
//
// Annotation overlay (comment pins + kinpaku strokes)
//
// Active while state === 'CONFIGURING'. The overlay is a fixed-positioned
// sibling of <body> mirroring selectedElement's bounding rect. Click (no
// drag) drops a comment pin; drag paints a kinpaku SVG stroke. All coords
// are stored in element-local CSS px so they survive scroll / resize and
// correlate directly with the captured PNG.
//
const DRAG_THRESHOLD = 5; // px - below this, treat pointerup as a click
const PIN_DBL_CLICK_MS = 300; // two clicks on the same pin within this delete it
let annotOverlayEl = null;
let annotSvgEl = null;
let annotPinsEl = null;
let annotClearChipEl = null;
let annotState = { comments: [], strokes: [] };
let annotActive = false;
// `annotPointer` is either:
// { kind: 'new', x0, y0, moved, strokeEl, strokePoints } creating a stroke/pin
// { kind: 'pin', idx, startPointer, startPin, moved } dragging an existing pin
let annotPointer = null;
let annotEditing = null; // { idx, input, wrapEl }
let annotLastPinClick = { idx: -1, time: 0 }; // for click-click-to-delete
let placeholderResizeLayerEl = null;
let placeholderResizeDrag = null;
function initAnnotOverlay() {
annotOverlayEl = document.createElement('div');
annotOverlayEl.id = PREFIX + '-annot';
Object.assign(annotOverlayEl.style, {
position: 'fixed', top: '0', left: '0', width: '0', height: '0',
pointerEvents: 'auto', zIndex: Z.highlight + 2,
display: 'none', overflow: 'visible',
cursor: 'crosshair', touchAction: 'none',
});
annotSvgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
annotSvgEl.id = PREFIX + '-annot-svg';
Object.assign(annotSvgEl.style, {
position: 'absolute', top: '0', left: '0',
width: '100%', height: '100%',
// The SVG itself doesn't absorb clicks; individual hit-paths opt-in via
// pointer-events=stroke so gaps still fall through to the overlay.
pointerEvents: 'none', overflow: 'visible',
});
annotOverlayEl.appendChild(annotSvgEl);
annotPinsEl = document.createElement('div');
annotPinsEl.id = PREFIX + '-annot-pins';
Object.assign(annotPinsEl.style, {
position: 'absolute', inset: '0',
pointerEvents: 'none',
});
annotOverlayEl.appendChild(annotPinsEl);
annotClearChipEl = document.createElement('div');
annotClearChipEl.id = PREFIX + '-annot-clear';
annotClearChipEl.dataset.annotClear = 'true';
annotClearChipEl.textContent = 'Clear';
Object.assign(annotClearChipEl.style, {
position: 'absolute', top: '8px', right: '8px',
background: C.ink, color: C.white,
fontFamily: FONT, fontSize: '10px', fontWeight: '500',
letterSpacing: '0.08em', textTransform: 'uppercase',
padding: '5px 12px', borderRadius: '999px',
cursor: 'pointer', pointerEvents: 'auto',
display: 'none', userSelect: 'none',
boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
});
annotOverlayEl.appendChild(annotClearChipEl);
placeholderResizeLayerEl = document.createElement('div');
placeholderResizeLayerEl.id = PREFIX + '-placeholder-resize';
Object.assign(placeholderResizeLayerEl.style, {
position: 'absolute',
inset: '0',
pointerEvents: 'none',
display: 'none',
zIndex: '2',
});
annotOverlayEl.appendChild(placeholderResizeLayerEl);
annotOverlayEl.addEventListener('pointerdown', onAnnotDown);
annotOverlayEl.addEventListener('pointermove', onAnnotMove);
annotOverlayEl.addEventListener('pointerup', onAnnotUp);
annotOverlayEl.addEventListener('pointercancel', onAnnotUp);
uiAppend(annotOverlayEl);
// Modal-host friendliness: pointer-events is already 'auto' on this
// overlay; we only need to silence the host's outside-interaction
// listeners. Don't override pointer-events here (the overlay toggles
// visibility via display:none, which is fine).
defangOutsideHandlers(annotOverlayEl, { setPointerEvents: false });
}
function updateClearChip() {
if (!annotClearChipEl) return;
const hasAny = annotState.comments.length > 0 || annotState.strokes.length > 0;
annotClearChipEl.style.display = hasAny ? 'block' : 'none';
}
function showAnnotOverlay(el) {
if (!annotOverlayEl || !el) return;
annotActive = true;
positionAnnotOverlay(el);
annotOverlayEl.style.display = 'block';
syncPlaceholderResizeHandles();
}
function hideAnnotOverlay() {
annotActive = false;
placeholderResizeDrag = null;
if (annotOverlayEl) annotOverlayEl.style.display = 'none';
syncPlaceholderResizeHandles();
// Drop any in-progress edit without touching annotState - clearAnnotations
// (if the caller is exiting configure mode) handles state reset.
annotEditing = null;
}
function positionAnnotOverlay(el) {
if (!annotOverlayEl || !el) return;
const r = el.getBoundingClientRect();
Object.assign(annotOverlayEl.style, {
top: r.top + 'px', left: r.left + 'px',
width: r.width + 'px', height: r.height + 'px',
});
annotSvgEl.setAttribute('viewBox', '0 0 ' + r.width + ' ' + r.height);
syncPlaceholderResizeHandles();
}
function clearAnnotations() {
annotState.comments = [];
annotState.strokes = [];
if (annotSvgEl) while (annotSvgEl.firstChild) annotSvgEl.removeChild(annotSvgEl.firstChild);
if (annotPinsEl) annotPinsEl.innerHTML = '';
annotPointer = null;
annotEditing = null;
annotLastPinClick = { idx: -1, time: 0 };
updateClearChip();
}
// Rebuild the SVG layer. Each stroke gets a wider invisible hit path
// beneath the visible kinpaku path so clicks register on thin lines.
function redrawStrokes() {
while (annotSvgEl.firstChild) annotSvgEl.removeChild(annotSvgEl.firstChild);
annotState.strokes.forEach((s, idx) => {
const d = pointsToPath(s.points);
const hit = document.createElementNS('http://www.w3.org/2000/svg', 'path');
hit.setAttribute('d', d);
hit.setAttribute('stroke', 'transparent');
hit.setAttribute('stroke-width', '16');
hit.setAttribute('stroke-linecap', 'round');
hit.setAttribute('stroke-linejoin', 'round');
hit.setAttribute('fill', 'none');
hit.setAttribute('pointer-events', 'stroke');
hit.style.cursor = 'pointer';
hit.dataset.annotStroke = String(idx);
annotSvgEl.appendChild(hit);
const visible = document.createElementNS('http://www.w3.org/2000/svg', 'path');
visible.setAttribute('d', d);
visible.setAttribute('stroke', C.brand);
visible.setAttribute('stroke-width', '3');
visible.setAttribute('stroke-linecap', 'round');
visible.setAttribute('stroke-linejoin', 'round');
visible.setAttribute('fill', 'none');
visible.setAttribute('pointer-events', 'none');
annotSvgEl.appendChild(visible);
});
updateClearChip();
}
function localCoords(e) {
const rect = annotOverlayEl.getBoundingClientRect();
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
}
function onAnnotDown(e) {
if (!annotActive) return;
// 0) Insert placeholder edge resize - wins over draw / pins.
const resizeEdge = e.target.closest?.('[data-impeccable-placeholder-resize]')?.dataset.impeccablePlaceholderResize;
if (resizeEdge && configureKind === 'insert' && placeholderElement) {
startPlaceholderEdgeResize(resizeEdge, e);
return;
}
// 1) Clear chip → wipe all annotations
if (e.target.closest?.('[data-annot-clear]')) {
if (annotEditing) annotEditing = null;
clearAnnotations();
renderAllPins();
redrawStrokes();
e.stopPropagation(); e.preventDefault();
return;
}
// 2) Stroke hit path → delete that stroke
const strokeHit = e.target.closest?.('[data-annot-stroke]');
if (strokeHit) {
const idx = parseInt(strokeHit.dataset.annotStroke, 10);
if (Number.isInteger(idx)) {
annotState.strokes.splice(idx, 1);
redrawStrokes();
}
e.stopPropagation(); e.preventDefault();
return;
}
// 3) Pin → drag, edit, or delete-on-double-click
const pinWrap = e.target.closest?.('[data-annot-pin]');
if (pinWrap) {
const idx = parseInt(pinWrap.dataset.annotPin, 10);
if (!Number.isInteger(idx)) return;
// Double-click (two pointerdowns on the same pin within window) → delete.
const now = Date.now();
if (annotLastPinClick.idx === idx && now - annotLastPinClick.time < PIN_DBL_CLICK_MS) {
if (annotEditing && annotEditing.idx === idx) annotEditing = null;
annotState.comments.splice(idx, 1);
annotLastPinClick = { idx: -1, time: 0 };
renderAllPins();
e.stopPropagation(); e.preventDefault();
return;
}
annotLastPinClick = { idx, time: now };
// If editing a different pin, commit that edit before starting here.
if (annotEditing && annotEditing.idx !== idx) finalizeEditingPin();
// If already editing THIS pin and the user clicked the dot, let the
// input keep focus (don't start a drag - the click wasn't meant as one).
if (annotEditing && annotEditing.idx === idx) return;
const p = localCoords(e);
const pin = annotState.comments[idx];
annotPointer = {
kind: 'pin', idx,
startPointer: p,
startPin: { x: pin.x, y: pin.y },
moved: false,
};
try { annotOverlayEl.setPointerCapture(e.pointerId); } catch {}
e.stopPropagation(); e.preventDefault();
return;
}
// 4) Empty area → commit any open edit, then start new annotation
if (annotEditing) {
finalizeEditingPin();
e.stopPropagation(); e.preventDefault();
return;
}
const p = localCoords(e);
annotPointer = { kind: 'new', x0: p.x, y0: p.y, moved: false, strokeEl: null, strokePoints: null };
try { annotOverlayEl.setPointerCapture(e.pointerId); } catch {}
e.stopPropagation(); e.preventDefault();
}
function onAnnotMove(e) {
if (!annotActive) return;
if (placeholderResizeDrag) {
const d = placeholderResizeDrag;
const next = resizePlaceholderFromEdge(
d.start,
d.edge,
e.clientX - d.startX,
e.clientY - d.startY,
d.parentWidth,
);
applyPlaceholderDimensions(next);
e.stopPropagation();
return;
}
if (!annotPointer) return;
const p = localCoords(e);
if (annotPointer.kind === 'pin') {
const dx = p.x - annotPointer.startPointer.x;
const dy = p.y - annotPointer.startPointer.y;
if (!annotPointer.moved) {
if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
annotPointer.moved = true;
}
const pin = annotState.comments[annotPointer.idx];
if (!pin) { annotPointer = null; return; }
pin.x = annotPointer.startPin.x + dx;
pin.y = annotPointer.startPin.y + dy;
renderAllPins();
e.stopPropagation();
return;
}
// kind === 'new'
const dx = p.x - annotPointer.x0, dy = p.y - annotPointer.y0;
if (!annotPointer.moved) {
if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
annotPointer.moved = true;
const strokeEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
strokeEl.setAttribute('stroke', C.brand);
strokeEl.setAttribute('stroke-width', '3');
strokeEl.setAttribute('stroke-linecap', 'round');
strokeEl.setAttribute('stroke-linejoin', 'round');
strokeEl.setAttribute('fill', 'none');
strokeEl.setAttribute('pointer-events', 'none');
annotSvgEl.appendChild(strokeEl);
annotPointer.strokeEl = strokeEl;
annotPointer.strokePoints = [[annotPointer.x0, annotPointer.y0]];
}
annotPointer.strokePoints.push([p.x, p.y]);
annotPointer.strokeEl.setAttribute('d', pointsToPath(annotPointer.strokePoints));
e.stopPropagation();
}
function pointsToPath(points) {
if (!points || points.length === 0) return '';
let d = 'M' + points[0][0].toFixed(1) + ' ' + points[0][1].toFixed(1);
for (let i = 1; i < points.length; i++) {
d += ' L' + points[i][0].toFixed(1) + ' ' + points[i][1].toFixed(1);
}
return d;
}
function onAnnotUp(e) {
if (placeholderResizeDrag) {
try { annotOverlayEl.releasePointerCapture(e.pointerId); } catch {}
placeholderResizeDrag = null;
e.stopPropagation();
return;
}
if (!annotActive || !annotPointer) return;
if (annotPointer.kind === 'pin') {
const wasDrag = annotPointer.moved;
const idx = annotPointer.idx;
try { annotOverlayEl.releasePointerCapture(e.pointerId); } catch {}
annotPointer = null;
if (wasDrag) {
// A drag is an intentional reposition; a follow-up click shouldn't be
// interpreted as a double-click-to-delete.
annotLastPinClick = { idx: -1, time: 0 };
} else {
beginEditPin(idx);
}
e.stopPropagation();
return;
}
// kind === 'new'
const wasDrag = annotPointer.moved;
if (wasDrag) {
annotState.strokes.push({ points: annotPointer.strokePoints });
// Swap the temporary preview SVG path for the full render with hit paths.
redrawStrokes();
} else {
const idx = annotState.comments.length;
annotState.comments.push({ x: annotPointer.x0, y: annotPointer.y0, text: '' });
renderAllPins();
beginEditPin(idx);
}
try { annotOverlayEl.releasePointerCapture(e.pointerId); } catch {}
annotPointer = null;
if (configureKind === 'insert') syncInsertCreateButton();
e.stopPropagation();
}
function renderAllPins() {
annotPinsEl.innerHTML = '';
annotState.comments.forEach((c, idx) => {
annotPinsEl.appendChild(buildPinElement(c, idx));
});
updateClearChip();
}
function buildPinElement(comment, idx) {
const interactive = idx >= 0;
const wrap = document.createElement('div');
if (interactive) wrap.dataset.annotPin = String(idx);
Object.assign(wrap.style, {
position: 'absolute',
left: (comment.x - 7) + 'px', top: (comment.y - 7) + 'px',
pointerEvents: interactive ? 'auto' : 'none',
display: 'flex', alignItems: 'flex-start', gap: '6px',
cursor: interactive ? 'grab' : 'default',
touchAction: 'none',
});
const dot = document.createElement('div');
Object.assign(dot.style, {
width: '14px', height: '14px', borderRadius: '50%',
background: C.brand, border: '2px solid ' + C.white,
boxShadow: '0 1px 3px rgba(0,0,0,0.25)',
flexShrink: '0',
});
wrap.appendChild(dot);
if (comment.text) {
const bubble = document.createElement('div');
bubble.textContent = comment.text;
Object.assign(bubble.style, {
background: C.ink, color: C.white,
fontFamily: FONT, fontSize: '12px', lineHeight: '1.4',
padding: '4px 8px', borderRadius: '3px',
marginTop: '-2px', maxWidth: '220px',
pointerEvents: 'none', whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
});
wrap.appendChild(bubble);
}
return wrap;
}
function beginEditPin(idx) {
const wrapEl = annotPinsEl.querySelector('[data-annot-pin="' + idx + '"]');
if (!wrapEl) return;
// Strip any existing bubble (but keep the dot)
wrapEl.querySelectorAll('div:not(:first-child)').forEach(n => n.remove());
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Note…';
Object.assign(input.style, {
background: C.ink, color: C.white,
fontFamily: FONT, fontSize: '12px', lineHeight: '1.4',
padding: '4px 8px', borderRadius: '3px',
border: '1px solid ' + C.brand,
outline: 'none', marginTop: '-2px',
width: '220px', pointerEvents: 'auto',
});
const originalText = annotState.comments[idx].text || '';
input.value = originalText;
wrapEl.appendChild(input);
annotEditing = { idx, input, wrapEl, originalText };
input.addEventListener('keydown', onAnnotInputKey, true);
input.addEventListener('blur', () => {
// Fires on both focus-loss and programmatic blur; commit unless we
// already handled it.
if (annotEditing && annotEditing.input === input) finalizeEditingPin();
});
// Stop clicks/pointerdowns inside the input from bubbling to the overlay
['pointerdown', 'click'].forEach(ev => {
input.addEventListener(ev, e => e.stopPropagation());
});
setTimeout(() => input.focus(), 0);
}
function onAnnotInputKey(e) {
if (e.key === 'Enter') {
e.preventDefault(); e.stopPropagation();
finalizeEditingPin();
} else if (e.key === 'Escape') {
e.preventDefault(); e.stopPropagation();
cancelEditingPin();
} else {
// Keep arrows / backspace from hitting global handlers
e.stopPropagation();
}
}
function finalizeEditingPin() {
if (!annotEditing) return;
const { idx, input } = annotEditing;
const text = input.value.trim();
annotEditing = null;
if (text) annotState.comments[idx].text = text;
else annotState.comments.splice(idx, 1);
renderAllPins();
}
function cancelEditingPin() {
if (!annotEditing) return;
const { idx, originalText } = annotEditing;
annotEditing = null;
// If the pin had text before this edit, restore it. If it was a
// just-created empty pin, Escape removes it.
if (originalText) {
annotState.comments[idx].text = originalText;
} else {
annotState.comments.splice(idx, 1);
}
renderAllPins();
}
// Build a detached annotation subtree suitable for injection into the clone
// modern-screenshot creates. Coordinates are element-local so this slots
// straight into an element that's been made position:relative. Takes an
// explicit snapshot so it works after annotState has been cleared.
function buildAnnotationsForCapture(rect, snapshot) {
const comments = snapshot ? snapshot.comments : annotState.comments;
const strokes = snapshot ? snapshot.strokes : annotState.strokes;
if (comments.length === 0 && strokes.length === 0) return null;
const wrap = document.createElement('div');
Object.assign(wrap.style, {
position: 'absolute', top: '0', left: '0',
width: rect.width + 'px', height: rect.height + 'px',
pointerEvents: 'none', overflow: 'visible',
});
if (strokes.length > 0) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 ' + rect.width + ' ' + rect.height);
Object.assign(svg.style, {
position: 'absolute', top: '0', left: '0',
width: '100%', height: '100%', overflow: 'visible',
});
for (const s of strokes) {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('stroke', C.brand);
path.setAttribute('stroke-width', '3');
path.setAttribute('stroke-linecap', 'round');
path.setAttribute('stroke-linejoin', 'round');
path.setAttribute('fill', 'none');
path.setAttribute('d', pointsToPath(s.points));
svg.appendChild(path);
}
wrap.appendChild(svg);
}
for (const c of comments) {
// idx=-1 means non-interactive; pointerEvents stay off in the clone
wrap.appendChild(buildPinElement(c, -1));
}
return wrap;
}
//
// Element context extraction
//
function stripManualEditRuntimeState(root) {
if (!root || root.nodeType !== 1) return;
unwrapMixedContentTextNodes(root);
const nodes = [root, ...root.querySelectorAll('[data-impeccable-editable], [data-impeccable-original-text], [data-impeccable-text-wrap]')];
for (const node of nodes) {
const runtimeEditable = node.hasAttribute('data-impeccable-editable')
|| node.hasAttribute('data-impeccable-original-text');
node.removeAttribute('data-impeccable-editable');
node.removeAttribute('data-impeccable-original-text');
node.removeAttribute('data-impeccable-text-wrap');
if (runtimeEditable) {
node.removeAttribute('contenteditable');
if (node.style) {
node.style.userSelect = '';
node.style.cursor = '';
node.style.outline = '';
node.style.webkitUserModify = '';
if (!node.getAttribute('style')?.trim()) node.removeAttribute('style');
}
}
}
}
function sanitizedContextOuterHTML(el, maxLength) {
if (!el || !el.cloneNode) return '';
const clone = el.cloneNode(true);
stripManualEditRuntimeState(clone);
return clone.outerHTML ? clone.outerHTML.slice(0, maxLength) : '';
}
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: sanitizedContextOuterHTML(el, 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) },
};
}
const MANUAL_CONTEXT_SKIP = { script: 1, style: 1, template: 1, noscript: 1, svg: 1, code: 1, pre: 1 };
function contextElementForManualEdit(selectedEl, rows, ops) {
if (!selectedEl) return selectedEl;
const leafOnly =
rows && rows.length === 1 && rows[0] && rows[0].el === selectedEl;
if (!leafOnly) return selectedEl;
const editedTexts = new Set();
for (const row of rows || []) addManualContextText(editedTexts, row.text);
for (const op of ops || []) {
addManualContextText(editedTexts, op.originalText);
addManualContextText(editedTexts, op.newText);
}
let cur = selectedEl.parentElement;
let depth = 0;
while (cur && cur !== document.body && cur !== document.documentElement && depth < 4) {
if (own(cur)) break;
if (isUsefulManualEditContext(cur, selectedEl, editedTexts)) return cur;
cur = cur.parentElement;
depth++;
}
return selectedEl;
}
function isUsefulManualEditContext(candidate, leafEl, editedTexts) {
if (!candidate || !candidate.contains(leafEl)) return false;
if (!candidate.id && candidate.classList.length === 0 && candidate.children.length < 2) return false;
return collectManualContextPieces(candidate, editedTexts).length > 0;
}
function collectManualContextPieces(rootEl, editedTexts) {
const pieces = [];
function walk(node) {
if (!node) return;
if (node.nodeType === 3) {
const text = normalizeManualContextText(node.nodeValue);
if (isMeaningfulManualContextPiece(text, editedTexts)) pieces.push(text);
return;
}
if (node.nodeType !== 1) return;
const tag = node.tagName.toLowerCase();
if (MANUAL_CONTEXT_SKIP[tag]) return;
if (node !== rootEl && own(node)) return;
for (const child of node.childNodes) walk(child);
}
walk(rootEl);
return pieces.slice(0, 12);
}
function addManualContextText(set, value) {
const text = normalizeManualContextText(value);
if (text) set.add(text);
}
function isMeaningfulManualContextPiece(text, editedTexts) {
if (!text || text.length < 3 || text.length > 160) return false;
if (/^[\d.,+\-%\s]+$/.test(text)) return false;
return !editedTexts.has(text);
}
function normalizeManualContextText(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
//
// The Bar - one floating element, three modes
//
// Contextual-bar palette. Cached at init so every build*Row reads a
// consistent set of colors; detectPageTheme runs once rather than on every
// phase transition.
let BP = null;
// Bar shadow variants. The default projects down + subtle around. When
// the Tune popover opens below the bar, a downward shadow lands on the
// dark popover and reads as a bright ghost line. We swap to UP-only while
// tune is open below so the popover's top edge is clean.
const BAR_SHADOW_DEFAULT = '0 4px 20px oklch(0% 0 0 / 0.08), 0 1px 3px oklch(0% 0 0 / 0.06)';
const BAR_SHADOW_UP = '0 -4px 20px oklch(0% 0 0 / 0.08), 0 -1px 3px oklch(0% 0 0 / 0.06)';
const BAR_SHADOW_DOWN = BAR_SHADOW_DEFAULT;
function initBar() {
BP = barPaletteForTheme(detectPageTheme());
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,
background: BP.surface,
border: '1px solid ' + BP.border,
borderRadius: '8px',
boxShadow: BP.shadow,
transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,
fontFamily: FONT, fontSize: '13px', color: BP.text,
padding: '5px',
maxWidth: '560px', minWidth: '340px',
});
uiAppend(barEl);
defangOutsideHandlers(barEl);
}
function positionBar() {
if (!barEl) return;
const barH = barEl.offsetHeight || 44;
const barW = barEl.offsetWidth || 380;
const GLOBAL_BAR_RESERVE = 64; // global bar height + bottom margin + breathing room
const GAP = 8;
// Recovery pins to document.body when the picked element is off-screen or
// missing. Center the generating bar above the global bar instead of
// stacking a duplicate toast in the same slot.
if (recoveryWaitingForAnchor) {
const barRect = globalBarEl?.getBoundingClientRect();
const reserve = barRect && barRect.height > 0
? Math.max(GLOBAL_BAR_RESERVE, window.innerHeight - barRect.top + 12)
: GLOBAL_BAR_RESERVE;
const top = window.innerHeight - barH - reserve;
const left = Math.max(GAP, (window.innerWidth - barW) / 2);
Object.assign(barEl.style, { top: top + 'px', left: left + 'px' });
return;
}
const anchor = resolveBarAnchor();
if (!anchor) return;
const r = anchor.getBoundingClientRect();
// Prefer below the element; fall back to above; if neither fits (element
// taller than viewport), pin to a stable viewport anchor so the bar
// doesn't teleport between top and bottom as the user scrolls.
let top;
const belowTop = r.bottom + GAP;
const aboveTop = r.top - barH - GAP;
if (belowTop + barH + GAP <= window.innerHeight - GLOBAL_BAR_RESERVE) {
top = belowTop;
} else if (aboveTop >= GAP) {
top = aboveTop;
} else {
top = window.innerHeight - barH - GLOBAL_BAR_RESERVE;
}
let left = r.left + (r.width - barW) / 2;
if (left < GAP) left = GAP;
if (left + barW > window.innerWidth - GAP) left = window.innerWidth - barW - GAP;
Object.assign(barEl.style, { top: top + 'px', left: left + 'px' });
}
function showBar(mode) {
barHideSeq += 1;
if (mode === 'cycling' && !ensureCyclingRenderable('show-bar')) return;
barEl.innerHTML = '';
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
applyConfigureBarChrome();
} else {
restorePickerBarChrome();
if (mode === 'generating') {
if (recoveryWaitingForAnchor) dismissToast();
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)';
syncPageChatFocus('show-bar');
});
}
function hideBar(instant) {
if (!barEl) return;
const hideSeq = ++barHideSeq;
stopVoice({ suppressSubmit: true });
if (configureKind === 'insert') clearInsertPicking();
barEl.style.opacity = '0';
barEl.style.transform = instant ? 'translateY(0)' : 'translateY(6px)';
if (instant) barEl.style.display = 'none';
else setTimeout(() => { if (barEl && hideSeq === barHideSeq) barEl.style.display = 'none'; }, 250);
hideActionPicker();
closeTunePopover();
hideConfigureBarTooltip();
if (state === 'EDITING') restoreInlineEditDrafts();
disableInlineEdit();
}
function updateBarContent(mode) {
if (!barEl || barEl.style.display === 'none') return;
if (mode === 'cycling' && !ensureCyclingRenderable('update-bar')) return;
barEl.innerHTML = '';
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
applyConfigureBarChrome();
} else {
restorePickerBarChrome();
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)';
}
}
syncPageChatFocus('update-bar-content');
}
// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.
const CONFIGURE_BAR_H = '36px';
// Compact selection pill + 7px inset balances vertical centering in the 36px bar.
const CONFIGURE_BAR_INSET = '7px';
const CONFIGURE_PILL_RADIUS = '7px';
const CONFIGURE_SELECTION_PILL_BORDER = '1px solid oklch(70% 0.12 188)';
const CONFIGURE_SELECTION_PILL_PAD = '1px 4px';
const CONFIGURE_ROW_FONT_SIZE = '12px';
const CONFIGURE_ROW_TRACK_H = '18px';
const CONFIGURE_PILL_PAD_Y = '3px';
const CONFIGURE_BAR_SURFACE = 'oklch(15% 0.008 95)';
const CONFIGURE_PILL_TEXT = 'oklch(94% 0.02 82)';
const ICON_CONFIGURE_SUBMIT =
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>';
function applyConfigureBarChrome() {
if (!barEl) return;
barEl.dataset.configureSurface = 'true';
barEl.style.padding = '0';
barEl.style.background = CONFIGURE_BAR_SURFACE;
barEl.style.overflow = 'hidden';
syncConfigureInputChrome();
}
function restorePickerBarChrome() {
if (!barEl) return;
barEl.dataset.configureSurface = 'false';
barEl.removeAttribute('data-input-focused');
barEl.removeAttribute('data-voice-listening');
barEl.style.padding = '5px';
barEl.style.background = BP.surface;
barEl.style.overflow = '';
barEl.style.border = '1px solid ' + BP.border;
barEl.style.borderColor = BP.border;
barEl.style.boxShadow = BP.shadow;
}
function syncConfigureInputChrome() {
const input = uiGetById(PREFIX + '-input') || uiGetById(PREFIX + '-insert-input');
const surface = barEl?.dataset.configureSurface === 'true' ? barEl : null;
if (!surface || !input) return;
const focused = activeElementDeep() === input;
const listening = voiceListening && voiceCtx?.mode === 'configure';
surface.dataset.inputFocused = focused ? 'true' : 'false';
surface.dataset.voiceListening = listening ? 'true' : 'false';
surface.style.borderColor = listening
? BP.patinaSoft
: (focused ? BP.accentSoft : BP.border);
surface.style.boxShadow = BP.shadow;
}
function configureBarPalette() {
return BP || barPaletteForTheme(detectPageTheme());
}
function configureRowTextMetrics(extra = {}) {
return {
fontFamily: FONT,
fontSize: CONFIGURE_ROW_FONT_SIZE,
fontWeight: '500',
lineHeight: CONFIGURE_ROW_TRACK_H,
...extra,
};
}
function configureInputFieldStyle(extra = {}) {
return {
flex: '1', minWidth: '0', width: '100%',
padding: '0', margin: '0',
border: 'none', background: 'transparent',
boxSizing: 'border-box',
height: CONFIGURE_ROW_TRACK_H,
color: CONFIGURE_PILL_TEXT,
caretColor: CONFIGURE_PILL_TEXT,
outline: 'none',
...configureRowTextMetrics(),
...extra,
};
}
function configureInputShellStyle() {
return {
display: 'flex', alignItems: 'center', gap: '6px',
flex: '1', minWidth: '0', height: '100%',
padding: '0 6px 0 ' + CONFIGURE_BAR_INSET,
};
}
function configureSelectionPillStyle(extra = {}) {
const P = configureBarPalette();
return {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
gap: '2px', height: 'auto', flexShrink: '0',
padding: CONFIGURE_SELECTION_PILL_PAD,
boxSizing: 'border-box',
border: CONFIGURE_SELECTION_PILL_BORDER,
borderRadius: CONFIGURE_PILL_RADIUS,
background: 'transparent',
color: P.patina,
cursor: 'pointer',
transition: 'background 0.15s ease, color 0.15s ease, border-color 0.15s ease',
whiteSpace: 'nowrap',
...configureRowTextMetrics({
fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.01em',
}),
...extra,
};
}
function configureModifierPillStyle(extra = {}) {
const P = configureBarPalette();
return {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
gap: '2px', height: 'auto', minHeight: CONFIGURE_ROW_TRACK_H,
padding: CONFIGURE_PILL_PAD_Y + ' 8px', flexShrink: '0',
boxSizing: 'border-box',
border: '1px solid transparent',
borderRadius: CONFIGURE_PILL_RADIUS,
background: 'transparent',
color: P.textDim, cursor: 'pointer',
transition: 'background 0.15s ease, color 0.15s ease, border-color 0.15s ease',
whiteSpace: 'nowrap',
...configureRowTextMetrics(),
...extra,
};
}
function configureInlineControlStyle(extra = {}) {
const P = configureBarPalette();
return {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
gap: '2px', height: CONFIGURE_ROW_TRACK_H, flexShrink: '0',
padding: '0', margin: '0',
boxSizing: 'border-box',
border: 'none', borderRadius: '0',
background: 'transparent',
color: P.textDim, cursor: 'pointer',
transition: 'color 0.12s ease, background 0.12s ease',
whiteSpace: 'nowrap',
...configureRowTextMetrics(),
...extra,
};
}
function bindConfigureInlineControlHover(btn, controlsLocked) {
btn.addEventListener('mouseenter', () => {
if (controlsLocked) return;
const P = configureBarPalette();
btn.style.color = P.text;
});
btn.addEventListener('mouseleave', () => {
if (controlsLocked) return;
btn.style.color = configureBarPalette().textDim;
});
}
function bindConfigureModifierPillHover(btn, controlsLocked) {
btn.addEventListener('mouseenter', () => {
if (controlsLocked) return;
const P = configureBarPalette();
btn.style.color = P.text;
btn.style.background = P.toggleActive;
});
btn.addEventListener('mouseleave', () => {
if (controlsLocked) return;
const P = configureBarPalette();
btn.style.color = P.textDim;
btn.style.background = 'transparent';
});
}
let configureBarTooltipEl = null;
function ensureConfigureBarTooltip() {
if (configureBarTooltipEl) return configureBarTooltipEl;
const P = configureBarPalette();
configureBarTooltipEl = el('div', {
position: 'fixed',
display: 'none',
zIndex: String(Z.bar + 7),
pointerEvents: 'none',
maxWidth: 'min(360px, calc(100vw - 16px))',
padding: '6px 9px',
borderRadius: '7px',
background: P.chatSurface,
border: '1px solid ' + P.hairline,
boxShadow: P.shadow,
color: P.text,
fontFamily: FONT,
fontSize: '11px',
fontWeight: '500',
lineHeight: '1.35',
letterSpacing: '0.01em',
whiteSpace: 'normal',
wordBreak: 'break-word',
});
configureBarTooltipEl.id = PREFIX + '-configure-bar-tooltip';
uiAppend(configureBarTooltipEl);
return configureBarTooltipEl;
}
function showConfigureBarTooltip(anchor, message) {
if (!anchor || !message) return;
const tip = ensureConfigureBarTooltip();
tip.textContent = message;
tip.style.transition = 'none';
tip.style.display = 'block';
tip.style.opacity = '1';
const r = anchor.getBoundingClientRect();
const tipW = tip.offsetWidth;
const tipH = tip.offsetHeight;
const left = Math.max(8, Math.min(window.innerWidth - tipW - 8, r.left + r.width / 2 - tipW / 2));
const top = Math.max(8, r.top - tipH - 8);
tip.style.left = left + 'px';
tip.style.top = top + 'px';
}
function hideConfigureBarTooltip() {
if (!configureBarTooltipEl) return;
configureBarTooltipEl.style.display = 'none';
configureBarTooltipEl.style.opacity = '0';
}
function selectionTagLabel(el) {
if (!el) return '';
if (el.hasAttribute?.('data-impeccable-insert-placeholder')) return 'slot';
return el.tagName.toLowerCase();
}
function elementPath(el, maxDepth = 8) {
if (!el) return '';
const parts = [];
let node = el;
while (node && node.nodeType === 1 && node !== document.body) {
let part = node.tagName.toLowerCase();
if (node.id) part += '#' + node.id;
else if (node.classList?.length) part += '.' + [...node.classList].slice(0, 2).join('.');
parts.unshift(part);
node = node.parentElement;
if (parts.length >= maxDepth) break;
}
return parts.join(' \u203a ');
}
function variantCountTooltipText(count) {
const n = Number(count) || selectedCount;
const word = n === 1 ? 'variant' : 'variants';
return 'Click to change \u00b7 ' + n + ' ' + word;
}
function removeConfigureSelection() {
hideConfigureBarTooltip();
if (configureKind === 'insert') {
cancelInsertConfigure();
return;
}
selectedElement = null;
exitConfigureToPicking('selection-pill-remove', { clearHover: true });
}
function buildSelectionPill({ el: targetEl, controlsLocked }) {
const tag = selectionTagLabel(targetEl);
const path = elementPath(targetEl);
const P = configureBarPalette();
const pill = el('button', configureSelectionPillStyle({ minWidth: '32px' }));
pill.id = PREFIX + '-selection-pill';
pill.type = 'button';
pill.setAttribute('aria-label', 'Selected element: ' + tag);
pill.disabled = controlsLocked;
pill.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
pill.style.opacity = controlsLocked ? '0.58' : '1';
pill.style.flexShrink = '0';
const faceStack = el('span', {
display: 'grid', placeItems: 'center',
width: '100%', minWidth: '1.25em',
lineHeight: CONFIGURE_ROW_TRACK_H,
});
const tagFace = el('span', {
gridArea: '1 / 1',
transition: 'opacity 0.12s ease',
color: P.patina,
});
const clearFace = el('span', {
gridArea: '1 / 1',
opacity: '0',
transition: 'opacity 0.12s ease',
color: 'oklch(58% 0.15 35)',
});
tagFace.textContent = tag;
clearFace.textContent = '\u00D7';
faceStack.appendChild(tagFace);
faceStack.appendChild(clearFace);
pill.appendChild(faceStack);
const setArmed = (armed) => {
tagFace.style.opacity = armed ? '0' : '1';
clearFace.style.opacity = armed ? '1' : '0';
pill.style.background = armed ? P.toggleActive : 'transparent';
pill.style.border = CONFIGURE_SELECTION_PILL_BORDER;
pill.setAttribute('aria-label', armed ? 'Clear selection' : 'Selected element: ' + tag);
};
const arm = () => {
if (controlsLocked) {
showConfigureBarTooltip(pill, 'Apply is still running');
return;
}
setArmed(true);
if (path) showConfigureBarTooltip(pill, path);
};
const disarm = () => {
hideConfigureBarTooltip();
setArmed(false);
};
pill.addEventListener('mouseenter', arm);
pill.addEventListener('mouseleave', disarm);
pill.addEventListener('focus', arm);
pill.addEventListener('blur', disarm);
pill.addEventListener('click', (e) => {
e.stopPropagation();
if (controlsLocked) { showManualApplyBusyToast(); return; }
removeConfigureSelection();
});
return pill;
}
function bindConfigureCountPillTooltip(count, controlsLocked) {
count.removeAttribute('title');
count.addEventListener('mouseenter', () => {
if (controlsLocked) {
showConfigureBarTooltip(count, 'Apply is still running');
return;
}
showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));
});
count.addEventListener('mouseleave', hideConfigureBarTooltip);
}
function buildConfigureActionControl({ controlsLocked, onClick }) {
const control = el('button', configureInlineControlStyle());
const label = document.createElement('span');
label.textContent = actionLabel();
const caret = el('span', {
fontSize: '10px', lineHeight: '1',
marginLeft: '2px', pointerEvents: 'none',
color: 'inherit',
});
caret.textContent = '\u25BE';
caret.setAttribute('aria-hidden', 'true');
control.appendChild(label);
control.appendChild(caret);
control.disabled = controlsLocked;
control.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
control.style.opacity = controlsLocked ? '0.58' : '1';
bindConfigureInlineControlHover(control, controlsLocked);
control.addEventListener('click', onClick);
return control;
}
const VARIANT_COUNT_MIN = 1;
const VARIANT_COUNT_MAX = 4;
function cycleSelectedCount() {
if (selectedCount >= VARIANT_COUNT_MAX) selectedCount = VARIANT_COUNT_MIN;
else selectedCount += 1;
return selectedCount;
}
function buildConfigureCountControl({ controlsLocked, onClick }) {
const count = el('button', configureInlineControlStyle({
fontFamily: MONO, fontWeight: '600', letterSpacing: '0',
}));
count.textContent = '\u00D7' + selectedCount;
count.disabled = controlsLocked;
count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
count.style.opacity = controlsLocked ? '0.58' : '1';
bindConfigureInlineControlHover(count, controlsLocked);
bindConfigureCountPillTooltip(count, controlsLocked);
count.addEventListener('click', onClick);
return count;
}
function buildConfigureVoiceButton({ id, controlsLocked, onClick }) {
const voiceBtn = el('button', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
boxSizing: 'border-box',
width: CONFIGURE_BAR_H, height: '100%', flexShrink: '0',
padding: '0', margin: '0',
border: 'none', borderRight: '1px solid ' + BP.hairline,
borderRadius: '0', background: 'transparent',
color: BP.textDim, cursor: 'pointer',
transition: 'color 0.12s ease, background 0.12s ease',
});
voiceBtn.id = id;
voiceBtn.type = 'button';
voiceBtn.setAttribute('aria-label', 'Voice input');
voiceBtn.innerHTML = ICON_PAGE_VOICE;
voiceBtn.disabled = controlsLocked;
voiceBtn.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
voiceBtn.style.opacity = controlsLocked ? '0.58' : '1';
voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
voiceBtn.addEventListener('click', onClick);
return voiceBtn;
}
function buildConfigureTrailingCluster(controls, voiceBtn, submitBtn) {
const cluster = el('div', {
display: 'inline-flex', alignItems: 'stretch', flexShrink: '0',
height: '100%', borderLeft: '1px solid ' + BP.hairline,
});
if (controls.length) {
const controlsWrap = el('div', {
display: 'inline-flex', alignItems: 'center', gap: '8px',
padding: '0 10px', flexShrink: '0', height: '100%',
});
controls.forEach((control) => controlsWrap.appendChild(control));
cluster.appendChild(controlsWrap);
}
voiceBtn.style.borderLeft = '1px solid ' + BP.hairline;
cluster.appendChild(voiceBtn);
cluster.appendChild(submitBtn);
return cluster;
}
function buildConfigureSubmitButton({ controlsLocked, onClick, ariaLabel }) {
const btn = el('button', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
boxSizing: 'border-box', width: CONFIGURE_BAR_H, height: CONFIGURE_BAR_H,
padding: '0', flexShrink: '0',
border: 'none', borderLeft: '1px solid ' + BP.hairline,
borderRadius: '0',
background: BP.accent, color: C.ink,
cursor: controlsLocked ? 'not-allowed' : 'pointer',
transition: 'filter 0.12s ease, transform 0.1s ease',
});
btn.type = 'button';
btn.setAttribute('aria-label', ariaLabel);
btn.innerHTML = ICON_CONFIGURE_SUBMIT;
btn.disabled = controlsLocked;
btn.style.opacity = controlsLocked ? '0.58' : '1';
if (controlsLocked) btn.title = 'Apply is still running';
btn.addEventListener('mouseenter', () => { if (!controlsLocked) btn.style.filter = 'brightness(1.1)'; });
btn.addEventListener('mouseleave', () => btn.style.filter = 'none');
btn.addEventListener('mousedown', () => { if (!controlsLocked) btn.style.transform = 'scale(0.97)'; });
btn.addEventListener('mouseup', () => btn.style.transform = 'scale(1)');
btn.addEventListener('click', onClick);
return btn;
}
// Insert mode helpers (mirrors skill/scripts/live/insert-ui.mjs)
function detectInsertAxisFromStyle(style) {
const display = style?.display || 'block';
if (display.includes('flex')) {
const dir = style.flexDirection || 'row';
return dir.startsWith('row') ? 'row' : 'column';
}
if (display === 'grid' || display === 'inline-grid') {
const flow = style.gridAutoFlow || 'row';
if (flow.includes('column')) return 'column';
const cols = (style.gridTemplateColumns || '').trim();
if (cols && cols !== 'none') {
const colCount = cols.split(/\s+/).filter(Boolean).length;
if (colCount > 1) return 'row';
}
return 'row';
}
return 'column';
}
function detectInsertAxis(parent) {
if (!parent || parent.nodeType !== 1) return 'column';
const st = getComputedStyle(parent);
return detectInsertAxisFromStyle({
display: st.display,
flexDirection: st.flexDirection,
gridTemplateColumns: st.gridTemplateColumns,
gridAutoFlow: st.gridAutoFlow,
});
}
function layoutFlowChildren(parent) {
if (!parent) return [];
return [...parent.children]
.filter(pickable)
.map((el) => ({ el, rect: el.getBoundingClientRect() }));
}
function computeInsertPosition(clientX, clientY, rect, axis) {
axis = axis || 'column';
if (!rect) return 'after';
if (axis === 'row') {
if (!Number.isFinite(rect.width) || rect.width <= 0) return 'after';
return clientX < rect.left + rect.width / 2 ? 'before' : 'after';
}
if (!Number.isFinite(rect.height) || rect.height <= 0) return 'after';
return clientY < rect.top + rect.height / 2 ? 'before' : 'after';
}
function groupSiblingRows(siblings, rowThreshold) {
rowThreshold = rowThreshold ?? 8;
const sorted = [...siblings].sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left);
const rows = [];
for (const entry of sorted) {
let placed = false;
for (const row of rows) {
if (Math.abs(entry.rect.top - row[0].rect.top) <= rowThreshold) {
row.push(entry);
placed = true;
break;
}
}
if (!placed) rows.push([entry]);
}
return rows;
}
function horizontalOverlap(a, b) {
const left = Math.max(a.left, b.left);
const right = Math.min(a.right, b.right);
return Math.max(0, right - left);
}
function hitSiblingInsertGap(clientX, clientY, siblings, opts) {
opts = opts || {};
if (!siblings || siblings.length < 2) return null;
const slop = opts.slop ?? 12;
const minOverlap = opts.minOverlap ?? 0.25;
for (const row of groupSiblingRows(siblings)) {
if (row.length < 2) continue;
const sorted = [...row].sort((a, b) => a.rect.left - b.rect.left);
for (let i = 0; i < sorted.length - 1; i++) {
const a = sorted[i];
const b = sorted[i + 1];
const aRight = a.rect.right;
const bLeft = b.rect.left;
if (bLeft <= aRight) continue;
const top = Math.max(a.rect.top, b.rect.top);
const bottom = Math.min(a.rect.bottom, b.rect.bottom);
const span = bottom - top;
const minH = Math.min(a.rect.height, b.rect.height);
if (span < minH * minOverlap) continue;
const inX = clientX >= aRight - slop && clientX <= bLeft + slop;
const inY = clientY >= top - slop && clientY <= bottom + slop;
if (!inX || !inY) continue;
return {
anchor: b.el,
position: 'before',
axis: 'row',
line: { axis: 'row', left: (aRight + bLeft) / 2, top, width: 0, height: span },
};
}
}
const sortedCol = [...siblings].sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left);
for (let i = 0; i < sortedCol.length - 1; i++) {
const a = sortedCol[i];
const b = sortedCol[i + 1];
const overlap = horizontalOverlap(a.rect, b.rect);
const minW = Math.min(a.rect.width, b.rect.width);
if (overlap < minW * minOverlap) continue;
const gapTop = a.rect.bottom;
const gapBottom = b.rect.top;
if (gapBottom <= gapTop) continue;
const overlapLeft = Math.max(a.rect.left, b.rect.left);
const overlapRight = Math.min(a.rect.right, b.rect.right);
const inY = clientY >= gapTop - slop && clientY <= gapBottom + slop;
const inX = clientX >= overlapLeft - slop && clientX <= overlapRight + slop;
if (!inY || !inX) continue;
return {
anchor: b.el,
position: 'before',
axis: 'column',
line: { axis: 'column', top: (gapTop + gapBottom) / 2, left: overlapLeft, width: overlap, height: 0 },
};
}
return null;
}
function insertLineCoords(rect, position, axis) {
axis = axis || 'column';
if (axis === 'row') {
const x = position === 'before' ? rect.left - 2 : rect.right + 2;
return { axis: 'row', top: rect.top, left: x, width: 0, height: rect.height };
}
const y = position === 'before' ? rect.top - 2 : rect.bottom + 2;
return { axis: 'column', top: y, left: rect.left, width: rect.width, height: 0 };
}
function resolveInsertHover({ clientX, clientY, target, rect, axis, siblings }) {
const gap = hitSiblingInsertGap(clientX, clientY, siblings);
if (gap) return gap;
const position = computeInsertPosition(clientX, clientY, rect, axis);
const line = insertLineCoords(rect, position, axis);
return { anchor: target, position, axis, line };
}
function cursorForInsertAxis(axis) {
return axis === 'row' ? 'ew-resize' : 'ns-resize';
}
function placeholderSizing({ axis, parentDisplay, parentWidth, anchorFlex }) {
const display = parentDisplay || 'block';
const w = Number.isFinite(parentWidth) ? parentWidth : 0;
if (axis === 'row') {
if (display.includes('flex')) {
const flex = anchorFlex && anchorFlex !== 'none' && anchorFlex !== '0 1 auto'
? anchorFlex
: '1 1 0';
return { kind: 'flex', flex, minWidth: 0 };
}
if (display === 'grid' || display === 'inline-grid') return { kind: 'auto' };
}
if (w >= PLACEHOLDER_MIN_WIDTH) return { kind: 'percent' };
return {
kind: 'explicit',
width: Math.max(PLACEHOLDER_MIN_WIDTH, w || PLACEHOLDER_MIN_WIDTH),
};
}
function placeholderWidthIsImplicit(kind) {
return kind === 'flex' || kind === 'percent' || kind === 'auto';
}
function applyPlaceholderSizingStyles(placeholder, sizing) {
placeholder.dataset.impeccablePlaceholderWidth = sizing.kind;
placeholder.style.flex = '';
placeholder.style.minWidth = '';
placeholder.style.maxWidth = '';
placeholder.style.width = '';
if (sizing.kind === 'flex') {
placeholder.style.flex = sizing.flex;
placeholder.style.minWidth = sizing.minWidth + 'px';
} else if (sizing.kind === 'percent') {
placeholder.style.width = '100%';
placeholder.style.maxWidth = '100%';
} else if (sizing.kind === 'explicit') {
placeholder.style.width = sizing.width + 'px';
}
}
function materializePlaceholderWidth(placeholder) {
if (!placeholder) return;
const kind = placeholder.dataset.impeccablePlaceholderWidth;
if (!placeholderWidthIsImplicit(kind)) return;
const w = Math.max(PLACEHOLDER_MIN_WIDTH, Math.round(placeholder.offsetWidth));
placeholder.style.flex = '';
placeholder.style.minWidth = '';
placeholder.style.maxWidth = '';
placeholder.style.width = w + 'px';
placeholder.dataset.impeccablePlaceholderWidth = 'explicit';
}
function canCreateInsert({ prompt, comments, strokes }) {
const hasPrompt = typeof prompt === 'string' && prompt.trim().length > 0;
const hasComments = Array.isArray(comments) && comments.length > 0;
const hasStrokes = Array.isArray(strokes) && strokes.some(
(s) => Array.isArray(s?.points) && s.points.length >= 2,
);
return hasPrompt || hasComments || hasStrokes;
}
function insertCreateDisabledReason({ prompt, comments, strokes }) {
if (canCreateInsert({ prompt, comments, strokes })) return null;
return 'Add a prompt or annotate the placeholder to create';
}
function clampPlaceholderSize(width, height, parentWidth) {
const maxW = Math.max(PLACEHOLDER_MIN_WIDTH, parentWidth || PLACEHOLDER_MIN_WIDTH);
return {
width: Math.min(maxW, Math.max(PLACEHOLDER_MIN_WIDTH, Math.round(width))),
height: Math.max(PLACEHOLDER_MIN_HEIGHT, Math.round(height)),
};
}
function cursorForPlaceholderEdge(edge) {
if (edge === 'n' || edge === 's') return 'ns-resize';
if (edge === 'e' || edge === 'w') return 'ew-resize';
return 'default';
}
function resizePlaceholderFromEdge(start, edge, dx, dy, parentWidth) {
const base = {
width: start.width,
height: start.height,
marginLeft: start.marginLeft ?? 0,
marginTop: start.marginTop ?? 0,
};
if (edge === 'e') base.width = start.width + dx;
else if (edge === 'w') {
base.width = start.width - dx;
base.marginLeft = start.marginLeft + dx;
} else if (edge === 's') base.height = start.height + dy;
else if (edge === 'n') {
base.height = start.height - dy;
base.marginTop = start.marginTop + dy;
}
const clamped = clampPlaceholderSize(base.width, base.height, parentWidth);
if (edge === 'w') base.marginLeft = start.marginLeft + start.width - clamped.width;
else if (edge === 'n') base.marginTop = start.marginTop + start.height - clamped.height;
return {
width: clamped.width,
height: clamped.height,
marginLeft: Math.round(base.marginLeft),
marginTop: Math.round(base.marginTop),
};
}
function ensureInsertLine() {
if (insertLineEl) return insertLineEl;
insertLineEl = document.createElement('div');
insertLineEl.id = PREFIX + '-insert-line';
Object.assign(insertLineEl.style, {
position: 'fixed',
zIndex: String(Z.highlight),
height: '0',
borderTop: '2px dotted ' + C.brand,
pointerEvents: 'none',
display: 'none',
opacity: '0.9',
});
uiAppend(insertLineEl);
defangOutsideHandlers(insertLineEl);
return insertLineEl;
}
function showInsertLine(resolved) {
if (!resolved?.anchor || !resolved.line) return;
const line = ensureInsertLine();
const coords = resolved.line;
if (coords.axis === 'row') {
Object.assign(line.style, {
display: 'block',
top: coords.top + 'px',
left: coords.left + 'px',
width: '0',
height: coords.height + 'px',
borderTop: 'none',
borderLeft: '2px dotted ' + C.brand,
});
} else {
Object.assign(line.style, {
display: 'block',
top: coords.top + 'px',
left: coords.left + 'px',
width: coords.width + 'px',
height: '0',
borderLeft: 'none',
borderTop: '2px dotted ' + C.brand,
});
}
insertHoverAnchor = resolved.anchor;
insertHoverPosition = resolved.position;
insertHoverAxis = resolved.axis || 'column';
}
function hideInsertLine() {
if (!insertLineEl) return;
insertLineEl.style.display = 'none';
insertHoverAnchor = null;
insertHoverPosition = null;
insertHoverAxis = null;
syncPageInteractionCursor();
}
/**
* Drive the page-level pick / insert cursor through the textContent of one
* injected <style>, never by mutating <html> (className or inline style).
* Frameworks that server-render the <html>/<body> roots (Next.js App Router)
* report a React 19 hydration mismatch when the client adds an attribute the
* server HTML never emitted, so a `class`/inline `style` toggled on
* `document.documentElement` trips "a tree hydrated but some attributes ...
* didn't match" on the next Fast-Refresh re-render. Keying the cursor off a
* stable-id <style> keeps the effect off the hydrated host elements (same
* shape as the scroll-anchor lock). A falsy cursor clears the rule.
*/
function setPageInteractionCursor(cursor) {
let style = document.getElementById(PICK_CURSOR_STYLE_ID);
if (!cursor) {
if (style) style.textContent = '';
return;
}
if (!style) {
style = document.createElement('style');
style.id = PICK_CURSOR_STYLE_ID;
// Styles the host page, not the chrome - inside the adapter's shadow UI
// root (uiAppendStyle's target) these selectors would match nothing.
(document.head || document.documentElement).appendChild(style);
}
style.textContent =
'* { cursor: ' + cursor + ' !important; }\n'
+ '[id^="' + PREFIX + '"],\n'
+ '[id^="' + PREFIX + '"] * { cursor: revert !important; }';
}
/** Page-level cursor while pick or insert mode is targeting page elements. */
function syncPageInteractionCursor() {
let cursor = '';
if (state === 'PICKING' && pickActive && !insertActive) {
cursor = 'crosshair';
} else if (state === 'PICKING' && insertActive && insertHoverAnchor) {
cursor = cursorForInsertAxis(insertHoverAxis || 'column');
}
setPageInteractionCursor(cursor);
}
/**
* Single entry point for interaction-state transitions. The pick-mode
* crosshair is derived from `state`, so a bare `state = ...` assignment
* leaves the page cursor out of sync with the mode it advertises.
*/
function setLiveState(next) {
state = next;
window.__IMPECCABLE_LIVE_STATE__ = next;
syncPageInteractionCursor();
// Whether a queued steer is still behind a generation is a function of this
// state, so the hint has to move with it, not only with the 5s poll.
syncSteerQueueHint();
}
/** Element used to position the floating bar / shader during a session. */
function resolveBarAnchor() {
if (svelteComponentSession?.sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
const anchor = resolveSvelteComponentAnchor();
if (anchor) return anchor;
}
if (currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (wrapper) {
const variantCount = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])').length;
if (variantCount > 0 && visibleVariant > 0) {
const visEl = pickVariantContent(wrapper, visibleVariant);
if (visEl) return visEl;
}
if (state === 'GENERATING') {
const ph = ensureInsertPlaceholder();
if (ph) return ph;
if (insertAnchorElement && document.body.contains(insertAnchorElement)) return insertAnchorElement;
}
}
}
if (selectedElement && document.body.contains(selectedElement)) return selectedElement;
if (placeholderElement && document.body.contains(placeholderElement)) return placeholderElement;
if (insertAnchorElement && document.body.contains(insertAnchorElement)) return insertAnchorElement;
return null;
}
function removeInsertPlaceholderDom() {
if (placeholderElement) {
placeholderElement.remove();
placeholderElement = null;
}
placeholderResizeDrag = null;
syncPlaceholderResizeHandles();
}
function finalizeInsertSession() {
removeInsertPlaceholderDom();
insertAnchorElement = null;
insertAnchorPosition = null;
insertAnchorLayoutAxis = null;
insertPlaceholderSnapshot = null;
if (configureKind === 'insert') configureKind = 'replace';
}
function buildInsertPlaceholderSnapshotFromDom(anchor, placeholder) {
return {
width: Math.round(placeholder.offsetWidth || 0),
height: Math.round(placeholder.offsetHeight || PLACEHOLDER_DEFAULT_HEIGHT),
marginLeft: parseFloat(placeholder.style.marginLeft) || 0,
marginTop: parseFloat(placeholder.style.marginTop) || 0,
position: insertAnchorPosition || 'before',
layoutAxis: insertAnchorLayoutAxis || 'column',
anchorTag: anchor.tagName || 'DIV',
anchorClasses: anchor.className || '',
anchorText: (anchor.textContent || '').trim().slice(0, 120),
};
}
function findInsertAnchorInDom() {
if (insertAnchorElement && document.body.contains(insertAnchorElement)) return insertAnchorElement;
const snap = insertPlaceholderSnapshot;
if (!snap) return null;
const tag = (snap.anchorTag || 'div').toLowerCase();
const cls = (snap.anchorClasses || '').split(/\s+/).filter(Boolean)[0];
const needle = snap.anchorText || '';
const sel = cls ? tag + '.' + cls : tag;
const candidates = document.querySelectorAll(sel);
for (const candidate of candidates) {
if (own(candidate)) continue;
if (needle && !(candidate.textContent || '').includes(needle.slice(0, 40))) continue;
return candidate;
}
return null;
}
function isInsertGeneratingSession() {
if (state !== 'GENERATING' || !currentSessionId) return false;
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
return !!wrapper && wrapper.dataset.impeccableMode === 'insert';
}
/** Recreate the dotted placeholder if Astro/Vite HMR removed it mid-generation. */
function ensureInsertPlaceholder() {
if (!isInsertGeneratingSession()) return placeholderElement;
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
const variantCount = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])').length;
if (variantCount > 0) return placeholderElement;
if (placeholderElement && document.body.contains(placeholderElement)) return placeholderElement;
const anchor = findInsertAnchorInDom();
if (!anchor) return null;
insertAnchorElement = anchor;
const position = insertPlaceholderSnapshot?.position || insertAnchorPosition || 'before';
const axis = insertPlaceholderSnapshot?.layoutAxis || insertAnchorLayoutAxis;
const ph = createInsertPlaceholder(anchor, position, axis);
if (!ph) return null;
if (insertPlaceholderSnapshot) {
applyPlaceholderDimensions({
width: insertPlaceholderSnapshot.width,
height: insertPlaceholderSnapshot.height,
marginLeft: insertPlaceholderSnapshot.marginLeft,
marginTop: insertPlaceholderSnapshot.marginTop,
});
}
selectedElement = ph;
return ph;
}
function applyPlaceholderDimensions({ width, height, marginLeft, marginTop }) {
const ph = placeholderElement;
if (!ph) return;
materializePlaceholderWidth(ph);
ph.style.width = width + 'px';
ph.style.height = height + 'px';
ph.style.marginLeft = marginLeft ? marginLeft + 'px' : '';
ph.style.marginTop = marginTop ? marginTop + 'px' : '';
positionAnnotOverlay(ph);
positionBar();
}
function showOrUpdateCyclingBar() {
if (barEl && barEl.style.display !== 'none') updateBarContent('cycling');
else showBar('cycling');
}
function buildPlaceholderResizeHandles() {
if (!placeholderResizeLayerEl) return;
placeholderResizeLayerEl.innerHTML = '';
const hit = 10;
const half = hit / 2;
const specs = [
{ edge: 'n', top: -half, left: 0, right: 0, height: hit },
{ edge: 's', bottom: -half, left: 0, right: 0, height: hit },
{ edge: 'e', top: 0, bottom: 0, right: -half, width: hit },
{ edge: 'w', top: 0, bottom: 0, left: -half, width: hit },
];
for (const spec of specs) {
const handle = el('div', {
position: 'absolute',
pointerEvents: 'auto',
cursor: cursorForPlaceholderEdge(spec.edge),
});
if (spec.top != null) handle.style.top = spec.top + 'px';
if (spec.bottom != null) handle.style.bottom = spec.bottom + 'px';
if (spec.left != null) handle.style.left = spec.left + 'px';
if (spec.right != null) handle.style.right = spec.right + 'px';
if (spec.width != null) handle.style.width = spec.width + 'px';
if (spec.height != null) handle.style.height = spec.height + 'px';
handle.dataset.impeccablePlaceholderResize = spec.edge;
handle.setAttribute('aria-label', 'Resize placeholder');
handle.title = 'Drag to resize';
placeholderResizeLayerEl.appendChild(handle);
}
}
function syncPlaceholderResizeHandles() {
if (!placeholderResizeLayerEl) return;
const show = configureKind === 'insert' && annotActive && !!placeholderElement && state === 'CONFIGURING';
placeholderResizeLayerEl.style.display = show ? 'block' : 'none';
if (!show) {
placeholderResizeLayerEl.innerHTML = '';
return;
}
if (!placeholderResizeLayerEl.childElementCount) buildPlaceholderResizeHandles();
}
function startPlaceholderEdgeResize(edge, e) {
const ph = placeholderElement;
if (!ph || configureKind !== 'insert') return;
materializePlaceholderWidth(ph);
placeholderResizeDrag = {
edge,
startX: e.clientX,
startY: e.clientY,
start: {
width: ph.offsetWidth,
height: ph.offsetHeight,
marginLeft: parseFloat(ph.style.marginLeft) || 0,
marginTop: parseFloat(ph.style.marginTop) || 0,
},
parentWidth: ph.parentNode?.getBoundingClientRect().width || PLACEHOLDER_MIN_WIDTH,
pointerId: e.pointerId,
};
try { annotOverlayEl.setPointerCapture(e.pointerId); } catch {}
e.stopPropagation();
e.preventDefault();
}
function createInsertPlaceholder(anchor, position, layoutAxis) {
removeInsertPlaceholderDom();
const parent = anchor.parentNode;
if (!parent) return null;
const axis = layoutAxis || detectInsertAxis(parent);
const pst = getComputedStyle(parent);
const ast = getComputedStyle(anchor);
const sizing = placeholderSizing({
axis,
parentDisplay: pst.display,
parentWidth: parent.getBoundingClientRect().width,
anchorFlex: ast.flex,
});
const placeholder = document.createElement('div');
placeholder.id = PREFIX + '-insert-placeholder';
placeholder.setAttribute('data-impeccable-insert-placeholder', 'true');
placeholder.setAttribute('aria-hidden', 'true');
Object.assign(placeholder.style, {
boxSizing: 'border-box',
height: PLACEHOLDER_DEFAULT_HEIGHT + 'px',
minHeight: PLACEHOLDER_MIN_HEIGHT + 'px',
border: '2px dotted ' + BP.accent,
borderRadius: '0',
background: 'transparent',
opacity: '1',
position: 'relative',
marginLeft: '',
marginTop: '',
});
applyPlaceholderSizingStyles(placeholder, sizing);
if (position === 'before') parent.insertBefore(placeholder, anchor);
else parent.insertBefore(placeholder, anchor.nextSibling);
placeholderElement = placeholder;
insertAnchorElement = anchor;
insertAnchorPosition = position;
insertAnchorLayoutAxis = axis;
return placeholder;
}
function clearInsertPicking() {
hideInsertLine();
finalizeInsertSession();
}
function isInsertCreateEnabled(btn) {
btn = btn || uiGetById(PREFIX + '-insert-create');
return !!btn && btn.getAttribute('aria-disabled') !== 'true';
}
let insertCreateTooltipEl = null;
function ensureInsertCreateTooltip() {
if (insertCreateTooltipEl) return insertCreateTooltipEl;
insertCreateTooltipEl = el('div', {
position: 'fixed',
display: 'none',
zIndex: String(Z.bar + 7),
pointerEvents: 'none',
maxWidth: '240px',
padding: '6px 9px',
borderRadius: '7px',
background: BP.chatSurface,
border: '1px solid ' + BP.hairline,
boxShadow: BP.shadow,
color: BP.text,
fontFamily: FONT,
fontSize: '11px',
fontWeight: '500',
lineHeight: '1.35',
});
insertCreateTooltipEl.id = PREFIX + '-insert-create-tooltip';
uiAppend(insertCreateTooltipEl);
return insertCreateTooltipEl;
}
function showInsertCreateTooltip(anchor, message) {
if (!anchor || !message) return;
const tip = ensureInsertCreateTooltip();
tip.textContent = message;
tip.style.display = 'block';
const r = anchor.getBoundingClientRect();
const tipW = tip.offsetWidth;
const tipH = tip.offsetHeight;
const left = Math.max(8, Math.min(window.innerWidth - tipW - 8, r.left + r.width / 2 - tipW / 2));
const top = Math.max(8, r.top - tipH - 8);
tip.style.left = left + 'px';
tip.style.top = top + 'px';
}
function hideInsertCreateTooltip() {
if (!insertCreateTooltipEl) return;
insertCreateTooltipEl.style.display = 'none';
}
function insertCreateGateState(input) {
return {
prompt: input?.value ?? '',
comments: annotState.comments,
strokes: annotState.strokes,
};
}
function syncInsertCreateButton(btn, input) {
btn = btn || uiGetById(PREFIX + '-insert-create');
input = input || uiGetById(PREFIX + '-insert-input');
if (!btn || !input) return;
const gate = insertCreateGateState(input);
const ok = canCreateInsert(gate);
const reason = ok ? 'Create variants' : insertCreateDisabledReason(gate);
btn.setAttribute('aria-disabled', ok ? 'false' : 'true');
btn.setAttribute('aria-label', reason);
if (ok) {
hideInsertCreateTooltip();
btn.style.background = BP.accent;
btn.style.color = C.ink;
btn.style.border = 'none';
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
} else {
btn.style.background = 'transparent';
btn.style.color = BP.textDim;
btn.style.border = '1px solid ' + BP.hairline;
btn.style.opacity = '0.72';
btn.style.cursor = 'not-allowed';
}
}
/** Stylesheet shared by the replace and insert configure rows. */
function ensureConfigureInputStyle() {
if (uiGetById(PREFIX + '-configure-input-style')) return;
const s = document.createElement('style');
s.id = PREFIX + '-configure-input-style';
s.textContent =
'@keyframes impeccable-configure-voice-pulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }' +
'#' + PREFIX + '-input, #' + PREFIX + '-insert-input { box-sizing: border-box; height: ' + CONFIGURE_ROW_TRACK_H + '; line-height: ' + CONFIGURE_ROW_TRACK_H + '; padding: 0; margin: 0; caret-color: ' + CONFIGURE_PILL_TEXT + '; }' +
'#' + PREFIX + '-input::placeholder, #' + PREFIX + '-insert-input::placeholder { color: ' + BP.textDim + '; opacity: 1; }' +
'#' + PREFIX + '-configure-voice[data-listening="true"] svg, #' + PREFIX + '-insert-voice[data-listening="true"] svg { animation: impeccable-configure-voice-pulse 1.1s ease-in-out infinite; }' +
'@media (prefers-reduced-motion: reduce) { #' + PREFIX + '-configure-voice[data-listening="true"] svg, #' + PREFIX + '-insert-voice[data-listening="true"] svg { animation: none; opacity: 1; } }' +
'#' + PREFIX + '-configure-voice:hover, #' + PREFIX + '-insert-voice:hover { background: oklch(27% 0 0); color: ' + BP.accent + '; }';
uiAppendStyle(s);
}
function buildConfigureRow() {
const controlsLocked = pendingApplyInFlight === true;
const row = el('div', {
display: 'flex', alignItems: 'stretch', width: '100%', height: CONFIGURE_BAR_H,
});
const inputShell = el('div', configureInputShellStyle());
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
input.placeholder = '';
input.setAttribute('aria-label', 'Describe the change');
Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
input.style.cursor = 'not-allowed';
input.style.opacity = '0.58';
}
const action = buildConfigureActionControl({
controlsLocked,
onClick: (e) => {
e.stopPropagation();
if (controlsLocked) { showManualApplyBusyToast(); return; }
toggleActionPicker();
},
});
const count = buildConfigureCountControl({
controlsLocked,
onClick: (e) => {
e.stopPropagation();
if (controlsLocked) { showManualApplyBusyToast(); return; }
count.textContent = '\u00D7' + cycleSelectedCount();
if (count.matches(':hover')) {
showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));
}
},
});
inputShell.appendChild(buildSelectionPill({ el: selectedElement, controlsLocked }));
inputShell.appendChild(input);
ensureConfigureInputStyle();
input.addEventListener('focus', () => syncConfigureInputChrome());
input.addEventListener('blur', () => syncConfigureInputChrome());
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.stopPropagation(); e.preventDefault(); handleGo(); return; }
if (e.key === 'Escape') {
e.stopPropagation();
e.preventDefault();
input.blur();
exitConfigureToPicking('configure-input-escape');
return;
}
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !input.value) return;
e.stopPropagation();
});
const voiceBtn = buildConfigureVoiceButton({
id: PREFIX + '-configure-voice',
controlsLocked,
onClick: (e) => {
e.stopPropagation();
if (controlsLocked) { showManualApplyBusyToast(); return; }
toggleConfigureVoice();
},
});
const go = buildConfigureSubmitButton({
controlsLocked,
ariaLabel: 'Generate variants',
onClick: (e) => { e.stopPropagation(); handleGo(); },
});
row.appendChild(inputShell);
row.appendChild(buildConfigureTrailingCluster([action, count], voiceBtn, go));
syncConfigureInputChrome();
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
}
function buildInsertConfigureRow() {
const controlsLocked = pendingApplyInFlight === true;
const row = el('div', {
display: 'flex', alignItems: 'stretch', width: '100%', height: CONFIGURE_BAR_H,
});
row.addEventListener('pointerdown', (e) => e.stopPropagation());
row.addEventListener('mousedown', (e) => e.stopPropagation());
row.addEventListener('click', (e) => e.stopPropagation());
const inputShell = el('div', configureInputShellStyle());
const input = document.createElement('input');
input.id = PREFIX + '-insert-input';
input.type = 'text';
input.placeholder = '';
input.setAttribute('aria-label', 'Describe the new element');
Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
input.style.cursor = 'not-allowed';
input.style.opacity = '0.58';
}
const count = buildConfigureCountControl({
controlsLocked,
onClick: (e) => {
e.stopPropagation();
if (controlsLocked) { showManualApplyBusyToast(); return; }
count.textContent = '\u00D7' + cycleSelectedCount();
if (count.matches(':hover')) {
showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));
}
},
});
inputShell.appendChild(buildSelectionPill({ el: selectedElement, controlsLocked }));
inputShell.appendChild(input);
ensureConfigureInputStyle();
input.addEventListener('input', () => syncInsertCreateButton());
input.addEventListener('pointerdown', (e) => e.stopPropagation());
input.addEventListener('mousedown', (e) => e.stopPropagation());
input.addEventListener('click', (e) => {
e.stopPropagation();
try { input.focus({ preventScroll: true }); } catch { input.focus(); }
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.stopPropagation(); e.preventDefault();
if (isInsertCreateEnabled()) handleInsertCreate();
return;
}
if (e.key === 'Escape') {
e.stopPropagation(); e.preventDefault();
cancelInsertConfigure();
return;
}
e.stopPropagation();
});
input.addEventListener('focus', () => syncConfigureInputChrome());
input.addEventListener('blur', () => syncConfigureInputChrome());
const voiceBtn = buildConfigureVoiceButton({
id: PREFIX + '-insert-voice',
controlsLocked,
onClick: (e) => {
e.stopPropagation();
if (controlsLocked) { showManualApplyBusyToast(); return; }
toggleConfigureVoice();
},
});
const create = buildConfigureSubmitButton({
controlsLocked,
ariaLabel: 'Create variants',
onClick: (e) => {
e.preventDefault();
e.stopPropagation();
if (controlsLocked) { showManualApplyBusyToast(); return; }
if (!isInsertCreateEnabled(create)) return;
handleInsertCreate();
},
});
create.id = PREFIX + '-insert-create';
create.addEventListener('mouseenter', () => {
if (controlsLocked) return;
if (isInsertCreateEnabled(create)) {
hideInsertCreateTooltip();
return;
}
showInsertCreateTooltip(create, insertCreateDisabledReason(insertCreateGateState(input)));
});
create.addEventListener('mouseleave', hideInsertCreateTooltip);
row.appendChild(inputShell);
row.appendChild(buildConfigureTrailingCluster([count], voiceBtn, create));
syncInsertCreateButton(create, input);
syncConfigureInputChrome();
if (!controlsLocked) 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: BP.text,
flexShrink: '0', whiteSpace: 'nowrap',
});
label.textContent = configureKind === 'insert' ? 'Insert' : actionLabel();
row.appendChild(label);
// Dots
row.appendChild(buildDots(false));
// Status
const status = el('span', {
fontSize: '11px', color: BP.textDim, whiteSpace: 'nowrap',
marginLeft: 'auto',
});
status.textContent = recoveryWaitingForAnchor
? 'Variants ready. Reveal the selected element to resume.'
: generationStatusText();
row.appendChild(status);
return row;
}
function generationStatusText() {
if (arrivedVariants >= expectedVariants && expectedVariants > 0) return 'Done';
if (generationPhase === 'picked_up') return 'Agent picked up the request...';
if (generationPhase === 'scaffolding') return 'Finding the source...';
if (generationPhase === 'source_ready') return 'Source ready. Generating...';
if (generationPhase === 'scaffold_fallback') return 'Agent is locating the source...';
if (generationPhase === 'first_reviewable') return 'First variant is ready. Exploring more...';
if (generationPhase === 'second_reviewable') return 'Checking the remaining variants...';
return 'Generating ' + expectedVariants + ' variants...';
}
// Cycling row
const TUNE_ICON_SVG = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" style="flex-shrink:0"><line x1="4" y1="8" x2="20" y2="8"/><circle cx="14" cy="8" r="2.4" fill="currentColor" stroke="none"/><line x1="4" y1="16" x2="20" y2="16"/><circle cx="10" cy="16" r="2.4" fill="currentColor" stroke="none"/></svg>';
/**
* Which variant the user is actually looking at. For component previews the
* mounted component is the truth; `visibleVariant` is the intent, and the two
* differ while a mount is in flight.
*/
function cyclingShownVariant() {
return svelteComponentSession?.sessionId === currentSessionId && svelteComponentSession.mountedVariant > 0
? svelteComponentSession.mountedVariant
: visibleVariant;
}
/**
* The single counter string. It is built here rather than at each call site
* because the row builder and the incremental sync used to disagree on the
* denominator: one showed the planned count, the other the arrived count, so
* "2/3" turned into "2/2" on the next sync without anything changing on
* screen. Arrived wins once anything has arrived; expected covers the window
* before the first variant lands.
*/
function cyclingCounterText() {
const total = arrivedVariants > 0 ? arrivedVariants : expectedVariants;
return cyclingShownVariant() + '/' + total;
}
function buildCyclingRow() {
if (!ensureCyclingRenderable('build-cycling-row')) {
return el('div', { display: 'none' });
}
const row = el('div', {
display: 'flex', alignItems: 'center', gap: '6px',
padding: '1px 2px',
});
// Prev
const prev = navBtn('\u2190');
prev.id = PREFIX + '-variant-prev';
prev.addEventListener('click', (e) => { e.stopPropagation(); cycleVariant(-1); });
if (cyclingShownVariant() <= 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: BP.textDim, minWidth: '24px', textAlign: 'center',
});
counter.id = PREFIX + '-variant-counter';
counter.textContent = cyclingCounterText();
row.appendChild(counter);
// Next
const next = navBtn('\u2192');
next.id = PREFIX + '-variant-next';
next.addEventListener('click', (e) => { e.stopPropagation(); cycleVariant(1); });
if (cyclingShownVariant() >= arrivedVariants) next.style.opacity = '0.3';
row.appendChild(next);
// Tune chip stays visible while the deferred parameter phase is running,
// then becomes interactive as soon as this variant exposes controls.
const visParams = parseVariantParams(getVisibleVariantEl());
const hasParams = visParams.length > 0;
const paramsPending = !hasParams && (parameterGenerationState === 'pending' || parameterGenerationState === 'loading');
if (hasParams || paramsPending) {
const tune = el('button', {
display: 'inline-flex', alignItems: 'center', gap: '6px',
padding: '4px 10px', borderRadius: '5px',
border: '1px solid transparent',
background: tuneOpen ? BP.accentSoft : 'transparent',
color: tuneOpen ? BP.accent : BP.text,
fontFamily: FONT, fontSize: '11px', fontWeight: '500',
cursor: paramsPending ? 'wait' : 'pointer',
transition: 'color 0.12s ease, background 0.12s ease',
whiteSpace: 'nowrap',
});
if (paramsPending) {
const spinner = el('span', {
width: '11px', height: '11px', borderRadius: '50%',
border: '1.5px solid ' + BP.hairline,
borderTopColor: BP.accent,
animation: 'impeccable-spin 0.6s linear infinite',
boxSizing: 'border-box', flexShrink: '0',
});
spinner.setAttribute('aria-hidden', 'true');
tune.appendChild(spinner);
} else {
tune.innerHTML = TUNE_ICON_SVG;
}
const tuneLabel = document.createElement('span');
tuneLabel.textContent = 'Tune';
tune.appendChild(tuneLabel);
if (hasParams) {
const tuneBadge = document.createElement('span');
Object.assign(tuneBadge.style, {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
minWidth: '16px', height: '16px', padding: '0 4px',
borderRadius: '999px',
background: tuneOpen ? C.brand : BP.hairline,
color: tuneOpen ? C.ink : 'inherit',
fontFamily: MONO, fontSize: '9.5px', fontWeight: '600',
lineHeight: '1',
boxSizing: 'border-box',
});
tuneBadge.textContent = String(visParams.length);
tune.appendChild(tuneBadge);
tune.title = 'Tune this variant (' + visParams.length + ' knob' + (visParams.length === 1 ? '' : 's') + ')';
tune.addEventListener('mouseenter', () => {
if (!tuneOpen) tune.style.background = BP.accentSoft;
});
tune.addEventListener('mouseleave', () => {
if (!tuneOpen) tune.style.background = 'transparent';
});
tune.addEventListener('click', (e) => { e.stopPropagation(); toggleTunePopover(); });
} else {
tune.disabled = true;
tune.setAttribute('aria-label', 'Tune controls are still being prepared');
tune.title = 'Tune controls are still being prepared';
tune.style.opacity = '0.72';
}
tune.dataset.iceqTune = '1';
row.appendChild(tune);
}
// Spacer
row.appendChild(el('div', { flex: '1' }));
if (arrivedVariants < expectedVariants) {
const remaining = expectedVariants - arrivedVariants;
const progress = el('span', {
fontSize: '11px', color: BP.textDim, whiteSpace: 'nowrap',
});
progress.textContent = remaining + ' more arriving...';
row.appendChild(progress);
}
// Accept - primary action, kinpaku gold + lacquer-deep (matches demo .live-demo-ctx-accept)
const accept = el('button', {
padding: '5px 14px', borderRadius: '5px',
border: 'none', background: C.brand, color: C.ink,
fontFamily: FONT, fontSize: '11px', fontWeight: '600',
cursor: 'pointer', transition: 'filter 0.12s ease, transform 0.1s ease',
whiteSpace: 'nowrap',
});
accept.textContent = '\u2713 Accept';
accept.addEventListener('mouseenter', () => accept.style.filter = 'brightness(1.08)');
accept.addEventListener('mouseleave', () => accept.style.filter = 'none');
accept.addEventListener('mousedown', () => accept.style.transform = 'scale(0.97)');
accept.addEventListener('mouseup', () => accept.style.transform = 'scale(1)');
accept.addEventListener('click', (e) => { e.stopPropagation(); handleAccept(); });
if (arrivedVariants === 0) {
accept.style.opacity = '0.3';
accept.style.pointerEvents = 'none';
accept.title = 'Accept becomes available when the first variant arrives';
}
row.appendChild(accept);
// Discard
const discard = el('button', {
padding: '4px 6px', borderRadius: '5px',
border: '1px solid ' + BP.hairline, background: 'transparent',
fontFamily: FONT, fontSize: '11px', color: BP.textDim,
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 = BP.text; discard.style.borderColor = BP.text; });
discard.addEventListener('mouseleave', () => { discard.style.color = BP.textDim; discard.style.borderColor = BP.hairline; });
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 ' + BP.hairline,
borderTopColor: BP.accent,
animation: 'impeccable-spin 0.6s linear infinite',
flexShrink: '0',
});
row.appendChild(spinner);
const label = el('span', {
fontSize: '12px', color: BP.textDim, fontWeight: '500',
});
label.textContent = 'Applying variant...';
row.appendChild(label);
ensureSpinKeyframes();
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.18 145)',
});
check.textContent = '\u2713';
row.appendChild(check);
const label = el('span', {
fontSize: '12px', color: 'oklch(49% 0.08 188)', 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;
// active: solid site-brand kinpaku dot. arrived+inactive: muted neutral.
// pending (not yet arrived): faint outline ring. No borders on arrived
// dots - the previous "accent ring + ash fill" combo read as noisy
// kinpaku chips, especially when all variants had arrived and every
// dot wore an accent ring.
const dotBg = active ? C.brand
: arrived ? BP.textDim
: 'transparent';
const dotBorder = arrived ? 'none' : '1.5px solid ' + BP.hairline;
const dot = el('div', {
width: active ? '8px' : '6px',
height: active ? '8px' : '6px',
borderRadius: '50%',
background: dotBg,
border: dotBorder,
boxSizing: 'border-box',
transition: 'all 0.2s ' + EASE,
cursor: (clickable && arrived) ? 'pointer' : 'default',
transform: arrived ? 'scale(1)' : 'scale(0.85)',
opacity: arrived ? (active ? '1' : '0.6') : '0.4',
});
if (clickable && arrived) {
const idx = i;
dot.addEventListener('click', (e) => {
e.stopPropagation();
selectVariant(idx, 'variant_changed');
});
}
container.appendChild(dot);
}
return container;
}
function navBtn(text) {
const b = el('button', {
width: '26px', height: '26px', borderRadius: '5px',
border: '1px solid ' + BP.hairline, background: 'transparent',
color: BP.text, 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 = BP.text; });
b.addEventListener('mouseleave', () => { b.style.borderColor = BP.hairline; });
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 (String(tag).toLowerCase() === 'button') e.type = 'button';
if (styles) Object.assign(e.style, styles);
return e;
}
//
// Action picker popover
//
function initActionPicker() {
const P = barPaletteForTheme(detectPageTheme());
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 right',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
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', {
display: 'flex', flexDirection: 'column', alignItems: 'center',
gap: '4px',
padding: '8px 6px', borderRadius: '6px',
border: 'none',
background: action.value === selectedAction ? P.accentSoft : 'transparent',
color: action.value === selectedAction ? P.accent : P.text,
fontFamily: FONT, fontSize: '11px', fontWeight: '500',
cursor: 'pointer',
transition: 'background 0.1s ease, color 0.1s ease',
textAlign: 'center', whiteSpace: 'nowrap',
});
const iconWrap = el('span', {
display: 'flex', alignItems: 'center', justifyContent: 'center',
height: '20px', opacity: '0.9',
});
iconWrap.innerHTML = ICONS[action.value] || '';
const labelEl = el('span', { lineHeight: '1' });
labelEl.textContent = action.label;
chip.appendChild(iconWrap);
chip.appendChild(labelEl);
chip.dataset.action = action.value;
chip.addEventListener('mouseenter', () => {
if (action.value !== selectedAction) chip.style.background = P.accentSoft;
});
chip.addEventListener('mouseleave', () => {
chip.style.background = action.value === selectedAction ? P.accentSoft : 'transparent';
});
chip.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const prompt = uiGetById(PREFIX + '-input')?.value || '';
selectedAction = action.value;
hideActionPicker();
updateBarContent('configure');
const input = uiGetById(PREFIX + '-input');
if (input && prompt) input.value = prompt;
});
grid.appendChild(chip);
});
pickerEl.appendChild(grid);
uiAppend(pickerEl);
defangOutsideHandlers(pickerEl);
// Cache the palette on the picker so toggleActionPicker's state refresh
// uses the same theme-aware colors when it repaints chips.
pickerEl.__iceq_palette = P;
}
function toggleActionPicker() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
if (pickerEl.style.display !== 'none') { hideActionPicker(); return; }
// Rebuild chips to reflect current selection
const P = pickerEl.__iceq_palette || barPaletteForTheme(detectPageTheme());
pickerEl.querySelectorAll('button').forEach(chip => {
const isActive = chip.dataset.action === selectedAction;
chip.style.background = isActive ? P.accentSoft : 'transparent';
chip.style.color = isActive ? P.accent : P.text;
});
// Position above the bar, right-aligned to the configure bar edge.
const barRect = barEl.getBoundingClientRect();
const pickerH = 170; // approximate; grows with icon + label rows
let top = barRect.top - pickerH - 6;
if (top < 8) top = barRect.bottom + 6;
pickerEl.style.display = 'block';
const pickerW = pickerEl.offsetWidth;
let left = barRect.right - pickerW;
left = Math.max(8, Math.min(left, window.innerWidth - pickerW - 8));
Object.assign(pickerEl.style, {
top: top + 'px',
left: left + 'px',
});
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);
}
function ensureCyclingRenderable(reason) {
if (arrivedVariants > 0) {
if (visibleVariant < 1 || visibleVariant > arrivedVariants) visibleVariant = 1;
return true;
}
recoverEmptyCycling(reason);
return false;
}
function recoverEmptyCycling(reason) {
if (recoveringEmptyCycling) return;
recoveringEmptyCycling = true;
try {
console.warn('[impeccable] Refusing to render empty variant cycling state:', reason);
const message = 'No variants were mounted. Please try again.';
if (svelteComponentSession?.sessionId === currentSessionId) {
resetSvelteComponentSession(currentSessionId, message);
return;
}
cleanup();
showToast(message, 5000);
} finally {
recoveringEmptyCycling = false;
}
}
//
// Params panel (per-variant coarse controls)
//
// Variants may declare a parameter manifest via a JSON attribute on the
// variant wrapper:
//
// <div data-impeccable-variant="1"
// data-impeccable-params='[{"id":"density","kind":"steps",...}]'>
//
// The panel docks to the right edge of the outline during CYCLING and
// exposes 2-5 coarse knobs. Values apply to the variant wrapper so scoped
// CSS can respond instantly without regeneration:
//
// range / numeric toggle -> CSS custom property used by variant styles
// steps / boolean toggle → data-p-<id> attribute used via :scope[data-p-foo="..."]
//
// On variant switch, values reset to that variant's declared defaults.
// On accept, current values are sent in the event payload so the agent
// can bake them into the source-file write.
//
let paramsPanelEl = null; // outer wrapper (overflow:hidden, clips the slide)
let paramsPanelInner = null; // translating content (carries bg, padding, knobs)
let paramsPanelBody = null; // grid holding the knob cells
let paramsCurrentValues = {}; // {paramId: value} - mirror of the visible variant's live values
let tuneOpen = false; // whether the Tune popover is open right now
// Theme-aware Tune popover. Appears as a drawer that slides out from the
// contextual bar's bar-facing edge (below if the bar sits below the
// element, above otherwise). Same width as the bar. Auto-wraps to extra
// rows when the knobs exceed one row. The bar's border-radius on the
// popover side goes flat while open so the two shapes read as one.
let paramsPanelPalette = null;
function initParamsPanel() {
paramsPanelPalette = barPaletteForTheme(detectPageTheme());
const P = paramsPanelPalette;
// Single element, always in the DOM. The slide animation is a CSS mask
// with mask-size growing from 0% to 100% along the bar-facing axis - no
// display toggle, no opacity toggle, no transform trickery. The mask
// hides everything initially; as it grows, content is revealed from
// the bar edge outward.
paramsPanelEl = document.createElement('div');
paramsPanelEl.id = PREFIX + '-params-panel';
Object.assign(paramsPanelEl.style, {
position: 'fixed', zIndex: String(Z.bar - 1),
background: P.surfaceDeep,
color: P.text,
fontFamily: FONT,
padding: '14px 18px',
boxSizing: 'border-box',
borderRadius: '0 0 10px 10px',
pointerEvents: 'none',
// clip-path is the same conceptual reveal as mask but with rock-solid
// transition support across engines. Closed state clips from the far
// edge; open = inset(0) shows everything.
clipPath: 'inset(0 0 100% 0)',
transition: 'clip-path 0.44s ' + EASE,
// Park off-screen until positionParamsPanel places it. These are NOT
// in the transition list, so they snap instantly - no fly-in from the
// top-left when first shown.
top: '-9999px', left: '-9999px', width: '0',
});
paramsPanelBody = el('div', {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))',
gap: '12px 16px',
});
paramsPanelEl.appendChild(paramsPanelBody);
uiAppend(paramsPanelEl);
// Don't override pointer-events: the panel toggles between 'none' (closed,
// click-through) and 'auto' (open) on its own. Just silence the host's
// outside-interaction listeners while the panel is open.
defangOutsideHandlers(paramsPanelEl, { setPointerEvents: false });
paramsPanelInner = paramsPanelEl; // compatibility alias for the rest of the code
}
function getMountedSvelteComponentAnchor(session = svelteComponentSession) {
const el = session?.mountTargetEl?.firstElementChild || null;
if (!el || !document.body.contains(el)) return null;
return rectIsUsableAnchor(el.getBoundingClientRect()) ? el : null;
}
function resolveSvelteComponentAnchor(session = svelteComponentSession) {
return getMountedSvelteComponentAnchor(session)
|| session?.swapAnchor
|| null;
}
function getVisibleVariantEl() {
if (!currentSessionId) return null;
if (svelteComponentSession?.sessionId === currentSessionId) {
return resolveSvelteComponentAnchor()
|| svelteComponentSession.wrapperEl
|| null;
}
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (!wrapper) return null;
return wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"]');
}
function parseVariantParams(variantEl) {
// Svelte component variants can't carry a `data-impeccable-params` attribute:
// the compiler reads `{` inside attribute values as expression delimiters, so
// JSON-with-braces breaks the build. For that path the params live in a sidecar
// params.json keyed by variant number, loaded into the session at mount time.
if (svelteComponentSession?.sessionId === currentSessionId) {
const byVariant = svelteComponentSession.paramsByVariant || {};
const params = byVariant[String(visibleVariant)] || byVariant[visibleVariant];
return Array.isArray(params) ? params : [];
}
if (!variantEl) return [];
const raw = variantEl.getAttribute('data-impeccable-params');
if (!raw) return [];
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch (err) {
console.warn('[impeccable] Invalid data-impeccable-params JSON:', err.message);
return [];
}
}
function applyParamValue(variantEl, param, value) {
if (!variantEl) return;
const attr = 'data-p-' + param.id;
if (param.kind === 'toggle') {
const on = !!value;
if (on) variantEl.setAttribute(attr, 'on');
else variantEl.removeAttribute(attr);
} else if (param.kind === 'steps') {
variantEl.setAttribute(attr, String(value));
}
// Svelte component variants are client-mounted into
// [data-impeccable-component-mount] with no [data-impeccable-variant="N"]
// wrapper for the state stylesheet to target, and the element is not SSR'd,
// so there is no React hydration to mismatch. Drive range/toggle --p-* inline
// on the mounted element so scoped preview CSS resolves them.
if (svelteComponentSession?.sessionId === currentSessionId) {
if (param.kind === 'range') variantEl.style.setProperty('--p-' + param.id, String(value));
else if (param.kind === 'toggle') variantEl.style.setProperty('--p-' + param.id, value ? '1' : '0');
return;
}
// range/toggle --p-* custom properties are driven through the injected
// variant-state stylesheet so we never mutate inline style on SSR'd divs.
updateVariantStateStylesheet(currentSessionId, visibleVariant);
}
function applyParamDefaults(variantEl, params) {
paramsCurrentValues = {};
for (const p of params) {
paramsCurrentValues[p.id] = p.default;
applyParamValue(variantEl, p, p.default);
}
}
function formatRangeValue(input) {
const max = parseFloat(input.max), min = parseFloat(input.min);
const v = parseFloat(input.value);
if (!isFinite(v)) return input.value;
return (max - min) <= 2 ? v.toFixed(2) : String(Math.round(v));
}
function buildParamsPanel(variantEl, params) {
const P = paramsPanelPalette || barPaletteForTheme(detectPageTheme());
paramsPanelBody.innerHTML = '';
for (const p of params) {
const row = el('div', { display: 'flex', flexDirection: 'column', gap: '6px' });
const labelRow = el('div', {
display: 'flex', justifyContent: 'space-between',
alignItems: 'baseline', gap: '8px',
});
const lbl = el('span', {
fontSize: '10.5px', fontWeight: '600', color: P.text,
letterSpacing: '0.03em',
});
lbl.textContent = p.label || p.id;
labelRow.appendChild(lbl);
const readout = el('span', {
fontSize: '10.5px', color: P.textDim,
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
});
labelRow.appendChild(readout);
row.appendChild(labelRow);
if (p.kind === 'range') {
const input = document.createElement('input');
input.type = 'range';
input.min = String(p.min != null ? p.min : 0);
input.max = String(p.max != null ? p.max : 1);
input.step = String(p.step != null ? p.step : 0.05);
input.value = String(p.default);
Object.assign(input.style, {
width: '100%', accentColor: C.brand, cursor: 'pointer',
});
readout.textContent = formatRangeValue(input);
input.addEventListener('input', (e) => {
e.stopPropagation();
const v = parseFloat(input.value);
paramsCurrentValues[p.id] = v;
readout.textContent = formatRangeValue(input);
applyParamValue(variantEl, p, v);
queueCheckpoint('param_changed');
});
row.appendChild(input);
} else if (p.kind === 'toggle') {
const initial = !!p.default;
readout.textContent = initial ? 'On' : 'Off';
const track = el('button', {
position: 'relative', width: '36px', height: '20px',
borderRadius: '10px', border: 'none', padding: '0',
cursor: 'pointer',
background: initial ? C.brand : P.hairline,
transition: 'background 0.15s ease',
alignSelf: 'flex-start',
});
const knob = el('span', {
position: 'absolute', top: '2px',
left: initial ? '18px' : '2px',
width: '16px', height: '16px', borderRadius: '50%',
background: C.ink,
transition: 'left 0.18s ' + EASE,
boxShadow: '0 1px 2px oklch(0% 0 0 / 0.2)',
});
track.appendChild(knob);
track.addEventListener('click', (e) => {
e.stopPropagation();
const next = !paramsCurrentValues[p.id];
paramsCurrentValues[p.id] = next;
track.style.background = next ? C.brand : P.hairline;
knob.style.left = next ? '18px' : '2px';
readout.textContent = next ? 'On' : 'Off';
applyParamValue(variantEl, p, next);
queueCheckpoint('param_changed');
});
row.appendChild(track);
} else if (p.kind === 'steps') {
const opts = (p.options || []).map(o =>
typeof o === 'string' ? { value: o, label: o } : o
);
const activeOpt = opts.find(o => o.value === p.default) || opts[0];
readout.textContent = activeOpt ? activeOpt.label : String(p.default);
const segRow = el('div', {
display: 'grid',
gridTemplateColumns: 'repeat(' + opts.length + ', 1fr)',
gap: '1px', padding: '2px',
background: P.hairline, borderRadius: '5px',
});
const segBtns = [];
opts.forEach(o => {
const active = o.value === p.default;
const b = el('button', {
padding: '5px 4px', border: 'none', borderRadius: '3px',
background: active ? C.brand : 'transparent',
color: active ? C.ink : P.text,
fontFamily: FONT, fontSize: '10.5px', fontWeight: '500',
cursor: 'pointer', whiteSpace: 'nowrap',
transition: 'background 0.1s ease, color 0.1s ease',
});
b.textContent = o.label;
b.addEventListener('click', (e) => {
e.stopPropagation();
paramsCurrentValues[p.id] = o.value;
readout.textContent = o.label;
segBtns.forEach(({ btn, val }) => {
const on = val === o.value;
btn.style.background = on ? C.brand : 'transparent';
btn.style.color = on ? C.ink : P.text;
});
applyParamValue(variantEl, p, o.value);
queueCheckpoint('param_changed');
});
segRow.appendChild(b);
segBtns.push({ btn: b, val: o.value });
});
row.appendChild(segRow);
}
paramsPanelBody.appendChild(row);
}
}
//
// Inline text editing - makes pure-text descendants of the picked element
// directly contenteditable. Save stages copy edits in the live buffer; the
// Apply copy edits dock later asks the AI to apply the staged batch.
//
let inlineEditRows = [];
let inlineEditDrafts = new Map();
// Mixed-content elements (e.g. <p>text<code>x</code>text</p>) skip the row
// walker's "all-children-are-text-nodes" rule. Wrap each non-whitespace direct
// text-node child in a marker span so the walker emits a row for it. The
// wrappers are inline display by default and inherit styles, so the page
// shouldn't visually shift. We unwrap in disableInlineEdit.
const MIXED_WRAP_SKIP = { script: 1, style: 1, template: 1, noscript: 1, svg: 1, code: 1, pre: 1 };
function collectEditableTextRows(rootEl, opts) {
if (!rootEl || rootEl.nodeType !== 1) return [];
const isOwn = (opts && opts.isOwn) || (() => false);
const rows = [];
function visit(el) {
if (!el || el.nodeType !== 1) return;
const tag = el.tagName.toLowerCase();
if (MIXED_WRAP_SKIP[tag]) return;
if (el.hasAttribute && el.hasAttribute('contenteditable')) return;
if (el !== rootEl && isOwn(el)) return;
const children = Array.from(el.childNodes);
const textNodes = [];
let allText = children.length > 0;
let hasNonWhitespaceText = false;
for (const node of children) {
if (node.nodeType === 3) {
textNodes.push(node);
if (node.nodeValue && /\S/.test(node.nodeValue)) hasNonWhitespaceText = true;
} else {
allText = false;
}
}
if (allText && hasNonWhitespaceText) {
rows.push({
el,
ref: documentRefForElement(el) || el.tagName.toLowerCase(),
text: textNodes.map((node) => node.nodeValue).join(''),
textNodes,
});
}
for (const child of children) {
if (child.nodeType === 1) visit(child);
}
}
visit(rootEl);
return rows;
}
function wrapMixedContentTextNodes(rootEl) {
if (!rootEl || rootEl.nodeType !== 1) return;
const tag = rootEl.tagName.toLowerCase();
if (MIXED_WRAP_SKIP[tag]) return;
if (rootEl.hasAttribute('contenteditable')) return;
const children = Array.from(rootEl.childNodes);
const hasText = children.some((n) => n.nodeType === 3 && /\S/.test(n.nodeValue || ''));
const hasElement = children.some((n) => n.nodeType === 1);
if (hasText && hasElement) {
for (const node of children) {
if (node.nodeType === 3 && /\S/.test(node.nodeValue || '')) {
const wrap = document.createElement('span');
wrap.dataset.impeccableTextWrap = 'true';
wrap.textContent = node.nodeValue;
rootEl.insertBefore(wrap, node);
rootEl.removeChild(node);
}
}
}
for (const child of Array.from(rootEl.children)) {
if (!child.dataset || !child.dataset.impeccableTextWrap) {
wrapMixedContentTextNodes(child);
}
}
}
function unwrapMixedContentTextNodes(rootEl) {
if (!rootEl || rootEl.nodeType !== 1) return;
const wraps = rootEl.querySelectorAll('[data-impeccable-text-wrap="true"]');
for (const wrap of wraps) {
const parent = wrap.parentNode;
if (!parent) continue;
const textNode = document.createTextNode(wrap.textContent);
parent.replaceChild(textNode, wrap);
parent.normalize();
}
}
let inlineEditRoot = null;
function enableInlineEdit(targetEl) {
if (!targetEl) return;
inlineEditRoot = targetEl;
wrapMixedContentTextNodes(targetEl);
const rows = collectEditableTextRows(targetEl, { isOwn: own });
inlineEditRows = rows;
inlineEditDrafts = new Map();
for (const row of rows) {
row.inlineWhiteSpace = row.el.style.whiteSpace;
row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;
row.el.setAttribute('contenteditable', 'true');
row.el.dataset.impeccableEditable = 'true';
row.el.dataset.impeccableOriginalText = row.text;
row.el.style.userSelect = 'text';
row.el.style.cursor = 'text';
row.el.style.outline = 'none';
row.el.addEventListener('input', onInlineInput);
}
}
function disableInlineEdit(opts = {}) {
for (const row of inlineEditRows) {
if (activeElementDeep() === row.el) row.el.blur();
row.el.removeAttribute('contenteditable');
delete row.el.dataset.impeccableEditable;
delete row.el.dataset.impeccableOriginalText;
row.el.style.whiteSpace = row.inlineWhiteSpace || '';
row.el.style.userSelect = '';
row.el.style.cursor = '';
row.el.style.outline = '';
row.el.removeEventListener('input', onInlineInput);
}
inlineEditRows = [];
inlineEditDrafts = new Map();
if (inlineEditRoot && !opts.preserveMixedWraps) {
unwrapMixedContentTextNodes(inlineEditRoot);
inlineEditRoot = null;
}
}
function onInlineInput(e) {
inlineEditDrafts.set(e.currentTarget, e.currentTarget.textContent);
}
function hasTextRows(el) {
if (!el) return false;
// Lightweight: any descendant outside SKIP_SUBTREE_TAGS with at least one
// non-whitespace direct text-node child means we have something editable
// (mixed-content paragraphs included). Mirrors what the wrap+walk path
// will produce in enableInlineEdit.
function check(node) {
if (!node || node.nodeType !== 1) return false;
const tag = node.tagName.toLowerCase();
if (MIXED_WRAP_SKIP[tag]) return false;
if (node !== el && own(node)) return false;
for (const child of node.childNodes) {
if (child.nodeType === 3 && /\S/.test(child.nodeValue || '')) return true;
}
for (const child of node.children) {
if (check(child)) return true;
}
return false;
}
return check(el);
}
function enterEditingMode() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
setLiveState('EDITING');
hideBar();
hideAnnotOverlay();
renderEditBadge('editing');
enableInlineEdit(selectedElement);
// Focus first editable element and position cursor at end
if (inlineEditRows.length > 0) {
const firstEditable = inlineEditRows[0] && inlineEditRows[0].el;
setTimeout(() => {
const el = firstEditable;
if (!el || !el.isConnected || state !== 'EDITING') return;
el.focus();
const range = document.createRange();
const sel = window.getSelection();
range.selectNodeContents(el);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
}, 50);
}
}
function restoreInlineEditDrafts() {
for (const row of inlineEditRows) {
if (inlineEditDrafts.has(row.el)) {
row.el.textContent = row.el.dataset.impeccableOriginalText;
}
}
}
function cancelEditing() {
restoreInlineEditDrafts();
disableInlineEdit();
setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
}
function cancelEditingToPicking() {
restoreInlineEditDrafts();
disableInlineEdit();
hideBar();
stopScrollTracking();
hideAnnotOverlay();
clearAnnotations();
renderEditBadge('hidden');
setLiveState('PICKING');
hoveredElement = null;
hideHighlight();
syncPageChatFocus('editing-outside-click');
}
function teardownConfigureChrome() {
hideConfigureBarTooltip();
// hideBar() restores unsaved EDITING drafts before it disables inline
// edit; disabling here first would wipe the draft metadata it needs.
hideBar();
stopScrollTracking();
hideAnnotOverlay();
clearAnnotations();
renderEditBadge('hidden');
}
function exitConfigureToPicking(reason, opts = {}) {
teardownConfigureChrome();
setLiveState('PICKING');
if (opts.clearHover) {
hoveredElement = null;
hideHighlight();
}
syncPageChatFocus(reason);
}
// Prefer the leaf's own id/class; if it has neither (e.g. a bare <em>),
// climb to the nearest ancestor with one. The CLI uses tag+class together,
// so tag must come from the same node as the locator.
function buildLocatorForLeaf(leafEl, fallbackEl) {
if (leafEl && (leafEl.id || leafEl.classList.length > 0)) {
return {
tag: leafEl.tagName.toLowerCase(),
elementId: leafEl.id || null,
classes: [...leafEl.classList],
};
}
let cur = leafEl?.parentElement;
while (cur && cur !== document.body) {
if (cur.id || cur.classList.length > 0) {
return {
tag: cur.tagName.toLowerCase(),
elementId: cur.id || null,
classes: [...cur.classList],
};
}
cur = cur.parentElement;
}
return {
tag: (fallbackEl || leafEl).tagName.toLowerCase(),
elementId: (fallbackEl || leafEl).id || null,
classes: [...((fallbackEl || leafEl).classList || [])],
};
}
function sourceHintForElement(el) {
if (!el || !el.getAttribute) return null;
const file = el.getAttribute('data-astro-source-file');
const loc = el.getAttribute('data-astro-source-loc');
if (file || loc) {
const parsed = parseSourceLoc(loc);
return {
file: file || '',
loc: loc || '',
line: parsed.line,
column: parsed.column,
};
}
return null;
}
function parseSourceLoc(loc) {
const match = String(loc || '').match(/^(\d+)(?::(\d+))?/);
return {
line: match ? Number(match[1]) : null,
column: match && match[2] ? Number(match[2]) : null,
};
}
function documentRefForElement(el) {
if (!el || el.nodeType !== 1) return null;
const parts = [];
let cur = el;
while (cur && cur.nodeType === 1) {
const tag = cur.tagName.toLowerCase();
if (tag === 'html') break;
if (tag === 'body') {
parts.unshift('body');
break;
}
parts.unshift(documentRefSegment(cur));
cur = cur.parentElement;
}
return parts.join('>') || null;
}
function documentRefSegment(el) {
const tag = el.tagName.toLowerCase();
return tag + documentRefIdSuffix(el) + documentRefClassSuffix(el) + ':nth-of-type(' + indexAmongSameTag(el) + ')';
}
function documentRefIdSuffix(el) {
return el.id ? '#' + normalizeDocumentRefToken(el.id) : '';
}
function documentRefClassSuffix(el) {
if (!el.classList || el.classList.length === 0) return '';
const classes = [];
for (const cls of el.classList) {
if (!cls || cls.indexOf('impeccable-') === 0) continue;
classes.push(normalizeDocumentRefToken(cls));
if (classes.length === 2) break;
}
return classes.length ? '.' + classes.join('.') : '';
}
function normalizeDocumentRefToken(value) {
return String(value || '').replace(/[>\s]+/g, '_');
}
function indexAmongSameTag(el) {
const parent = el.parentElement;
if (!parent) return 1;
const tag = el.tagName.toLowerCase();
let n = 0;
for (const sib of parent.children) {
if (sib.tagName.toLowerCase() === tag) {
n++;
if (sib === el) return n;
}
}
return 1;
}
function copyEditLeafContext(el, originalText, newText) {
if (!el) return null;
return {
ref: documentRefForElement(el),
tagName: el.tagName ? el.tagName.toLowerCase() : null,
id: el.id || null,
classes: el.classList ? [...el.classList].filter((cls) => cls.indexOf('impeccable-') !== 0) : [],
originalText,
newText,
textContent: (el.textContent || '').slice(0, 500),
outerHTML: sanitizedContextOuterHTML(el, 3000) || null,
};
}
function nearbyEditableTextsForManualEdit(rows, activeEl, originalText, newText) {
const out = [];
const seen = new Set();
const skip = new Set([normalizeManualContextText(originalText), normalizeManualContextText(newText)]);
for (const row of rows || []) {
if (!row || row.el === activeEl) continue;
const text = normalizeManualContextText(row.text);
if (!text || text.length < 2 || seen.has(text) || skip.has(text)) continue;
seen.add(text);
out.push({
ref: documentRefForElement(row.el),
tag: row.el?.tagName ? row.el.tagName.toLowerCase() : null,
classes: row.el?.classList ? [...row.el.classList].filter((cls) => cls.indexOf('impeccable-') !== 0) : [],
text,
});
if (out.length >= 12) break;
}
return out;
}
function copyEditContainerContext(el) {
if (!el) return null;
return {
ref: documentRefForElement(el),
tagName: el.tagName ? el.tagName.toLowerCase() : null,
id: el.id || null,
classes: el.classList ? [...el.classList].filter((cls) => cls.indexOf('impeccable-') !== 0) : [],
textContent: (el.textContent || '').slice(0, 1000),
outerHTML: sanitizedContextOuterHTML(el, 10000) || null,
};
}
function forbiddenManualTextChars(text) {
const out = [];
for (const ch of ['<', '{', '}', '`']) {
if (String(text || '').includes(ch)) out.push(ch);
}
return out;
}
async function applyEditing() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
const ops = [];
for (const row of inlineEditRows) {
const newText = inlineEditDrafts.get(row.el);
if (newText !== undefined && newText !== row.text) {
if (String(newText || '').trim() === '') {
showToast('Save rejected: copy edits cannot be empty.', 5500);
return;
}
const forbidden = forbiddenManualTextChars(newText);
if (forbidden.length > 0) {
showToast('Save rejected: newText cannot contain ' + forbidden.join(' ') + ' (plain text only; ask the AI to insert markup)', 5500);
return;
}
const locator = buildLocatorForLeaf(row.el, selectedElement);
const op = {
ref: row.ref,
tag: locator.tag,
elementId: locator.elementId,
classes: locator.classes,
originalText: row.text,
newText,
};
op.leaf = copyEditLeafContext(row.el, row.text, newText);
op.nearbyEditableTexts = nearbyEditableTextsForManualEdit(inlineEditRows, row.el, row.text, newText);
const restoreHint = mixedTextWrapRestoreHint(row.el);
if (restoreHint) op.restore = restoreHint;
const sourceHint = sourceHintForElement(row.el);
if (sourceHint) op.sourceHint = sourceHint;
ops.push(op);
}
}
if (ops.length === 0) { cancelEditing(); return; }
const contextElement = contextElementForManualEdit(selectedElement, inlineEditRows, ops);
const contextRef = documentRefForElement(contextElement);
if (contextRef) for (const op of ops) op.contextRef = contextRef;
const container = copyEditContainerContext(contextElement);
if (container) for (const op of ops) op.container = container;
try {
const res = await fetch('http://localhost:' + PORT + '/manual-edit-stash', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: TOKEN,
id: id8(),
pageUrl: location.pathname,
element: extractContext(contextElement),
ops,
}),
});
if (!res.ok) {
const errBody = await res.json().catch(() => ({}));
throw new Error(errBody.error || ('HTTP ' + res.status));
}
const stashResult = await res.json();
updatePendingCounter(stashResult.pendingCount || 0);
maybeShowFirstSaveToast();
disableInlineEdit();
setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
} catch (err) {
console.error('[impeccable] manual edit stash failed:', err);
const detail = String(err?.message || '');
if (detail.includes('newText cannot contain') || detail.includes('newText cannot be empty')) {
showToast('Save rejected: ' + detail.replace(/^manual_edits:\s*/, ''), 5500);
} else {
showToast('Save failed - retry or cancel', 4000);
}
}
}
function schedulePendingDockPosition() {
if (!pendingDockEl || !globalBarEl) return;
requestAnimationFrame(positionPendingDock);
}
function positionPendingDock() {
if (!pendingDockEl || !globalBarEl) return;
const width = globalBarEl.offsetWidth;
const height = globalBarEl.offsetHeight;
if (!width || !height) return;
pendingDockEl.style.left = Math.round((window.innerWidth / 2) - (width / 2) - 18) + 'px';
pendingDockEl.style.top = 'auto';
pendingDockEl.style.bottom = Math.round(14 + (height / 2)) + 'px';
}
function playPendingIntroAnimation() {
if (!pendingPillEl || !pendingPillEl.animate || (matchMedia?.('(prefers-reduced-motion: reduce)').matches)) return;
if (pendingIntroAnimation) pendingIntroAnimation.cancel();
pendingIntroAnimation = pendingPillEl.animate([
{
opacity: 0,
transform: 'scale(0.82)',
filter: 'brightness(1.2)',
boxShadow: '0 0 0 0 oklch(84% 0.19 80.46 / 0.45), 0 8px 24px oklch(0% 0 0 / 0.16)',
},
{
opacity: 1,
transform: 'scale(1.08)',
filter: 'brightness(1.15)',
boxShadow: '0 0 0 12px oklch(84% 0.19 80.46 / 0), 0 12px 34px oklch(0% 0 0 / 0.22)',
offset: 0.55,
},
{
opacity: 1,
transform: 'scale(1)',
filter: 'none',
boxShadow: '0 4px 16px oklch(0% 0 0 / 0.16), 0 1px 3px oklch(0% 0 0 / 0.1)',
},
], { duration: 620, easing: EASE });
pendingIntroAnimation.addEventListener('finish', () => { pendingIntroAnimation = null; }, { once: true });
}
function ensureSpinKeyframes() {
if (uiGetById(PREFIX + '-keyframes')) return;
const style = document.createElement('style');
style.id = PREFIX + '-keyframes';
style.textContent = '@keyframes impeccable-spin { to { transform: rotate(360deg); } }';
uiAppendStyle(style);
}
function pendingApplyLabel(count) {
return count === 1 ? 'Apply copy edit' : 'Apply copy edits';
}
function showManualApplyBusyToast() {
showToast('Apply is still running. Wait for it to finish.', 2800);
}
function manualApplyStateKey() {
return PREFIX + ':manual-apply:' + PORT + ':' + TOKEN + ':' + location.pathname;
}
function readStoredManualApplyState() {
try {
const raw = sessionStorage.getItem(manualApplyStateKey());
if (!raw) return null;
const storedState = JSON.parse(raw);
if (!storedState || storedState.pageUrl !== location.pathname || Date.now() > Number(storedState.expiresAt || 0)) {
sessionStorage.removeItem(manualApplyStateKey());
return null;
}
return storedState;
} catch {
return null;
}
}
function writeManualApplyState(applyState) {
try {
sessionStorage.setItem(manualApplyStateKey(), JSON.stringify({
...applyState,
pageUrl: location.pathname,
updatedAt: Date.now(),
expiresAt: Date.now() + MANUAL_APPLY_STATE_TTL_MS,
}));
} catch {
// Best-effort only. The in-memory flag still covers non-reload flows.
}
}
function storeManualApplyState(count, patch) {
const currentCount = Number(count) || 0;
const existing = readStoredManualApplyState() || {};
const totalOps = Number(existing.totalOps) || Number(existing.count) || currentCount;
if (totalOps <= 0 && currentCount <= 0) return;
writeManualApplyState({
count: Number(existing.count) || currentCount || totalOps,
totalOps: totalOps || currentCount,
completedOps: Number(existing.completedOps) || 0,
remainingCount: Number.isFinite(Number(existing.remainingCount)) ? Number(existing.remainingCount) : currentCount,
phase: existing.phase || 'applying',
startedAt: Number(existing.startedAt) || Date.now(),
...(patch || {}),
});
}
function clearStoredManualApplyState() {
try {
sessionStorage.removeItem(manualApplyStateKey());
} catch {
// Ignore storage failures; UI state can still clear in memory.
}
}
function shouldResumeManualApplyLoading(count) {
return Number(count) > 0 && readStoredManualApplyState() !== null;
}
function manualApplyLoadingText(fallbackCount) {
const stored = readStoredManualApplyState();
if (stored?.phase === 'repair-decision') return 'Apply needs attention';
if (stored?.phase === 'repairing') {
const attempt = Number(stored.repairAttempt) || 1;
const max = Number(stored.repairMaxAttempts) || 3;
return 'Fixing apply issue, attempt ' + attempt + '/' + max;
}
if (stored?.phase === 'verifying') return 'Verifying copy edits';
const remaining = Number.isFinite(Number(stored?.remainingCount))
? Number(stored.remainingCount)
: Number(fallbackCount) || 0;
return remaining > 0
? 'Applying ' + remaining + ' copy edit' + (remaining === 1 ? '' : 's')
: 'Verifying copy edits';
}
function resetManualApplyProgress(count) {
const total = Number(count) || 0;
if (total <= 0) return;
writeManualApplyState({
count: total,
totalOps: total,
completedOps: 0,
remainingCount: total,
phase: 'applying',
startedAt: Date.now(),
});
}
function updateManualApplyProgressFromChunk(chunk) {
if (!chunk || !pendingApplyInFlight) return;
const stored = readStoredManualApplyState() || {};
const totalOps = Number(chunk.totalOpCount) || Number(stored.totalOps) || Number(stored.count) || parseInt(pendingPillEl?.dataset.count || '0', 10) || 0;
const completedOps = Math.min(totalOps, (Number(stored.completedOps) || 0) + (Number(chunk.opCount) || 0));
const remainingCount = Math.max(0, totalOps - completedOps);
storeManualApplyState(Number(stored.count) || totalOps, {
totalOps,
completedOps,
remainingCount,
phase: remainingCount > 0 ? 'applying' : 'verifying',
});
setPendingApplyLoading(true, remainingCount);
}
function updateManualApplyRepairState(repair, phase) {
const count = parseInt(pendingPillEl?.dataset.count || '0', 10) || Number(readStoredManualApplyState()?.count) || 0;
if (count <= 0) return;
storeManualApplyState(count, {
phase,
repairAttempt: Number(repair?.attempt || repair?.attempts) || 1,
repairMaxAttempts: Number(repair?.maxAttempts) || 3,
});
setPendingApplyLoading(true, count);
}
function refreshLiveControlsForManualApply() {
if (pendingApplyInFlight) {
hideActionPicker();
closeTunePopover();
}
if (barEl && barEl.style.display !== 'none' && state === 'CONFIGURING') {
const input = uiGetById(PREFIX + '-input');
const prompt = input ? input.value : '';
updateBarContent('configure');
const nextInput = uiGetById(PREFIX + '-input');
if (nextInput) nextInput.value = prompt;
}
if (editBadgeEl && editBadgeEl.style.display !== 'none') {
if (pendingApplyInFlight) renderEditBadge('idle-disabled');
else if (state === 'CONFIGURING' && selectedElement && hasTextRows(selectedElement)) renderEditBadge('idle');
}
updateGlobalBarState();
}
function hidePendingApplyDock() {
pendingApplyInFlight = false;
clearStoredManualApplyState();
if (pendingIntroAnimation) { pendingIntroAnimation.cancel(); pendingIntroAnimation = null; }
if (pendingDockEl) pendingDockEl.style.display = 'none';
if (pendingPillEl) {
pendingPillEl.dataset.count = '0';
pendingPillEl.style.display = 'none';
pendingPillEl.disabled = false;
pendingPillEl.setAttribute('aria-busy', 'false');
pendingPillEl.setAttribute('aria-label', 'Apply copy edits to source');
pendingPillEl.style.cursor = 'pointer';
pendingPillEl.style.filter = 'none';
pendingPillEl.style.transform = 'scale(1)';
}
if (pendingPillSpinnerEl) pendingPillSpinnerEl.style.display = 'none';
if (pendingPillLabelEl) pendingPillLabelEl.textContent = pendingApplyLabel(0);
if (pendingPillCountEl) {
pendingPillCountEl.textContent = '0';
pendingPillCountEl.style.display = 'inline-flex';
}
if (pendingTrashBtn) {
pendingTrashBtn.style.display = 'none';
pendingTrashBtn.disabled = false;
pendingTrashBtn.style.cursor = 'pointer';
pendingTrashBtn.style.opacity = '1';
}
if (pendingKeepFixingBtn) pendingKeepFixingBtn.style.display = 'none';
if (pendingRollbackBtn) pendingRollbackBtn.style.display = 'none';
refreshLiveControlsForManualApply();
}
function setPendingApplyLoading(loading, count) {
if (!pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return;
pendingApplyInFlight = loading === true;
const currentCount = count || parseInt(pendingPillEl.dataset.count || '0', 10) || 0;
if (pendingApplyInFlight) storeManualApplyState(currentCount);
else clearStoredManualApplyState();
if (pendingPillSpinnerEl) pendingPillSpinnerEl.style.display = pendingApplyInFlight ? 'inline-block' : 'none';
pendingPillLabelEl.textContent = pendingApplyInFlight
? manualApplyLoadingText(currentCount)
: pendingApplyLabel(currentCount);
pendingPillCountEl.style.display = pendingApplyInFlight ? 'none' : 'inline-flex';
pendingPillEl.disabled = pendingApplyInFlight;
pendingPillEl.setAttribute('aria-busy', pendingApplyInFlight ? 'true' : 'false');
pendingPillEl.style.cursor = pendingApplyInFlight ? 'wait' : 'pointer';
pendingPillEl.style.filter = pendingApplyInFlight ? 'brightness(0.98)' : 'none';
pendingPillEl.style.transform = 'scale(1)';
pendingTrashBtn.disabled = pendingApplyInFlight;
pendingTrashBtn.style.cursor = pendingApplyInFlight ? 'not-allowed' : 'pointer';
pendingTrashBtn.style.opacity = pendingApplyInFlight ? '0.58' : '1';
if (pendingApplyInFlight) {
if (pendingKeepFixingBtn) pendingKeepFixingBtn.style.display = 'none';
if (pendingRollbackBtn) pendingRollbackBtn.style.display = 'none';
pendingTrashBtn.style.display = 'inline-flex';
}
schedulePendingDockPosition();
refreshLiveControlsForManualApply();
}
function updatePendingCounter(currentPageCount) {
if (!pendingDockEl || !pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return;
const previousCount = parseInt(pendingPillEl.dataset.count || '0', 10);
if (!currentPageCount || currentPageCount <= 0) {
hidePendingApplyDock();
return;
}
pendingPillLabelEl.textContent = pendingApplyLabel(currentPageCount);
pendingPillCountEl.textContent = String(currentPageCount);
pendingPillEl.setAttribute('aria-label', 'Apply ' + currentPageCount + ' copy edit' + (currentPageCount === 1 ? '' : 's') + ' to source');
pendingPillEl.style.display = 'inline-flex';
pendingTrashBtn.style.display = 'inline-flex';
pendingDockEl.style.display = 'inline-flex';
pendingPillEl.dataset.count = String(currentPageCount);
if (pendingApplyInFlight || shouldResumeManualApplyLoading(currentPageCount)) setPendingApplyLoading(true, currentPageCount);
schedulePendingDockPosition();
if (previousCount <= 0) playPendingIntroAnimation();
}
function maybeShowFirstSaveToast() {
if (!firstSaveOfSession) return;
firstSaveOfSession = false;
showToast('Saved. Click "Apply copy edits" to write changes.', 4500);
}
async function fetchPendingCount() {
try {
const res = await fetch(
'http://localhost:' + PORT + '/manual-edit-stash?token=' + encodeURIComponent(TOKEN) + '&pageUrl=' + encodeURIComponent(location.pathname),
);
if (!res.ok) return;
const data = await res.json();
updatePendingCounter(data.count || 0);
} catch (err) {
console.warn('[impeccable] failed to fetch pending count:', err);
}
}
async function onPendingPillClick() {
const count = parseInt(pendingPillEl?.dataset.count || '0', 10);
if (count <= 0 || pendingApplyInFlight) return;
const ok = confirm('Apply ' + count + ' copy edit' + (count === 1 ? '' : 's') + ' to source?');
if (!ok) return;
let waitForSseCompletion = false;
resetManualApplyProgress(count);
setPendingApplyLoading(true, count);
try {
const res = await fetch(
'http://localhost:' + PORT + '/manual-edit-commit?token=' + encodeURIComponent(TOKEN) + '&pageUrl=' + encodeURIComponent(location.pathname) + '&async=1',
{ method: 'POST', keepalive: true },
);
if (!res.ok) {
const errBody = await res.json().catch(() => ({}));
throw new Error(errBody.error || ('HTTP ' + res.status));
}
const result = await res.json();
if (res.status === 202 || result.status === 'started') {
waitForSseCompletion = true;
return;
}
const remaining = remainingManualEditCount(result);
updatePendingCounter(remaining);
if (result.failed && result.failed.length > 0) {
console.warn('[impeccable] some copy edits failed:', result.failed);
showToast('Applied ' + (result.applied?.length || 0) + ', ' + result.failed.length + ' failed - see console', 5000);
} else {
const n = Array.isArray(result.applied) ? result.applied.length : (result.cleared || 0);
if (n > 0) {
showToast('Applied ' + n + ' edit' + (n === 1 ? '' : 's'), 2500);
} else {
console.warn('[impeccable] apply returned no verified edits:', result);
showToast('No edits applied - see console', 4000);
}
}
} catch (err) {
console.error('[impeccable] commit failed:', err);
showToast('Apply failed - see console', 4000);
} finally {
if (waitForSseCompletion) return;
const remainingCount = parseInt(pendingPillEl?.dataset.count || '0', 10) || 0;
if (remainingCount > 0) setPendingApplyLoading(false);
else hidePendingApplyDock();
}
}
async function onPendingTrashClick() {
const count = parseInt(pendingPillEl?.dataset.count || '0', 10);
if (count <= 0 || pendingApplyInFlight) return;
const ok = confirm('Discard ' + count + ' copy edit' + (count === 1 ? '' : 's') + ' on this page?');
if (!ok) return;
try {
const res = await fetch(
'http://localhost:' + PORT + '/manual-edit-discard?token=' + encodeURIComponent(TOKEN) + '&pageUrl=' + encodeURIComponent(location.pathname),
{ method: 'POST' },
);
if (!res.ok) throw new Error('HTTP ' + res.status);
const result = await res.json().catch(() => ({}));
const restoreFailures = restoreDiscardedManualEdits(result.entries || []);
updatePendingCounter(0);
if (restoreFailures > 0) {
showToast('Discarded ' + count + ' copy edit' + (count === 1 ? '' : 's') + ' - refresh to reset ' + restoreFailures, 4000);
} else {
showToast('Discarded ' + count + ' copy edit' + (count === 1 ? '' : 's'), 2500);
}
} catch (err) {
console.error('[impeccable] discard failed:', err);
showToast('Discard failed - see console', 4000);
}
}
function showManualApplyDecision(msg) {
const count = parseInt(pendingPillEl?.dataset.count || '0', 10) || numberOrNull(msg?.remainingCount) || 0;
pendingApplyInFlight = false;
storeManualApplyState(count, {
phase: 'repair-decision',
repairAttempt: numberOrNull(msg?.repair?.attempts) || numberOrNull(msg?.repair?.attempt) || 3,
repairMaxAttempts: numberOrNull(msg?.repair?.maxAttempts) || 3,
});
if (pendingPillSpinnerEl) pendingPillSpinnerEl.style.display = 'none';
if (pendingPillLabelEl) pendingPillLabelEl.textContent = 'Apply needs attention';
if (pendingPillCountEl) pendingPillCountEl.style.display = 'none';
if (pendingPillEl) {
pendingPillEl.disabled = true;
pendingPillEl.setAttribute('aria-busy', 'false');
pendingPillEl.style.cursor = 'default';
pendingPillEl.style.display = 'inline-flex';
}
if (pendingTrashBtn) pendingTrashBtn.style.display = 'none';
if (pendingKeepFixingBtn) pendingKeepFixingBtn.style.display = 'inline-flex';
if (pendingRollbackBtn) pendingRollbackBtn.style.display = 'inline-flex';
if (pendingDockEl) pendingDockEl.style.display = 'inline-flex';
schedulePendingDockPosition();
refreshLiveControlsForManualApply();
}
async function onPendingKeepFixingClick() {
const count = parseInt(pendingPillEl?.dataset.count || '0', 10) || numberOrNull(readStoredManualApplyState()?.count) || 0;
if (count <= 0) return;
updateManualApplyRepairState({ attempt: 1, maxAttempts: 3 }, 'repairing');
try {
const res = await fetch(
'http://localhost:' + PORT + '/manual-edit-commit?token=' + encodeURIComponent(TOKEN) + '&pageUrl=' + encodeURIComponent(location.pathname) + '&async=1&repair=1',
{ method: 'POST', keepalive: true },
);
if (!res.ok) throw new Error('HTTP ' + res.status);
if (pendingKeepFixingBtn) pendingKeepFixingBtn.style.display = 'none';
if (pendingRollbackBtn) pendingRollbackBtn.style.display = 'none';
if (pendingTrashBtn) pendingTrashBtn.style.display = 'inline-flex';
} catch (err) {
console.error('[impeccable] repair retry failed:', err);
showToast('Repair retry failed - see console', 4000);
showManualApplyDecision({ remainingCount: count, repair: readStoredManualApplyState() });
}
}
async function onPendingRollbackClick() {
const ok = confirm('Rollback source files to before this Apply and keep the edits staged?');
if (!ok) return;
try {
const res = await fetch(
'http://localhost:' + PORT + '/manual-edit-repair-decision?token=' + encodeURIComponent(TOKEN) + '&pageUrl=' + encodeURIComponent(location.pathname),
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: TOKEN, pageUrl: location.pathname, action: 'rollback' }),
},
);
if (!res.ok) throw new Error('HTTP ' + res.status);
const result = await res.json().catch(() => ({}));
clearStoredManualApplyState();
updatePendingCounter(numberOrNull(result.remainingCount) || 0);
showToast('Rolled back source; copy edits are still staged.', 3500);
} catch (err) {
console.error('[impeccable] manual Apply rollback failed:', err);
showToast('Rollback failed - see console', 4000);
}
}
function manualEditEventForCurrentPage(msg) {
return !msg?.pageUrl || msg.pageUrl === location.pathname;
}
function numberOrNull(value) {
const n = Number(value);
return Number.isFinite(n) ? n : null;
}
function remainingManualEditCount(payload) {
const perPageCount = numberOrNull(payload?.perPage?.[location.pathname]);
if (perPageCount !== null) return perPageCount;
const remainingCount = numberOrNull(payload?.remainingCount);
if (remainingCount !== null) return remainingCount;
const totalCount = numberOrNull(payload?.totalCount);
if (totalCount === 0) return 0;
return null;
}
function handleManualEditActivity(msg) {
if (!manualEditEventForCurrentPage(msg)) return;
if (msg.type === 'manual_edit_stashed') {
const pendingCount = numberOrNull(msg.pendingCount);
if (pendingCount !== null) updatePendingCounter(pendingCount);
return;
}
if (msg.type === 'manual_edit_commit_started') {
const pendingCount = numberOrNull(msg.pendingCount);
if (pendingCount !== null && pendingCount > 0) updatePendingCounter(pendingCount);
if (!msg.repairOnly && pendingCount !== null && pendingCount > 0) resetManualApplyProgress(pendingCount);
if (msg.repairOnly) updateManualApplyRepairState({ attempt: 1, maxAttempts: 3 }, 'repairing');
setPendingApplyLoading(true, pendingCount || undefined);
return;
}
if (msg.type === 'manual_edit_apply_reply_received') {
if (msg.chunk) updateManualApplyProgressFromChunk(msg.chunk);
if (msg.repair) updateManualApplyRepairState(msg.repair, 'repairing');
return;
}
if (msg.type === 'manual_edit_apply_dispatched' && msg.repair) {
updateManualApplyRepairState(msg.repair, 'repairing');
return;
}
if (msg.type === 'manual_edit_repair_needs_decision') {
showManualApplyDecision(msg);
return;
}
if (msg.type === 'manual_edit_repair_rollback_done') {
clearStoredManualApplyState();
fetchPendingCount();
return;
}
if (msg.type === 'manual_edit_commit_done') {
if (msg.reason === 'manual_edit_repair_needs_decision' || msg.needsManualDecision === true) {
showManualApplyDecision(msg);
return;
}
// Clear the in-flight flag BEFORE updating the counter. updatePendingCounter
// re-asserts setPendingApplyLoading(true) whenever the flag is still set and
// edits remain (failed entries stay staged), which would otherwise leave the
// picker frozen forever after a partial/failed apply.
const wasApplying = pendingApplyInFlight;
setPendingApplyLoading(false);
const remainingCount = remainingManualEditCount(msg);
updatePendingCounter(remainingCount === null ? 0 : remainingCount);
if (wasApplying) {
const failedCount = numberOrNull(msg.failedCount) || 0;
const appliedCount = numberOrNull(msg.appliedCount) || numberOrNull(msg.cleared) || 0;
if (failedCount > 0) {
showToast('Applied ' + appliedCount + ', ' + failedCount + ' failed - see console', 5000);
} else if (appliedCount > 0) {
showToast('Applied ' + appliedCount + ' edit' + (appliedCount === 1 ? '' : 's'), 2500);
}
}
return;
}
if (msg.type === 'manual_edit_commit_failed') {
setPendingApplyLoading(false);
fetchPendingCount();
return;
}
if (msg.type === 'manual_edit_discarded') {
fetchPendingCount();
}
}
function restoreDiscardedManualEdits(entries) {
let failures = 0;
for (const entry of entries || []) {
for (const op of entry.ops || []) {
if (restoreMixedTextNodeManualEdit(op)) continue;
const el = findManualEditRestoreElement(op);
if (!el || typeof op.originalText !== 'string' || !canRestoreManualEditElement(el, op)) {
failures += 1;
continue;
}
el.textContent = op.originalText;
}
}
if (failures > 0) {
console.warn('[impeccable] skipped unsafe copy edit DOM restore for', failures, 'edit(s). Refresh to reset the page DOM.');
}
return failures;
}
function canRestoreManualEditElement(el, op) {
if (!el || typeof op?.originalText !== 'string') return false;
if (el.children && el.children.length > 0) return false;
return normalizeManualContextText(el.textContent) === normalizeManualContextText(op.newText);
}
function mixedTextWrapRestoreHint(el) {
if (!el || !el.dataset || el.dataset.impeccableTextWrap !== 'true' || !el.parentElement) return null;
const siblings = directMixedTextRestoreNodes(el.parentElement);
const textIndex = siblings.indexOf(el);
return {
kind: 'mixedTextNode',
parentRef: documentRefForElement(el.parentElement),
textIndex,
};
}
function restoreMixedTextNodeManualEdit(op) {
const restore = op?.restore;
if (!restore || restore.kind !== 'mixedTextNode' || typeof op?.originalText !== 'string') return false;
const parent = queryManualEditRef(restore.parentRef);
if (!parent) return false;
const textNodes = directMixedTextRestoreNodes(parent).filter((node) => node.nodeType === 3);
const newText = normalizeManualContextText(op.newText);
const byIndex = textNodes[Number(restore.textIndex)];
if (byIndex && normalizeManualContextText(byIndex.nodeValue) === newText) {
byIndex.nodeValue = op.originalText;
return true;
}
const matches = textNodes.filter((node) => normalizeManualContextText(node.nodeValue) === newText);
if (matches.length !== 1) return false;
matches[0].nodeValue = op.originalText;
return true;
}
function directMixedTextRestoreNodes(parent) {
return Array.from(parent?.childNodes || []).filter((node) => {
if (node.nodeType === 3) return /\S/.test(node.nodeValue || '');
return node.nodeType === 1
&& node.dataset
&& node.dataset.impeccableTextWrap === 'true'
&& /\S/.test(node.textContent || '');
});
}
function findManualEditRestoreElement(op) {
for (const ref of [op?.ref, op?.leaf?.ref]) {
const byRef = queryManualEditRef(ref);
if (byRef) return byRef;
}
const tag = op?.tag || op?.leaf?.tagName || '*';
const classes = Array.isArray(op?.classes) ? op.classes : (Array.isArray(op?.leaf?.classes) ? op.leaf.classes : []);
const selector = (tag === '*' ? '' : tag) + classes.map((cls) => '.' + cssIdent(cls)).join('') || '*';
let matches = [];
try {
matches = Array.from(document.querySelectorAll(selector));
} catch {
matches = [];
}
const newText = normalizeManualContextText(op?.newText);
const filtered = matches.filter((el) => normalizeManualContextText(el.textContent) === newText);
return filtered.length === 1 ? filtered[0] : null;
}
function queryManualEditRef(ref) {
if (!ref || typeof ref !== 'string') return null;
const parts = ref.split('>').map((part) => part.trim()).filter(Boolean);
let current = null;
for (let index = 0; index < parts.length; index += 1) {
const segment = parseManualEditRefSegment(parts[index]);
if (!segment) return null;
if (index === 0 && segment.tag === 'body') {
current = document.body;
if (!elementMatchesManualRefSegment(current, segment)) return null;
continue;
}
const scope = current || document.body;
const children = Array.from(scope.children || []);
current = children.find((child) => elementMatchesManualRefSegment(child, segment)) || null;
if (!current) return null;
}
return current;
}
function parseManualEditRefSegment(segment) {
const nthMatch = String(segment || '').match(/:nth-of-type\((\d+)\)$/);
const nth = nthMatch ? Number(nthMatch[1]) : null;
const base = nthMatch ? segment.slice(0, nthMatch.index) : segment;
const tagMatch = base.match(/^[^#.:\s]+/);
const tag = tagMatch ? tagMatch[0].toLowerCase() : null;
if (!tag) return null;
const idMatch = base.match(/#([^#.]+)/);
const classes = base
.slice(tag.length)
.replace(/#[^#.]+/, '')
.split('.')
.filter(Boolean);
return { tag, id: idMatch ? idMatch[1] : null, classes, nth };
}
function elementMatchesManualRefSegment(el, segment) {
if (!el || !segment) return false;
if (el.tagName.toLowerCase() !== segment.tag) return false;
if (segment.id && el.id !== segment.id) return false;
for (const cls of segment.classes) {
if (!el.classList || !el.classList.contains(cls)) return false;
}
if (segment.nth && indexAmongSameTag(el) !== segment.nth) return false;
return true;
}
function cssIdent(value) {
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(String(value));
return String(value).replace(/[^a-zA-Z0-9_-]/g, '\\$&');
}
//
// Edit content badge - floating button at element top-right to enter EDITING mode
//
const EDIT_COPY_LABEL = 'Edit copy';
const EDIT_COPY_ICON =
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
'<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/>' +
'</svg>';
function usesShadowChromeRoot() {
const root = liveUiRoot();
return root && root !== document.body && root.host && root.host.id === PREFIX + '-root';
}
function setImportantStyle(el, name, value) {
el.style.setProperty(name, value, 'important');
}
function initEditBadgeHitProxies() {
if (!usesShadowChromeRoot() || editBadgeProxyRoot) return;
editBadgeProxyRoot = document.createElement('div');
editBadgeProxyRoot.id = PREFIX + '-edit-badge-hit-proxies';
editBadgeProxyRoot.setAttribute('aria-hidden', 'true');
const styles = {
all: 'initial',
position: 'fixed',
inset: '0',
width: '100vw',
height: '100vh',
zIndex: String(Z.toast + 1),
pointerEvents: 'none',
background: 'transparent',
overflow: 'visible',
};
for (const [name, value] of Object.entries(styles)) {
setImportantStyle(editBadgeProxyRoot, name.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()), value);
}
document.body.appendChild(editBadgeProxyRoot);
}
function styleEditBadgeProxy(proxy, target) {
const rect = target.getBoundingClientRect();
const cursor = getComputedStyle(target).cursor || 'pointer';
const styles = {
all: 'initial',
position: 'fixed',
left: rect.left + 'px',
top: rect.top + 'px',
width: rect.width + 'px',
height: rect.height + 'px',
margin: '0',
padding: '0',
border: '0',
borderRadius: '0',
background: 'transparent',
color: 'transparent',
opacity: '0.001',
pointerEvents: 'auto',
cursor,
zIndex: String(Z.toast + 2),
};
for (const [name, value] of Object.entries(styles)) {
setImportantStyle(proxy, name.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()), value);
}
}
function proxyMouseEvent(type, source, target) {
let event;
try {
event = new MouseEvent(type, {
bubbles: type !== 'mouseenter' && type !== 'mouseleave',
cancelable: true,
composed: true,
clientX: source.clientX,
clientY: source.clientY,
screenX: source.screenX,
screenY: source.screenY,
button: source.button || 0,
buttons: source.buttons || 0,
ctrlKey: source.ctrlKey,
metaKey: source.metaKey,
shiftKey: source.shiftKey,
altKey: source.altKey,
});
target.dispatchEvent(event);
} catch {}
}
function bindEditBadgeProxy(proxy, target) {
const stop = (event) => {
event.preventDefault();
event.stopPropagation();
};
proxy.addEventListener('mouseenter', (event) => {
stop(event);
proxyMouseEvent('mouseenter', event, target);
proxyMouseEvent('mouseover', event, target);
});
proxy.addEventListener('mouseleave', (event) => {
stop(event);
proxyMouseEvent('mouseleave', event, target);
proxyMouseEvent('mouseout', event, target);
});
proxy.addEventListener('mousedown', (event) => {
stop(event);
target.focus?.({ preventScroll: true });
proxyMouseEvent('mousedown', event, target);
});
proxy.addEventListener('mouseup', (event) => {
stop(event);
proxyMouseEvent('mouseup', event, target);
});
proxy.addEventListener('click', (event) => {
stop(event);
target.click();
syncEditBadgeHitProxies();
});
}
function editBadgeProxyTargets() {
if (!usesShadowChromeRoot() || !editBadgeEl || editBadgeEl.style.display === 'none') return [];
return [...editBadgeEl.querySelectorAll('button')].filter((target) => {
if (target.disabled) return false;
const rect = target.getBoundingClientRect();
if (rect.width < 1 || rect.height < 1) return false;
const style = getComputedStyle(target);
return style.display !== 'none' && style.visibility !== 'hidden';
});
}
function syncEditBadgeHitProxies() {
if (!usesShadowChromeRoot()) {
if (editBadgeProxyRoot) editBadgeProxyRoot.remove();
editBadgeProxyRoot = null;
editBadgeProxyByTarget = new Map();
return;
}
initEditBadgeHitProxies();
if (!editBadgeProxyRoot) return;
const targets = editBadgeProxyTargets();
const active = new Set(targets);
for (const [target, proxy] of editBadgeProxyByTarget) {
if (!active.has(target) || !target.isConnected) {
proxy.remove();
editBadgeProxyByTarget.delete(target);
}
}
for (const target of targets) {
let proxy = editBadgeProxyByTarget.get(target);
if (!proxy) {
proxy = document.createElement('button');
proxy.type = 'button';
proxy.tabIndex = -1;
proxy.dataset.impeccableEditBadgeProxy = 'true';
proxy.setAttribute('aria-hidden', 'true');
bindEditBadgeProxy(proxy, target);
editBadgeProxyRoot.appendChild(proxy);
editBadgeProxyByTarget.set(target, proxy);
}
proxy.title = target.title || target.getAttribute('aria-label') || target.textContent || EDIT_COPY_LABEL;
styleEditBadgeProxy(proxy, target);
}
}
function initEditBadge() {
editBadgeEl = document.createElement('div');
editBadgeEl.id = PREFIX + '-edit-badge';
Object.assign(editBadgeEl.style, {
position: 'fixed',
zIndex: String(Z.highlight + 1),
cursor: 'default',
display: 'none',
userSelect: 'none',
});
uiAppend(editBadgeEl);
initEditBadgeHitProxies();
// Remove focus rings on edit badge buttons + contenteditable elements
if (!uiGetById(PREFIX + '-edit-badge-focus-style')) {
const s = document.createElement('style');
s.id = PREFIX + '-edit-badge-focus-style';
s.textContent =
'#' + PREFIX + '-edit-badge button { outline: none !important; box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important; }' +
'#' + PREFIX + '-edit-badge button:focus { outline: none !important; }' +
'#' + PREFIX + '-edit-badge button:focus-visible { outline: none !important; }' +
'[data-impeccable-editable="true"] { outline: none !important; box-shadow: none !important; }' +
'[data-impeccable-editable="true"]:focus { outline: none !important; box-shadow: none !important; }' +
'[data-impeccable-editable="true"]:focus-visible { outline: none !important; box-shadow: none !important; }';
uiAppendStyle(s);
}
}
function positionEditBadge() {
if (!selectedElement || !editBadgeEl || editBadgeEl.style.display === 'none') {
syncEditBadgeHitProxies();
return;
}
const r = selectedElement.getBoundingClientRect();
const bw = editBadgeEl.offsetWidth;
// Match showHighlight's 2px outset so the badge right edge lines up with the outline.
const outlineRight = r.right + 2;
editBadgeEl.style.top = Math.max(4, r.top - 28) + 'px';
editBadgeEl.style.left = Math.min(window.innerWidth - bw - 4, outlineRight - bw) + 'px';
syncEditBadgeHitProxies();
}
function renderEditBadge(mode) {
if (mode === 'hidden' || !editBadgeEl) {
hideConfigureBarTooltip();
if (editBadgeEl) editBadgeEl.style.display = 'none';
syncEditBadgeHitProxies();
return;
}
editBadgeEl.style.display = 'flex';
editBadgeEl.style.alignItems = 'center';
editBadgeEl.style.cursor = 'default';
const P = BP || barPaletteForTheme(detectPageTheme());
const ACCENT = P.accent;
const PRIMARY_TEXT = C.ink;
const SURFACE = P.chatSurface;
const MUTED = P.textDim;
const HAIRLINE = P.hairline;
const calloutStyle = (color, borderColor) => ({
fontFamily: FONT,
fontSize: '10px',
fontWeight: '600',
lineHeight: '16px',
letterSpacing: '0.06em',
color: color,
background: SURFACE,
padding: '2px 8px',
border: '1px solid ' + (borderColor || color),
borderRadius: '6px',
boxSizing: 'border-box',
minHeight: '22px',
margin: '0',
appearance: 'none',
whiteSpace: 'nowrap',
boxShadow: '0 4px 16px oklch(0% 0 0 / 0.16), 0 1px 3px oklch(0% 0 0 / 0.08)',
cursor: 'pointer',
transition: 'background 0.18s ease, color 0.18s ease, border-color 0.18s ease, filter 0.18s ease',
});
if (mode === 'idle' || mode === 'idle-disabled') {
const disabled = mode === 'idle-disabled';
editBadgeEl.innerHTML = '';
const btn = document.createElement('button');
btn.type = 'button';
btn.innerHTML = EDIT_COPY_ICON;
btn.setAttribute('aria-label', EDIT_COPY_LABEL);
Object.assign(btn.style, calloutStyle(
disabled ? MUTED : PRIMARY_TEXT,
disabled ? HAIRLINE : ACCENT,
));
Object.assign(btn.style, {
padding: '4px',
minWidth: '22px',
width: '22px',
height: '22px',
minHeight: '22px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
lineHeight: '0',
letterSpacing: '0',
background: disabled ? SURFACE : ACCENT,
});
if (disabled) {
btn.style.cursor = 'not-allowed';
btn.style.opacity = '0.55';
btn.disabled = true;
const disabledTip = EDIT_COPY_LABEL + ' is disabled while the current copy edit is applying';
btn.addEventListener('mouseenter', () => showConfigureBarTooltip(btn, disabledTip));
btn.addEventListener('mouseleave', hideConfigureBarTooltip);
} else {
btn.addEventListener('mouseenter', () => showConfigureBarTooltip(btn, EDIT_COPY_LABEL));
btn.addEventListener('mouseleave', hideConfigureBarTooltip);
btn.onclick = enterEditingMode;
}
editBadgeEl.appendChild(btn);
} else {
// 'editing' - show Cancel + Save separated
editBadgeEl.innerHTML = '';
editBadgeEl.style.gap = '8px';
const cancel = document.createElement('button');
cancel.textContent = 'Cancel';
Object.assign(cancel.style, calloutStyle(MUTED, HAIRLINE));
cancel.addEventListener('mouseenter', () => { cancel.style.color = P.text; });
cancel.addEventListener('mouseleave', () => { cancel.style.color = P.textDim; });
cancel.onclick = cancelEditing;
const save = document.createElement('button');
save.textContent = 'Save';
Object.assign(save.style, calloutStyle(PRIMARY_TEXT, ACCENT));
save.style.background = ACCENT;
save.onclick = applyEditing;
editBadgeEl.append(cancel, save);
}
positionEditBadge();
}
// Decide which way the popover opens: away from the picked element. If the
// bar landed below the element, popover slides DOWN from the bar's bottom.
// If the bar landed above, popover slides UP from the bar's top.
function popoverDirection() {
if (!barEl || !selectedElement) return 'below';
const br = barEl.getBoundingClientRect();
const er = selectedElement.getBoundingClientRect();
return br.top >= er.bottom - 4 ? 'below' : 'above';
}
// The popover overlaps the bar by OVERLAP px on the bar-facing side. With
// popover z-index below bar, that overlap sits behind bar (invisible) and
// reinforces the "tucked behind" feel. Padding compensates so the real
// content starts flush with bar's outer edge.
const TUNE_OVERLAP = 6;
// Closed clip-path depends on direction: for 'below' clip from the far
// (bottom) edge so the reveal grows downward from the bar; for 'above'
// clip from the top edge so the reveal grows upward from the bar.
function closedClipPath(direction) {
return direction === 'below' ? 'inset(0 0 100% 0)' : 'inset(100% 0 0 0)';
}
function setClipPath(value, withTransition) {
const saved = paramsPanelEl.style.transition;
if (!withTransition) paramsPanelEl.style.transition = 'none';
paramsPanelEl.style.clipPath = value;
if (!withTransition) {
void paramsPanelEl.offsetHeight;
paramsPanelEl.style.transition = saved;
}
}
function positionParamsPanel() {
if (!paramsPanelEl || !barEl || barEl.style.display === 'none') return;
const br = barEl.getBoundingClientRect();
const direction = popoverDirection();
const prevDirection = paramsPanelEl.dataset.tuneDirection;
// top/left/width are NOT in the transition list, so they snap instantly.
paramsPanelEl.style.left = br.left + 'px';
paramsPanelEl.style.width = br.width + 'px';
if (direction === 'below') {
paramsPanelEl.style.top = (br.bottom - TUNE_OVERLAP) + 'px';
paramsPanelEl.style.borderRadius = '0 0 10px 10px';
paramsPanelEl.style.paddingTop = (14 + TUNE_OVERLAP) + 'px';
paramsPanelEl.style.paddingBottom = '14px';
} else {
const ih = paramsPanelEl.offsetHeight || 80;
paramsPanelEl.style.top = (br.top - ih + TUNE_OVERLAP) + 'px';
paramsPanelEl.style.borderRadius = '10px 10px 0 0';
paramsPanelEl.style.paddingTop = '14px';
paramsPanelEl.style.paddingBottom = (14 + TUNE_OVERLAP) + 'px';
}
paramsPanelEl.dataset.tuneDirection = direction;
// If currently closed and direction flipped (or first-time setup),
// snap the clip-path to the new direction's closed pose without
// transitioning (so the clip doesn't slide across the element).
if (!tuneOpen && (!prevDirection || prevDirection !== direction)) {
setClipPath(closedClipPath(direction), false);
}
}
function showParamsPanel() {
if (!paramsPanelEl) return;
positionParamsPanel();
paramsPanelEl.style.pointerEvents = 'auto';
// rAF so the positioning paint commits before the transition fires.
requestAnimationFrame(() => {
setClipPath('inset(0 0 0 0)', true);
});
}
function hideParamsPanel() {
if (!paramsPanelEl) return;
paramsPanelEl.style.pointerEvents = 'none';
const direction = paramsPanelEl.dataset.tuneDirection || 'below';
setClipPath(closedClipPath(direction), true);
}
// Build/rebuild the panel's contents for the current variant AND apply
// its defaults to the variant wrapper (so scoped CSS responds even before
// the user opens the popover). Visibility is governed by tuneOpen.
function refreshParamsPanel() {
if (state !== 'CYCLING') {
paramsCurrentValues = {};
tuneOpen = false;
hideParamsPanel();
return;
}
const variantEl = getVisibleVariantEl();
const params = parseVariantParams(variantEl);
if (!variantEl || params.length === 0) {
paramsCurrentValues = {};
tuneOpen = false;
hideParamsPanel();
if (currentSessionId && visibleVariant) updateVariantStateStylesheet(currentSessionId, visibleVariant);
return;
}
applyParamDefaults(variantEl, params);
buildParamsPanel(variantEl, params);
if (tuneOpen) {
// If already visible (variant cycled while open), refresh in place
// instead of re-running the clip-path animation.
const alreadyVisible = paramsPanelEl.style.display === 'block'
&& paramsPanelEl.style.opacity === '1';
if (alreadyVisible) positionParamsPanel();
else showParamsPanel();
} else {
hideParamsPanel();
}
}
function mountedParameterCount() {
if (svelteComponentSession?.sessionId === currentSessionId) {
return Object.values(svelteComponentSession.paramsByVariant || {})
.reduce((total, params) => total + (Array.isArray(params) ? params.length : 0), 0);
}
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (!wrapper) return 0;
return [...wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])')]
.reduce((total, variant) => total + parseVariantParams(variant).length, 0);
}
function completeParameterPublication() {
if (!currentSessionId) return;
const ready = mountedParameterCount() > 0;
parameterGenerationState = ready ? 'ready' : 'none';
if (ready && parameterReadyAnnouncedSession !== currentSessionId) {
parameterReadyAnnouncedSession = currentSessionId;
showToast('Tune controls are ready.', 3000);
}
if (state === 'CYCLING') {
refreshParamsPanel();
showOrUpdateCyclingBar();
}
saveSession();
}
function toggleTunePopover() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
if (tuneOpen) { closeTunePopover(); return; }
openTunePopover();
}
function openTunePopover() {
if (state !== 'CYCLING') return;
const variantEl = getVisibleVariantEl();
const params = parseVariantParams(variantEl);
if (!variantEl || params.length === 0) return;
// Build fresh to ensure the current variant's controls are shown.
applyParamDefaults(variantEl, params);
buildParamsPanel(variantEl, params);
tuneOpen = true;
showParamsPanel();
// Kill the bar's shadow on the popover-facing side so the dark popover
// doesn't pick up a bright glow line.
if (barEl) {
const direction = paramsPanelEl?.dataset.tuneDirection || 'below';
barEl.style.boxShadow = direction === 'below' ? BAR_SHADOW_UP : BAR_SHADOW_DOWN;
}
// Re-render the bar so the Tune chip picks up the active styling.
showOrUpdateCyclingBar();
}
function closeTunePopover() {
tuneOpen = false;
hideParamsPanel();
if (barEl) barEl.style.boxShadow = BAR_SHADOW_DEFAULT;
if (barEl && barEl.style.display !== 'none' && state === 'CYCLING') {
showOrUpdateCyclingBar();
}
}
//
// Variant cycling in DOM
//
function isVariantShown(el) {
if (!el) return false;
return getComputedStyle(el).display !== 'none';
}
function scheduleCyclingBarSync(sessionId, variantNum) {
requestAnimationFrame(() => {
if (state !== 'CYCLING') return;
if (currentSessionId !== sessionId) return;
if (visibleVariant !== variantNum) return;
showOrUpdateCyclingBar();
syncCyclingControls();
positionBar();
});
}
function syncCyclingControls() {
const shown = cyclingShownVariant();
const counter = uiGetById(PREFIX + '-variant-counter');
if (counter) counter.textContent = cyclingCounterText();
const prev = uiGetById(PREFIX + '-variant-prev');
const next = uiGetById(PREFIX + '-variant-next');
if (prev) prev.style.opacity = shown <= 1 ? '0.3' : '1';
if (next) next.style.opacity = shown >= arrivedVariants ? '0.3' : '1';
if (currentSessionId && state === 'CYCLING') saveSession();
}
async function showVariantInDOM(sessionId, num) {
if (svelteComponentSession?.sessionId === sessionId) {
visibleVariant = num;
const mounted = await mountSvelteComponentVariant(num);
if (!mounted) return false;
updateSelectedElement();
refreshParamsPanel();
scheduleCyclingBarSync(sessionId, num);
return true;
}
const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!wrapper) return false;
updateVariantStateStylesheet(sessionId, num);
// Unconditional refresh - covers first-reveal (no-op if state isn't
// CYCLING yet, the subsequent CYCLING transition triggers its own
// refresh) and every cycle step.
refreshParamsPanel();
return true;
}
function isSvelteComponentManifestPath(filePath) {
return String(filePath || '').endsWith('manifest.json');
}
function isFrameworkComponentPreviewMode(mode) {
return mode === 'svelte-component';
}
function parseOriginalMarkupElement(originalMarkup) {
const parser = new DOMParser();
const doc = parser.parseFromString('<div id="impeccable-anchor">' + originalMarkup + '</div>', 'text/html');
return doc.getElementById('impeccable-anchor')?.firstElementChild || null;
}
function normalizeElementClassName(el) {
if (!el) return '';
const raw = el.getAttribute?.('class');
if (typeof raw === 'string') return raw.trim();
if (el.className != null) {
const cls = el.className;
if (typeof cls === 'string') return cls.trim();
if (typeof cls.baseVal === 'string') return cls.baseVal.trim();
}
return '';
}
function buildPickedAnchorSnapshot(el) {
if (!el || el.nodeType !== 1) return null;
return {
tag: el.tagName,
id: el.id || '',
classes: [...el.classList],
text: (el.textContent || '').trim().slice(0, 120),
};
}
function isUsableInjectionAnchor(el) {
return !!el
&& el.parentElement
&& document.body.contains(el)
&& !own(el)
&& !el.closest?.('[data-impeccable-variants]');
}
function elementMatchesOriginalMarkup(liveEl, origContent) {
if (!isUsableInjectionAnchor(liveEl) || !origContent) return false;
// A matching id is decisive on its own: ids are unique, while the source
// tag and class names may not survive the build (component tags, hashed
// CSS-module class names).
if (origContent.id) return liveEl.id === origContent.id;
if (liveEl.tagName !== origContent.tagName) return false;
const origClasses = normalizeElementClassName(origContent).split(/\s+/).filter(Boolean)
.filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
if (origClasses.length > 0 && !origClasses.every((name) => liveEl.classList.contains(name))) return false;
const origText = (origContent.textContent || '').trim();
if (origClasses.length === 0 && origText.length >= 4) {
const liveText = (liveEl.textContent || '').trim();
const needle = origText.slice(0, Math.min(40, origText.length));
if (!liveText.includes(needle) && !(liveText.length >= 4 && origText.includes(liveText.slice(0, 40)))) return false;
}
return true;
}
function findLiveElementFromAnchorSnapshot(snapshot) {
if (!snapshot) return null;
const tag = String(snapshot.tag || '').toLowerCase();
if (!tag) return null;
if (snapshot.id) {
const byId = document.getElementById(snapshot.id);
if (isUsableInjectionAnchor(byId)) return byId;
}
const classes = (snapshot.classes || []).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
const needle = (snapshot.text || '').trim();
const candidates = [...document.getElementsByTagName(tag)];
for (const c of candidates) {
if (!isUsableInjectionAnchor(c)) continue;
if (classes.length > 0 && !classes.every((name) => c.classList.contains(name))) continue;
if (!snapshot.id && classes.length === 0 && needle.length >= 4) {
const text = (c.textContent || '').trim();
if (!text.includes(needle.slice(0, 40)) && !(text.length >= 4 && needle.includes(text.slice(0, 40)))) continue;
}
return c;
}
return null;
}
function findLiveElementForOriginalMarkup(originalMarkup) {
const origContent = parseOriginalMarkupElement(originalMarkup);
if (!origContent) return null;
const tag = origContent.tagName.toLowerCase();
const cls = normalizeElementClassName(origContent);
const candidates = [...document.getElementsByTagName(tag)];
if (origContent.id) {
const byId = document.getElementById(origContent.id);
if (elementMatchesOriginalMarkup(byId, origContent)) return byId;
}
if (cls) {
const expectedClasses = cls.split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
if (expectedClasses.length > 0) {
for (const c of candidates) {
if (!isUsableInjectionAnchor(c)) continue;
if (expectedClasses.every((name) => c.classList.contains(name))) return c;
}
}
}
const origText = (origContent.textContent || '').trim();
if (origText.length >= 4) {
const needle = origText.slice(0, 40);
let best = null;
let bestLen = Infinity;
for (const c of candidates) {
if (!isUsableInjectionAnchor(c)) continue;
const text = (c.textContent || '').trim();
if (!text.includes(needle) && !(text.length >= 4 && origText.includes(text.slice(0, 40)))) continue;
if (text.length < bestLen) { best = c; bestLen = text.length; }
}
if (best) return best;
}
return null;
}
function resolveLiveInjectionAnchor(originalMarkup) {
const origContent = parseOriginalMarkupElement(originalMarkup);
if (!origContent) return null;
const attempts = [
selectedElement,
findLiveElementFromAnchorSnapshot(pickedAnchorSnapshot),
findLiveElementForOriginalMarkup(originalMarkup),
];
for (const candidate of attempts) {
if (elementMatchesOriginalMarkup(candidate, origContent)) return candidate;
}
if (isUsableInjectionAnchor(selectedElement) && selectedElement.tagName === origContent.tagName) {
const origClasses = normalizeElementClassName(origContent).split(/\s+/).filter(Boolean);
if (origContent.id && selectedElement.id === origContent.id) return selectedElement;
if (origClasses.length === 0) return selectedElement;
const overlap = origClasses.filter((name) => selectedElement.classList.contains(name));
if (overlap.length >= 1) return selectedElement;
}
return null;
}
function isSvelteInsertManifest(manifest) {
return manifest?.previewMode === 'svelte-component' && manifest?.mode === 'insert';
}
function findLiveElementForSvelteManifest(manifest) {
if (isSvelteInsertManifest(manifest)) {
const anchor = findInsertAnchorInDom();
if (anchor?.parentElement) return anchor;
}
return resolveLiveInjectionAnchor(manifest?.originalMarkup || manifest?.anchorMarkup || '');
}
function waitForVariantAnchorAndRetry({ filePath, sessionId, srcWrapper, checkpointReason }) {
if (pendingVariantAnchorRetryObserver) pendingVariantAnchorRetryObserver.disconnect();
const origContent = srcWrapper?.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const originalMarkup = origContent.outerHTML;
pendingVariantAnchorRetryObserver = new MutationObserver(() => {
// Retry once either the anchor element or the session wrapper shows up.
// A wrapper can land incomplete ("wrap HMR landed, variant insert did
// not"); injectVariantsFromSource owns both cases - it replaces an
// existing wrapper from source and clears recoveryWaitingForAnchor.
const wrapperLanded = !!document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!wrapperLanded) {
const liveEl = resolveLiveInjectionAnchor(originalMarkup);
if (!liveEl?.parentElement) return;
}
pendingVariantAnchorRetryObserver.disconnect();
pendingVariantAnchorRetryObserver = null;
injectVariantsFromSource(filePath, sessionId);
});
pendingVariantAnchorRetryObserver.observe(document.body, { childList: true, subtree: true });
if (checkpointReason) queueCheckpoint(checkpointReason);
}
function enterRecoveryWaitingForAnchor({ filePath, sessionId, srcWrapper, checkpointReason, trackScroll }) {
recoveryWaitingForAnchor = true;
selectedElement = document.body;
setLiveState('GENERATING');
showBar('generating');
if (trackScroll !== false) startScrollTracking();
saveSession();
if (srcWrapper && filePath && sessionId) {
waitForVariantAnchorAndRetry({ filePath, sessionId, srcWrapper, checkpointReason });
} else if (checkpointReason) {
queueCheckpoint(checkpointReason);
}
}
// The dev server may serve under a non-root base (vite `base`) or a root
// that differs from where the helper wrote the preview tree. Root-relative
// URLs are tried against the detected base first; the /@fs/ absolute form
// is the fallback that works regardless of base and root, as long as the
// path is inside the server's fs.allow.
let detectedDevBase = null;
function detectDevServerBase() {
if (detectedDevBase !== null) return detectedDevBase;
detectedDevBase = '/';
const scripts = document.querySelectorAll('script[type="module"][src]');
for (const script of scripts) {
const src = script.getAttribute('src') || '';
const idx = src.indexOf('/@vite/client');
if (idx > 0) { detectedDevBase = src.slice(0, idx) + '/'; break; }
if (idx === 0) { detectedDevBase = '/'; break; }
}
return detectedDevBase;
}
function componentModuleCandidates(manifest, modulePath, absPath) {
const base = detectDevServerBase();
const rel = String(modulePath || '').replace(/^\/+/, '');
const candidates = [new URL(base + rel, location.origin).href];
if (base !== '/') candidates.push(new URL('/' + rel, location.origin).href);
if (absPath) {
const fsRel = '@fs/' + String(absPath).replace(/^\/+/, '');
candidates.push(new URL(base + fsRel, location.origin).href);
// Vite versions differ on whether @fs is served under base or at the
// server root; with a non-root base, try both.
if (base !== '/') candidates.push(new URL('/' + fsRel, location.origin).href);
}
return candidates;
}
async function importFirstReachable(candidates, bust) {
let lastErr = null;
for (const candidate of candidates) {
try {
const url = bust ? candidate + (candidate.includes('?') ? '&' : '?') + 't=' + Date.now() : candidate;
const mod = await import(/* @vite-ignore */ url);
return { mod, url: candidate };
} catch (err) {
lastErr = err;
}
}
throw Object.assign(lastErr || new Error('no module candidates'), {
impeccableTriedUrls: candidates,
});
}
// Distinguishes "this variant is broken" from "the preview tree is not
// reachable from the dev server at all" (wrong root, unserved directory).
async function probePreviewTree(manifest) {
if (!manifest?.probeModule) return { ok: true, skipped: true };
const candidates = componentModuleCandidates(manifest, manifest.probeModule, manifest.probeModuleAbs);
try {
await importFirstReachable(candidates, false);
return { ok: true };
} catch (err) {
return { ok: false, tried: err.impeccableTriedUrls || candidates };
}
}
function loadSvelteRuntime(runtimeModule, manifest) {
const modulePath = runtimeModule || '/src/lib/impeccable/__runtime.js';
if (!svelteRuntimePromise) {
const candidates = componentModuleCandidates(manifest, modulePath, manifest?.runtimeModuleAbs);
svelteRuntimePromise = importFirstReachable(candidates, false).then((r) => r.mod);
}
return svelteRuntimePromise;
}
// Svelte component variants declare their params in a sidecar params.json under
// componentDir (keyed by variant number), because a `data-impeccable-params`
// attribute with JSON braces can't survive the Svelte compiler. Returns a map of
// { "1": [...params], "2": [...] }; an empty object when the agent declared none.
async function loadSvelteComponentParams(manifest) {
const dir = String(manifest?.revisionDir || manifest?.componentDir || '').replace(/^\/+/, '');
if (!dir) return {};
const paramsPath = dir + '/params.json';
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(paramsPath);
try {
const res = await fetch(url);
if (!res.ok) return {};
const parsed = JSON.parse(await res.text());
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
const out = {};
for (const [key, value] of Object.entries(parsed)) {
if (Array.isArray(value)) out[String(key)] = value;
}
return out;
} catch {
return {};
}
}
// NOTE: the compiled component imported from the dev server already carries
// its own scoped styles (vite-plugin-svelte injects them on module
// evaluation). The old second injection re-fetched the raw source through
// the helper and re-prefixed every selector un-hashed, so the same rules
// applied twice with different specificity: preview and accepted cascades
// disagreed. The single compiled copy is the truth now.
function removeSvelteComponentVariantStyle(session = svelteComponentSession) {
const style = session?.styleEl;
if (style?.parentNode) style.parentNode.removeChild(style);
if (session) session.styleEl = null;
}
function scopeCssBlock(css, prefix) {
let out = '';
let i = 0;
while (i < css.length) {
const open = css.indexOf('{', i);
if (open === -1) {
out += css.slice(i);
break;
}
const semi = css.indexOf(';', i);
if (semi !== -1 && semi < open) {
out += css.slice(i, semi + 1);
i = semi + 1;
continue;
}
const prelude = css.slice(i, open).trim();
const close = findMatchingCssBrace(css, open);
if (close === -1) {
out += css.slice(i);
break;
}
const body = css.slice(open + 1, close);
if (shouldScopeNestedCssAtRule(prelude)) {
out += prelude + ' {\n' + scopeCssBlock(body, prefix) + '\n}';
} else if (prelude.startsWith('@')) {
out += prelude + ' {' + body + '}';
} else {
out += prefixCssSelectors(prelude, prefix) + ' {' + body + '}';
}
i = close + 1;
}
return out;
}
function shouldScopeNestedCssAtRule(prelude) {
return /^@(media|supports|container|layer)\b/i.test(prelude || '');
}
function findMatchingCssBrace(css, openIndex) {
let depth = 0;
let quote = '';
for (let i = openIndex; i < css.length; i++) {
const ch = css[i];
const prev = css[i - 1];
if (quote) {
if (ch === quote && prev !== '\\') quote = '';
continue;
}
if (ch === '"' || ch === "'") {
quote = ch;
} else if (ch === '{') {
depth++;
} else if (ch === '}') {
depth--;
if (depth === 0) return i;
}
}
return -1;
}
function prefixCssSelectors(prelude, prefix) {
return splitCssSelectorList(prelude)
.map((selector) => {
const s = unwrapSvelteGlobalSelector(selector.trim());
if (!s) return '';
if (s.startsWith(prefix.trim())) return s;
if (s.startsWith(':host')) return s.replace(/^:host\b/, prefix.trim());
return prefix + s;
})
.filter(Boolean)
.join(', ');
}
function splitCssSelectorList(selectorList) {
const selectors = [];
let start = 0;
let depth = 0;
let quote = '';
for (let i = 0; i < selectorList.length; i++) {
const ch = selectorList[i];
const prev = selectorList[i - 1];
if (quote) {
if (ch === quote && prev !== '\\') quote = '';
continue;
}
if (ch === '"' || ch === "'") {
quote = ch;
} else if (ch === '(' || ch === '[') {
depth++;
} else if ((ch === ')' || ch === ']') && depth > 0) {
depth--;
} else if (ch === ',' && depth === 0) {
selectors.push(selectorList.slice(start, i));
start = i + 1;
}
}
selectors.push(selectorList.slice(start));
return selectors;
}
function unwrapSvelteGlobalSelector(selector) {
return selector.replace(/:global\(([^()]*)\)/g, '$1');
}
function buildSveltePropValuesFromLiveElement(liveEl, manifest) {
const contract = manifest?.propContract || [];
const values = {};
if (!liveEl || contract.length === 0) return values;
if (Number(manifest.contractVersion) === 2) {
return buildSveltePropValuesV2(liveEl, manifest);
}
const sourceOriginal = parseOriginalMarkupElement(manifest.originalMarkup || '');
if (!sourceOriginal) return values;
const map = buildSvelteExpressionTextMap(sourceOriginal, liveEl);
for (const entry of contract) {
const token = entry.previewToken || ('{' + entry.expr + '}');
values[entry.prop] = map.get(token) || '';
}
return values;
}
// Contract v2 hydration. The scaffolder preserved control flow, so props
// come in kinds: `collection` hydrates from the live DOM's rendered items
// (count by the item root selector, texts by slot order), `condition` from
// whether the branch's probe element is currently rendered, `text` from the
// v1 index-zip run over the markup WITH control-flow regions stripped and
// the live tree WITH item elements excluded, so loop tokens can never shift
// slots again. `handler` props keep their no-op defaults.
function buildSveltePropValuesV2(liveEl, manifest) {
const contract = manifest.propContract || [];
const values = {};
const itemElsByProp = new Map();
for (const entry of contract) {
if (entry.kind === 'collection' && entry.item && entry.item.rootTag) {
const selector = entry.item.rootTag + (entry.item.rootClasses || []).map((c) => '.' + cssEscapeIdent(c)).join('');
let matches = [];
try { matches = Array.from(liveEl.querySelectorAll(selector)); } catch { matches = []; }
itemElsByProp.set(entry.prop, matches);
const statics = new Set((entry.item.staticTexts || []).map((t) => String(t).trim()));
const slots = entry.item.textSlots || [];
values[entry.prop] = matches.map((itemEl, index) => {
const texts = collectVisibleTexts(itemEl).filter((t) => !statics.has(t));
const item = {};
slots.forEach((slot, i) => { item[slot.key] = texts[i] != null ? texts[i] : ''; });
// Attribute-bound values (href={link.href}) hydrate from the
// rendered attribute on the live item element or a descendant.
for (const slot of entry.item.attrSlots || []) {
if (item[slot.key] != null || !slot.tag) continue;
const sel = slot.tag + (slot.classes || []).map((c) => '.' + cssEscapeIdent(c)).join('');
let el = null;
try { el = itemEl.matches(sel) ? itemEl : itemEl.querySelector(sel); } catch { el = null; }
const value = el ? el.getAttribute(slot.attr) : null;
if (value != null) item[slot.key] = value;
}
// Keyed each: the key field is never rendered, so hydrate it with a
// unique per-index value or Svelte throws each_key_duplicate.
if (entry.item.keyField && item[entry.item.keyField] == null) {
item[entry.item.keyField] = 'impeccable-live-' + index;
}
return item;
});
} else if (entry.kind === 'condition') {
if (entry.probe && entry.probe.tag) {
const selector = entry.probe.tag + (entry.probe.classes || []).map((c) => '.' + cssEscapeIdent(c)).join('');
try { values[entry.prop] = !!liveEl.querySelector(selector); } catch { /* keep default */ }
} else if (entry.probe && entry.probe.className) {
// class:name directive: the live DOM answers directly, either on
// the picked element itself or on a descendant carrying the class.
try {
values[entry.prop] = liveEl.classList.contains(entry.probe.className)
|| !!liveEl.querySelector('.' + cssEscapeIdent(entry.probe.className));
} catch { /* keep default */ }
}
}
}
// Text props outside control flow: strip block regions from the source
// markup, exclude live text nodes inside any hydrated item element, then
// run the existing zip.
const textEntries = contract.filter((e) => e.kind === 'text' || e.kind === 'raw');
if (textEntries.length > 0) {
const strippedMarkup = stripSvelteBlockRegions(manifest.originalMarkup || '');
const sourceOriginal = parseOriginalMarkupElement(strippedMarkup);
if (sourceOriginal) {
const excluded = [];
for (const els of itemElsByProp.values()) excluded.push(...els);
const filteredLive = cloneWithoutElements(liveEl, excluded);
const map = buildSvelteExpressionTextMap(sourceOriginal, filteredLive);
for (const entry of textEntries) {
const token = '{' + entry.expr + '}';
if (map.has(token)) values[entry.prop] = map.get(token) || '';
}
}
}
return values;
}
function cssEscapeIdent(value) {
try { return CSS.escape(value); } catch { return String(value).replace(/[^a-zA-Z0-9_-]/g, ''); }
}
function collectVisibleTexts(rootEl) {
const texts = [];
const walker = document.createTreeWalker(rootEl, NodeFilter.SHOW_TEXT);
let node;
while ((node = walker.nextNode())) {
const trimmed = String(node.textContent || '').trim();
if (trimmed) texts.push(trimmed);
}
return texts;
}
// Remove balanced {#each}...{/each} and {#if}...{/if} regions (including
// the delimiters) from a markup string. Nesting-aware. {#key} blocks keep
// their CONTENT (it always renders) but lose their delimiter tokens, which
// would otherwise consume live text slots in the zip and shift every
// following expression.
function stripSvelteBlockRegions(markup) {
let out = String(markup || '');
out = stripSvelteKeyDelimiters(out);
for (const kind of ['each', 'if']) {
const open = '{#' + kind;
const close = '{/' + kind + '}';
for (;;) {
const start = out.indexOf(open);
if (start === -1) break;
let depth = 0;
let i = start;
let end = -1;
while (i < out.length) {
if (out.startsWith(open, i)) { depth++; i += open.length; continue; }
if (out.startsWith(close, i)) {
depth--;
i += close.length;
if (depth === 0) { end = i; break; }
continue;
}
i++;
}
if (end === -1) break;
out = out.slice(0, start) + out.slice(end);
}
}
return out;
}
function stripSvelteKeyDelimiters(markup) {
let out = String(markup || '');
for (;;) {
const start = out.indexOf('{#key');
if (start === -1) break;
// The opening tag runs to its matching close brace (expressions inside
// may nest braces).
let depth = 0;
let i = start;
let openEnd = -1;
while (i < out.length) {
if (out[i] === '{') depth++;
else if (out[i] === '}') {
depth--;
if (depth === 0) { openEnd = i + 1; break; }
}
i++;
}
if (openEnd === -1) break;
out = out.slice(0, start) + out.slice(openEnd);
}
return out.split('{/key}').join('');
}
function cloneWithoutElements(rootEl, excludedEls) {
if (!excludedEls || excludedEls.length === 0) return rootEl;
const excludedSet = new Set(excludedEls);
// Mark originals, clone, then strip marked clones: identity does not
// survive cloneNode, attributes do.
const MARK = 'data-impeccable-hydration-excluded';
for (const el of excludedSet) { try { el.setAttribute(MARK, '1'); } catch { /* detached */ } }
let clone;
try {
clone = rootEl.cloneNode(true);
clone.querySelectorAll('[' + MARK + ']').forEach((el) => el.remove());
} finally {
for (const el of excludedSet) { try { el.removeAttribute(MARK); } catch { /* detached */ } }
}
return clone || rootEl;
}
async function mountSvelteComponentVariant(variantNum) {
if (!svelteComponentSession || !variantNum) return false;
const { manifest, mountTargetEl, sessionId } = svelteComponentSession;
// Resolved before the first await so the failure report can name the module
// the browser could not reach, whichever step threw.
const extension = manifest.componentExtension || 'svelte';
// Prefer the server-stamped revision dir: its path changes on every
// publish, which is what defeats stale transform caches for files the
// dev server does not watch.
const dirRel = manifest.revisionDir || manifest.componentDir || '';
const dirAbs = manifest.revisionDirAbs || manifest.componentDirAbs || null;
const moduleBase = manifest.componentModuleBase
|| ('/' + String(dirRel).replace(/^\/+/, ''));
const modulePath = String(moduleBase).replace(/\/+$/, '') + '/v' + variantNum + '.' + extension;
const moduleAbs = dirAbs
? String(dirAbs).replace(/\/+$/, '') + '/v' + variantNum + '.' + extension
: null;
const candidates = componentModuleCandidates(manifest, modulePath, moduleAbs);
let moduleUrl = candidates[0];
try {
const previousAnchor = getMountedSvelteComponentAnchor(svelteComponentSession) || selectedElement;
svelteComponentSession.swapAnchor = makeFrozenAnchor(previousAnchor) || svelteComponentSession.swapAnchor || null;
const runtime = await loadSvelteRuntime(manifest.runtimeModule, manifest);
const imported = await importFirstReachable(candidates, true);
moduleUrl = imported.url;
const mod = imported.mod;
const Component = mod.default;
if (svelteComponentSession.mountedInstance && runtime.unmount) {
await runtime.unmount(svelteComponentSession.mountedInstance);
svelteComponentSession.mountedInstance = null;
}
svelteComponentSession.mountedInstance = runtime.mount(Component, {
target: mountTargetEl,
props: { ...svelteComponentSession.propValues },
intro: false,
});
svelteComponentSession.mountedVariant = variantNum;
svelteComponentSession.runtime = runtime;
removeSvelteComponentVariantStyle(svelteComponentSession);
if (state === 'CYCLING') syncCyclingControls();
const nextAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
if (nextAnchor) {
if (!isSvelteInsertManifest(manifest)) {
applyOriginalAttrsToSvelteAnchor(nextAnchor, manifest.originalMarkup || '');
}
svelteComponentSession.swapAnchor = null;
selectedElement = nextAnchor;
} else {
requestAnimationFrame(() => {
if (svelteComponentSession?.sessionId !== sessionId) return;
const settledAnchor = getMountedSvelteComponentAnchor(svelteComponentSession);
if (!settledAnchor) return;
if (!isSvelteInsertManifest(manifest)) {
applyOriginalAttrsToSvelteAnchor(settledAnchor, manifest.originalMarkup || '');
}
svelteComponentSession.swapAnchor = null;
selectedElement = settledAnchor;
});
}
// Render truth, not publish truth: this is the only point in the whole
// pipeline that proves the user can see variant N.
reportVariantMounted(sessionId, variantNum, moduleUrl);
if (mountErrorState?.sessionId === sessionId && mountErrorState.variant === variantNum) {
clearMountErrorCard();
}
return true;
} catch (err) {
if (svelteComponentSession?.sessionId === sessionId) {
svelteComponentSession.swapAnchor = null;
}
console.error('[impeccable] Failed to mount component variant ' + variantNum + ' for ' + sessionId + ':', err);
reportVariantMountFailed(sessionId, variantNum, moduleUrl, err);
// Every mount failure gets the card, so the variant-switch path (which
// used to revert with no feedback whatsoever) says what broke too.
showMountErrorCard(sessionId, {
variant: variantNum,
url: moduleUrl,
message: await describeMountFailure(manifest, err),
});
return false;
}
}
// Distinguishes a broken variant from an unreachable preview tree; the
// recovery differs (fix the component vs fix the root/dev-server pair).
async function describeMountFailure(manifest, err) {
try {
const probe = await probePreviewTree(manifest);
if (probe.ok === false) {
return 'The preview tree is not reachable from the dev server (probe failed on '
+ (probe.tried || []).join(', ')
+ '). The resolved app root and the dev server root likely disagree; restart live from the app the dev server serves.';
}
} catch { /* probe is best-effort */ }
return 'The compiled component could not be imported or mounted. ' + (err?.message || 'Unknown error');
}
function teardownSvelteComponentSession(restoreOriginal) {
if (!svelteComponentSession) return;
const { wrapperEl, detachedOriginal, runtime, mountedInstance } = svelteComponentSession;
removeSvelteComponentVariantStyle(svelteComponentSession);
if (mountedInstance && runtime?.unmount) {
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
}
if (restoreOriginal && detachedOriginal && wrapperEl?.parentElement) {
wrapperEl.parentElement.replaceChild(detachedOriginal, wrapperEl);
} else if (wrapperEl?.parentElement) {
wrapperEl.remove();
}
svelteComponentSession = null;
svelteRuntimePromise = null;
}
function applyOriginalAttrsToSvelteAnchor(el, originalMarkup) {
if (!el || !originalMarkup) return;
const original = parseOriginalMarkupElement(originalMarkup);
if (!original || original.tagName !== el.tagName) return;
for (const attr of original.attributes) {
if (attr.name === 'class') {
for (const className of attr.value.split(/\s+/).filter(Boolean)) {
el.classList.add(className);
}
} else if (!el.hasAttribute(attr.name)) {
el.setAttribute(attr.name, attr.value);
}
}
}
function commitAcceptedSvelteComponentToDom(sessionId) {
if (!svelteComponentSession || svelteComponentSession.sessionId !== sessionId) return false;
const { wrapperEl, runtime, mountedInstance, manifest } = svelteComponentSession;
const anchor = getMountedSvelteComponentAnchor(svelteComponentSession);
if (!anchor || !wrapperEl?.parentElement) return false;
const committed = anchor.cloneNode(true);
if (!isSvelteInsertManifest(manifest)) {
applyOriginalAttrsToSvelteAnchor(committed, manifest.originalMarkup || '');
}
if (mountedInstance && runtime?.unmount) {
try { runtime.unmount(mountedInstance); } catch { /* non-fatal */ }
}
removeSvelteComponentVariantStyle(svelteComponentSession);
wrapperEl.parentElement.replaceChild(committed, wrapperEl);
svelteComponentSession = null;
svelteRuntimePromise = null;
selectedElement = committed;
return true;
}
async function injectSvelteComponentsFromManifest(manifestPath, sessionId) {
// Every (re)injection is a fresh attempt: reset the failure dedupe so a
// republish that is STILL broken at the same URL reports again instead of
// being swallowed while the agent believes the repair landed.
lastReportedMountFailure = null;
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(manifestPath);
try {
const res = await fetch(url);
if (!res.ok) throw new Error(String(res.status));
const manifest = JSON.parse(await res.text());
if (manifest.id !== sessionId) {
// A manifest at the expected path belonging to a different session is
// an agent-side publish error. Left as a bare return it stranded the
// bar in GENERATING with no explanation and no event.
const mismatch = 'Manifest at ' + manifestPath + ' belongs to session ' + (manifest.id || 'unknown') + ', not ' + sessionId + '.';
reportVariantMountFailed(sessionId, visibleVariant || 1, manifestPath, mismatch);
showMountErrorCard(sessionId, {
variant: visibleVariant || 0,
url: manifestPath,
message: 'The variant manifest is for a different session. Ask the agent to republish.',
previewFile: manifestPath,
});
return;
}
const paramsByVariant = await loadSvelteComponentParams(manifest);
const availableVariants = Number(manifest.arrivedVariants) || Number(manifest.count) || 1;
const componentPreviewMode = isFrameworkComponentPreviewMode(manifest.previewMode)
? manifest.previewMode
: 'svelte-component';
currentSessionId = sessionId;
expectedVariants = Number(manifest.count) || expectedVariants || 1;
rememberSessionFileMeta({
sourceFile: manifest.sourceFile,
previewFile: manifestPath,
previewMode: componentPreviewMode,
});
if (state !== 'CYCLING') setLiveState('GENERATING');
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper && svelteComponentSession?.sessionId === sessionId) {
recoveryWaitingForAnchor = false;
svelteComponentSession.manifest = manifest;
svelteComponentSession.paramsByVariant = paramsByVariant;
arrivedVariants = availableVariants;
expectedVariants = Number(manifest.count) || expectedVariants || arrivedVariants;
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants ? visibleVariant : 1;
const remounted = await mountSvelteComponentVariant(visibleVariant || 1);
if (!remounted) {
// The mount already reported the failure and raised the card.
// Advancing to CYCLING here would show a bar claiming variants are
// ready over a page where nothing rendered.
saveSession();
return;
}
setLiveState('CYCLING');
showOrUpdateCyclingBar();
saveSession();
if (parameterGenerationState === 'loading') completeParameterPublication();
return;
}
const liveEl = findLiveElementForSvelteManifest(manifest);
if (!liveEl?.parentElement) {
console.warn('[impeccable] Could not find original element in live DOM.');
arrivedVariants = availableVariants;
expectedVariants = Number(manifest.count) || expectedVariants || arrivedVariants;
const saved = loadSession();
const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0;
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants
? visibleVariant
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
enterRecoveryWaitingForAnchor({ checkpointReason: 'component_preview_anchor_missing', trackScroll: true });
waitForSvelteComponentTargetAndRetry({ manifestPath, sessionId, manifest });
return;
}
const wrapper = document.createElement('div');
wrapper.dataset.impeccableVariants = sessionId;
wrapper.dataset.impeccableVariantCount = String(manifest.count || expectedVariants || 1);
wrapper.dataset.impeccablePreview = componentPreviewMode;
wrapper.style.display = 'contents';
const mountTarget = document.createElement('div');
mountTarget.dataset.impeccableComponentMount = sessionId;
mountTarget.style.display = 'contents';
wrapper.appendChild(mountTarget);
const insertMode = isSvelteInsertManifest(manifest);
const detachedOriginal = insertMode ? null : liveEl;
if (insertMode) {
removeInsertPlaceholderDom();
if (manifest.position === 'before') liveEl.parentElement.insertBefore(wrapper, liveEl);
else liveEl.parentElement.insertBefore(wrapper, liveEl.nextSibling);
} else {
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
svelteComponentSession = {
sessionId,
manifest,
insertMode,
wrapperEl: wrapper,
mountTargetEl: mountTarget,
detachedOriginal,
mountedInstance: null,
mountedVariant: 0,
runtime: null,
propValues: buildSveltePropValuesFromLiveElement(detachedOriginal, manifest),
paramsByVariant,
};
if (pendingSvelteComponentRetryObserver) {
pendingSvelteComponentRetryObserver.disconnect();
pendingSvelteComponentRetryObserver = null;
}
recoveryWaitingForAnchor = false;
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
arrivedVariants = availableVariants;
expectedVariants = Number(manifest.count) || expectedVariants || arrivedVariants;
const saved = loadSession();
const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0;
visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants
? previousVisibleVariant
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
const mounted = await mountSvelteComponentVariant(visibleVariant);
if (!mounted) {
// The compiled component threw (e.g. a Svelte compile error in the
// variant file). mountSvelteComponentVariant already reported the
// failure and raised the card; tear the half-built preview down but
// keep the session so Retry and a republish still have something to
// act on.
abortSvelteComponentInjection(sessionId);
return;
}
selectedElement = mountTarget.firstElementChild || mountTarget;
setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
disableInlineEdit();
refreshParamsPanel();
positionBar();
saveSession();
if (parameterGenerationState === 'loading') completeParameterPublication();
console.log('[impeccable] Mounted ' + arrivedVariants + ' ' + manifest.framework + ' component variants.');
} catch (err) {
console.error('[impeccable] Failed to mount component-preview variants:', err);
// Report the manifest PATH, never the fetch URL: that URL carries the
// live helper token and this string is journaled.
reportVariantMountFailed(sessionId, visibleVariant || 1, manifestPath, err);
abortSvelteComponentInjection(sessionId, {
variant: visibleVariant || 0,
url: manifestPath,
message: 'Could not read the variant manifest. ' + (err?.message || 'Unknown error'),
previewFile: manifestPath,
});
}
}
function waitForSvelteComponentTargetAndRetry({ manifestPath, sessionId, manifest }) {
if (pendingSvelteComponentRetryObserver) pendingSvelteComponentRetryObserver.disconnect();
pendingSvelteComponentRetryObserver = new MutationObserver(() => {
if (svelteComponentSession?.sessionId === sessionId) {
pendingSvelteComponentRetryObserver.disconnect();
pendingSvelteComponentRetryObserver = null;
return;
}
const liveEl = findLiveElementForSvelteManifest(manifest);
if (!liveEl?.parentElement) return;
pendingSvelteComponentRetryObserver.disconnect();
pendingSvelteComponentRetryObserver = null;
injectSvelteComponentsFromManifest(manifestPath, sessionId);
});
pendingSvelteComponentRetryObserver.observe(document.body, { childList: true, subtree: true });
}
//
// Mount acknowledgements
//
// The agent's `done` says it published files. Only the browser knows whether
// the import resolved and the component reached the DOM. These two events
// carry that answer back, so the journal, `live-status`, and `live-resume`
// can tell "the user is comparing variants" from "nothing ever rendered".
// Mirror of the caps in live/event-validation.mjs. Trimming here keeps a
// stack-trace-sized error from being rejected outright and lost.
const MOUNT_URL_MAX = 2000;
const MOUNT_ERROR_MAX = 1000;
function reportVariantMounted(sessionId, variantNum, moduleUrl) {
const variant = Math.floor(Number(variantNum) || 0);
if (!sessionId || variant < 1) return;
sendEvent({
type: 'variant_mounted',
id: sessionId,
variant,
url: moduleUrl ? String(moduleUrl).slice(0, MOUNT_URL_MAX) : undefined,
});
}
function reportVariantMountFailed(sessionId, variantNum, moduleUrl, error) {
if (!sessionId) return;
const parsed = Math.floor(Number(variantNum) || 0);
const variant = parsed >= 1 ? parsed : 1;
const url = String(moduleUrl || 'unknown').slice(0, MOUNT_URL_MAX);
const message = String(error?.message || error || 'Unknown mount error').slice(0, MOUNT_ERROR_MAX);
// Progressive delivery and the Retry button both re-enter the same failure.
// Report each distinct one once so the agent's poll queue and the journal
// stay readable; a genuinely new failure (different variant, URL, or
// message) still gets through.
const key = sessionId + '|' + variant + '|' + url + '|' + message;
if (lastReportedMountFailure === key) return;
lastReportedMountFailure = key;
sendEvent({ type: 'variant_mount_failed', id: sessionId, variant, url, error: message });
}
function truncateMiddle(value, max) {
const text = String(value || '');
if (text.length <= max) return text;
const head = Math.ceil((max - 1) / 2);
const tail = max - 1 - head;
return text.slice(0, head) + '…' + text.slice(text.length - tail);
}
/**
* Persistent failure surface. Replaces the old 5s toast: a toast that
* disappears while the session is unusable is indistinguishable from no
* feedback at all, and the wipe that came with it deleted the only handle on
* a session the server still considered live.
*/
function showMountErrorCard(sessionId, details) {
mountErrorState = {
sessionId: sessionId || currentSessionId || null,
variant: Math.floor(Number(details?.variant) || 0),
url: details?.url ? String(details.url) : '',
message: details?.message || 'A variant failed to load.',
previewFile: details?.previewFile || currentPreviewFile || null,
};
renderMountErrorCard();
}
function clearMountErrorCard() {
mountErrorState = null;
if (mountErrorEl) {
mountErrorEl.remove();
mountErrorEl = null;
}
}
function mountErrorCardBottomOffset() {
const barRect = globalBarEl?.getBoundingClientRect();
return barRect && barRect.height > 0
? Math.max(16, window.innerHeight - barRect.top + 12)
: 16;
}
function renderMountErrorCard() {
if (!mountErrorState) return;
if (mountErrorEl) mountErrorEl.remove();
const P = BP || barPaletteForTheme(detectPageTheme());
const card = el('div', {
position: 'fixed', bottom: mountErrorCardBottomOffset() + 'px', left: '50%',
transform: 'translateX(-50%)',
display: 'flex', flexDirection: 'column', gap: '6px',
background: P.surface, color: P.text,
border: '1px solid oklch(65% 0.18 30 / 0.55)',
borderRadius: '8px', padding: '10px 12px',
fontFamily: FONT, fontSize: '12px',
boxShadow: P.shadow, zIndex: Z.toast,
maxWidth: 'min(520px, calc(100vw - 32px))',
pointerEvents: 'auto', textAlign: 'left',
});
card.id = PREFIX + '-mount-error';
const head = el('div', { display: 'flex', alignItems: 'center', gap: '8px' });
const glyph = el('span', { fontSize: '13px', lineHeight: '1', color: 'oklch(62% 0.19 30)', flexShrink: '0' });
glyph.textContent = '⚠';
head.appendChild(glyph);
const title = el('span', { fontWeight: '600', flex: '1' });
title.textContent = mountErrorState.variant > 0
? 'Variant ' + mountErrorState.variant + ' failed to load'
: 'Variants failed to load';
head.appendChild(title);
const dismiss = el('button', {
border: 'none', background: 'transparent', color: P.textDim,
cursor: 'pointer', fontFamily: FONT, fontSize: '14px', lineHeight: '1',
padding: '0 2px', flexShrink: '0',
});
dismiss.textContent = '×';
dismiss.setAttribute('aria-label', 'Dismiss');
dismiss.addEventListener('click', (e) => {
e.stopPropagation();
clearMountErrorCard();
// The card was the only recovery affordance while the bar is hidden;
// dismissing it must hand the user back a usable surface. PICKING
// reactivates the global mark and the picker. The saved session and
// server truth survive, so a later republish (SSE `done`) still
// resurrects the comparison through the normal handlers.
if (state === 'GENERATING') setLiveState('PICKING');
});
head.appendChild(dismiss);
card.appendChild(head);
const body = el('div', { color: P.textDim, lineHeight: '1.4' });
body.textContent = mountErrorState.message;
card.appendChild(body);
if (mountErrorState.url) {
const urlLine = el('div', {
fontFamily: MONO, fontSize: '11px', color: P.textDim,
wordBreak: 'break-all', opacity: '0.85',
});
urlLine.textContent = truncateMiddle(mountErrorState.url, 72);
urlLine.title = mountErrorState.url;
card.appendChild(urlLine);
}
const actions = el('div', { display: 'flex', gap: '8px', marginTop: '2px' });
const retry = el('button', {
border: '1px solid ' + P.hairline, background: 'transparent',
color: P.text, fontFamily: FONT, fontSize: '12px', fontWeight: '500',
borderRadius: '5px', padding: '4px 10px', cursor: 'pointer',
});
retry.textContent = 'Retry';
retry.dataset.impeccableMountRetry = 'true';
retry.addEventListener('click', (e) => { e.stopPropagation(); retryMountErrorCard(); });
actions.appendChild(retry);
card.appendChild(actions);
mountErrorEl = card;
uiAppend(card);
defangOutsideHandlers(card);
}
function retryMountErrorCard() {
const info = mountErrorState;
if (!info) return;
const sessionId = info.sessionId || currentSessionId;
const manifestPath = info.previewFile || currentPreviewFile;
clearMountErrorCard();
if (!sessionId || !manifestPath) {
showToast('No variant manifest to retry. Ask the agent to republish.', 5000);
return;
}
// A retry must be able to report the same failure again, otherwise a second
// attempt against an unchanged broken module would look silent.
lastReportedMountFailure = null;
if (state !== 'CYCLING') setLiveState('GENERATING');
injectSvelteComponentsFromManifest(manifestPath, sessionId);
}
// Tear down a component preview that could not mount, WITHOUT touching
// session identity. The old version cleared localStorage, nulled
// currentSessionId, and reset to PICKING, which orphaned a session the server
// still had in its journal and made every recovery path unreachable. The DOM
// teardown and observer cleanup are still right; the state wipe never was.
function abortSvelteComponentInjection(sessionId, details) {
try {
if (svelteComponentSession?.sessionId === sessionId) {
teardownSvelteComponentSession(true);
} else {
const orphan = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (orphan) orphan.remove();
}
} catch (err) {
console.warn('[impeccable] Svelte component abort cleanup failed:', err);
}
hideShaderOverlay();
if (pendingSvelteComponentRetryObserver) { pendingSvelteComponentRetryObserver.disconnect(); pendingSvelteComponentRetryObserver = null; }
if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
// The generate submit armed a scroll lock and a variant observer; a page
// the user cannot scroll, watched by a stale observer, is exactly the
// wrong place to show a card asking them to act.
stopScrollLock();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
removeVariantStateStylesheet();
hideBar(true);
// currentSessionId, the saved session, and the file metadata all survive on
// purpose: Retry, a republish from the agent, and a page reload all need
// them. saveSession keeps the localStorage cache in step with the server.
saveSession();
if (details) showMountErrorCard(sessionId, details);
else if (!mountErrorState) {
showMountErrorCard(sessionId, { message: 'Variants could not be mounted. Retry, or ask the agent to republish.' });
}
}
// Hard reset for the one case that is not a mount failure: a cycling state
// with nothing to cycle. There is no variant to retry and no URL to report,
// so the session really is over.
function resetSvelteComponentSession(sessionId, message) {
try {
if (svelteComponentSession?.sessionId === sessionId) {
teardownSvelteComponentSession(true);
} else {
const orphan = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (orphan) orphan.remove();
}
} catch (err) {
console.warn('[impeccable] Svelte component reset cleanup failed:', err);
}
hideShaderOverlay();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
if (pendingSvelteComponentRetryObserver) { pendingSvelteComponentRetryObserver.disconnect(); pendingSvelteComponentRetryObserver = null; }
if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
removeVariantStateStylesheet();
clearMountErrorCard();
clearSession();
clearHandled();
resetSessionFileMeta();
currentSessionId = null;
parameterGenerationState = 'idle';
parameterReadyAnnouncedSession = null;
expectedVariants = 0;
arrivedVariants = 0;
visibleVariant = 0;
selectedElement = null;
setLiveState('PICKING');
hideBar();
if (message) showToast(message, 5000);
}
// How many delayed re-reads a completion-driven source fallback gets when
// the fetched source still shows only the preflight scaffold, before the
// failure is surfaced via recoverEmptyCycling.
const COMPLETED_SOURCE_FALLBACK_RETRIES = 3;
const COMPLETED_SOURCE_FALLBACK_RETRY_MS = 1200;
/**
* Terminal recovery for a session whose source-side scaffolding no longer
* exists. The discard event is best-effort: with no agent polling it parks
* the durable session in discard_requested, which no resume path adopts;
* with an agent attached it triggers the normal discard finalization.
*/
function discardOrphanedSession(reason) {
const sessionId = currentSessionId;
if (!sessionId) return;
console.warn('[impeccable] Discarding orphaned session ' + sessionId + ': ' + reason);
sendEvent({ type: 'discard', id: sessionId, orphaned: true }).catch(() => {});
markSessionHandled();
cleanup({ instantChrome: true });
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* 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).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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 => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
recoveryWaitingForAnchor = false;
if (pendingVariantAnchorRetryObserver) {
pendingVariantAnchorRetryObserver.disconnect();
pendingVariantAnchorRetryObserver = null;
}
// Update state: count variants, preserving the user's current variant
// when a late HMR/source reinjection lands after they have cycled.
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
arrivedVariants = variants.length;
expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants);
if (arrivedVariants <= 0) {
if (state === 'GENERATING') {
// Mid-generation the source legitimately holds a scaffold wrapper
// with no variants yet (the server-side preflight wraps before the
// agent writes). Tearing the session down here would destroy an
// in-flight generation; stay in GENERATING — the variant observer
// is armed and the server re-delivers a missed `done`.
if (!opts.generationCompleted) {
console.log('[impeccable] Source has scaffold but no variants yet; still generating.');
return;
}
// Generation finished, yet the read shows only the scaffold: the
// source view is stale and no further event will fire. Re-read a
// few times before surfacing recovery — a single silent return
// here would strand the tab in GENERATING forever.
const attempt = opts.attempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
console.log('[impeccable] Generation is done but source shows no variants yet; retrying read ('
+ (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').');
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
}
recoverEmptyCycling('source-fallback-empty');
return;
}
const saved = loadSession();
const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0;
visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants
? previousVisibleVariant
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
disableInlineEdit();
refreshParamsPanel();
positionBar();
saveSession();
if (parameterGenerationState === 'loading') completeParameterPublication();
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 normalizeSourceFallbackBlock(block, filePath) {
if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
return String(block)
.replace(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
const sourceNodes = collectTextNodes(sourceOriginal)
.filter((node) => /\{[^{}]+\}/.test(node.nodeValue || ''));
const liveTexts = collectTextNodes(liveOriginal)
.map((node) => normalizePreviewText(node.nodeValue || ''))
.filter(Boolean);
let liveIndex = 0;
for (const sourceNode of sourceNodes) {
const sourceText = sourceNode.nodeValue || '';
const tokens = sourceText.match(/\{[^{}]+\}/g) || [];
if (tokens.length === 0) continue;
const liveText = liveTexts[liveIndex++] || '';
if (!liveText) continue;
if (tokens.length === 1) {
const token = tokens[0];
const normalizedSource = normalizePreviewText(sourceText);
if (normalizedSource === token) {
map.set(token, liveText);
continue;
}
const match = liveText.match(expressionTextMatcher(sourceText, [token]));
if (match && match[1]) map.set(token, match[1].trim());
continue;
}
if (normalizePreviewText(sourceText) === tokens.join(' ')) {
for (const token of tokens) {
const tokenLiveText = liveTexts[liveIndex - 1] || '';
if (tokenLiveText) map.set(token, tokenLiveText);
}
}
}
return map;
}
function expressionTextMatcher(sourceText, tokens) {
let pattern = '^';
let cursor = 0;
for (const token of tokens) {
const index = sourceText.indexOf(token, cursor);
if (index === -1) continue;
pattern += escapeRegExp(sourceText.slice(cursor, index)).replace(/\s+/g, '\\s*');
pattern += '(.*?)';
cursor = index + token.length;
}
pattern += escapeRegExp(sourceText.slice(cursor)).replace(/\s+/g, '\\s*') + '$';
return new RegExp(pattern);
}
function collectTextNodes(root) {
if (!root) return [];
const nodes = [];
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
let node = walker.nextNode();
while (node) {
nodes.push(node);
node = walker.nextNode();
}
return nodes;
}
function normalizePreviewText(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
async function selectVariant(next, checkpointReason) {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
if (variantSelectionInFlight) return;
if (next < 1 || next > arrivedVariants) return;
if (next === visibleVariant) return;
const previous = visibleVariant;
variantSelectionInFlight = true;
const selectionPromise = (async () => {
visibleVariant = next;
showOrUpdateCyclingBar();
saveSession();
const shown = await showVariantInDOM(currentSessionId, next); // calls refreshParamsPanel itself
if (!shown) {
visibleVariant = previous;
await showVariantInDOM(currentSessionId, previous);
showOrUpdateCyclingBar();
saveSession();
return;
}
updateSelectedElement();
showOrUpdateCyclingBar();
positionBar();
saveSession();
if (checkpointReason) queueCheckpoint(checkpointReason);
})();
variantSelectionPromise = selectionPromise;
try {
await selectionPromise;
} finally {
if (variantSelectionPromise === selectionPromise) variantSelectionPromise = null;
variantSelectionInFlight = false;
}
}
function cycleVariant(dir) {
selectVariant(visibleVariant + dir, 'variant_changed');
}
function updateSelectedElement() {
if (!currentSessionId) return;
if (svelteComponentSession?.sessionId === currentSessionId) {
const anchor = resolveSvelteComponentAnchor();
if (anchor && !anchor.__impeccableFrozenAnchor) selectedElement = anchor;
return;
}
const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (!wrapper) return;
const visEl = pickVariantContent(wrapper, visibleVariant);
if (visEl) selectedElement = visEl;
}
function readVisibleVariantFromDOM(sessionId) {
if (svelteComponentSession?.sessionId === sessionId && svelteComponentSession.mountedVariant > 0) {
return svelteComponentSession.mountedVariant;
}
const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!wrapper) return 0;
const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
for (const variant of variants) {
if (!isVariantShown(variant)) continue;
const idx = parseInt(variant.dataset.impeccableVariant || '0', 10);
if (idx > 0) return idx;
}
return 0;
}
// Resolve the element that represents the variant's visible content.
// Contract: each variant div should contain exactly one top-level element
// (the full replacement). In practice a model may ship loose siblings or
// lead with <style>/<script>. Be defensive: skip non-visual elements, and
// if the variant has multiple element children, use the variant div itself
// (it wraps all of them and gets correct bounds).
function pickVariantContent(wrapper, index) {
if (!wrapper) return null;
const variantDiv = wrapper.querySelector('[data-impeccable-variant="' + index + '"]');
if (!variantDiv) return null;
const NON_VISUAL = new Set(['STYLE', 'SCRIPT', 'LINK', 'META', 'TEMPLATE']);
const visual = [];
for (const child of variantDiv.children) {
if (!NON_VISUAL.has(child.tagName)) visual.push(child);
}
if (visual.length === 1) return visual[0];
return variantDiv;
}
// Variant visibility and range/toggle params are expressed through ONE
// injected stylesheet, never inline attributes on the variant divs. Those
// divs are scaffolded into page source, so SSR frameworks (Next.js App
// Router) server-render them; toggling their `hidden` / inline `style` /
// `--p-*` client-side trips a React 19 hydration mismatch on the next
// Fast-Refresh re-render — the same failure mode the scroll-anchor (#276)
// and pick-cursor (#286) fixes address. A stylesheet rule has the same
// computed effect without mutating any hydrated element's attributes.
// (steps params keep driving `data-p-*` attributes, matching scoped CSS.)
const VARIANT_HIDE_DECL = 'display: none !important;';
const VARIANT_SHOW_DECL = 'display: block !important;';
// Build a direct-child variant selector for a session. With `num`, targets a
// single variant (`… > [data-impeccable-variant="N"]`); without it, targets
// every variant via the bare `[data-impeccable-variant]` attribute.
function variantStateSelector(sessionId, num) {
const wrapper = '[data-impeccable-variants="' + sessionId + '"]';
const variant = num == null
? '[data-impeccable-variant]'
: '[data-impeccable-variant="' + num + '"]';
return wrapper + ' > ' + variant;
}
// Serialize the visible variant's knob values into `--p-<id>` custom-property
// declarations. Only range (number) and toggle (boolean) values become a
// custom property; steps params drive `data-p-*` attributes instead.
function variantParamDecls(values) {
return Object.entries(values || {})
.map(([id, val]) => {
if (typeof val === 'number') return ' --p-' + id + ': ' + val + ';';
if (typeof val === 'boolean') return ' --p-' + id + ': ' + (val ? '1' : '0') + ';';
return '';
})
.join('');
}
function updateVariantStateStylesheet(sessionId, num) {
if (!sessionId || num == null || num < 1) return;
let styleEl = document.getElementById(VARIANT_STATE_STYLE_ID);
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = VARIANT_STATE_STYLE_ID;
(document.head || document.documentElement).appendChild(styleEl);
}
// Hide every variant except the visible one (incl. the SSR'd "original").
const hideOthers = variantStateSelector(sessionId)
+ ':not([data-impeccable-variant="' + num + '"]) { ' + VARIANT_HIDE_DECL + ' }';
// Force-show the visible variant (beats the source inline display:none on
// v2/v3) and apply its knob values as custom properties.
const showVisible = variantStateSelector(sessionId, num)
+ ' { ' + VARIANT_SHOW_DECL + variantParamDecls(paramsCurrentValues) + ' }';
styleEl.textContent = hideOthers + '\n' + showVisible + '\n';
}
function removeVariantStateStylesheet() {
document.getElementById(VARIANT_STATE_STYLE_ID)?.remove();
}
function showOriginalDuringDiscard(sessionId) {
if (!sessionId) return;
let styleEl = document.getElementById(DISCARD_STATE_STYLE_ID);
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = DISCARD_STATE_STYLE_ID;
(document.head || document.documentElement).appendChild(styleEl);
}
const wrapper = '[data-impeccable-variants="' + sessionId + '"]';
styleEl.textContent = wrapper + ' > [data-impeccable-variant]:not([data-impeccable-variant="original"]) { display:none !important; }\n'
+ wrapper + ' > [data-impeccable-variant="original"] { display:block !important; }';
}
function resolveScrollLockAnchorTop() {
const anchor = resolveBarAnchor();
if (!anchor?.isConnected) return null;
const top = anchor.getBoundingClientRect().top;
return Number.isFinite(top) ? top : null;
}
// Hold window.scrollY at a fixed value across DOM mutations inside the
// session's wrapper (HMR patches, variant inserts, cycle swaps).
function startScrollLock(sessionId, initialTargetY, initialAnchorTop) {
stopScrollLock();
scrollLockTargetY = typeof initialTargetY === 'number' && isFinite(initialTargetY)
? initialTargetY
: window.scrollY;
scrollLockAnchorTop = typeof initialAnchorTop === 'number' && isFinite(initialAnchorTop)
? initialAnchorTop
: resolveScrollLockAnchorTop();
try { history.scrollRestoration = 'manual'; } catch {}
// Suppress the browser's scroll-anchoring on the scroll root so it can't
// fight our manual scroll correction. Apply this as a stylesheet rule, not
// as inline `style` on <html>/<body>: those elements are server-rendered by
// frameworks like Next.js App Router, and mutating their inline style makes
// React 19 report a hydration mismatch on the next Fast-Refresh re-render.
// A <style> rule has the same computed effect without touching any hydrated
// element's attributes. Like the inline version, it is recreated on every
// startScrollLock call, so reload survival (driven by the persisted scroll
// key) is unaffected.
let anchorLockStyle = document.getElementById(SCROLL_ANCHOR_LOCK_ID);
if (!anchorLockStyle) {
anchorLockStyle = document.createElement('style');
anchorLockStyle.id = SCROLL_ANCHOR_LOCK_ID;
anchorLockStyle.textContent = 'html,body{overflow-anchor:none !important;}';
(document.head || document.documentElement).appendChild(anchorLockStyle);
}
const correct = (why) => {
scrollLockRaf = null;
if (scrollLockTargetY == null) return;
const anchor = resolveBarAnchor();
if (anchor?.isConnected && typeof scrollLockAnchorTop === 'number' && isFinite(scrollLockAnchorTop)) {
const anchorTop = anchor.getBoundingClientRect().top;
const anchorDelta = anchorTop - scrollLockAnchorTop;
if (Math.abs(anchorDelta) >= 0.5) {
window.scrollTo({ top: window.scrollY + anchorDelta, left: window.scrollX, behavior: 'instant' });
scrollLockTargetY = window.scrollY;
writeScrollY(scrollLockTargetY);
return;
}
}
const before = window.scrollY;
const delta = before - scrollLockTargetY;
if (Math.abs(delta) < 0.5) {
return;
}
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
};
const schedule = (why) => {
if (scrollLockRaf != null) return;
scrollLockRaf = requestAnimationFrame(() => correct(why));
};
scrollLockObserver = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.target?.closest?.('[data-impeccable-variants="' + sessionId + '"]')) {
schedule('mutation-in-wrapper');
return;
}
for (const n of m.addedNodes) {
if (n.nodeType === 1 && (n.matches?.('[data-impeccable-variants="' + sessionId + '"]') || n.querySelector?.('[data-impeccable-variants="' + sessionId + '"]'))) {
schedule('wrapper-added');
return;
}
}
}
});
scrollLockObserver.observe(document.body, { childList: true, subtree: true });
scrollLockAbort = new AbortController();
scrollLockAbort.signal.addEventListener('abort', () => {
document.getElementById(SCROLL_ANCHOR_LOCK_ID)?.remove();
}, { once: true });
const sig = { signal: scrollLockAbort.signal };
// Track whether the most recent scroll came from a user gesture. We
// gate user-scroll re-anchoring on this flag so programmatic smooth
// scrolls (browser reload-restore, scrollIntoView from other scripts)
// don't accidentally update our target.
let userGestureAt = 0;
const USER_GESTURE_WINDOW_MS = 250;
const reanchor = (why) => {
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
const prevTarget = scrollLockTargetY;
scrollLockTargetY = window.scrollY;
scrollLockAnchorTop = resolveScrollLockAnchorTop();
writeScrollY(scrollLockTargetY);
};
const markGesture = (why) => {
userGestureAt = performance.now();
reanchor(why);
};
window.addEventListener('wheel', () => markGesture('wheel'), { passive: true, ...sig });
window.addEventListener('touchstart', () => markGesture('touchstart'), { passive: true, ...sig });
window.addEventListener('touchmove', () => markGesture('touchmove'), { passive: true, ...sig });
window.addEventListener('keydown', (e) => {
if (['PageDown', 'PageUp', ' ', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(e.key)) markGesture('key:' + e.key);
}, sig);
// Correct on EVERY scroll event: whether it's the browser's
// post-reload animated restore or some other script calling
// scrollIntoView, we want to snap back immediately. Only skip if a
// user gesture fired in the last 250ms.
window.addEventListener('scroll', () => {
const now = window.scrollY;
if (scrollLockTargetY == null) return;
if (performance.now() - userGestureAt < USER_GESTURE_WINDOW_MS) return;
if (Math.abs(now - scrollLockTargetY) < 0.5) return;
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
}, { passive: true, ...sig });
// Apply target synchronously, not via rAF - racing the browser's
// restore or a smooth-scroll animation means we want to win now.
if (Math.abs(window.scrollY - scrollLockTargetY) > 0.5) {
window.scrollTo({ top: scrollLockTargetY, left: window.scrollX, behavior: 'instant' });
}
}
function stopScrollLock() {
if (scrollLockObserver) { scrollLockObserver.disconnect(); scrollLockObserver = null; }
if (scrollLockRaf != null) { cancelAnimationFrame(scrollLockRaf); scrollLockRaf = null; }
if (scrollLockAbort) { scrollLockAbort.abort(); scrollLockAbort = null; }
scrollLockTargetY = null;
scrollLockAnchorTop = null;
// NOTE: do NOT clear the persistent scroll key here. startScrollLock
// calls us as a reset, and clearing the key would nuke the Go-time
// scrollY that the next resume needs to read.
}
//
// 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) continue;
// Direct hit: the added node itself is the wrapper or a variant.
if (n.dataset?.impeccableVariants || n.dataset?.impeccableVariant) {
dominated = true; break;
}
// Subtree hit: framework HMR (notably SvelteKit) sometimes replaces
// a whole subtree where the wrapper is a descendant of the added
// node. Without this check, the observer ignores those mutations
// and the session stays in GENERATING forever.
if (n.querySelector?.('[data-impeccable-variants],[data-impeccable-variant]')) {
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;
// Re-anchor selectedElement if it was detached by live-wrap's HMR swap.
// Without this, the shader / highlight / bar track a zero-rect phantom
// and the overlay appears frozen.
if (selectedElement && !document.body.contains(selectedElement)) {
const isInsert = wrapper.dataset.impeccableMode === 'insert';
if (isInsert) {
const visEl = count > 0 ? pickVariantContent(wrapper, visibleVariant || 1) : null;
if (visEl) {
selectedElement = visEl;
if (count > 0) removeInsertPlaceholderDom();
} else {
const ph = ensureInsertPlaceholder();
if (ph) selectedElement = ph;
else if (insertAnchorElement && document.body.contains(insertAnchorElement)) {
selectedElement = insertAnchorElement;
}
}
} else {
selectedElement = pickVariantContent(wrapper, 'original') || wrapper;
}
} else if (isInsertGeneratingSession() && count === 0) {
ensureInsertPlaceholder();
}
// Nothing new
if (count <= arrivedVariants) return;
updating = true;
arrivedVariants = count;
generationPhase = arrivedVariants >= expectedVariants ? 'variants_ready' : 'variants_progress';
if (visibleVariant === 0 && arrivedVariants > 0) {
const saved = loadSession();
const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0;
visibleVariant = savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1;
showVariantInDOM(sessionId, visibleVariant);
// showVariantInDOM hid the original (display:none); if we were still
// anchored to the original's content, its boundingRect is now zero
// and the bar snaps to (0,0). Re-point at the visible variant instead.
const visEl = pickVariantContent(wrapper, visibleVariant);
if (visEl) selectedElement = visEl;
}
const expected = parseInt(wrapper.dataset.impeccableVariantCount || '0');
if (expected > 0) expectedVariants = expected;
if (arrivedVariants > 0) {
setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
if (wrapper.dataset.impeccableMode === 'insert') finalizeInsertSession();
updateSelectedElement();
showOrUpdateCyclingBar();
disableInlineEdit();
if (arrivedVariants >= expectedVariants && expectedVariants > 0) refreshParamsPanel();
else hideParamsPanel();
positionBar();
} else if (state === 'GENERATING') {
updateBarContent('generating');
}
saveSession();
sendCheckpoint(
arrivedVariants >= expectedVariants && expectedVariants > 0
? 'variants_ready'
: 'variants_progress',
);
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') {
if (isInsertGeneratingSession()) ensureInsertPlaceholder();
positionBar();
if (state === 'CONFIGURING') positionEditBadge();
const hiTarget = resolveBarAnchor();
if (hiTarget && !hiTarget.hasAttribute?.('data-impeccable-insert-placeholder')) {
showHighlight(hiTarget);
} else {
hideHighlight();
}
if (tuneOpen) positionParamsPanel();
}
if (state === 'EDITING') {
positionEditBadge();
showHighlight(selectedElement);
}
if (annotActive) {
const annotTarget = resolveBarAnchor();
if (annotTarget) positionAnnotOverlay(annotTarget);
}
// Shader overlay (via debug P toggle or generation) is repositioned
// by its own branch below; debug no longer has a separate overlay.
if (shaderState) positionShaderOverlay();
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 PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000);
console.log('[impeccable] Live mode connected.');
syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll();
restoreFromActiveSessions(msg.activeSessions, 'sse_connected');
recoverMissedGenerationCompletion(msg.activeSessions);
if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
syncPageInteractionCursor();
syncPageChatFocus('sse-connected');
break;
case 'agent_polling':
syncAgentPollingUi(!!msg.connected);
break;
case 'agent_phase':
if (msg.id === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
// Advance the visible phase monotonically. A behind/resumed
// checkpoint may carry an earlier phase for internal bookkeeping,
// but the bar must not move backward.
if (shouldAdvancePhase(generationPhase, msg.phase)) generationPhase = msg.phase;
// The deferred parameter pass reports through `variant_progress`
// with publicationKind 'params', not through agent_phase.
updateBarContent(state === 'CYCLING' ? 'cycling' : 'generating');
saveSession();
}
break;
case 'variant_progress':
if (msg.id === currentSessionId) {
if (msg.publicationKind === 'params') parameterGenerationState = 'loading';
rememberSessionFileMeta(msg);
if (isFrameworkComponentPreviewMode(msg.previewMode) && msg.previewFile) {
// Component-preview (Svelte/Vue) progressive delivery: the browser
// mounts compiled components, so there is no framework-owned DOM
// to race. Keep streaming each checkpoint into the preview.
injectSvelteComponentsFromManifest(msg.previewFile, msg.id);
}
// Source-preview targets: do NOT source-inject per checkpoint.
// Immediate injection races framework (React/Vue) ownership mid-
// generation and triggers removeChild errors on the next HMR
// commit. Let HMR own reconciliation while variants stream in;
// source injection runs only on the final `done` (which keeps its
// 750ms settle + retry ladder for non-HMR harnesses like Cursor).
// The visible progress count still advances from the variant
// MutationObserver as HMR lands each variant.
}
break;
case 'steer_done':
maybeCompleteSteer(msg);
break;
case 'manual_edit_stashed':
case 'manual_edit_discarded':
case 'manual_edit_commit_started':
case 'manual_edit_apply_reply_received':
case 'manual_edit_apply_dispatched':
case 'manual_edit_repair_needs_decision':
case 'manual_edit_repair_rollback_done':
case 'manual_edit_commit_done':
case 'manual_edit_commit_failed':
handleManualEditActivity(msg);
break;
case 'done':
if (maybeCompleteSteer(msg)) break;
rememberSessionFileMeta(msg);
if (msg.id === currentSessionId && isFrameworkComponentPreviewMode(currentPreviewMode) && currentPreviewFile) {
injectSvelteComponentsFromManifest(currentPreviewFile, msg.id);
break;
}
// Variants already arrived via HMR → normal transition.
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
if (state === 'GENERATING') {
setLiveState('CYCLING');
showOrUpdateCyclingBar();
disableInlineEdit();
refreshParamsPanel();
}
break;
}
// Source fallback when HMR did not land variants in this tab.
if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) {
setTimeout(() => {
if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
injectVariantsFromSource(msg.file, msg.id, { generationCompleted: true });
}, 750);
break;
}
// Variants are in source but not in the DOM yet. Common when the
// picked element lived inside conditional render (closed modal,
// hidden tab, a route the user navigated away from). The variant
// MutationObserver stays armed and auto-transitions to CYCLING
// the moment the wrapper actually mounts. Nudge the user toward
// that path with a toast - better than the prior force-reload
// which reset framework state and left the session stuck.
setTimeout(() => {
if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
if (state !== 'GENERATING') return;
showToast(
"Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.",
15000,
);
}, 2000);
break;
case 'complete':
case 'accept':
// The real accept result arrived: the awaited failure window closed.
if (awaitingAcceptResult?.id && msg.id === awaitingAcceptResult.id) awaitingAcceptResult = null;
if (maybeCompleteAcceptedSession(msg)) break;
break;
case 'agent_done':
// The deterministic accept has already committed the reviewed DOM
// and fenced generation. Carbonize may continue in the background;
// it must not hold the foreground picker hostage.
// Only a carbonize agent_done is provably accept-side: accept
// unlocks at the first variant, so a late generation agent_done
// for the same session id can still arrive after Accept and must
// not close the awaited failure window early (the SSE broadcast
// carries no sourceEventType to tell the two apart).
if (msg.data?.carbonize === true && awaitingAcceptResult?.id && msg.id === awaitingAcceptResult.id) awaitingAcceptResult = null;
if (msg.data?.carbonize === true && maybeCompleteAcceptedSession(msg)) break;
break;
case 'discarded':
if (msg.id && msg.id === currentSessionId) {
markSessionHandled();
cleanup();
}
break;
case 'error':
if (pendingAcceptedSession?.id && msg.id === pendingAcceptedSession.id) {
pendingAcceptedSession = null;
awaitingAcceptResult = null;
setLiveState('CYCLING');
updateBarContent('cycling');
showToast('Could not complete accept cleanup. Try Accept again.', 5000);
break;
}
// The optimistic teardown already released the session, so the
// CYCLING recovery above can no longer match; without this branch
// the failure fell through to the generic toast and the user had
// no hint their variant was never written (issue #384).
if (awaitingAcceptResult?.id && msg.id === awaitingAcceptResult.id) {
awaitingAcceptResult = null;
console.error('[impeccable] Accept failed after teardown:', msg.message);
// Hedged on purpose: a carbonize-phase failure raises this same
// error after the source WAS promoted, so "was not saved" would
// overclaim. Normalize the server message's terminal punctuation
// so the two sentences don't run together.
const acceptFailDetail = String(msg.message || 'unknown error').trim().replace(/[.!?]?$/, '.');
showToast('Accept failed: ' + acceptFailDetail + ' The variant may not have been saved. If the change is missing, pick the element and generate again.', 8000);
break;
}
if (maybeCompleteSteer(msg)) break;
console.error('[impeccable] Error:', msg.message);
showToast('Error: ' + msg.message, 5000);
// An agent error reply is terminal for the session it names: tear
// it down exactly like 'discarded' (cleanup includes clearSession),
// or the durable localStorage checkpoint survives and every reload
// resurrects a GENERATING bar for a session the server no longer
// knows about (issue #362).
if (msg.id && msg.id === currentSessionId) {
markSessionHandled();
cleanup();
break;
}
// A stored-but-not-current checkpoint naming the errored session
// (the error raced a reload) must not resurrect either.
if (msg.id && loadSession()?.id === msg.id) clearSession();
hideBar();
renderEditBadge('hidden');
setLiveState('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() {
const recoveryState = currentSessionId ? state : 'IDLE';
if (state === 'GENERATING' || state === 'CYCLING' || state === 'SAVING') {
showToast('Live server connection lost. Your session is saved; reopen this page or restart live-poll.mjs to continue.', 6000);
}
hideBar();
hideHighlight();
hideShaderOverlay();
hideAnnotOverlay();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
stopScrollLock();
// Preserve local session state on server loss. The durable journal is the
// source of truth, but localStorage plus the variant wrapper lets the UI
// resume after a helper restart or page reload instead of treating a
// transient disconnect as an explicit discard.
selectedElement = null;
selectedAction = 'impeccable';
setLiveState(recoveryState);
if (currentSessionId) saveSession();
}
// Progress events must never overtake the event that CREATES their session:
// the Go-time checkpoint and the generate POST are concurrent fetches, and
// when the checkpoint lands first the server rightly refuses it as
// unknown_session — which must mean "foreign leftovers", not "you raced
// your own Go click". The gate serializes creation before progress.
let sessionCreationGate = Promise.resolve();
function sendEvent(msg, opts) {
msg.token = TOKEN;
function handleFailure(err) {
if (opts && opts.throwOnError) {
console.error('[impeccable] Failed to send event:', err);
throw err;
}
console.debug('[impeccable] Dropped optional live event:', err);
return null;
}
const doSend = () => fetch('http://localhost:' + PORT + '/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(msg),
}).then(async res => {
if (res.ok) return res;
const body = await res.json().catch(() => ({}));
// The server refused to journal progress for a session it has never
// seen: this browser is carrying state from another project or a
// wiped store (two apps sharing a localhost port). Continuing to
// report it would freeze the picker behind a session that can never
// complete, so drop the local state and hand the surface back.
if (body.error === 'unknown_session' && msg.type === 'checkpoint'
&& msg.id && msg.id === currentSessionId) {
abandonForeignSession(msg.id);
return null;
}
return handleFailure(new Error(body.error || ('HTTP ' + res.status + ' ' + res.statusText)));
}).catch(handleFailure);
if (msg.type === 'generate' || msg.type === 'steer') {
const creation = doSend();
sessionCreationGate = creation.then(() => {}, () => {});
return creation;
}
return sessionCreationGate.then(doSend);
}
let abandonedForeignSessionId = null;
function abandonForeignSession(sessionId) {
if (abandonedForeignSessionId === sessionId || sessionId !== currentSessionId) return;
abandonedForeignSessionId = sessionId;
console.warn('[impeccable] The live server has no record of session ' + sessionId + '; clearing stale local state.');
markSessionHandled();
cleanup({ instantChrome: true });
showToast('A saved live session belonged to a different project, so it was cleared. Pick an element to start fresh.', 6000);
}
function checkpointPayload(reason) {
return {
type: 'checkpoint',
id: currentSessionId,
revision: sessionState.nextCheckpointRevision(),
revisionDomain: 'browser',
owner: browserOwner,
phase: String(state || '').toLowerCase(),
reason,
pageUrl: location.pathname,
expectedVariants,
arrivedVariants,
visibleVariant,
sourceFile: currentSourceFile || undefined,
previewFile: currentPreviewFile || undefined,
previewMode: currentPreviewMode || undefined,
paramValues: { ...paramsCurrentValues },
};
}
function sendCheckpoint(reason) {
if (!currentSessionId) return Promise.resolve(null);
return sendEvent(checkpointPayload(reason)).catch(() => null);
}
function sendSteerCheckpoint(id, reason, extra) {
if (!id) return Promise.resolve(null);
return sendEvent({
type: 'checkpoint',
id,
revision: sessionState.nextCheckpointRevision(),
revisionDomain: 'browser',
owner: browserOwner,
phase: 'steer',
reason,
pageUrl: location.pathname,
...(extra || {}),
}).catch(() => null);
}
function queueCheckpoint(reason) {
if (!currentSessionId) return;
if (checkpointTimer) clearTimeout(checkpointTimer);
checkpointTimer = setTimeout(() => {
checkpointTimer = null;
sendCheckpoint(reason);
}, 120);
}
//
// Event handlers
//
function handleMouseMove(e) {
if (pendingApplyInFlight) return;
if (state === 'PICKING' && insertActive) {
const target = document.elementFromPoint(e.clientX, e.clientY);
if (!target || own(target) || !pickable(target)) {
hideInsertLine();
return;
}
const parent = target.parentElement;
const axis = detectInsertAxis(parent);
const siblings = layoutFlowChildren(parent);
const rect = target.getBoundingClientRect();
const resolved = resolveInsertHover({
clientX: e.clientX,
clientY: e.clientY,
target,
rect,
axis,
siblings,
});
if (
resolved.anchor !== insertHoverAnchor
|| resolved.position !== insertHoverPosition
|| resolved.axis !== insertHoverAxis
) {
showInsertLine(resolved);
}
syncPageInteractionCursor();
return;
}
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) {
if (pendingApplyInFlight && !pendingDockEl?.contains(e.target)) {
if (pickerEl?.style.display !== 'none') hideActionPicker();
if (own(e.target)) {
e.preventDefault();
e.stopPropagation();
showManualApplyBusyToast();
}
return;
}
// Close action picker on any outside click
if (pickerEl?.style.display !== 'none' && !own(e.target)) {
hideActionPicker();
}
// Close Tune popover on outside click (anything outside panel + bar)
if (tuneOpen && paramsPanelEl && !paramsPanelEl.contains(e.target) && barEl && !barEl.contains(e.target)) {
closeTunePopover();
}
// In EDITING: click outside exits the text edit flow without rebuilding configure UI first.
if (state === 'EDITING' && !own(e.target) && selectedElement && !selectedElement.contains(e.target)) {
cancelEditingToPicking();
return;
}
// In CONFIGURING: click outside the bar and selected element returns to PICKING.
if (
state === 'CONFIGURING' && !own(e.target) && selectedElement
&& !selectedElement.contains(e.target)
) {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
exitConfigureToPicking('configure-outside-click', { clearHover: true });
return;
}
if (state === 'PICKING' && insertActive) {
if (own(e.target)) return;
if (!insertHoverAnchor || !insertHoverPosition) return;
e.preventDefault();
e.stopPropagation();
const placeholder = createInsertPlaceholder(
insertHoverAnchor,
insertHoverPosition,
insertHoverAxis,
);
if (!placeholder) return;
hideInsertLine();
configureKind = 'insert';
selectedElement = placeholder;
setLiveState('CONFIGURING');
hideHighlight();
clearAnnotations();
showAnnotOverlay(placeholder);
showBar('configure');
startScrollTracking();
return;
}
if (state !== 'PICKING' || !pickActive) return;
if (own(e.target)) return;
if (pagePickSkipClick || pageHasHostTextSelection()) {
pagePickSkipClick = false;
return;
}
if (!hoveredElement || !pickable(hoveredElement)) return;
e.preventDefault();
e.stopPropagation();
selectedElement = hoveredElement;
setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
showBar('configure');
renderEditBadge(hasTextRows(selectedElement) ? 'idle' : 'hidden');
startScrollTracking();
maybePrefetchPage();
maybeWarnConditionalAncestor(selectedElement);
}
/**
* Surface a brief, non-blocking heads-up when the picked element lives
* inside a container whose visibility is gated by ephemeral state - modals,
* collapsible panels, popovers, off-screen tab panels. If HMR remounts the
* parent during generation (Vite Fast Refresh, SvelteKit page reload), the
* variants land in source but stay invisible until the user re-opens the
* container. Telling the user upfront is much friendlier than the silent
* timeout-then-toast that they'd otherwise hit.
*
* Heuristic, intentionally narrow - only fires for unambiguous cases so
* we don't cry wolf on every nested element.
*/
function maybeWarnConditionalAncestor(el) {
let node = el?.parentElement;
let depth = 0;
while (node && depth < 12) {
// 1. Active dialog / modal
if (node.getAttribute && node.getAttribute('role') === 'dialog'
&& node.getAttribute('aria-modal') === 'true') {
showToast('Heads up: this element lives inside a dialog. If state resets during generation, you may need to re-open it.', 6000);
return;
}
// 2. Common Radix / shadcn / headless-ui open-state attribute
if (node.dataset && node.dataset.state === 'open') {
showToast('Heads up: this element lives inside an open panel. If state resets during generation, you may need to re-open it.', 6000);
return;
}
// 3. Tab panel - only meaningful when the page also shows ANOTHER
// tab as selected. A single tabpanel with no tablist is just a static
// section in disguise and isn't conditional.
if (node.getAttribute && node.getAttribute('role') === 'tabpanel') {
const list = document.querySelector('[role="tablist"]');
if (list) {
const tabs = list.querySelectorAll('[role="tab"]');
if (tabs.length > 1) {
showToast('Heads up: this element lives in a tab panel. If state resets during generation, switch back to this tab.', 6000);
return;
}
}
}
// 4. Collapsible: aria-expanded sibling. Look for the trigger button.
if (node.id) {
const trigger = document.querySelector(`[aria-controls="${CSS.escape(node.id)}"][aria-expanded="true"]`);
if (trigger) {
showToast('Heads up: this element lives inside an expandable section. If state resets during generation, re-expand it.', 6000);
return;
}
}
node = node.parentElement;
depth++;
}
}
// Fire a lightweight prefetch event the first time the user selects an
// element on a given route. The agent uses this to Read the underlying file
// into context before Go is hit, shaving the read off the critical path.
// Dedupe per session by pathname - clicking around on the same page doesn't
// re-fire.
//
// DISABLED: quick-Go workflows pay an extra harness round trip because
// prefetch + generate arrive as two events instead of one. Re-enable with
// a browser-side debounce (~800-1000ms, cancelled on Go) if we want to
// resurrect this. Server validator and skill dispatch remain in place so
// flipping this flag is the only change needed.
const PREFETCH_ENABLED = false;
const prefetchedPaths = new Set();
function maybePrefetchPage() {
if (!PREFETCH_ENABLED) return;
const path = location.pathname;
if (prefetchedPaths.has(path)) return;
prefetchedPaths.add(path);
sendEvent({ type: 'prefetch', pageUrl: path });
}
function shouldPassthroughElementNav(deepActive, e) {
if (!deepActive || !own(deepActive)) return false;
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return false;
if (!/^(INPUT|TEXTAREA)$/.test(deepActive.tagName || '')) return false;
if (deepActive.value) return false;
if (deepActive.id === PREFIX + '-input' && state === 'CONFIGURING') return true;
if (deepActive.id === PREFIX + '-page-chat-input' && state === 'PICKING') return true;
return false;
}
function handleKeyDown(e) {
// When the annotation input is focused, let it handle its own keys.
if (annotEditing && annotEditing.input && e.target === annotEditing.input) return;
const deepActive = activeElementDeep();
if (
deepActive
&& own(deepActive)
&& /^(INPUT|TEXTAREA|SELECT)$/.test(deepActive.tagName || '')
&& !shouldPassthroughElementNav(deepActive, e)
) {
return;
}
if (isPageEditableElement(deepActive) && !isInlineEditActive(deepActive)) {
return;
}
// While a contenteditable text-leaf is focused, let the browser handle
// all keys except Escape. Escape cancels the current edit (restores
// original text) and blurs without saving, staying in CONFIGURING.
if (e.target.isContentEditable && isInlineEditActive(e.target)) {
if (e.key !== 'Escape') return;
e.preventDefault();
e.stopPropagation();
const original = e.target.dataset.impeccableOriginalText;
if (original !== undefined) e.target.textContent = original;
// Programmatic textContent doesn't fire the 'input' event, so the draft
// map would otherwise hold the pre-cancel value and Apply would commit
// changes the user explicitly undid.
inlineEditDrafts.delete(e.target);
e.target.blur();
return;
}
if (pendingApplyInFlight) {
const liveNavKey = e.key === 'Enter'
|| e.key === 'ArrowUp'
|| e.key === 'ArrowDown'
|| e.key === 'ArrowLeft'
|| e.key === 'ArrowRight';
if (liveNavKey && (state === 'PICKING' || state === 'CONFIGURING' || state === 'CYCLING')) {
e.preventDefault();
e.stopPropagation();
if (e.key === 'Enter') showManualApplyBusyToast();
}
return;
}
if (e.key === 'Escape') {
e.preventDefault();
if (pickerEl?.style.display !== 'none') { hideActionPicker(); return; }
if (state === 'EDITING') { cancelEditing(); return; }
if (state === 'CONFIGURING') {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
exitConfigureToPicking('escape-from-configure');
return;
}
if (state === 'CYCLING') { handleDiscard(); return; }
if (state === 'SAVING' || state === 'CONFIRMED') return; // don't interrupt
if (state === 'PICKING') {
if (insertActive) toggleInsert();
else if (pickActive) togglePick();
else { hideHighlight(); setLiveState('IDLE'); }
return;
}
}
// Arrow/Enter nav works in PICKING (hover) and CONFIGURING (selected, input empty)
var navEl = (state === 'PICKING') ? hoveredElement : (state === 'CONFIGURING') ? selectedElement : null;
if (navEl && (e.key === 'ArrowUp' || e.key === 'ArrowDown' || (e.key === 'Enter' && state === 'PICKING'))) {
let next = null;
if (e.key === 'ArrowDown' && !e.shiftKey) {
next = navEl.nextElementSibling;
while (next && !pickable(next)) next = next.nextElementSibling;
} else if (e.key === 'ArrowUp' && !e.shiftKey) {
next = navEl.previousElementSibling;
while (next && !pickable(next)) next = next.previousElementSibling;
} else if (e.key === 'ArrowUp' && e.shiftKey) {
next = navEl.parentElement;
if (next && !pickable(next)) next = null;
} else if (e.key === 'ArrowDown' && e.shiftKey) {
next = navEl.firstElementChild;
while (next && !pickable(next)) next = next.nextElementSibling;
} else if (e.key === 'Enter') {
e.preventDefault();
selectedElement = hoveredElement;
setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
showBar('configure');
renderEditBadge(hasTextRows(selectedElement) ? 'idle' : 'hidden');
startScrollTracking();
return;
}
if (next) {
e.preventDefault();
if (state === 'PICKING') {
hoveredElement = next;
} else {
// CONFIGURING: re-select the new element
selectedElement = next;
clearAnnotations();
showAnnotOverlay(next);
showBar('configure');
disableInlineEdit();
renderEditBadge(hasTextRows(selectedElement) ? 'idle' : 'hidden');
startScrollTracking();
}
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 (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
if (!selectedElement || state !== 'CONFIGURING') return;
stopVoice({ suppressSubmit: true });
const input = uiGetById(PREFIX + '-input');
const prompt = input ? input.value.trim() : '';
// Commit any pending pin edit BEFORE we snapshot annotations.
if (annotEditing) finalizeEditingPin();
// Go captures page content, not manual-edit runtime state.
disableInlineEdit();
stripManualEditRuntimeState(selectedElement);
// A new cycle publishes new modules, so the previous cycle's mount failure
// is about files that no longer matter.
clearMountErrorCard();
lastReportedMountFailure = null;
pendingAcceptedSession = null;
// A new session supersedes any accept still awaiting its result; a late
// failure toast for the previous session would only mislead here.
awaitingAcceptResult = null;
currentSessionId = id8();
expectedVariants = selectedCount;
arrivedVariants = 0;
visibleVariant = 0;
generationPhase = 'queued';
parameterGenerationState = 'pending';
parameterReadyAnnouncedSession = null;
resetSessionFileMeta();
// Flip to GENERATING immediately so the bar morphs without waiting on
// capture + upload. The event is emitted from captureAndEmit() once the
// screenshot is uploaded (or capture fails - we still emit, just without
// screenshotPath).
const elForCapture = selectedElement;
pickedAnchorSnapshot = buildPickedAnchorSnapshot(elForCapture);
const captureRect = elForCapture.getBoundingClientRect();
pickedAnchorViewportTop = captureRect.top;
const snapshot = {
comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })),
strokes: annotState.strokes.map(s => ({ points: s.points.map(p => [p[0], p[1]]) })),
};
const basePayload = {
type: 'generate', id: currentSessionId,
action: selectedAction,
freeformPrompt: prompt || undefined,
count: selectedCount,
pageUrl: location.pathname,
element: extractContext(elForCapture),
};
if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments;
if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes;
// Hide the interactive overlay so it doesn't linger during generation.
hideAnnotOverlay();
clearAnnotations();
setLiveState('GENERATING');
// Disable the Edit badge: starting a manual text edit mid-generation would
// conflict with the variant wrap that's about to land in the same DOM
// region. Only swap if the badge was visible - picked elements with no
// text rows have it hidden already.
if (editBadgeEl && editBadgeEl.style.display !== 'none') renderEditBadge('idle-disabled');
showBar('generating');
saveSession();
sendCheckpoint('generate_started');
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
startScrollLock(currentSessionId, window.scrollY, pickedAnchorViewportTop);
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
}
function cancelInsertConfigure() {
hideBar();
stopScrollTracking();
hideAnnotOverlay();
clearAnnotations();
clearInsertPicking();
configureKind = 'replace';
selectedElement = null;
setLiveState(insertActive ? 'PICKING' : 'IDLE');
hideHighlight();
syncPageChatFocus('insert-configure-cancel');
}
function handleInsertCreate() {
if (!placeholderElement || !insertAnchorElement || state !== 'CONFIGURING' || configureKind !== 'insert') return;
const input = uiGetById(PREFIX + '-insert-input');
const prompt = input ? input.value.trim() : '';
if (annotEditing) finalizeEditingPin();
const snapshot = {
comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })),
strokes: annotState.strokes.map(s => ({ points: s.points.map(p => [p[0], p[1]]) })),
};
if (!canCreateInsert({ prompt, comments: snapshot.comments, strokes: snapshot.strokes })) return;
stopVoice({ suppressSubmit: true });
// A new cycle publishes new modules, so the previous cycle's mount failure
// is about files that no longer matter.
clearMountErrorCard();
lastReportedMountFailure = null;
pendingAcceptedSession = null;
// A new session supersedes any accept still awaiting its result; a late
// failure toast for the previous session would only mislead here.
awaitingAcceptResult = null;
currentSessionId = id8();
expectedVariants = selectedCount;
arrivedVariants = 0;
visibleVariant = 0;
generationPhase = 'queued';
parameterGenerationState = 'pending';
parameterReadyAnnouncedSession = null;
resetSessionFileMeta();
selectedElement = placeholderElement;
insertPlaceholderSnapshot = buildInsertPlaceholderSnapshotFromDom(insertAnchorElement, placeholderElement);
const elForCapture = placeholderElement;
const captureRect = elForCapture.getBoundingClientRect();
pickedAnchorViewportTop = captureRect.top;
const basePayload = {
type: 'generate',
mode: 'insert',
id: currentSessionId,
count: selectedCount,
pageUrl: location.pathname,
insert: {
position: insertAnchorPosition,
anchor: extractContext(insertAnchorElement),
},
placeholder: {
width: Math.round(captureRect.width),
height: Math.round(captureRect.height),
},
freeformPrompt: prompt || undefined,
};
if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments;
if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes;
hideAnnotOverlay();
clearAnnotations();
setLiveState('GENERATING');
showBar('generating');
startScrollTracking();
saveSession();
sendCheckpoint('generate_started');
writeScrollY(window.scrollY);
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
startScrollLock(currentSessionId, window.scrollY, pickedAnchorViewportTop);
captureAndEmit(elForCapture, basePayload, snapshot, captureRect);
}
//
// Screenshot capture + upload
//
let msLoadPromise = null;
function loadModernScreenshot() {
if (window.modernScreenshot) return Promise.resolve(window.modernScreenshot);
if (msLoadPromise) return msLoadPromise;
msLoadPromise = new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = 'http://localhost:' + PORT + '/modern-screenshot.js';
s.onload = () => resolve(window.modernScreenshot);
s.onerror = () => { msLoadPromise = null; reject(new Error('modern-screenshot failed to load')); };
uiAppendStyle(s);
});
return msLoadPromise;
}
// Collect @font-face rules from every stylesheet on the page. Cross-origin
// sheets (Google Fonts, Typekit, etc.) throw SecurityError on .cssRules
// access, so modern-screenshot can't embed them on its own - the resulting
// SVG falls back to system fonts and text re-wraps + renders with different
// weight. We fetch the raw CSS text (CORS-permitted for these providers),
// extract @font-face blocks, inline the referenced font files as base64
// data URIs (SVGs rasterized via canvas can't fetch external resources,
// so URLs inside the SVG silently fail without this), and pass the result
// to modern-screenshot as font.cssText.
const FONT_EXT_RE = /\.(woff2?|ttf|otf|eot)(\?.*)?$/i;
const FONT_MIME = {
woff2: 'font/woff2', woff: 'font/woff', ttf: 'font/ttf', otf: 'font/otf', eot: 'application/vnd.ms-fontobject',
};
function bufferToBase64(buf) {
const bytes = new Uint8Array(buf);
let binary = '';
const CHUNK = 0x8000;
for (let i = 0; i < bytes.length; i += CHUNK) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
}
return btoa(binary);
}
async function inlineFontUrls(cssText) {
const urlRe = /url\((['"]?)(https?:\/\/[^'")\s]+)\1\)/g;
const urls = new Set();
let m;
while ((m = urlRe.exec(cssText))) {
if (FONT_EXT_RE.test(m[2])) urls.add(m[2]);
}
const map = new Map();
await Promise.all([...urls].map(async (url) => {
try {
const res = await fetch(url);
if (!res.ok) return;
const buf = await res.arrayBuffer();
const ext = url.toLowerCase().match(FONT_EXT_RE)?.[1] || 'woff2';
const mime = FONT_MIME[ext] || 'application/octet-stream';
map.set(url, 'data:' + mime + ';base64,' + bufferToBase64(buf));
} catch { /* skip; fall through to URL */ }
}));
return cssText.replace(urlRe, (orig, q, url) => {
const data = map.get(url);
return data ? 'url(' + q + data + q + ')' : orig;
});
}
async function collectFontCssText() {
const chunks = [];
const fontFaceRe = /@font-face\s*\{[^}]*\}/g;
for (const sheet of document.styleSheets) {
try {
const rules = sheet.cssRules;
for (const rule of rules) {
if (rule.constructor.name === 'CSSFontFaceRule' || rule.cssText?.startsWith('@font-face')) {
chunks.push(rule.cssText);
}
}
} catch {
if (!sheet.href) continue;
try {
const res = await fetch(sheet.href);
if (!res.ok) continue;
const text = await res.text();
let m2;
while ((m2 = fontFaceRe.exec(text))) chunks.push(m2[0]);
} catch { /* ignore; capture is best-effort */ }
}
}
if (chunks.length === 0) return '';
return inlineFontUrls(chunks.join('\n'));
}
// True if `s` is a computed color string that renders as nothing
// (explicit `transparent`, or `rgba(...)` with alpha 0).
function isTransparentColor(s) {
if (!s) return true;
if (s === 'transparent') return true;
const m = /rgba?\(([^)]+)\)/.exec(s);
if (!m) return false;
const parts = m[1].split(',').map((p) => p.trim());
if (parts.length === 4) return parseFloat(parts[3]) === 0;
return false;
}
// modern-screenshot force-sets `background-color: X !important` on the
// cloned root whenever `backgroundColor` is passed, clobbering the
// element's own background. So we only pass it when the element is
// genuinely transparent (no own color, no own image) - in that case
// we resolve up the DOM to the nearest opaque ancestor so the capture
// sits on the page's real background instead of rendering black.
function resolveCanvasBackground(el) {
const own = getComputedStyle(el);
if (!isTransparentColor(own.backgroundColor)) return null;
if (own.backgroundImage && own.backgroundImage !== 'none') return null;
let node = el.parentElement;
while (node) {
const cs = getComputedStyle(node);
if (!isTransparentColor(cs.backgroundColor)) return cs.backgroundColor;
node = node.parentElement;
}
// The walk already passed through <body> and <html>; if they had been
// opaque we would have returned. Falling through with the previous
// `getComputedStyle(body).backgroundColor || …` chain is a trap: that
// call returns the literal string `"rgba(0, 0, 0, 0)"` for a page that
// never set its own bg, which is truthy and short-circuits the chain to
// transparent-black - modern-screenshot then renders the capture on a
// black canvas and the shader overlay flashes solid black during load.
// The browser canvas defaults to white, so we do too.
return '#ffffff';
}
function captureChromeNodes() {
const nodes = [];
const add = (node) => {
if (!node || node === document.body || nodes.includes(node)) return;
nodes.push(node);
};
add(document.getElementById(PREFIX + '-root'));
[
PREFIX + '-highlight',
PREFIX + '-tooltip',
PREFIX + '-bar',
PREFIX + '-picker',
PREFIX + '-params-panel',
PREFIX + '-insert-line',
PREFIX + '-insert-placeholder',
PREFIX + '-insert-create-tooltip',
PREFIX + '-annot',
PREFIX + '-design-host',
PREFIX + '-toast',
PREFIX + '-shader',
].forEach((id) => add(uiGetById(id)));
return nodes;
}
async function hideCaptureChromeForShaderProxy(fn) {
const saved = captureChromeNodes().map((node) => ({
node,
visibility: node.style.visibility,
priority: node.style.getPropertyPriority('visibility'),
}));
for (const { node } of saved) {
node.style.setProperty('visibility', 'hidden', 'important');
}
await new Promise((resolve) => requestAnimationFrame(resolve));
try {
return await fn();
} finally {
for (const { node, visibility, priority } of saved) {
node.style.setProperty('visibility', visibility, priority);
}
}
}
function shouldUseAncestorCropShaderProxy(el) {
// TODO: Enable this proxy for React/Vue/etc. adapters once their live
// preview mounts are covered by the same shader regression checks.
const adapter = String(window.__IMPECCABLE_LIVE_ADAPTER__ || '').toLowerCase();
if (adapter === 'svelte' || adapter === 'sveltekit') return true;
if (isFrameworkComponentPreviewMode(currentPreviewMode) || svelteComponentSession) return true;
const wrapper = el?.closest?.('[data-impeccable-variants]');
return isFrameworkComponentPreviewMode(wrapper?.dataset?.impeccablePreview);
}
function paintsShaderProxySurface(node) {
const s = getComputedStyle(node);
return !isTransparentColor(s.backgroundColor)
|| (s.backgroundImage && s.backgroundImage !== 'none')
|| paintsBackdrop(node);
}
function findShaderProxyCaptureRoot(el) {
const doc = el.ownerDocument || document;
const er = el.getBoundingClientRect();
let node = el.parentElement;
while (node && node !== doc.documentElement) {
const nr = node.getBoundingClientRect();
const containsElement =
nr.width > 0 && nr.height > 0 &&
nr.left <= er.left + 0.5 &&
nr.top <= er.top + 0.5 &&
nr.right >= er.right - 0.5 &&
nr.bottom >= er.bottom - 0.5;
if (containsElement && paintsShaderProxySurface(node)) return node;
node = node.parentElement;
}
return null;
}
// Capture the element (with current annotations baked in) and return
// { blob, paper }: the PNG Blob, plus the representative backdrop tone for the
// shader's halftone ground (so capture, upload, and shader all agree on what
// sits behind the element). Shared between the Go flow (uploads the blob) and
// the shader-resume path.
async function captureElementFromRenderedAncestor(ms, el, opts) {
const doc = el.ownerDocument || document;
const captureRoot = findShaderProxyCaptureRoot(el);
if (!captureRoot) throw new Error('No painted ancestor for Svelte shader proxy');
const rootCanvas = await ms.domToCanvas(captureRoot, opts);
const S = opts.scale;
const er = el.getBoundingClientRect();
const rr = captureRoot.getBoundingClientRect();
const sx = (er.left - rr.left) * S;
const sy = (er.top - rr.top) * S;
const sw = er.width * S;
const sh = er.height * S;
if (sw <= 0 || sh <= 0) throw new Error('Selected element has no visible capture rect');
const crop = doc.createElement('canvas');
crop.width = Math.max(1, Math.round(sw));
crop.height = Math.max(1, Math.round(sh));
const cctx = crop.getContext('2d', { willReadFrequently: true });
cctx.drawImage(rootCanvas, sx, sy, sw, sh, 0, 0, crop.width, crop.height);
const paper = dominantRgb01(cctx, crop.width, crop.height) || averageRgb01(cctx, crop.width, crop.height);
const blob = await new Promise((res) => crop.toBlob(res, 'image/png'));
if (!blob) throw new Error('Ancestor crop failed to produce a PNG blob');
return { blob, paper };
}
async function captureElementToBlob(el, snapshot, rect) {
try { if (document.fonts?.ready) await document.fonts.ready; } catch {}
const hasAnnotations = snapshot && (snapshot.comments.length > 0 || snapshot.strokes.length > 0);
let annotNode = null;
let savedPosition = null;
if (hasAnnotations) {
const pos = getComputedStyle(el).position;
if (pos === 'static') {
savedPosition = el.style.position;
el.style.position = 'relative';
}
annotNode = buildAnnotationsForCapture(rect, snapshot);
el.appendChild(annotNode);
}
try {
const ms = await loadModernScreenshot();
const fontCssText = await collectFontCssText();
const opts = {
scale: Math.min(window.devicePixelRatio || 1, 2),
font: fontCssText ? { cssText: fontCssText } : undefined,
};
if (shouldUseAncestorCropShaderProxy(el)) {
try {
return await hideCaptureChromeForShaderProxy(() => captureElementFromRenderedAncestor(ms, el, opts));
} catch (err) {
console.warn('[impeccable] Svelte ancestor crop capture failed, falling back to element capture:', err);
}
}
const bg = resolveCanvasBackground(el);
// Fast path: the element paints its own background, or an opaque ancestor
// color was found. modern-screenshot bakes that color; paper matches it.
if (bg !== '#ffffff') {
const blob = await ms.domToBlob(el, { ...opts, ...(bg ? { backgroundColor: bg } : {}) });
return { blob, paper: bg ? cssColorToRgb01(bg) : resolvePaperRgb(el) };
}
// Transparent up to the root. The visible backdrop may still come from an
// ancestor's background-image or a covering positioned layer (e.g. a hero
// art div) that the color walk can't see. Capture that ancestor and crop
// to the element so the real backdrop is embedded - correct for both the
// shader and the screenshot sent to the model. Fall back to white only
// when nothing is actually painted behind the element.
const backdrop = findBackdropAncestor(el);
if (!backdrop) {
const blob = await ms.domToBlob(el, { ...opts, backgroundColor: '#ffffff' });
return { blob, paper: SHADER_PAPER_FALLBACK };
}
const ancestorCanvas = await ms.domToCanvas(backdrop, opts);
const S = opts.scale;
const er = el.getBoundingClientRect();
const ar = backdrop.getBoundingClientRect();
const sx = (er.left - ar.left) * S, sy = (er.top - ar.top) * S;
const sw = er.width * S, sh = er.height * S;
const crop = document.createElement('canvas');
crop.width = Math.max(1, Math.round(sw));
crop.height = Math.max(1, Math.round(sh));
const cctx = crop.getContext('2d', { willReadFrequently: true });
cctx.drawImage(ancestorCanvas, sx, sy, sw, sh, 0, 0, crop.width, crop.height);
// Ground = backdrop sampled around the element, falling back to the crop
// mean only if the surround is fully transparent.
const actx = ancestorCanvas.getContext('2d', { willReadFrequently: true });
const paper = sampleSurroundingRgb(actx, sx, sy, sw, sh, ancestorCanvas.width, ancestorCanvas.height)
|| averageRgb01(cctx, crop.width, crop.height);
const blob = await new Promise((res) => crop.toBlob(res, 'image/png'));
return { blob, paper };
} finally {
if (annotNode) annotNode.remove();
if (savedPosition !== null) el.style.position = savedPosition;
}
}
async function captureAndEmit(el, basePayload, snapshot, rect) {
const hasAnnotations = snapshot && (snapshot.comments.length > 0 || snapshot.strokes.length > 0);
// Plain requests do not send a screenshot to the agent, so capture is
// presentation-only. Wait only for the helper to accept the event before
// starting CPU-heavy capture; this yields the browser task and prevents
// rasterization from delaying the fetch itself.
if (!hasAnnotations) {
basePayload.clientSentAt = Date.now();
await sendEvent(basePayload);
}
let screenshotPath;
let blob;
let paper;
try {
({ blob, paper } = await captureElementToBlob(el, snapshot, rect));
} catch (err) {
console.warn('[impeccable] capture failed, proceeding without screenshot:', err);
}
// Light up the shader overlay the moment capture is ready - no reason to
// wait for the upload to complete before the user sees something alive.
if (blob && state === 'GENERATING') {
showShaderOverlay(el, blob, rect, paper);
}
// Only upload + forward the screenshot when annotations (comments/strokes)
// are present. Without annotations the image is pure visual anchoring -
// it biases the model toward the current rendering and works against the
// three-distinct-directions brief.
if (blob && hasAnnotations) {
try {
const uploadRes = await fetch(
'http://localhost:' + PORT + '/annotation?token=' + encodeURIComponent(TOKEN) +
'&eventId=' + encodeURIComponent(basePayload.id),
{ method: 'POST', headers: { 'Content-Type': 'image/png' }, body: blob },
);
if (uploadRes.ok) {
const { path: p } = await uploadRes.json();
screenshotPath = p;
} else {
console.warn('[impeccable] annotation upload failed:', uploadRes.status);
}
} catch (err) {
console.warn('[impeccable] annotation upload failed:', err);
}
}
// Annotated requests must wait for capture + upload because the screenshot
// is semantic input. Plain requests were already dispatched above.
if (hasAnnotations) {
basePayload.clientSentAt = Date.now();
sendEvent(screenshotPath ? { ...basePayload, screenshotPath } : basePayload);
}
}
//
// Shader overlay - renders the captured screenshot as a WebGL texture and
// runs an editorial "ink-wash" fragment shader over it during generation.
// A single rolling band sweeps top-to-bottom, desaturating + tinting kinpaku
// and leaving a soft trail. Makes the wait feel like a letterpress scan
// instead of a dead spinner.
//
const SHADER_VS = `attribute vec2 a_position;
attribute vec2 a_uv;
varying vec2 v_uv;
void main() {
v_uv = a_uv;
gl_Position = vec4(a_position, 0.0, 1.0);
}`;
const SHADER_FS = `precision highp float;
uniform sampler2D u_texture;
uniform float u_time;
uniform vec2 u_resolution;
uniform vec3 u_accent;
uniform vec3 u_paper;
varying vec2 v_uv;
// Asymmetric roller band. Product of two one-sided smoothsteps - peaks at
// d=0 with a short sharp leading ramp and a longer soft trailing tail. Clean
// outside the [-leadW, trailW] range (no rogue "trail=1 everywhere below"
// failure that reversed-edge smoothstep would give).
float bandAt(float d, float leadW, float trailW) {
float above = smoothstep(-leadW, 0.0, d);
float below = 1.0 - smoothstep(0.0, trailW, d);
return above * below;
}
void main() {
vec2 uv = v_uv;
// Roller sweeps top-to-bottom with small overshoot so each cycle enters
// and exits the element cleanly.
float phase = fract(u_time / 3.4);
float y = phase * 1.25 - 0.12;
float band = bandAt(uv.y - y, 0.05, 0.32);
// Halftone cell grid (fixed ~10 px pitch).
float cellPx = 10.0;
vec2 gridUv = uv * u_resolution / cellPx;
vec2 cellId = floor(gridUv);
vec2 cellUv = fract(gridUv) - 0.5;
vec2 sampleCenter = (cellId + 0.5) * cellPx / u_resolution;
vec3 cellImg = texture2D(u_texture, sampleCenter).rgb;
// Dot size tracks how much the cell DIFFERS from the element's own ground
// (u_paper), not absolute darkness. So the content - text, buttons, anything
// that deviates from the background - always becomes the dots, on light AND
// dark surfaces. A plain darkness curve inverts on dark elements: the dark
// background fills with ink and the lighter content punches holes instead.
// Capped below the cell half-width so dense content stays separated dots.
float contrast = clamp(length(cellImg - u_paper) / 1.732, 0.0, 1.0);
float radius = min(sqrt(contrast) * 0.6, 0.38);
float dotMask = smoothstep(radius + 0.06, radius, length(cellUv));
// Two-stage dissolve as the roller passes, so the element is rebuilt purely
// from dot size (its own halftone) and never bleeds through as raw pixels
// behind the dots:
// 1. cover - the element flattens to the uniform paper ground first.
// 2. dotAmt - kinpaku dots then emerge, sized by each cell's luma.
// A plain mix(base, halftone, band) instead left the raw element visible
// through the band's soft core/trail. The paper ground is u_paper (the
// element's own bg tone) rather than a fixed white, so the dissolve reads the
// same over light and dark surfaces.
vec4 tex = texture2D(u_texture, uv);
vec3 base = tex.rgb;
float cover = smoothstep(0.0, 0.35, band);
float dotAmt = dotMask * smoothstep(0.15, 0.6, band);
vec3 ground = mix(base, u_paper, cover);
// Carry the capture's own alpha through, so a rounded corner or any genuinely
// transparent region stays transparent (the live backdrop shows through the
// canvas) instead of rendering as solid black.
gl_FragColor = vec4(mix(ground, u_accent, dotAmt), tex.a);
}`;
// Kinpaku gold converted to approximate sRGB 0-1 (matches oklch(84% 0.19 80.46))
const SHADER_ACCENT = [1.0, 0.78, 0.31];
// Fallback ground when an element and all its ancestors are transparent -
// matches the original off-white risograph paper.
const SHADER_PAPER_FALLBACK = [0.975, 0.965, 0.955];
let shaderState = null; // { canvas, gl, program, texture, rafId, startTime }
// The element's effective background tone, used as the uniform halftone
// ground so content dissolves into dots over it. Unlike resolveCanvasBackground
// (which returns null when the element paints its own bg), this always returns
// a usable color: the element's own background if any, else the nearest opaque
// ancestor, else the paper fallback.
// Rasterize any CSS color (oklch, color(), named, hex, rgb) through a 1x1
// canvas and read back the sRGB pixel. String-parsing computed colors is a
// trap: Chrome returns backgroundColor as oklch()/color() for oklch inputs,
// which a hex/rgb regex misses - every site token would fall back to white.
let colorParseCtx = null;
function cssColorToRgb01(str) {
if (!colorParseCtx) {
colorParseCtx = document.createElement('canvas').getContext('2d', { willReadFrequently: true });
}
// Clear first: the ctx is cached across calls, so a semi-transparent color
// would otherwise blend (source-over) with the previous call's leftover
// pixel, making the result depend on call history.
colorParseCtx.clearRect(0, 0, 1, 1);
colorParseCtx.fillStyle = '#000'; // invalid input leaves this default
colorParseCtx.fillStyle = str;
colorParseCtx.fillRect(0, 0, 1, 1);
const d = colorParseCtx.getImageData(0, 0, 1, 1).data;
return [d[0] / 255, d[1] / 255, d[2] / 255];
}
function resolvePaperRgb(el) {
let node = el;
while (node) {
const bg = getComputedStyle(node).backgroundColor;
if (!isTransparentColor(bg)) return cssColorToRgb01(bg);
node = node.parentElement;
}
return SHADER_PAPER_FALLBACK;
}
// When an element is transparent up to the root, its visible backdrop can
// still come from an ancestor's background-image or a covering positioned
// layer that is a *child* of an ancestor (e.g. a hero's absolute art div) -
// neither of which the ancestor background-COLOR walk can see. Return the
// nearest such ancestor so we can capture it and crop, embedding the real
// backdrop. Returns null when nothing is actually painted behind the element
// (genuinely transparent → white is correct).
function paintsBackdrop(node) {
const s = getComputedStyle(node);
if (s.backgroundImage && s.backgroundImage !== 'none') return true;
const nr = node.getBoundingClientRect();
for (const child of node.children) {
const ccs = getComputedStyle(child);
if (ccs.position !== 'absolute' && ccs.position !== 'fixed') continue;
const paints = !isTransparentColor(ccs.backgroundColor)
|| (ccs.backgroundImage && ccs.backgroundImage !== 'none');
if (!paints) continue;
const cr = child.getBoundingClientRect();
if (cr.width >= nr.width * 0.9 && cr.height >= nr.height * 0.9) return true;
}
return false;
}
function findBackdropAncestor(el) {
let node = el.parentElement;
while (node && node !== node.ownerDocument.documentElement) {
if (paintsBackdrop(node)) return node;
node = node.parentElement;
}
return null;
}
// Mean sRGB (0-1) of a canvas region, used as the halftone ground when the
// backdrop was captured from an ancestor rather than read from a CSS color.
function averageRgb01(ctx, w, h) {
const data = ctx.getImageData(0, 0, w, h).data;
let r = 0, g = 0, b = 0, n = 0;
// Stride a few pixels for speed; exact average is unnecessary for a ground.
for (let i = 0; i < data.length; i += 16) { r += data[i]; g += data[i + 1]; b += data[i + 2]; n++; }
return n ? [r / n / 255, g / n / 255, b / n / 255] : SHADER_PAPER_FALLBACK;
}
// Pick the most common visible color cluster from a crop. A straight average
// gets pulled by text and icons; the dominant bucket usually represents the
// surface the shader should dissolve into.
function dominantRgb01(ctx, w, h) {
const data = ctx.getImageData(0, 0, w, h).data;
const stride = Math.max(1, Math.floor((w * h) / 6000));
const buckets = new Map();
for (let p = 0; p < w * h; p += stride) {
const i = p * 4;
if (data[i + 3] < 16) continue;
const key = (data[i] >> 4) + ',' + (data[i + 1] >> 4) + ',' + (data[i + 2] >> 4);
const bucket = buckets.get(key) || { count: 0, r: 0, g: 0, b: 0 };
bucket.count += 1;
bucket.r += data[i];
bucket.g += data[i + 1];
bucket.b += data[i + 2];
buckets.set(key, bucket);
}
let best = null;
for (const bucket of buckets.values()) {
if (!best || bucket.count > best.count) best = bucket;
}
return best ? [best.r / best.count / 255, best.g / best.count / 255, best.b / best.count / 255] : null;
}
// Average the backdrop sampled just OUTSIDE an element's rect within a larger
// canvas. The ground tone for the dissolve must be the real backdrop, not the
// mean of the element's own crop - averaging the crop folds in the element's
// content (e.g. bright heading text), pulling the ground toward muddy gray.
function sampleSurroundingRgb(ctx, sx, sy, sw, sh, W, H) {
const pad = Math.max(2, Math.round(Math.min(sw, sh) * 0.12));
const fx = [0.2, 0.5, 0.8].map((f) => sx + sw * f);
const fy = [0.2, 0.5, 0.8].map((f) => sy + sh * f);
const pts = [];
for (const x of fx) { pts.push([x, sy - pad], [x, sy + sh + pad]); }
for (const y of fy) { pts.push([sx - pad, y], [sx + sw + pad, y]); }
let r = 0, g = 0, b = 0, n = 0;
for (const [px, py] of pts) {
const cx = Math.max(0, Math.min(W - 1, Math.round(px)));
const cy = Math.max(0, Math.min(H - 1, Math.round(py)));
const d = ctx.getImageData(cx, cy, 1, 1).data;
if (d[3] === 0) continue; // outside the ancestor's paint
r += d[0]; g += d[1]; b += d[2]; n++;
}
return n ? [r / n / 255, g / n / 255, b / n / 255] : null;
}
function compileShader(gl, type, source) {
const sh = gl.createShader(type);
gl.shaderSource(sh, source);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
const info = gl.getShaderInfoLog(sh);
gl.deleteShader(sh);
throw new Error('shader compile failed: ' + info);
}
return sh;
}
function positionShaderOverlay() {
if (!shaderState) return;
const anchor = resolveBarAnchor();
if (!anchor) return;
const r = anchor.getBoundingClientRect();
Object.assign(shaderState.canvas.style, {
top: r.top + 'px', left: r.left + 'px',
width: r.width + 'px', height: r.height + 'px',
});
}
function hideShaderOverlay() {
if (!shaderState) return;
if (shaderState.rafId) cancelAnimationFrame(shaderState.rafId);
if (shaderState.canvas) shaderState.canvas.remove();
if (shaderState.objectUrl) URL.revokeObjectURL(shaderState.objectUrl);
const lose = shaderState.gl?.getExtension?.('WEBGL_lose_context');
try { lose?.loseContext(); } catch {}
shaderState = null;
}
function showShaderBitmapFallback(canvas, blob) {
canvas.remove();
const objectUrl = URL.createObjectURL(blob);
const fallback = document.createElement('div');
fallback.id = PREFIX + '-shader';
// Copy positioning via cssText. Object.assign across CSSStyleDeclaration
// throws in modern Chromium because the source's indexed properties
// (style[0], [1], ...) are read-only and the engine forbids writing
// them on the destination.
fallback.style.cssText = canvas.style.cssText;
fallback.style.backgroundImage = 'url("' + objectUrl + '")';
fallback.style.backgroundSize = '100% 100%';
fallback.style.backgroundRepeat = 'no-repeat';
fallback.style.outline = '2px dashed ' + C.brand;
fallback.style.outlineOffset = '-2px';
uiAppend(fallback);
shaderState = { canvas: fallback, gl: null, program: null, texture: null, rafId: 0, startTime: 0, objectUrl };
}
async function showShaderOverlay(el, blob, rect, paper) {
hideShaderOverlay();
if (!blob || !el) return;
const canvas = document.createElement('canvas');
canvas.id = PREFIX + '-shader';
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const radius = getComputedStyle(el).borderRadius;
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
Object.assign(canvas.style, {
position: 'fixed',
top: rect.top + 'px', left: rect.left + 'px',
width: rect.width + 'px', height: rect.height + 'px',
borderRadius: radius,
overflow: 'hidden',
pointerEvents: 'none',
zIndex: Z.bar - 1,
});
uiAppend(canvas);
const gl = canvas.getContext('webgl', { premultipliedAlpha: false, preserveDrawingBuffer: false })
|| canvas.getContext('experimental-webgl');
if (!gl) {
// WebGL unavailable: use the captured bitmap as a background overlay so
// the user still sees something meaningful during generation.
showShaderBitmapFallback(canvas, blob);
return;
}
let program, texture;
try {
const vs = compileShader(gl, gl.VERTEX_SHADER, SHADER_VS);
const fs = compileShader(gl, gl.FRAGMENT_SHADER, SHADER_FS);
program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error('program link failed: ' + gl.getProgramInfoLog(program));
}
// Full-screen quad
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1, 0, 1,
1, -1, 1, 1,
-1, 1, 0, 0,
-1, 1, 0, 0,
1, -1, 1, 1,
1, 1, 1, 0,
]), gl.STATIC_DRAW);
const posLoc = gl.getAttribLocation(program, 'a_position');
const uvLoc = gl.getAttribLocation(program, 'a_uv');
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 16, 0);
gl.enableVertexAttribArray(uvLoc);
gl.vertexAttribPointer(uvLoc, 2, gl.FLOAT, false, 16, 8);
} catch (err) {
console.warn('[impeccable] shader setup failed:', err);
canvas.remove();
return;
}
// Upload the screenshot as a texture
let bitmap;
try {
bitmap = await createImageBitmap(blob);
} catch (err) {
console.warn('[impeccable] shader bitmap decode failed:', err);
const lose = gl.getExtension?.('WEBGL_lose_context');
try { lose?.loseContext(); } catch {}
showShaderBitmapFallback(canvas, blob);
return;
}
texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmap);
if (bitmap.close) bitmap.close();
const uTime = gl.getUniformLocation(program, 'u_time');
const uRes = gl.getUniformLocation(program, 'u_resolution');
const uAccent = gl.getUniformLocation(program, 'u_accent');
const uPaper = gl.getUniformLocation(program, 'u_paper');
const uTex = gl.getUniformLocation(program, 'u_texture');
const paperRgb = paper || resolvePaperRgb(el);
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
shaderState = { canvas, gl, program, texture, rafId: 0, startTime: performance.now(), reduced };
function frame() {
if (!shaderState) return;
const elapsed = (performance.now() - shaderState.startTime) / 1000;
const t = shaderState.reduced ? 0.0 : elapsed;
gl.viewport(0, 0, canvas.width, canvas.height);
gl.useProgram(program);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(uTex, 0);
gl.uniform1f(uTime, t);
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform3f(uAccent, SHADER_ACCENT[0], SHADER_ACCENT[1], SHADER_ACCENT[2]);
gl.uniform3f(uPaper, paperRgb[0], paperRgb[1], paperRgb[2]);
gl.drawArrays(gl.TRIANGLES, 0, 6);
shaderState.rafId = requestAnimationFrame(frame);
}
frame();
}
async function handleAccept() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
if (pendingAcceptedSession || state === 'SAVING') return;
if (variantSelectionPromise) {
try { await variantSelectionPromise; } catch { /* failed selection falls back below */ }
}
const domVisibleVariant = readVisibleVariantFromDOM(currentSessionId);
if (domVisibleVariant > 0) visibleVariant = domVisibleVariant;
const acceptPayload = {
type: 'accept',
id: currentSessionId,
variantId: String(visibleVariant),
pageUrl: location.pathname,
clientSentAt: Date.now(),
};
if (!currentSessionId || arrivedVariants === 0) return;
const acceptWrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');
if (Object.keys(paramsCurrentValues).length > 0) {
acceptPayload.paramValues = { ...paramsCurrentValues };
}
// The accepted variant is already the only visible child of the wrapper
// (all other variants are display:none). HMR from the source rewrite will
// replace the wrapper imminently. Don't eagerly replaceChild here - React
// reconciliation races with our mutation and throws NotFoundError in Next
// 16 / Turbopack. Schedule a fallback that runs the manual swap only if
// HMR hasn't cleaned up by then (keeps static-server flows working).
const acceptedSessionId = currentSessionId;
const acceptedVariant = visibleVariant;
const acceptedIsSvelteComponent = svelteComponentSession?.sessionId === acceptedSessionId
|| isFrameworkComponentPreviewMode(acceptWrapper?.dataset?.impeccablePreview);
const acceptedSnapshot = snapshotAcceptedVariantDom(acceptedSessionId, acceptedVariant);
setLiveState('SAVING');
updateBarContent('saving');
pendingAcceptedSession = {
id: acceptedSessionId,
variant: String(acceptedVariant),
isSvelteComponent: acceptedIsSvelteComponent,
...acceptedSnapshot,
finalizing: false,
};
saveSession();
sendEvent(acceptPayload, { throwOnError: true })
.then(() => {
const pending = pendingAcceptedSession;
if (!pending || pending.id !== acceptedSessionId) return;
// POST /events returns only after the accept intent is durable and the
// generation epoch is fenced. Source promotion/carbonize can finish in
// the background; the foreground picker is free immediately.
markSessionHandled();
setLiveState('CONFIRMED');
document.documentElement.dataset.impeccableAcceptToPickingMs = String(Date.now() - acceptPayload.clientSentAt);
awaitingAcceptResult = { id: acceptedSessionId };
scheduleAcceptCleanup(pending);
})
.catch(() => {
if (pendingAcceptedSession?.id === acceptedSessionId) pendingAcceptedSession = null;
setLiveState('CYCLING');
showOrUpdateCyclingBar();
showToast('Could not confirm accept with the live server. Session kept for recovery; try Accept again.', 5000);
});
}
function maybeCompleteAcceptedSession(msg) {
const pending = pendingAcceptedSession;
if (!pending || !msg?.id || msg.id !== pending.id) return false;
if (currentSessionId && currentSessionId !== pending.id) {
pendingAcceptedSession = null;
return false;
}
if (pending.finalizing) return true;
pending.finalizing = true;
markSessionHandled();
if (pending.isSvelteComponent) {
commitAcceptedSvelteComponentToDom(pending.id);
}
setLiveState('CONFIRMED');
updateBarContent('confirmed');
scheduleAcceptCleanup(pending);
return true;
}
function scheduleAcceptCleanup(accepted) {
queueMicrotask(function() {
if (pendingAcceptedSession?.id !== accepted?.id) return;
// Svelte previews live in an adapter-owned mount rather than in source
// wrapper markup. Promote the mounted variant before releasing the
// session so the old adapter instance cannot linger behind the next
// Pick → Go loop while carbonize finishes in the background.
if (accepted?.isSvelteComponent) {
commitAcceptedSvelteComponentToDom(accepted.id);
}
cleanupAcceptedSession();
});
// Let React/Vue/Svelte own the HMR reconciliation. Mutating their DOM in
// the same turn as the source update causes removeChild/NotFoundError
// races. Static servers still need a fallback, but it must not keep Live
// in SAVING or block the user's next pick.
if (!accepted?.isSvelteComponent) {
setTimeout(function() {
if (!acceptedDomAlreadyClean(accepted)) ensureAcceptedDomClean(accepted);
}, 1200);
}
}
function snapshotAcceptedVariantDom(sessionId, variantId) {
const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
const accepted = wrapper?.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
const root = accepted?.firstElementChild || null;
return {
acceptedHtml: accepted ? accepted.innerHTML : '',
acceptedSelector: selectorForAcceptedRoot(root),
parentElement: wrapper?.parentElement || null,
parentSelector: selectorForAcceptedRoot(wrapper?.parentElement || null),
nextSibling: wrapper?.nextSibling || null,
};
}
function selectorForAcceptedRoot(root) {
if (!root || !root.tagName) return '';
const tag = root.tagName.toLowerCase();
const classes = [...(root.classList || [])].filter(Boolean);
if (classes.length === 0) return tag;
return tag + classes.map((cls) => '.' + cssIdent(cls)).join('');
}
function acceptedDomAlreadyClean(pending) {
if (!pending?.acceptedSelector) return false;
const matches = [...document.querySelectorAll(pending.acceptedSelector)];
return matches.length > 0
&& matches.every((el) => !el.closest('[data-impeccable-variants],[data-impeccable-variant],[data-impeccable-carbonize]'));
}
function ensureAcceptedDomClean(pending) {
if (acceptedDomAlreadyClean(pending)) return;
const sessionId = pending?.id;
const variantId = pending?.variant;
const wrappers = findAcceptedRuntimeWrappers(sessionId);
if (wrappers.length === 0) {
restoreAcceptedDomFromSnapshot(pending);
return;
}
for (const wrapper of wrappers) {
if (!wrapper?.isConnected) continue;
const accepted = wrapper.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
if (!accepted) {
wrapper.remove();
continue;
}
const parent = wrapper.parentElement;
if (!parent) continue;
while (accepted.firstChild) {
parent.insertBefore(accepted.firstChild, wrapper);
}
wrapper.remove();
}
if (!acceptedDomAlreadyClean(pending)) restoreAcceptedDomFromSnapshot(pending);
}
function findAcceptedRuntimeWrappers(sessionId) {
if (!sessionId) return [];
return [...new Set([
...document.querySelectorAll('[data-impeccable-variants="' + sessionId + '"]'),
...document.querySelectorAll('[data-impeccable-carbonize="' + sessionId + '"]'),
])];
}
function restoreAcceptedDomFromSnapshot(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (!pending?.acceptedHtml) {
reloadAfterMissingAcceptedDom(pending);
return;
}
const parent = pending.parentElement?.isConnected
? pending.parentElement
: (pending.parentSelector ? document.querySelector(pending.parentSelector) : null);
if (!parent) {
reloadAfterMissingAcceptedDom(pending);
return;
}
const template = document.createElement('template');
template.innerHTML = pending.acceptedHtml;
const anchor = pending.nextSibling?.isConnected && pending.nextSibling.parentElement === parent
? pending.nextSibling
: null;
parent.insertBefore(template.content, anchor);
if (!acceptedDomAlreadyClean(pending)) reloadAfterMissingAcceptedDom(pending);
}
function reloadAfterMissingAcceptedDom(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (pending?.id && document.querySelector('[data-impeccable-variants="' + pending.id + '"]')) return;
location.reload();
}
function cleanupAcceptedSession() {
hideBar();
hideHighlight();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
stopScrollLock();
removeVariantStateStylesheet();
clearScrollY();
clearSession();
resetSessionFileMeta();
selectedElement = null;
hoveredElement = null;
pagePickSkipClick = false;
currentSessionId = null;
parameterGenerationState = 'idle';
parameterReadyAnnouncedSession = null;
selectedAction = 'impeccable';
pendingAcceptedSession = null;
renderEditBadge('hidden');
setLiveState('PICKING');
}
function commitAcceptedVariantToDom(sessionId, variantId) {
const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!wrapper) return false;
const accepted = wrapper.querySelector('[data-impeccable-variant="' + variantId + '"]');
if (!accepted || !accepted.firstElementChild) return false;
const parent = wrapper.parentElement;
if (!parent) return false;
const style = wrapper.querySelector('style[data-impeccable-css]');
if (style && !document.querySelector('style[data-impeccable-accepted-css="' + sessionId + '"]')) {
const promotedStyle = style.cloneNode(true);
promotedStyle.setAttribute('data-impeccable-accepted-css', sessionId);
parent.insertBefore(promotedStyle, wrapper);
}
const committed = accepted.cloneNode(true);
committed.removeAttribute('hidden');
committed.style.display = 'contents';
parent.replaceChild(committed, wrapper);
return true;
}
function handleDiscard() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
if (!currentSessionId) return;
sendEvent({ type: 'discard', id: currentSessionId }, { throwOnError: true })
.then(() => {
markSessionHandled();
cleanup({ restoreOriginal: true, instantChrome: true });
})
.catch(() => showToast('Could not confirm discard with the live server. Session kept for recovery.', 5000));
}
//
// Session persistence via live-browser-session.js
//
// Survives page reloads, browser close/reopen, HMR, and accidental refreshes.
function normalizeSessionPath(value) {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
return trimmed ? trimmed.replace(/\\/g, '/') : null;
}
function resetSessionFileMeta() {
currentSourceFile = null;
currentPreviewFile = null;
currentPreviewMode = null;
recoveryWaitingForAnchor = false;
pickedAnchorSnapshot = null;
pickedAnchorViewportTop = null;
}
function rememberSessionFileMeta(meta = {}) {
const file = normalizeSessionPath(meta.file);
const sourceFile = normalizeSessionPath(meta.sourceFile);
const previewFile = normalizeSessionPath(meta.previewFile);
const previewMode = meta.previewMode || (isSvelteComponentManifestPath(previewFile || file) ? 'svelte-component' : null);
if (isFrameworkComponentPreviewMode(previewMode) || isSvelteComponentManifestPath(file)) {
currentPreviewMode = isFrameworkComponentPreviewMode(previewMode) ? previewMode : 'svelte-component';
currentPreviewFile = previewFile || (isSvelteComponentManifestPath(file) ? file : currentPreviewFile);
currentSourceFile = sourceFile || currentSourceFile;
return;
}
if (sourceFile || file) currentSourceFile = sourceFile || file;
if (previewFile) currentPreviewFile = previewFile;
if (previewMode) currentPreviewMode = previewMode;
}
function applySavedSessionMeta(saved) {
if (!saved) return;
rememberSessionFileMeta(saved);
if (saved.insertPlaceholder) insertPlaceholderSnapshot = saved.insertPlaceholder;
if (saved.pickedAnchor) pickedAnchorSnapshot = saved.pickedAnchor;
if (Number.isFinite(saved.pickedAnchorViewportTop)) pickedAnchorViewportTop = saved.pickedAnchorViewportTop;
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
if (saved.previewMode) currentPreviewMode = saved.previewMode;
if (saved.paramValues && typeof saved.paramValues === 'object') {
paramsCurrentValues = { ...saved.paramValues };
}
if (saved.parameterState) parameterGenerationState = saved.parameterState;
if (saved.generationPhase) generationPhase = saved.generationPhase;
}
function normalizePagePath(value) {
if (!value || typeof value !== 'string') return null;
try {
return new URL(value, location.origin).pathname;
} catch {
return value.split(/[?#]/)[0] || null;
}
}
function pageMatchesCurrent(value) {
const path = normalizePagePath(value);
return !path || path === location.pathname;
}
function isTerminalSessionSummary(session) {
return /^(completed|discarded|discard_requested|accept_requested)$/.test(String(session?.phase || ''));
}
function findActiveSessionSummary(saved, activeSessions) {
if (!saved?.id || !Array.isArray(activeSessions)) return null;
return activeSessions.find((session) =>
session?.id === saved.id
&& pageMatchesCurrent(session.pageUrl || saved.pageUrl)
&& !isTerminalSessionSummary(session)
) || null;
}
function clampVariantIndex(value, count) {
const num = Number(value);
const max = Number(count);
if (!Number.isFinite(num) || num < 1) return 0;
if (Number.isFinite(max) && max > 0 && num > max) return 0;
return Math.floor(num);
}
/**
* A durable server session this page can adopt when the browser has no local
* record of it. Requires an explicit pageUrl match: a summary with no page is
* not evidence that it belongs to THIS page, and adopting it would hijack an
* unrelated route.
*/
// Phases in which the user is (or should be) comparing variants. Only these
// are adoptable by a browser with no local record. Steer and manual-edit
// sessions have no wrapper to restore, and accept/carbonize phases are
// agent-side work: a reload mid-carbonize must not resurrect the bar over a
// page whose comparison is already decided (a slow-CI reload hit exactly
// that window and left the bar stranded after accept).
const ADOPTABLE_SESSION_PHASES = new Set([
'generate_requested', 'variants_ready', 'generating', 'cycling',
]);
function findAdoptableServerSession(activeSessions) {
if (!Array.isArray(activeSessions)) return null;
return activeSessions.find((session) => (
session?.id
&& !isTerminalSessionSummary(session)
&& !isSessionHandled(session.id)
&& session.pageUrl
&& pageMatchesCurrent(session.pageUrl)
&& (session.previewFile || session.sourceFile)
&& Number(session.expectedVariants) > 0
&& ADOPTABLE_SESSION_PHASES.has(String(session.phase || ''))
)) || null;
}
// Shape a server summary like a saved local session so one restore path
// serves both. The server has no browser state machine, so an adopted session
// always re-enters GENERATING and lets the injection settle the final state.
function serverSessionAsSavedShape(session) {
return {
id: session.id,
state: 'GENERATING',
expected: Number(session.expectedVariants) || 0,
arrived: Number(session.arrivedVariants) || 0,
visible: Number(session.visibleVariant) || 0,
sourceFile: session.sourceFile || undefined,
previewFile: session.previewFile || undefined,
previewMode: session.previewMode || undefined,
pageUrl: session.pageUrl || undefined,
paramValues: session.paramValues && typeof session.paramValues === 'object' ? session.paramValues : {},
};
}
function restoreSessionWithoutWrapper(reason, activeSessions) {
const cached = loadSession();
// localStorage is a cache, not a gate. A cleared tab, a second browser
// profile, or a teardown that dropped local state all leave the durable
// server session as the only record of work in progress; adopt it instead
// of stranding a session the server still considers live.
const adopted = cached?.id ? null : findAdoptableServerSession(activeSessions);
const saved = cached?.id ? cached : (adopted ? serverSessionAsSavedShape(adopted) : null);
if (!saved?.id || isSessionHandled(saved.id)) return false;
const savedState = String(saved.state || '').toUpperCase();
if (savedState !== 'GENERATING' && savedState !== 'CYCLING') return false;
const serverSession = findActiveSessionSummary(saved, activeSessions);
if (Array.isArray(activeSessions) && activeSessions.length > 0 && !serverSession) {
return false;
}
currentSessionId = saved.id;
applySavedSessionMeta(serverSession);
applySavedSessionMeta(saved);
expectedVariants = Number(saved.expected || serverSession?.expectedVariants || selectedCount || 0);
arrivedVariants = Number(saved.arrived || serverSession?.arrivedVariants || 0);
if (arrivedVariants <= 0 && currentPreviewFile) arrivedVariants = Number(serverSession?.expectedVariants || saved.expected || selectedCount || 0);
if (expectedVariants <= 0) expectedVariants = Number(serverSession?.expectedVariants || arrivedVariants || selectedCount || 0);
visibleVariant = clampVariantIndex(saved.visible, arrivedVariants || expectedVariants)
|| clampVariantIndex(serverSession?.visibleVariant, arrivedVariants || expectedVariants)
|| (arrivedVariants > 0 ? 1 : 0);
const restoredAnchor = findLiveElementFromAnchorSnapshot(pickedAnchorSnapshot);
selectedElement = restoredAnchor || document.body;
setLiveState('GENERATING');
recoveryWaitingForAnchor = !restoredAnchor;
showBar('generating');
startScrollTracking();
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
saveSession();
queueCheckpoint(reason || 'browser_restore_without_wrapper');
const restoreFile = isFrameworkComponentPreviewMode(currentPreviewMode)
? currentPreviewFile
: (currentSourceFile || currentPreviewFile);
if (restoreFile) {
// A restored CYCLING session promises variants already written into
// source; if they are not there (after retries), the session is an
// orphan and must self-discard instead of freezing the picker (#439).
// GENERATING restores make no such promise: deferred-wrapper flows
// legitimately have no wrapper in source until the agent's write lands.
injectVariantsFromSource(restoreFile, currentSessionId, {
orphanDiscard: savedState === 'CYCLING' && !isFrameworkComponentPreviewMode(currentPreviewMode),
});
return true;
}
return true;
}
function restoreFromActiveSessions(activeSessions, reason) {
const wrapper = document.querySelector('[data-impeccable-variants]');
if (wrapper && !isFrameworkComponentPreviewMode(wrapper.dataset.impeccablePreview)) return false;
if (svelteComponentSession?.sessionId === currentSessionId) return false;
return restoreSessionWithoutWrapper(reason || 'sse_connected', activeSessions);
}
// Self-heal on SSE (re)connect. The preflight scaffold write triggers a
// framework full-reload (Astro reloads pages for any .astro edit); if the
// agent's variant write + `done` broadcast land while this page is
// mid-reload, both the done SSE and the second HMR reload are missed and
// the resumed page would wait in GENERATING at 0/N forever. The server's
// session summary carries the durable generationCompletedAt marker, so on
// every connect compare it against our own progress and pull the finished
// variants from source when behind. Mirrors the `done` handler's source
// fallback, including its give-HMR-the-first-chance settle delay.
function recoverMissedGenerationCompletion(activeSessions) {
if (!currentSessionId || state !== 'GENERATING') return;
if (!Array.isArray(activeSessions)) return;
const summary = activeSessions.find((session) => session?.id === currentSessionId);
if (!summary?.generationCompletedAt || summary.generationCanceled) return;
if (isTerminalSessionSummary(summary)) return;
if (arrivedVariants > 0 && arrivedVariants >= expectedVariants) return;
rememberSessionFileMeta(summary);
const sessionId = currentSessionId;
const file = isFrameworkComponentPreviewMode(currentPreviewMode)
? currentPreviewFile
: (summary.sourceFile || summary.previewFile || currentSourceFile || currentPreviewFile);
if (!file) return;
console.log('[impeccable] Reconnected after generation completed; recovering variants from source.');
setTimeout(() => {
if (sessionId !== currentSessionId || state !== 'GENERATING') return;
if (arrivedVariants > 0 && arrivedVariants >= expectedVariants) return;
injectVariantsFromSource(file, sessionId, { generationCompleted: true });
}, 750);
}
function saveSession() {
if (!currentSessionId) return;
// NOTE: scrollY is stored under a separate key (writeScrollY). Storing
// it here would overwrite the Go-time value every time state changes.
sessionState.saveSession({
id: currentSessionId,
appRoot: APP_ROOT || undefined,
state,
action: selectedAction,
count: selectedCount,
expected: expectedVariants,
arrived: arrivedVariants,
visible: visibleVariant,
sourceFile: currentSourceFile || undefined,
previewFile: currentPreviewFile || undefined,
previewMode: currentPreviewMode || undefined,
pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues },
parameterState: parameterGenerationState,
insertPlaceholder: insertPlaceholderSnapshot || undefined,
pickedAnchor: pickedAnchorSnapshot || undefined,
pickedAnchorViewportTop: Number.isFinite(pickedAnchorViewportTop) ? pickedAnchorViewportTop : undefined,
pageHash: location.hash || undefined,
pageSearch: location.search || undefined,
});
}
function loadSession() {
const saved = sessionState.loadSession();
// localStorage is per-origin, and two projects routinely reuse the same
// localhost port. A saved session stamped with another project's appRoot
// is that project's leftover, never a session this server can complete;
// resuming it freezes the picker behind an unfinishable banner.
if (saved?.appRoot && APP_ROOT && saved.appRoot !== APP_ROOT) {
console.warn('[impeccable] Ignoring saved live session from another project (' + saved.appRoot + ').');
sessionState.clearSession();
return null;
}
return saved;
}
function clearSession() {
sessionState.clearSession();
}
/** 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;
sessionState.markHandled(currentSessionId);
}
function isSessionHandled(id) {
return sessionState.isHandled(id);
}
function clearHandled() {
sessionState.clearHandled();
}
function cleanup(options) {
const restoreOriginal = options?.restoreOriginal === true;
const instantChrome = options?.instantChrome === true;
const cleanupSessionId = currentSessionId;
clearMountErrorCard();
lastReportedMountFailure = null;
if (svelteComponentSession?.sessionId === cleanupSessionId) {
teardownSvelteComponentSession(true);
} else if (cleanupSessionId) {
// Switch visibility immediately without structurally mutating the DOM.
// HMR from the agent's source rewrite may still be on its way,
// and a manual replaceChild under React causes NotFoundError when the
// reconciler later tries to remove a wrapper we already removed.
// Schedule a 2s fallback that does the manual swap only if HMR hasn't
// replaced the wrapper by then (keeps static-server / no-HMR flows alive).
const wrapper = document.querySelector('[data-impeccable-variants="' + cleanupSessionId + '"]');
if (wrapper) {
if (restoreOriginal) showOriginalDuringDiscard(cleanupSessionId);
else wrapper.style.display = 'none';
}
setTimeout(function() {
document.getElementById(DISCARD_STATE_STYLE_ID)?.remove();
if (!cleanupSessionId) return;
const lateWrapper = document.querySelector('[data-impeccable-variants="' + cleanupSessionId + '"]');
if (!lateWrapper) return;
const orig = lateWrapper.querySelector('[data-impeccable-variant="original"]');
if (orig) {
const content = orig.firstElementChild;
if (content) {
lateWrapper.parentElement.replaceChild(content, lateWrapper);
return;
}
}
lateWrapper.remove();
}, 2000);
}
hideBar(instantChrome);
hideHighlight();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
removeVariantStateStylesheet();
clearScrollY();
finalizeInsertSession();
clearSession();
resetSessionFileMeta();
selectedElement = null;
hoveredElement = null;
pagePickSkipClick = false;
currentSessionId = null;
parameterGenerationState = 'idle';
parameterReadyAnnouncedSession = null;
selectedAction = 'impeccable';
renderEditBadge('hidden');
setLiveState('PICKING');
}
//
// Toast
//
function dismissToast() {
if (!toastEl) return;
toastEl.remove();
toastEl = null;
}
function showToast(message, duration) {
dismissToast();
// Stack the toast above the global bar (which sits at bottom:14px) so
// the two never overlap. Read the bar's actual rect - its height varies
// with hover-expanded labels - and fall back to a sensible default
// when the bar isn't mounted yet.
const barRect = globalBarEl?.getBoundingClientRect();
const barTopFromBottom = barRect && barRect.height > 0
? Math.max(16, window.innerHeight - barRect.top + 12)
: 16;
const currentToast = el('div', {
position: 'fixed', bottom: barTopFromBottom + 'px', 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 = currentToast;
currentToast.id = PREFIX + '-toast';
currentToast.textContent = message;
uiAppend(currentToast);
requestAnimationFrame(() => {
if (toastEl !== currentToast) return;
currentToast.style.opacity = '1';
currentToast.style.transform = 'translateX(-50%) translateY(0)';
});
setTimeout(() => {
if (toastEl !== currentToast) return;
currentToast.style.opacity = '0';
currentToast.style.transform = 'translateX(-50%) translateY(8px)';
setTimeout(() => {
if (toastEl !== currentToast) return;
currentToast.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) {
if (restoreSessionWithoutWrapper('browser_resumed_without_wrapper')) return true;
clearSession();
clearHandled();
return false;
}
const sessionId = wrapper.dataset.impeccableVariants;
// Don't resume if this session was already accepted/discarded
if (isSessionHandled(sessionId)) return false;
// Svelte component sessions can't be resumed by counting DOM children: the
// wrapper holds a single mount target, not [data-impeccable-variant] nodes,
// and a page reload unmounts every compiled variant. Counting children here
// would strand the bar in CYCLING at 0/0. If there's no live in-memory mount
// for this wrapper, it's an orphan (reload / failed mount): drop it and let
// the live-server's SSE re-inject the manifest if the session is still live.
if (isFrameworkComponentPreviewMode(wrapper.dataset.impeccablePreview)
&& svelteComponentSession?.sessionId !== sessionId) {
wrapper.remove();
if (restoreSessionWithoutWrapper('browser_resumed_svelte_orphan_wrapper')) return true;
clearSession();
clearHandled();
return false;
}
if (isFrameworkComponentPreviewMode(wrapper.dataset.impeccablePreview)) {
if (!svelteComponentSession?.mountedVariant) {
return true;
}
currentSessionId = sessionId;
expectedVariants = Number(wrapper.dataset.impeccableVariantCount)
|| Number(svelteComponentSession.manifest?.count)
|| expectedVariants
|| 1;
arrivedVariants = expectedVariants;
const saved = loadSession();
applySavedSessionMeta(saved);
const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0;
visibleVariant = svelteComponentSession.mountedVariant > 0 && svelteComponentSession.mountedVariant <= arrivedVariants
? svelteComponentSession.mountedVariant
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
selectedElement = resolveSvelteComponentAnchor()
|| wrapper.parentElement;
setLiveState('CYCLING');
hideShaderOverlay();
showBar('cycling');
startScrollTracking();
refreshParamsPanel();
saveSession();
queueCheckpoint('browser_resumed_svelte_component');
return true;
}
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) {
applySavedSessionMeta(saved);
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;
}
if (saved && saved.id === sessionId && saved.insertPlaceholder) {
insertPlaceholderSnapshot = saved.insertPlaceholder;
}
const resumedState = arrivedVariants > 0 ? 'CYCLING' : 'GENERATING';
// Find the visible variant's content element for highlight positioning.
const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
const origEl = pickVariantContent(wrapper, 'original');
setLiveState(resumedState);
if (isInsert && resumedState === 'GENERATING' && arrivedVariants === 0) {
selectedElement = ensureInsertPlaceholder() || findInsertAnchorInDom() || wrapper;
} else {
selectedElement = visEl || origEl || (isInsert ? findInsertAnchorInDom() : null) || wrapper.parentElement;
}
// Set display state BEFORE starting observer (avoid triggering it)
if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant);
showBar(state === 'CYCLING' ? 'cycling' : 'generating');
startScrollTracking();
// Build the params panel for the restored visible variant. Previously
// this was missed on page-reload resume: showVariantInDOM above fires
// refreshParamsPanel, but state was still IDLE at that moment so it
// hid. Now that state is CYCLING, re-fire.
if (state === 'CYCLING') refreshParamsPanel();
saveSession();
if (arrivedVariants > 0 && arrivedVariants < expectedVariants) {
sendCheckpoint('variants_progress');
} else {
queueCheckpoint('browser_resumed');
}
// Start observing for more variants AFTER initial setup
if (variantObserver) variantObserver.disconnect();
variantObserver = startVariantObserver(currentSessionId);
// Hold the target at its saved viewport top through any subsequent
// HMR patches, variant inserts, or cycle swaps.
startScrollLock(currentSessionId, readScrollY(), pickedAnchorViewportTop);
// If we reloaded mid-generation (Bun's HTML HMR destroys the shader
// canvas), re-capture the original's content and restart the shader so
// the wait doesn't go dead.
if (state === 'GENERATING') {
const shaderTarget = isInsert
? (ensureInsertPlaceholder() || findInsertAnchorInDom())
: origEl;
if (shaderTarget) {
(async () => {
try {
const rect = shaderTarget.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const { blob, paper } = await captureElementToBlob(shaderTarget, null, rect);
if (blob && state === 'GENERATING') {
showShaderOverlay(shaderTarget, blob, rect, paper);
}
} catch (err) {
console.warn('[impeccable] shader resume failed:', err);
}
})();
}
}
return true;
}
//
// Global bar (always visible at bottom)
//
let globalBarEl = null;
let globalBarBrandEl = null;
let agentPollTooltipEl = null;
let agentPollingConnected = false;
let agentStatusMessage = null;
let agentStatusPollTimer = null;
let steerFocusSuspended = false;
let steerFocusPauseUntil = 0;
let pagePointerGesture = null;
let pagePickSkipClick = false;
let steerFocusRecoverTimer = null;
const STEER_PAGE_FOCUS_PAUSE_MS = 500;
let detectActive = false;
let detectScanSeq = 0;
let activeDetectScanId = null;
let pendingDetectScanId = null;
const DETECT_EMPTY_MESSAGE = 'No detector issues found.';
const PICK_PREFS_KEY = 'impeccable-live-pick';
const INTERACTION_PREFS_KEY = 'impeccable-live-interaction';
const PLACEHOLDER_DEFAULT_HEIGHT = 80;
const PLACEHOLDER_MIN_HEIGHT = 48;
const PLACEHOLDER_MIN_WIDTH = 120;
function loadInteractionPrefs() {
try {
const raw = localStorage.getItem(INTERACTION_PREFS_KEY);
if (raw) {
const prefs = JSON.parse(raw);
return {
pickActive: !!prefs.pickActive,
insertActive: !!prefs.insertActive,
};
}
const legacy = localStorage.getItem(PICK_PREFS_KEY);
if (legacy) {
const prefs = JSON.parse(legacy);
return { pickActive: !!prefs.pickActive, insertActive: false };
}
} catch { /* ignore */ }
return { pickActive: false, insertActive: false };
}
function saveInteractionPrefs() {
try {
localStorage.setItem(INTERACTION_PREFS_KEY, JSON.stringify({ pickActive, insertActive }));
} catch { /* ignore */ }
}
function loadPickPref() {
return loadInteractionPrefs().pickActive;
}
function savePickPref() {
saveInteractionPrefs();
}
let pickActive = loadInteractionPrefs().pickActive;
let insertActive = loadInteractionPrefs().insertActive;
let configureKind = 'replace';
let insertLineEl = null;
let insertHoverAnchor = null;
let insertHoverPosition = null;
let insertHoverAxis = null;
let insertAnchorElement = null;
let insertAnchorPosition = null;
let insertAnchorLayoutAxis = null;
let insertPlaceholderSnapshot = null;
let placeholderElement = null;
let detectCount = 0;
let detectScriptLoaded = false;
let pendingDockEl = null;
let pendingPillEl = null;
let pendingPillSpinnerEl = null;
let pendingPillLabelEl = null;
let pendingPillCountEl = null;
let pendingTrashBtn = null;
let pendingKeepFixingBtn = null;
let pendingRollbackBtn = null;
let pendingDockResizeObserver = null;
let pendingIntroAnimation = null;
let pendingApplyInFlight = false;
let firstSaveOfSession = true;
// Steer - collapsed pill in the global bar; expands while typing for page-level chat.
let pageChatEl = null;
let pageChatInput = null;
let pageChatHint = null;
let pageChatVoiceBtn = null;
let pageChatSendBtn = null;
let pageChatQueueHintEl = null;
let pageChatExpanded = false;
let steerLocked = false;
let steerRequestId = null;
let steerPendingMessage = '';
let steerInputWasFocused = false;
let pageChatDotsEl = null;
let steerAwaitTimer = null;
let voiceRecognition = null;
let voiceListening = false;
let voiceSuppressSubmit = false;
let voiceInterimBase = '';
/** @type {{ mode: 'steer'|'configure', input: HTMLInputElement, submit: () => void, beforeStart?: () => void } | null} */
let voiceCtx = null;
const PAGE_CHAT_COLLAPSED_W = '104px';
const PAGE_CHAT_QUEUED_W = '212px';
const PAGE_CHAT_PLACEHOLDER_COLLAPSED = 'Steer…';
const PAGE_CHAT_PLACEHOLDER_EXPANDED = 'Steer the page…';
const STEER_AWAIT_TIMEOUT_MS = 120000;
const AGENT_STATUS_POLL_MS = 5000;
const AGENT_DISCONNECTED_MARK = 'oklch(62% 0 0 / 0.78)';
const AGENT_DISCONNECTED_TIP = 'Agent disconnected - run live-poll.mjs to connect';
// The indicator tracks whether a poll is parked, which is what decides if
// steering can reach the agent right now. That goes quiet two ways, and they
// need different copy: nobody is polling at all, or the agent took the work
// and is busy with it. Under one-shot foreground polling the second case is
// every normal generation, and telling the user to start a poll loop then is
// wrong advice about a healthy session.
const AGENT_BUSY_TIP = 'Agent is working - steering resumes when it finishes';
// Same distinction, said where the steer request is waiting. A submitted
// steer that lands while a generate holds the poll lease is not stuck, it is
// second in line, and the pulsing dots alone read as "nothing is happening".
const STEER_QUEUED_HINT = 'Queued behind current generation';
const GLOBAL_BAR_SECTION_GAP = 8;
const GLOBAL_BAR_INNER_GAP = 2;
const GLOBAL_BAR_INNER_PAD_LEFT = 2;
const PAGE_CHAT_EXPANDED_MAX_W = 280;
const ICON_PAGE_CHAT =
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
const ICON_PAGE_VOICE =
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>';
// Theme-aware color palette for the global bar. We detect the page's
// ambient background and invert - dark bar on light pages, light bar on
// dark pages. This keeps the bar from fighting with the host design.
function detectPageTheme() {
try {
// Dev override: set localStorage 'impeccable-dev-theme' to 'light' or
// 'dark' to preview the opposite palette without actually changing the
// page bg. Used for screenshots and theme QA.
const override = localStorage.getItem('impeccable-dev-theme');
if (override === 'light' || override === 'dark') return override;
// Walk body → html, taking the first opaque background. The browser's
// default body / html background is `rgba(0, 0, 0, 0)`, which a naive
// regex would read as black and mislabel a perfectly white page as
// dark. Honoring alpha avoids that - and falling through to <html>
// catches the common pattern of a bg only on <html> (or only on body).
function readOpaque(el) {
if (!el) return null;
const bg = getComputedStyle(el).backgroundColor;
const m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
if (!m) return null;
const alpha = m[4] == null ? 1 : parseFloat(m[4]);
if (alpha < 0.5) return null; // transparent / nearly transparent → skip
return [+m[1], +m[2], +m[3]];
}
const rgb = readOpaque(document.body) || readOpaque(document.documentElement);
// Both transparent → fall back to the browser's effective canvas color.
// White is the universal default; only one in a thousand sites swaps it
// via `color-scheme: dark` on <html>, and `prefers-color-scheme` lets
// us catch that case.
if (!rgb) {
return matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
const [r, g, b] = rgb;
// Perceptual luminance (Rec. 709)
const L = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
return L > 0.55 ? 'light' : 'dark';
} catch { return 'light'; }
}
function barPaletteForTheme(_theme) {
// Picker chrome always uses neo-kinpaku styling (homepage /live-mode demo
// bars in kinpaku-kit.css), regardless of host page light/dark theme.
return {
surface: C.ink,
surfaceDeep: C.ink,
// Quiet neutral hairline (was the loud kinpaku gold border). Gold lives on
// the brand mark and the active control instead.
border: 'oklch(92% 0 0 / 0.13)',
// Crisp graphite pill behind the active toggle (was a murky kinpaku-dim
// wash); the gold text/icon carries the "selected" signal.
toggleActive: 'oklch(27% 0 0)',
// Neutral hairline for internal control borders / dividers (was a warm
// gold rule that read as muddy champagne edges on the pill / input / count).
hairline: 'oklch(92% 0 0 / 0.12)',
text: 'oklch(91% 0 0)',
textDim: 'oklch(72% 0 0)',
accent: C.brand,
accentSoft: C.brandSoft,
exitHover: 'oklch(58% 0.15 35 / 0.18)',
shadow: PICKER_SHADOW,
chatSurface: 'oklch(22% 0.012 82)',
// Verdigris patina - secondary state (see site/styles/kinpaku-tokens.css)
patina: 'oklch(70% 0.12 188)',
patinaPale: 'oklch(82% 0.07 188)',
patinaSoft: 'oklch(70% 0.12 188 / 0.28)',
};
}
function pageChatPalette() {
return barPaletteForTheme(globalBarEl?.dataset.theme || detectPageTheme());
}
function globalBarModeToggles() {
return [
uiGetById(PREFIX + '-pick-toggle'),
uiGetById(PREFIX + '-insert-toggle'),
uiGetById(PREFIX + '-detect-toggle'),
uiGetById(PREFIX + '-design-toggle'),
].filter(Boolean);
}
function applyGlobalBarLabelState(expandInactive, forceCollapse = false) {
globalBarModeToggles().forEach((toggle) => {
if (forceCollapse) toggle._collapseLabel?.(true);
else if (expandInactive || toggle.dataset.active === 'true') toggle._expandLabel?.();
else toggle._collapseLabel?.();
});
}
function syncGlobalBarExpandedLabels(expanded = globalBarEl?.matches(':hover')) {
const expandInactive = !!(expanded && !pageChatExpanded);
applyGlobalBarLabelState(expandInactive, pageChatExpanded);
if (expandInactive && globalBarEl && globalBarEl.scrollWidth > window.innerWidth - 16) {
applyGlobalBarLabelState(false);
}
}
function pageChatCollapsedWidthPx() {
const parsed = parseFloat(PAGE_CHAT_COLLAPSED_W);
return Number.isFinite(parsed) ? parsed : 104;
}
function pageChatExpandedWidth() {
if (!pageChatEl || !globalBarEl) return PAGE_CHAT_EXPANDED_MAX_W + 'px';
const currentChatWidth = pageChatEl.getBoundingClientRect().width || pageChatCollapsedWidthPx();
const barWidth = Math.max(globalBarEl.getBoundingClientRect().width || 0, globalBarEl.scrollWidth || 0);
const nonChatWidth = Math.max(0, barWidth - currentChatWidth);
const available = window.innerWidth - 16 - nonChatWidth;
const next = Math.max(pageChatCollapsedWidthPx(), Math.min(PAGE_CHAT_EXPANDED_MAX_W, available));
return Math.round(next) + 'px';
}
function syncPageChatExpandedWidth() {
if (!pageChatEl || !pageChatExpanded) return;
pageChatEl.style.width = pageChatExpandedWidth();
}
function syncPageChatChrome() {
if (!pageChatEl) return;
const P = pageChatPalette();
const inputFocused = pageChatInput && activeElementDeep() === pageChatInput;
pageChatEl.style.background = P.chatSurface;
pageChatEl.style.borderColor = 'transparent';
if (pageChatHint) pageChatHint.style.color = steerLocked ? P.patinaPale : P.textDim;
const chatIcon = pageChatEl?.firstElementChild;
if (chatIcon) {
chatIcon.style.color = steerLocked
? P.patinaPale
: (inputFocused || pageChatExpanded ? P.text : P.textDim);
}
if (pageChatInput) pageChatInput.style.color = P.text;
if (pageChatVoiceBtn) {
const listening = pageChatVoiceBtn.dataset.listening === 'true';
pageChatVoiceBtn.style.color = listening || pageChatVoiceBtn.dataset.active === 'true'
? P.accent
: P.textDim;
}
}
function syncPageChatVisual() {
if (!pageChatInput || steerLocked) {
syncPageChatSendButton();
return;
}
const hasText = pageChatInput.value.length > 0;
if (hasText && !pageChatExpanded) expandPageChat({ focus: false });
else if (!hasText && pageChatExpanded) collapsePageChat();
syncPageChatSendButton();
}
/**
* Send is visible once the pill is open for typing and enabled once there is
* something to send. It disappears entirely while a steer is in flight, the
* same way the mic does: a second submit during the lock has nowhere to go.
*/
function syncPageChatSendButton() {
if (!pageChatSendBtn) return;
const P = pageChatPalette();
const hasText = !!pageChatInput?.value.trim();
const focused = pageChatInput && activeElementDeep() === pageChatInput;
const visible = !steerLocked && (pageChatExpanded || hasText || focused);
pageChatSendBtn.style.display = visible ? 'inline-flex' : 'none';
pageChatSendBtn.disabled = !visible || !hasText;
pageChatSendBtn.style.background = P.accent;
pageChatSendBtn.style.color = C.ink;
pageChatSendBtn.style.borderLeft = '1px solid ' + P.hairline;
pageChatSendBtn.style.opacity = pageChatSendBtn.disabled ? '0.42' : '1';
pageChatSendBtn.style.cursor = pageChatSendBtn.disabled ? 'not-allowed' : 'pointer';
pageChatSendBtn.title = pageChatSendBtn.disabled ? 'Type what to change first' : 'Send (Enter)';
}
/**
* A submitted steer is queued, not ignored, whenever no poll is parked and
* the browser knows a generation is in flight: the agent holds the lease and
* will not see the steer until it finishes. Saying so beats three dots that
* look identical to a lost request.
*/
function steerQueuedBehindGeneration() {
return steerLocked && !agentPollingConnected && agentHasWorkInFlight();
}
function buildSteerQueueHint() {
const P = pageChatPalette();
const hint = el('span', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'flex-end',
flex: '1', minWidth: '0', marginLeft: 'auto',
padding: '0 10px 0 6px',
fontFamily: FONT, fontSize: '10.5px', fontWeight: '500',
color: P.patinaPale,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
pointerEvents: 'none',
});
hint.id = PREFIX + '-page-chat-queue';
return hint;
}
function syncSteerQueueHint() {
if (!pageChatEl) return;
const queued = steerQueuedBehindGeneration();
if (queued) {
if (!pageChatQueueHintEl) {
pageChatQueueHintEl = buildSteerQueueHint();
pageChatEl.appendChild(pageChatQueueHintEl);
}
pageChatQueueHintEl.textContent = STEER_QUEUED_HINT;
if (pageChatDotsEl) pageChatDotsEl.style.display = 'none';
pageChatEl.style.width = PAGE_CHAT_QUEUED_W;
pageChatEl.setAttribute('aria-label', STEER_QUEUED_HINT);
return;
}
if (pageChatQueueHintEl?.parentNode) {
pageChatQueueHintEl.remove();
pageChatQueueHintEl = null;
if (steerLocked) {
pageChatEl.style.width = pageChatExpanded ? pageChatExpandedWidth() : PAGE_CHAT_COLLAPSED_W;
pageChatEl.setAttribute('aria-label', 'Processing steer request');
}
}
if (pageChatDotsEl) pageChatDotsEl.style.display = '';
}
function shouldFocusSteerChat() {
return state !== 'CONFIGURING'
&& state !== 'EDITING'
&& !steerLocked;
}
function isPageEditableElement(el) {
if (!el || own(el)) return false;
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName || '')) return true;
return !!el.isContentEditable;
}
function isInlineEditActive(el) {
return !!el && inlineEditRows.some((r) => r.el === el);
}
function isPageEditableActive() {
const active = activeElementDeep();
return isPageEditableElement(active) && !isInlineEditActive(active);
}
function pageHasHostTextSelection() {
const sel = window.getSelection?.();
if (!sel || sel.isCollapsed) return false;
if (!(sel.toString() || '').trim()) return false;
const node = sel.anchorNode;
const el = node?.nodeType === 1 ? node : node?.parentElement;
if (el && own(el)) return false;
return true;
}
function shouldSteerAutoFocus() {
return shouldFocusSteerChat()
&& !steerFocusSuspended
&& !isPageEditableActive()
&& performance.now() >= steerFocusPauseUntil;
}
function clearSteerFocusRecoverTimer() {
if (steerFocusRecoverTimer) {
clearTimeout(steerFocusRecoverTimer);
steerFocusRecoverTimer = null;
}
}
function scheduleSteerFocusRecover(reason) {
clearSteerFocusRecoverTimer();
const attempt = () => {
steerFocusRecoverTimer = null;
if (state === 'CONFIGURING' || steerLocked || voiceListening) return;
if (pageChatEl?.contains(activeElementDeep())) return;
if (pageHasHostTextSelection()) {
steerFocusRecoverTimer = setTimeout(attempt, 120);
return;
}
const pauseLeft = steerFocusPauseUntil - performance.now();
if (pauseLeft > 0) {
steerFocusRecoverTimer = setTimeout(attempt, pauseLeft);
return;
}
if (!shouldFocusSteerChat()) return;
syncPageChatFocus(reason);
};
steerFocusRecoverTimer = setTimeout(attempt, 0);
}
function notePagePointerDown(e) {
if (!shouldFocusSteerChat() || own(e.target)) return;
steerFocusSuspended = true;
steerFocusPauseUntil = performance.now() + STEER_PAGE_FOCUS_PAUSE_MS;
pagePointerGesture = { x: e.clientX, y: e.clientY, dragged: false };
if (pageChatInput && activeElementDeep() === pageChatInput) {
pageChatInput.blur();
}
}
function attachSteerFocusGuard() {
if (window.__IMPECCABLE_STEER_FOCUS_GUARD__) return;
window.__IMPECCABLE_STEER_FOCUS_GUARD__ = true;
document.addEventListener('mousedown', (e) => {
notePagePointerDown(e);
}, true);
document.addEventListener('mousemove', (e) => {
if (!pagePointerGesture || pagePointerGesture.dragged) return;
const dx = e.clientX - pagePointerGesture.x;
const dy = e.clientY - pagePointerGesture.y;
if (Math.hypot(dx, dy) > 4) pagePointerGesture.dragged = true;
}, true);
document.addEventListener('mouseup', () => {
if (!shouldFocusSteerChat()) return;
pagePickSkipClick = !!(pagePointerGesture?.dragged || pageHasHostTextSelection());
if (pageHasHostTextSelection()) {
steerFocusSuspended = true;
} else {
steerFocusSuspended = false;
scheduleSteerFocusRecover('page-mouseup-recover');
}
pagePointerGesture = null;
}, true);
document.addEventListener('selectionchange', () => {
if (!shouldFocusSteerChat()) return;
const wasSuspended = steerFocusSuspended;
steerFocusSuspended = pageHasHostTextSelection();
if (wasSuspended && !steerFocusSuspended) {
scheduleSteerFocusRecover('selection-cleared');
}
});
}
function steerFocusTargetLabel(el) {
if (!el || el === document.body) return 'body';
if (el === document.documentElement) return 'html';
if (el.id) return el.tagName.toLowerCase() + '#' + el.id;
return el.tagName?.toLowerCase() || String(el);
}
function steerFocusDebugEnabled() {
try { return localStorage.getItem('impeccable-steer-debug') === '1'; } catch { return false; }
}
function steerFocusLog(reason, extra) {
if (!steerFocusDebugEnabled()) return;
console.log('[impeccable.steer]', reason, {
state,
pickActive,
pageChatReady: !!pageChatInput,
pageChatExpanded,
active: steerFocusTargetLabel(activeElementDeep()),
shouldSteer: shouldFocusSteerChat(),
...(extra || {}),
});
}
function attachSteerFocusDebug() {
if (!steerFocusDebugEnabled()) return;
if (window.__IMPECCABLE_STEER_FOCUS_DEBUG__) return;
window.__IMPECCABLE_STEER_FOCUS_DEBUG__ = true;
document.addEventListener('focusin', (e) => {
if (!pageChatInput) return;
steerFocusLog('focusin', { target: steerFocusTargetLabel(e.target) });
}, true);
}
function focusConfigureInput(reason) {
steerFocusLog('focusConfigureInput', { reason });
const inputId = configureKind === 'insert' ? PREFIX + '-insert-input' : PREFIX + '-input';
const input = uiGetById(inputId);
if (!input) {
steerFocusLog('focusConfigureInput missing', { reason });
return;
}
setTimeout(() => {
const before = activeElementDeep();
input.focus();
steerFocusLog('focusConfigureInput result', {
reason,
before: steerFocusTargetLabel(before),
after: steerFocusTargetLabel(activeElementDeep()),
stuck: activeElementDeep() !== input,
});
}, 60);
}
function syncPageChatFocusRing() {
if (!pageChatEl || !pageChatInput) return;
syncPageChatSendButton();
const focused = activeElementDeep() === pageChatInput;
const typingReady = focused && !steerLocked;
pageChatEl.dataset.inputFocused = focused ? 'true' : 'false';
pageChatEl.style.boxShadow = 'none';
if (pageChatExpanded) {
pageChatInput.placeholder = PAGE_CHAT_PLACEHOLDER_EXPANDED;
pageChatInput.style.width = '';
pageChatInput.style.padding = '0 6px';
pageChatInput.style.opacity = steerLocked ? '0.72' : '1';
pageChatInput.style.pointerEvents = steerLocked ? 'none' : 'auto';
return;
}
if (typingReady) {
// Collapsed type-to-steer: show the real input + caret instead of a
// truncated patina "Steer" label with an invisible focused field.
pageChatInput.placeholder = PAGE_CHAT_PLACEHOLDER_COLLAPSED;
if (pageChatHint) {
pageChatHint.style.display = 'none';
pageChatHint.style.opacity = '0';
}
pageChatInput.style.width = '';
pageChatInput.style.padding = '0 4px';
pageChatInput.style.opacity = '1';
pageChatInput.style.pointerEvents = 'auto';
return;
}
pageChatInput.placeholder = PAGE_CHAT_PLACEHOLDER_COLLAPSED;
if (pageChatHint) {
pageChatHint.style.display = '';
pageChatHint.style.opacity = '1';
pageChatHint.style.visibility = '';
}
pageChatInput.style.width = '0';
pageChatInput.style.padding = '0';
pageChatInput.style.opacity = '0';
pageChatInput.style.pointerEvents = 'none';
}
function focusSteerChat(reason) {
steerFocusLog('focusSteerChat called', { reason });
if (!pageChatInput || !shouldSteerAutoFocus()) {
steerFocusLog('focusSteerChat skipped', {
reason,
hasInput: !!pageChatInput,
shouldSteer: shouldFocusSteerChat(),
suspended: steerFocusSuspended,
});
return;
}
syncPageChatVisual();
pageChatInput.style.pointerEvents = 'auto';
const before = activeElementDeep();
try { window.focus(); } catch { /* embed may block */ }
try { pageChatInput.focus({ preventScroll: true }); } catch { pageChatInput.focus(); }
syncPageChatFocusRing();
syncPageChatChrome();
steerFocusLog('focusSteerChat result', {
reason,
before: steerFocusTargetLabel(before),
after: steerFocusTargetLabel(activeElementDeep()),
stuck: activeElementDeep() !== pageChatInput,
});
}
function syncPageChatFocus(reason) {
steerFocusLog('syncPageChatFocus', { reason });
if (state === 'CONFIGURING') focusConfigureInput(reason);
else if (shouldSteerAutoFocus()) focusSteerChat(reason);
}
function buildSteerProcessingDots() {
const P = pageChatPalette();
const wrap = el('span', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'flex-end',
gap: '5px', flex: '0 0 auto', minWidth: '0', marginLeft: 'auto',
padding: '0 12px 0 8px',
pointerEvents: 'none',
});
wrap.setAttribute('aria-hidden', 'true');
for (let i = 0; i < 3; i++) {
wrap.appendChild(el('span', {
display: 'inline-block',
width: '4px', height: '4px', borderRadius: '50%',
background: P.patinaPale,
boxShadow: '0 0 6px ' + P.patinaSoft,
animation: 'impeccable-steer-dot 1.05s ease-in-out ' + (i * 0.14) + 's infinite',
}));
}
return wrap;
}
function keepSteerPointerInside(e, opts = {}) {
e.stopPropagation();
if (opts.preventDefault !== false) e.preventDefault();
}
function preparePageChatInputForTyping() {
if (!pageChatEl || !pageChatInput) return false;
pageChatExpanded = true;
pageChatEl.dataset.expanded = 'true';
syncGlobalBarExpandedLabels(false);
pageChatEl.style.width = pageChatExpandedWidth();
pageChatEl.style.cursor = steerLocked ? 'default' : 'text';
pageChatInput.placeholder = PAGE_CHAT_PLACEHOLDER_EXPANDED;
if (pageChatHint) {
pageChatHint.style.display = 'none';
pageChatHint.style.opacity = '0';
}
pageChatInput.style.width = '';
pageChatInput.style.padding = '0 6px';
pageChatInput.style.opacity = steerLocked ? '0.72' : '1';
pageChatInput.style.pointerEvents = steerLocked ? 'none' : 'auto';
return true;
}
function armPageChatForTyping(opts = {}) {
if (!pageChatEl || !pageChatInput || steerLocked) return false;
const expand = opts.expand !== false;
const focus = opts.focus !== false;
if (expand && !pageChatExpanded) {
preparePageChatInputForTyping();
syncPageChatChrome();
}
if (focus) return focusPageChatInput('arm-page-chat');
syncPageChatFocusRing();
syncPageChatChrome();
return true;
}
function focusPageChatInput(reason) {
if (!preparePageChatInputForTyping() || steerLocked) return false;
try { pageChatInput.focus({ preventScroll: true }); } catch { pageChatInput.focus(); }
const focused = activeElementDeep() === pageChatInput;
if (focused) steerInputWasFocused = true;
syncPageChatFocusRing();
return focused;
}
function clearSteerAwaitTimer() {
if (steerAwaitTimer) {
clearTimeout(steerAwaitTimer);
steerAwaitTimer = null;
}
}
function scheduleSteerAwaitTimeout(id) {
clearSteerAwaitTimer();
steerAwaitTimer = setTimeout(() => {
if (!steerLocked || steerRequestId !== id) return;
unlockSteerChat({
error: steerTimeoutMessage(),
restoreMessage: steerPendingMessage,
});
}, STEER_AWAIT_TIMEOUT_MS);
}
/**
* Two minutes of silence has three different causes and only one of them is
* "live-poll is not running". Naming the wrong one sends the user to restart
* a poll loop that was never the problem.
*/
function steerTimeoutMessage() {
const head = 'Steer timed out after 2 minutes. ';
if (steerQueuedBehindGeneration()) {
return head + 'The agent is still busy with the current generation - your message was not lost, but it never got picked up. Send it again once the variants land.';
}
if (!agentPollingConnected) {
return head + 'No agent is polling right now. Run live-poll.mjs, then send it again.';
}
return head + 'The agent picked it up but never replied with steer_done. Check the agent session for a stalled or failed steer.';
}
function lockSteerChat() {
if (!pageChatEl || !pageChatInput) return;
stopVoice({ suppressSubmit: true });
steerLocked = true;
pageChatEl.dataset.processing = 'true';
pageChatInput.disabled = true;
preparePageChatInputForTyping();
if (pageChatVoiceBtn) {
pageChatVoiceBtn.disabled = true;
pageChatVoiceBtn.style.display = 'none';
}
pageChatEl.style.cursor = 'default';
pageChatInput.style.pointerEvents = 'none';
if (pageChatHint) {
pageChatHint.style.display = 'none';
pageChatHint.style.visibility = 'hidden';
}
pageChatEl.setAttribute('aria-busy', 'true');
pageChatEl.setAttribute('aria-label', 'Processing steer request');
if (!pageChatDotsEl) {
pageChatDotsEl = buildSteerProcessingDots();
pageChatEl.appendChild(pageChatDotsEl);
}
syncSteerQueueHint();
syncPageChatFocusRing();
syncPageChatChrome();
}
function unlockSteerChat(opts) {
clearSteerAwaitTimer();
const restoreMessage = typeof opts?.restoreMessage === 'string' ? opts.restoreMessage : '';
const keepExpanded = Boolean(opts?.error && restoreMessage);
steerLocked = false;
const completedId = steerRequestId;
steerRequestId = null;
if (!pageChatEl) return;
pageChatEl.dataset.processing = 'false';
pageChatEl.removeAttribute('aria-busy');
pageChatEl.setAttribute('aria-label', 'Steer the page');
pageChatExpanded = keepExpanded;
pageChatEl.dataset.expanded = keepExpanded ? 'true' : 'false';
pageChatEl.style.width = keepExpanded ? pageChatExpandedWidth() : PAGE_CHAT_COLLAPSED_W;
pageChatEl.style.cursor = 'pointer';
if (pageChatInput) {
pageChatInput.disabled = false;
pageChatInput.value = keepExpanded ? restoreMessage : '';
pageChatInput.style.width = keepExpanded ? '' : '0';
pageChatInput.style.padding = keepExpanded ? '0 6px' : '0';
pageChatInput.style.opacity = keepExpanded ? '1' : '0';
pageChatInput.style.pointerEvents = 'auto';
}
if (pageChatVoiceBtn) {
pageChatVoiceBtn.disabled = false;
pageChatVoiceBtn.style.display = '';
}
if (pageChatHint) {
pageChatHint.textContent = 'Steer';
pageChatHint.style.display = keepExpanded ? 'none' : '';
pageChatHint.style.visibility = keepExpanded ? 'hidden' : '';
pageChatHint.style.opacity = keepExpanded ? '0' : '1';
}
if (pageChatDotsEl?.parentNode) {
pageChatDotsEl.remove();
pageChatDotsEl = null;
}
if (pageChatQueueHintEl?.parentNode) {
pageChatQueueHintEl.remove();
pageChatQueueHintEl = null;
}
steerPendingMessage = keepExpanded ? restoreMessage : '';
steerInputWasFocused = false;
syncPageChatChrome();
syncPageChatFocusRing();
if (opts?.error) showToast(String(opts.error), 5000);
else if (opts?.message) showToast(String(opts.message), 4000);
if (completedId) {
sendSteerCheckpoint(completedId, opts?.error ? 'steer_error' : 'steer_done', {
message: opts?.message || opts?.error || '',
file: opts?.file || '',
});
}
if (keepExpanded) focusPageChatInput('steer-error-restore');
else syncPageChatFocus('steer-unlock');
}
function steerSpeechRecognitionCtor() {
return window.SpeechRecognition || window.webkitSpeechRecognition || null;
}
function isEmbeddedPreviewBrowser() {
const ua = navigator.userAgent || '';
if (/Electron/i.test(ua)) return true;
if (/Cursor/i.test(ua)) return true;
try {
return !!(window.cursor || window.__CURSOR__ || window.__GLASS_BROWSER__);
} catch { return false; }
}
function steerVoiceUnavailableMessage() {
return 'Voice input works in Chrome or Safari. Cursor\'s preview browser cannot reach speech services.';
}
function steerVoiceErrorMessage(code) {
switch (code) {
case 'not-allowed':
return 'Microphone access blocked';
case 'audio-capture':
return 'No microphone found';
case 'network':
return isEmbeddedPreviewBrowser()
? steerVoiceUnavailableMessage()
: 'Voice input needs a network connection (browser speech uses a cloud service)';
case 'service-not-allowed':
return 'Voice input is not available in this browser tab';
case 'language-not-supported':
return 'Speech language not supported';
case 'no-speech':
case 'aborted':
return null;
default:
return 'Voice input failed (' + code + ')';
}
}
function syncVoiceUi(listening) {
voiceListening = !!listening;
if (voiceCtx?.mode === 'steer') {
if (pageChatVoiceBtn) {
pageChatVoiceBtn.dataset.active = listening ? 'true' : 'false';
pageChatVoiceBtn.dataset.listening = listening ? 'true' : 'false';
pageChatVoiceBtn.setAttribute('aria-label', listening ? 'Stop voice input' : 'Voice input');
pageChatVoiceBtn.setAttribute('aria-pressed', listening ? 'true' : 'false');
}
if (pageChatEl) pageChatEl.dataset.voiceListening = listening ? 'true' : 'false';
syncPageChatChrome();
} else if (voiceCtx?.mode === 'configure') {
// The bar shows either the replace row's voice button or the insert
// row's - both run voice through the 'configure' mode.
const voiceBtn = uiGetById(PREFIX + '-configure-voice') || uiGetById(PREFIX + '-insert-voice');
if (voiceBtn) {
voiceBtn.dataset.active = listening ? 'true' : 'false';
voiceBtn.dataset.listening = listening ? 'true' : 'false';
voiceBtn.setAttribute('aria-label', listening ? 'Stop voice input' : 'Voice input');
voiceBtn.setAttribute('aria-pressed', listening ? 'true' : 'false');
}
syncConfigureInputChrome();
}
}
function releaseVoiceEngine(opts) {
if (opts && opts.suppressSubmit) voiceSuppressSubmit = true;
const rec = voiceRecognition;
voiceRecognition = null;
if (!rec) return;
rec.onstart = null;
rec.onresult = null;
rec.onerror = null;
rec.onend = null;
try {
if (opts && opts.abort) rec.abort();
else rec.stop();
} catch { /* already ended */ }
}
function stopVoice(opts) {
releaseVoiceEngine(opts);
syncVoiceUi(false);
voiceCtx = null;
if (opts && opts.message) showToast(String(opts.message), opts.duration || 4000);
}
function finishVoiceSession() {
voiceRecognition = null;
const ctx = voiceCtx;
syncVoiceUi(false);
const suppress = voiceSuppressSubmit;
voiceSuppressSubmit = false;
voiceCtx = null;
const input = ctx?.input;
const text = input?.value.trim() || '';
if (suppress || !text || !ctx) return;
if (ctx.mode === 'steer' && !steerLocked) ctx.submit();
else if (ctx.mode === 'configure' && state === 'CONFIGURING') ctx.submit();
}
function startVoice(ctx) {
if (!ctx?.input || voiceListening) return;
if (ctx.mode === 'steer' && (steerLocked || state === 'CONFIGURING')) return;
if (ctx.mode === 'configure' && state !== 'CONFIGURING') return;
const Ctor = steerSpeechRecognitionCtor();
if (!Ctor) {
showToast('Voice input needs Speech Recognition (Chrome, Safari, or Edge)', 4500);
return;
}
if (!window.isSecureContext) {
showToast('Voice input needs HTTPS or localhost', 4500);
return;
}
if (isEmbeddedPreviewBrowser()) {
showToast(steerVoiceUnavailableMessage(), 5200);
return;
}
releaseVoiceEngine({ suppressSubmit: true, abort: true });
voiceSuppressSubmit = false;
voiceCtx = ctx;
if (ctx.beforeStart) ctx.beforeStart();
voiceInterimBase = ctx.input.value.trim()
? ctx.input.value.trim() + ' '
: '';
const rec = new Ctor();
rec.continuous = false;
rec.interimResults = true;
rec.lang = document.documentElement.lang || navigator.language || 'en-US';
rec.maxAlternatives = 1;
rec.onstart = () => {
syncVoiceUi(true);
};
rec.onresult = (event) => {
if (!voiceCtx?.input) return;
let transcript = '';
for (let i = 0; i < event.results.length; i++) {
transcript += event.results[i][0]?.transcript || '';
}
voiceCtx.input.value = (voiceInterimBase + transcript).trim();
if (voiceCtx.mode === 'steer') syncPageChatVisual();
else syncConfigureInputChrome();
};
rec.onerror = (event) => {
const code = event.error || 'unknown';
console.warn('[impeccable.voice] recognition error:', code);
const message = steerVoiceErrorMessage(code);
stopVoice({ suppressSubmit: true, message: message || undefined });
};
rec.onend = () => {
if (voiceRecognition !== rec) return;
finishVoiceSession();
};
voiceRecognition = rec;
try {
rec.start();
} catch (err) {
console.warn('[impeccable.voice] start failed:', err);
stopVoice({
suppressSubmit: true,
message: err?.message?.includes('already started')
? 'Voice input already running'
: 'Could not start voice input',
});
}
}
function steerVoiceContext() {
return {
mode: 'steer',
input: pageChatInput,
beforeStart: () => {
if (!pageChatExpanded) expandPageChat({ focus: false });
},
submit: submitSteerMessage,
};
}
function configureVoiceContext() {
const input = uiGetById(
configureKind === 'insert' ? PREFIX + '-insert-input' : PREFIX + '-input',
);
return {
mode: 'configure',
input,
beforeStart: () => { input?.focus(); },
submit: configureKind === 'insert' ? handleInsertCreate : handleGo,
};
}
function toggleSteerVoice() {
if (voiceListening && voiceCtx?.mode === 'steer') {
voiceSuppressSubmit = true;
stopVoice({ suppressSubmit: true, abort: true });
return;
}
startVoice(steerVoiceContext());
}
function toggleConfigureVoice() {
if (voiceListening && voiceCtx?.mode === 'configure') {
voiceSuppressSubmit = true;
stopVoice({ suppressSubmit: true, abort: true });
return;
}
startVoice(configureVoiceContext());
}
function submitSteerMessage() {
stopVoice({ suppressSubmit: true });
const text = pageChatInput?.value.trim();
if (!text || steerLocked) return;
const id = id8();
steerRequestId = id;
steerPendingMessage = text;
lockSteerChat();
scheduleSteerAwaitTimeout(id);
// Checkpoints follow the steer event, never precede it: the steer event
// is what creates the session journal server-side, and a checkpoint for
// a not-yet-created session is rejected as unknown_session.
sendEvent({
type: 'steer',
id,
message: text,
pageUrl: location.href,
}).then((res) => {
if (!res) {
unlockSteerChat({ error: 'Could not reach live server', restoreMessage: text });
return;
}
if (steerInputWasFocused) sendSteerCheckpoint(id, 'steer_input_focused', { focused: true });
sendSteerCheckpoint(id, 'steer_submitted', { message: text, pageUrl: location.href });
});
}
function maybeCompleteSteer(msg) {
if (!steerRequestId || msg.id !== steerRequestId) return false;
if (msg.type === 'steer_done') {
unlockSteerChat({ message: msg.message, file: msg.file });
if (msg.file && /\.svelte(?:$|\?)/.test(String(msg.file))) {
setTimeout(() => {
if (!steerLocked) showToast('Steer applied. Reload if the page has not refreshed yet.', 5000);
}, 4500);
}
return true;
}
if (msg.type === 'error') {
unlockSteerChat({ error: msg.message || 'Steer failed', restoreMessage: steerPendingMessage });
return true;
}
return false;
}
function expandPageChat(opts) {
const focus = !opts || opts.focus !== false;
if (!pageChatEl || !pageChatInput || steerLocked) return;
preparePageChatInputForTyping();
syncPageChatChrome();
syncPageChatFocusRing();
if (focus) focusPageChatInput('expand-page-chat');
}
function collapsePageChat(opts) {
const blur = opts && opts.blur === true;
if (voiceListening) return;
if (!pageChatEl || !pageChatInput) return;
pageChatExpanded = false;
pageChatEl.dataset.expanded = 'false';
pageChatEl.style.width = PAGE_CHAT_COLLAPSED_W;
pageChatEl.style.cursor = 'pointer';
syncGlobalBarExpandedLabels(globalBarEl?.matches(':hover'));
if (blur) {
pageChatInput.blur();
pageChatInput.style.pointerEvents = 'none';
} else {
pageChatInput.style.pointerEvents = 'auto';
}
if (pageChatHint && activeElementDeep() !== pageChatInput) {
pageChatHint.style.display = '';
pageChatHint.style.opacity = '1';
}
if (pageChatVoiceBtn) pageChatVoiceBtn.dataset.active = 'false';
syncPageChatChrome();
syncPageChatFocusRing();
}
function initPageChat(parent, P) {
pageChatEl = el('div', {
display: 'inline-flex', alignItems: 'center',
height: '28px', margin: '0 4px 0 ' + (GLOBAL_BAR_SECTION_GAP - GLOBAL_BAR_INNER_GAP) + 'px',
borderRadius: '7px',
background: P.chatSurface,
border: '1px solid transparent',
overflow: 'hidden',
cursor: 'pointer',
flexShrink: '0',
width: PAGE_CHAT_COLLAPSED_W,
transition: 'border-color 0.15s ease',
});
pageChatEl.id = PREFIX + '-page-chat';
pageChatEl.dataset.expanded = 'false';
pageChatEl.title = 'Steer the page';
const chatIcon = el('span', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
width: '28px', height: '28px', flexShrink: '0',
color: P.textDim, pointerEvents: 'none',
});
chatIcon.innerHTML = ICON_PAGE_CHAT;
pageChatHint = el('span', {
fontSize: '11.5px', fontWeight: '500',
color: P.textDim,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
flex: '1', minWidth: '0',
pointerEvents: 'none',
transition: 'opacity 0.15s ease',
});
pageChatHint.textContent = 'Steer';
pageChatInput = document.createElement('input');
pageChatInput.id = PREFIX + '-page-chat-input';
pageChatInput.type = 'text';
pageChatInput.placeholder = PAGE_CHAT_PLACEHOLDER_COLLAPSED;
pageChatInput.setAttribute('aria-label', 'Steer the page');
Object.assign(pageChatInput.style, {
flex: '1', minWidth: '0', width: '0',
padding: '0', border: 'none', background: 'transparent',
fontFamily: FONT, fontSize: '11.5px', color: P.text,
outline: 'none', opacity: '0', pointerEvents: 'none',
caretColor: P.accent,
transition: 'opacity 0.15s ease',
});
pageChatVoiceBtn = el('button', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
padding: '0', boxSizing: 'border-box',
width: '28px', height: '28px', flexShrink: '0',
border: 'none', background: 'transparent',
color: P.textDim, cursor: 'pointer',
transition: 'color 0.12s ease, background 0.12s ease',
});
pageChatVoiceBtn.id = PREFIX + '-page-chat-voice';
pageChatVoiceBtn.type = 'button';
pageChatVoiceBtn.setAttribute('aria-label', 'Voice input');
pageChatVoiceBtn.innerHTML = ICON_PAGE_VOICE;
// Visible Send, same affordance the element-level Go bar gets from
// buildConfigureSubmitButton. Enter still submits; the button exists so a
// typed steer does not look like a dead-end text field.
pageChatSendBtn = el('button', {
display: 'none', alignItems: 'center', justifyContent: 'center',
padding: '0', boxSizing: 'border-box',
width: '28px', height: '28px', flexShrink: '0',
border: 'none', borderLeft: '1px solid ' + P.hairline,
borderRadius: '0',
background: P.accent, color: C.ink,
cursor: 'pointer',
transition: 'filter 0.12s ease, opacity 0.12s ease',
});
pageChatSendBtn.id = PREFIX + '-page-chat-send';
pageChatSendBtn.type = 'button';
pageChatSendBtn.setAttribute('aria-label', 'Send steer message');
pageChatSendBtn.innerHTML = ICON_CONFIGURE_SUBMIT;
pageChatSendBtn.addEventListener('pointerdown', keepSteerPointerInside);
pageChatSendBtn.addEventListener('mousedown', keepSteerPointerInside);
pageChatSendBtn.addEventListener('mouseenter', () => {
if (!pageChatSendBtn.disabled) pageChatSendBtn.style.filter = 'brightness(1.1)';
});
pageChatSendBtn.addEventListener('mouseleave', () => { pageChatSendBtn.style.filter = 'none'; });
pageChatSendBtn.addEventListener('click', (e) => {
e.stopPropagation();
keepSteerPointerInside(e);
if (steerLocked || pageChatSendBtn.disabled) return;
submitSteerMessage();
});
pageChatEl.appendChild(chatIcon);
pageChatEl.appendChild(pageChatHint);
pageChatEl.appendChild(pageChatInput);
pageChatEl.appendChild(pageChatVoiceBtn);
pageChatEl.appendChild(pageChatSendBtn);
if (!uiGetById(PREFIX + '-page-chat-style')) {
const s = document.createElement('style');
s.id = PREFIX + '-page-chat-style';
s.textContent =
'@keyframes impeccable-steer-dot { 0%, 70%, 100% { opacity: 0.28; transform: scale(0.82); } 35% { opacity: 1; transform: scale(1); } }' +
'@keyframes impeccable-steer-processing { 0%, 100% { border-color: oklch(70% 0.12 188 / 0.28); box-shadow: 0 0 0 0 oklch(70% 0.12 188 / 0); } 50% { border-color: oklch(82% 0.07 188 / 0.55); box-shadow: 0 0 14px oklch(70% 0.12 188 / 0.18); } }' +
'@keyframes impeccable-voice-pulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }' +
'#' + PREFIX + '-page-chat[data-processing="true"] { animation: impeccable-steer-processing 1.6s ease-in-out infinite; }' +
'@media (prefers-reduced-motion: reduce) { #' + PREFIX + '-page-chat[data-processing="true"] { animation: none; border-color: oklch(70% 0.12 188 / 0.45); } #' + PREFIX + '-page-chat[data-processing="true"] [aria-hidden="true"] span { animation: none; opacity: 0.85; } }' +
'#' + PREFIX + '-page-chat[data-voice-listening="true"] { border-color: oklch(70% 0.12 188 / 0.45); }' +
'#' + PREFIX + '-page-chat-voice[data-listening="true"] svg { animation: impeccable-voice-pulse 1.1s ease-in-out infinite; }' +
'@media (prefers-reduced-motion: reduce) { #' + PREFIX + '-page-chat-voice[data-listening="true"] svg { animation: none; opacity: 1; } }' +
'#' + PREFIX + '-page-chat-input::placeholder { color: oklch(72% 0 0); opacity: 1; }' +
'#' + PREFIX + '-page-chat-input { caret-color: oklch(84% 0.19 80.46); }' +
'#' + PREFIX + '-page-chat[data-input-focused="true"]:not([data-expanded="true"]) #' + PREFIX + '-page-chat-input::placeholder { color: oklch(72% 0 0); }' +
'#' + PREFIX + '-page-chat-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
uiAppendStyle(s);
}
pageChatEl.addEventListener('pointerdown', (e) => {
keepSteerPointerInside(e);
if (steerLocked || pageChatVoiceBtn.contains(e.target) || pageChatSendBtn.contains(e.target)) return;
armPageChatForTyping({ expand: true, focus: false });
});
pageChatEl.addEventListener('mousedown', keepSteerPointerInside);
pageChatEl.addEventListener('click', (e) => {
keepSteerPointerInside(e);
if (steerLocked) return;
if (pageChatVoiceBtn.contains(e.target) || pageChatSendBtn.contains(e.target)) return;
armPageChatForTyping({ expand: true, focus: true });
});
pageChatVoiceBtn.addEventListener('pointerdown', keepSteerPointerInside);
pageChatVoiceBtn.addEventListener('mousedown', keepSteerPointerInside);
pageChatVoiceBtn.addEventListener('click', (e) => {
keepSteerPointerInside(e);
if (steerLocked) return;
toggleSteerVoice();
});
pageChatInput.addEventListener('pointerdown', keepSteerPointerInside);
pageChatInput.addEventListener('mousedown', keepSteerPointerInside);
pageChatInput.addEventListener('click', (e) => {
keepSteerPointerInside(e);
if (!steerLocked) focusPageChatInput('page-chat-input-click');
});
pageChatInput.addEventListener('input', () => {
syncPageChatVisual();
syncPageChatSendButton();
});
pageChatInput.addEventListener('focus', () => {
steerInputWasFocused = true;
syncPageChatFocusRing();
syncPageChatChrome();
});
pageChatInput.addEventListener('blur', () => {
syncPageChatFocusRing();
setTimeout(() => {
if (state === 'CONFIGURING' || steerLocked || voiceListening) return;
if (pageChatEl?.contains(activeElementDeep())) return;
if (!pageChatInput.value.trim()) collapsePageChat();
scheduleSteerFocusRecover('steer-blur-recover');
}, 120);
});
pageChatInput.addEventListener('keydown', (e) => {
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !pageChatInput.value) return;
e.stopPropagation();
if (e.key === 'Escape') {
e.preventDefault();
if (pageChatInput.value) {
pageChatInput.value = '';
syncPageChatVisual();
} else {
collapsePageChat();
}
return;
}
if (e.key === 'Enter') {
e.preventDefault();
submitSteerMessage();
}
});
parent.appendChild(pageChatEl);
steerFocusLog('page-chat-mounted', {});
}
// Impeccable mark - same paths as site/components/Header.astro + favicon.svg.
function brandMarkSvg(color = C.brand, size = 18) {
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="${color}" aria-hidden="true">
<path d="M5 2.5 L13.5 2.5 L5.5 21.5 L5 21.5 Q2.5 21.5 2.5 19 L2.5 5 Q2.5 2.5 5 2.5 Z"/>
<path d="M16.5 2.5 L19 2.5 Q21.5 2.5 21.5 5 L21.5 19 Q21.5 21.5 19 21.5 L8.5 21.5 Z"/>
</svg>`;
}
/**
* True while the browser is waiting on work it already handed to the agent.
* In these states a quiet poll indicator means "busy", not "absent".
*/
function agentHasWorkInFlight() {
return state === 'GENERATING' || state === 'SAVING';
}
/**
* Derived at read time, not cached: which of the two reasons applies depends on
* the live state, which moves between the 5s status polls. The truthiness is
* the same either way, so the indicator's visuals can stay driven by the
* cached value while the wording stays current.
*/
function agentStatusText() {
if (agentPollingConnected) return null;
return agentHasWorkInFlight() ? AGENT_BUSY_TIP : AGENT_DISCONNECTED_TIP;
}
function syncAgentPollingUi(connected) {
agentPollingConnected = !!connected;
syncSteerQueueHint();
if (!globalBarBrandEl) return;
const P = barPaletteForTheme(globalBarEl?.dataset.theme || detectPageTheme());
agentStatusMessage = agentStatusText();
globalBarBrandEl.dataset.agentConnected = connected ? 'true' : 'false';
// The tooltip is mouse-only, so carry the same distinction in the label or
// screen-reader users are left with the vaguer of the two readings.
globalBarBrandEl.setAttribute('aria-label', agentStatusMessage
? 'Impeccable live mode - ' + (agentHasWorkInFlight() ? 'agent is working' : 'agent not polling')
: 'Impeccable live mode');
globalBarBrandEl.removeAttribute('title');
globalBarBrandEl.style.cursor = agentStatusMessage ? 'help' : 'default';
const mark = globalBarBrandEl.querySelector('[data-brand-mark]');
if (mark) {
mark.innerHTML = brandMarkSvg(connected ? P.accent : AGENT_DISCONNECTED_MARK, 18);
mark.style.opacity = '1';
}
const dot = globalBarBrandEl.querySelector('[data-agent-dot]');
if (dot) dot.style.display = agentStatusMessage ? 'block' : 'none';
if (!agentStatusMessage) hideAgentPollTooltip();
}
function ensureAgentPollTooltip() {
if (agentPollTooltipEl) return agentPollTooltipEl;
const P = barPaletteForTheme(globalBarEl?.dataset.theme || detectPageTheme());
agentPollTooltipEl = el('div', {
position: 'fixed',
display: 'none',
opacity: '0',
zIndex: String(Z.bar + 6),
pointerEvents: 'none',
maxWidth: '220px',
padding: '6px 9px',
borderRadius: '7px',
background: P.chatSurface,
border: '1px solid ' + P.hairline,
boxShadow: P.shadow,
color: P.text,
fontFamily: FONT,
fontSize: '11px',
fontWeight: '500',
lineHeight: '1.35',
letterSpacing: '0.01em',
whiteSpace: 'normal',
});
agentPollTooltipEl.id = PREFIX + '-agent-poll-tooltip';
agentPollTooltipEl.textContent = agentStatusText() || AGENT_DISCONNECTED_TIP;
uiAppend(agentPollTooltipEl);
return agentPollTooltipEl;
}
function showAgentPollTooltip(anchor) {
if (!agentStatusMessage || !anchor) return;
const tip = ensureAgentPollTooltip();
// Re-derive rather than reuse the cached copy: the live state may have moved
// since the last status poll set it.
tip.textContent = agentStatusText() || AGENT_DISCONNECTED_TIP;
tip.style.transition = 'none';
tip.style.display = 'block';
tip.style.opacity = '1';
const r = anchor.getBoundingClientRect();
const tipW = tip.offsetWidth;
const tipH = tip.offsetHeight;
const left = Math.max(8, Math.min(window.innerWidth - tipW - 8, r.left + r.width / 2 - tipW / 2));
const top = Math.max(8, r.top - tipH - 8);
tip.style.left = left + 'px';
tip.style.top = top + 'px';
}
function hideAgentPollTooltip() {
if (!agentPollTooltipEl) return;
agentPollTooltipEl.style.display = 'none';
agentPollTooltipEl.style.opacity = '0';
}
function stopAgentStatusPoll() {
if (agentStatusPollTimer) {
clearInterval(agentStatusPollTimer);
agentStatusPollTimer = null;
}
}
function fetchAgentPollingStatus() {
fetch('http://localhost:' + PORT + '/status?token=' + TOKEN, { cache: 'no-store' })
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (data && typeof data.agentPolling === 'boolean') {
syncAgentPollingUi(data.agentPolling);
}
})
.catch(() => { /* server loss handled elsewhere */ });
}
function startAgentStatusPoll() {
stopAgentStatusPoll();
fetchAgentPollingStatus();
agentStatusPollTimer = setInterval(fetchAgentPollingStatus, AGENT_STATUS_POLL_MS);
}
function initGlobalBar() {
const theme = detectPageTheme();
const P = barPaletteForTheme(theme);
// Custom focus-visible for bar buttons. Browser default is a heavy
// blue ring that looks jarring on the dark capsule. Replace with a
// soft accent-tinted inner ring that respects the bar's palette.
if (!uiGetById(PREFIX + '-bar-focus-style')) {
const s = document.createElement('style');
s.id = PREFIX + '-bar-focus-style';
s.textContent =
'#' + PREFIX + '-global-bar button:focus { outline: none; }' +
'#' + PREFIX + '-global-bar button:focus-visible {' +
' outline: none;' +
' box-shadow: 0 0 0 2px ' + P.accentSoft + ', 0 0 0 3px ' + P.accent + ';' +
'}' +
'@keyframes impeccable-agent-dot { 0%, 100% { opacity: 0.45; transform: scale(0.9); } 50% { opacity: 1; transform: scale(1); } }' +
'#' + PREFIX + '-global-bar-brand[data-agent-connected="false"] [data-agent-dot] { animation: impeccable-agent-dot 1.4s ease-in-out infinite; }' +
'@media (prefers-reduced-motion: reduce) { #' + PREFIX + '-global-bar-brand[data-agent-connected="false"] [data-agent-dot] { animation: none; opacity: 0.9; } }';
uiAppendStyle(s);
}
globalBarEl = el('div', {
position: 'fixed', bottom: '14px', left: '50%',
transform: 'translateX(-50%) translateY(20px)',
zIndex: Z.bar + 5,
display: 'flex', alignItems: 'stretch',
gap: '0',
width: 'max-content',
background: P.surface,
border: '1px solid ' + P.border,
borderRadius: '8px',
boxShadow: P.shadow,
fontFamily: FONT, fontSize: '12px', lineHeight: '1',
opacity: '0',
overflow: 'hidden', // clip the full-bleed brand mark to the bar radius
maxWidth: 'calc(100vw - 16px)',
boxSizing: 'border-box',
transition: 'opacity 0.3s ' + EASE + ', transform 0.3s ' + EASE,
});
globalBarEl.id = PREFIX + '-global-bar';
globalBarEl.dataset.theme = theme;
// Brand mark - kinpaku Impeccable icon (site header / favicon paths).
const brand = el('span', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
alignSelf: 'stretch', position: 'relative',
padding: '0 ' + (GLOBAL_BAR_SECTION_GAP - GLOBAL_BAR_INNER_PAD_LEFT) + 'px 0 14px',
background: 'transparent',
color: P.accent,
flexShrink: '0',
});
brand.id = PREFIX + '-global-bar-brand';
brand.dataset.agentConnected = 'false';
brand.setAttribute('role', 'img');
brand.setAttribute('aria-label', 'Impeccable live mode - agent not polling');
const brandMark = el('span', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
position: 'relative',
});
brandMark.dataset.brandMark = 'true';
brandMark.innerHTML = brandMarkSvg(P.accent, 18);
const agentDot = el('span', {
position: 'absolute', right: '-1px', bottom: '7px',
width: '6px', height: '6px', borderRadius: '50%',
background: 'oklch(77% 0.13 82)',
boxShadow: '0 0 0 2px ' + P.surface,
display: 'none', pointerEvents: 'none',
});
agentDot.dataset.agentDot = 'true';
agentDot.setAttribute('aria-hidden', 'true');
brandMark.appendChild(agentDot);
brand.appendChild(brandMark);
brand.addEventListener('mouseenter', () => showAgentPollTooltip(brand));
brand.addEventListener('mouseleave', hideAgentPollTooltip);
globalBarBrandEl = brand;
globalBarEl.appendChild(brand);
syncAgentPollingUi(false);
// Inner wrapper: holds the toggles with normal bar padding.
const inner = el('div', {
display: 'flex', alignItems: 'center',
padding: '4px 5px 4px ' + GLOBAL_BAR_INNER_PAD_LEFT + 'px', gap: GLOBAL_BAR_INNER_GAP + 'px',
flex: '0 0 auto',
});
inner.id = PREFIX + '-global-bar-inner';
globalBarEl.appendChild(inner);
// Button factory: icon-only at rest, label slides in on hover/active.
function makeIconBtn({ id, svg, label, ariaLabel, labelFont, onClick }) {
const b = el('button', {
position: 'relative',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
boxSizing: 'border-box',
flex: '0 0 auto',
minWidth: '30px',
padding: '6px 8px', borderRadius: '7px',
border: 'none', background: 'transparent',
color: P.textDim, fontFamily: FONT, fontSize: '11.5px', fontWeight: '500',
cursor: 'pointer',
transition: 'background 0.15s ease, color 0.15s ease',
whiteSpace: 'nowrap', overflow: 'hidden',
});
b.id = id;
b.title = ariaLabel || label || '';
b.setAttribute('aria-label', ariaLabel || label || '');
b.innerHTML = svg + (label
? `<span class="icon-btn-label" style="display:inline-block;max-width:0;opacity:0;margin-left:0;overflow:hidden;font-family:${labelFont || FONT};transform:translateX(-4px);transition:opacity 0.2s ease, transform 0.25s ${EASE};">${label}</span>`
: '');
const labelEl = b.querySelector('.icon-btn-label');
const expand = () => {
if (!labelEl) return;
labelEl.style.maxWidth = '120px'; labelEl.style.opacity = '1'; labelEl.style.marginLeft = '6px'; labelEl.style.transform = 'translateX(0)';
};
const collapse = (force = false) => {
if (!labelEl || (!force && b.dataset.active === 'true')) return;
labelEl.style.maxWidth = '0'; labelEl.style.opacity = '0'; labelEl.style.marginLeft = '0'; labelEl.style.transform = 'translateX(-4px)';
};
// Per-button hover only changes color (no layout). The label expand/
// collapse is driven by the bar-level mouseenter/mouseleave so moving
// the mouse between adjacent buttons doesn't trigger per-button width
// thrashing - the whole bar grows once and shrinks once.
b.addEventListener('mouseenter', () => { if (b.dataset.active !== 'true') b.style.color = P.text; });
b.addEventListener('mouseleave', () => { if (b.dataset.active !== 'true') b.style.color = P.textDim; });
b.addEventListener('click', onClick);
b._expandLabel = expand;
b._collapseLabel = collapse;
return b;
}
// Pick toggle - restored from localStorage; both pick and insert may be off.
const pickBtn = makeIconBtn({
id: PREFIX + '-pick-toggle',
svg: '<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>',
label: 'Pick',
ariaLabel: 'Pick element',
onClick: () => togglePick(),
});
inner.appendChild(pickBtn);
const insertBtn = makeIconBtn({
id: PREFIX + '-insert-toggle',
svg: '<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="M12 5v14"/><path d="M5 12h14"/></svg>',
label: 'Insert',
ariaLabel: 'Insert new element',
onClick: () => toggleInsert(),
});
inner.appendChild(insertBtn);
// Detect toggle
const detectBtn = makeIconBtn({
id: PREFIX + '-detect-toggle',
svg: '<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>',
label: 'Detect',
ariaLabel: 'Detect anti-patterns',
onClick: () => toggleDetect(),
});
const detectBadge = el('span', {
fontSize: '10px', fontWeight: '600',
padding: '0px 5px', borderRadius: '7px', lineHeight: '16px',
background: P.accent, color: C.ink,
display: 'none', fontFamily: MONO, marginLeft: '4px',
});
detectBadge.id = PREFIX + '-detect-badge';
detectBtn.appendChild(detectBadge);
inner.appendChild(detectBtn);
// DESIGN.md panel toggle - quartet of color squares as the mark.
const designBtn = makeIconBtn({
id: PREFIX + '-design-toggle',
svg: `<span style="display:inline-grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;width:14px;height:14px;border-radius:3px;overflow:hidden;box-shadow:inset 0 0 0 1px oklch(92% 0 0 / 0.13);flex-shrink:0">
<span style="background:oklch(84% 0.19 80.46)"></span>
<span style="background:oklch(70% 0.12 188)"></span>
<span style="background:oklch(91% 0 0)"></span>
<span style="background:oklch(34% 0 0)"></span>
</span>`,
label: 'DESIGN.md',
ariaLabel: 'Toggle DESIGN.md panel',
labelFont: MONO,
onClick: () => toggleDesignPanel(),
});
inner.appendChild(designBtn);
initPageChat(inner, P);
// Pending manual edits live outside the bar so applying staged copy edits
// reads as a distinct next step instead of another chrome toggle.
pendingDockEl = el('div', {
position: 'fixed',
left: '0',
bottom: '0',
transform: 'translate(-100%, 50%)',
zIndex: String(Z.bar + 6),
display: 'none',
alignItems: 'center',
gap: '6px',
fontFamily: FONT,
pointerEvents: 'auto',
});
pendingDockEl.id = PREFIX + '-pending-dock';
pendingPillEl = el('button', {
display: 'none',
alignItems: 'center',
gap: '8px',
fontFamily: FONT,
fontSize: '12px',
fontWeight: '600',
letterSpacing: '0',
color: C.ink,
background: P.accent,
padding: '7px 12px 7px 14px',
border: 'none',
borderRadius: '999px',
whiteSpace: 'nowrap',
cursor: 'pointer',
boxShadow: '0 4px 16px oklch(0% 0 0 / 0.16), 0 1px 3px oklch(0% 0 0 / 0.1)',
transition: 'filter 0.12s ease, transform 0.1s ease, box-shadow 0.18s ease',
});
pendingPillEl.title = 'Apply copy edits to source';
pendingPillSpinnerEl = el('span', {
display: 'none',
width: '12px',
height: '12px',
borderRadius: '50%',
border: '2px solid currentColor',
borderTopColor: 'transparent',
color: C.ink,
opacity: '0.9',
animation: 'impeccable-spin 0.6s linear infinite',
flex: '0 0 auto',
boxSizing: 'border-box',
});
pendingPillLabelEl = el('span', { lineHeight: '1', whiteSpace: 'nowrap' });
pendingPillLabelEl.textContent = 'Apply copy edits';
pendingPillCountEl = el('span', {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: '17px',
height: '17px',
padding: '0 5px',
borderRadius: '999px',
background: 'oklch(4% 0.004 95 / 0.18)',
color: C.ink,
fontFamily: MONO,
fontSize: '10px',
fontWeight: '700',
lineHeight: '1',
});
ensureSpinKeyframes();
pendingPillEl.appendChild(pendingPillSpinnerEl);
pendingPillEl.appendChild(pendingPillLabelEl);
pendingPillEl.appendChild(pendingPillCountEl);
pendingPillEl.addEventListener('mouseenter', () => {
if (pendingApplyInFlight) return;
pendingPillEl.style.filter = 'brightness(1.1)';
pendingPillEl.style.boxShadow = '0 7px 22px oklch(0% 0 0 / 0.18), 0 2px 5px oklch(0% 0 0 / 0.12)';
});
pendingPillEl.addEventListener('mouseleave', () => {
if (pendingApplyInFlight) return;
pendingPillEl.style.filter = 'none';
pendingPillEl.style.transform = 'scale(1)';
pendingPillEl.style.boxShadow = '0 4px 16px oklch(0% 0 0 / 0.16), 0 1px 3px oklch(0% 0 0 / 0.1)';
});
pendingPillEl.addEventListener('mousedown', () => { if (!pendingApplyInFlight) pendingPillEl.style.transform = 'scale(0.97)'; });
pendingPillEl.addEventListener('mouseup', () => { pendingPillEl.style.transform = 'scale(1)'; });
pendingPillEl.addEventListener('click', onPendingPillClick);
pendingTrashBtn = el('button', {
position: 'relative',
display: 'none',
alignItems: 'center',
justifyContent: 'center',
padding: '0', boxSizing: 'border-box',
width: '30px', height: '30px', borderRadius: '999px',
border: '1px solid ' + P.hairline,
background: P.chatSurface,
color: P.textDim,
overflow: 'visible',
boxShadow: '0 4px 16px oklch(0% 0 0 / 0.12), 0 1px 3px oklch(0% 0 0 / 0.08)',
cursor: 'pointer',
transition: 'color 0.12s ease, background 0.12s ease, box-shadow 0.18s ease',
});
pendingTrashBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="flex:0 0 auto"><path d="M3 4h8"/><path d="M5 4V3a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1"/><path d="M4 4l.5 7a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1L10 4"/></svg>';
const pendingTrashTooltipEl = el('span', {
position: 'absolute',
bottom: 'calc(100% + 8px)',
left: '50%',
transform: 'translateX(-50%) translateY(4px)',
opacity: '0',
pointerEvents: 'none',
padding: '8px 16px',
borderRadius: '8px',
background: C.ink,
color: C.white,
fontFamily: FONT,
fontSize: '12px',
fontWeight: '400',
lineHeight: '1',
whiteSpace: 'nowrap',
textAlign: 'center',
transition: 'opacity 0.16s ease, transform 0.18s ' + EASE,
});
pendingTrashTooltipEl.textContent = 'Discard copy edits';
pendingTrashTooltipEl.setAttribute('role', 'tooltip');
pendingTrashBtn.appendChild(pendingTrashTooltipEl);
pendingTrashBtn.setAttribute('aria-label', 'Discard copy edits on this page');
const showTrashTooltip = () => {
pendingTrashBtn.style.color = P.accent;
pendingTrashBtn.style.boxShadow = '0 7px 22px oklch(0% 0 0 / 0.16), 0 2px 5px oklch(0% 0 0 / 0.1)';
pendingTrashTooltipEl.style.opacity = '1';
pendingTrashTooltipEl.style.transform = 'translateX(-50%) translateY(0)';
};
const hideTrashTooltip = () => {
pendingTrashBtn.style.color = P.textDim;
pendingTrashBtn.style.background = P.chatSurface;
pendingTrashBtn.style.boxShadow = '0 4px 16px oklch(0% 0 0 / 0.12), 0 1px 3px oklch(0% 0 0 / 0.08)';
pendingTrashTooltipEl.style.opacity = '0';
pendingTrashTooltipEl.style.transform = 'translateX(-50%) translateY(4px)';
};
pendingTrashBtn.addEventListener('mouseenter', showTrashTooltip);
pendingTrashBtn.addEventListener('mouseleave', hideTrashTooltip);
pendingTrashBtn.addEventListener('focus', showTrashTooltip);
pendingTrashBtn.addEventListener('blur', hideTrashTooltip);
pendingTrashBtn.addEventListener('click', onPendingTrashClick);
const makePendingDecisionBtn = (label, accent) => {
const btn = el('button', {
display: 'none',
alignItems: 'center',
justifyContent: 'center',
height: '30px',
padding: '0 12px',
borderRadius: '999px',
border: '1px solid ' + (accent ? P.accent : P.hairline),
background: accent ? P.accent : P.chatSurface,
color: accent ? C.ink : P.textDim,
fontFamily: FONT,
fontSize: '12px',
fontWeight: '600',
letterSpacing: '0',
cursor: 'pointer',
whiteSpace: 'nowrap',
boxShadow: '0 4px 16px oklch(0% 0 0 / 0.12), 0 1px 3px oklch(0% 0 0 / 0.08)',
});
btn.textContent = label;
return btn;
};
pendingKeepFixingBtn = makePendingDecisionBtn('Keep fixing', true);
pendingKeepFixingBtn.setAttribute('aria-label', 'Ask the agent to keep fixing Apply errors');
pendingKeepFixingBtn.addEventListener('click', onPendingKeepFixingClick);
pendingRollbackBtn = makePendingDecisionBtn('Rollback', false);
pendingRollbackBtn.setAttribute('aria-label', 'Rollback source and keep copy edits staged');
pendingRollbackBtn.addEventListener('click', onPendingRollbackClick);
pendingDockEl.appendChild(pendingPillEl);
pendingDockEl.appendChild(pendingTrashBtn);
pendingDockEl.appendChild(pendingKeepFixingBtn);
pendingDockEl.appendChild(pendingRollbackBtn);
// Thin divider before the exit button
const divider = el('span', {
width: '1px', height: '18px',
background: P.hairline,
margin: '0 4px 0 2px',
flexShrink: '0',
});
inner.appendChild(divider);
// Exit × on the right - intentionally subtle (textDim at rest, text on
// hover) so it sits behind the active toggles in visual hierarchy.
//
// Explicit padding + box-sizing here is load-bearing: a host page like
// `button { padding: 0.5rem 1rem; }` (very common in resets) would
// otherwise inflate this 24x24 button into 56x40 and push the SVG out
// of the visible bar - the X stays invisible even though the styles in
// DevTools look fine. Every other chrome button sets padding inline;
// this one needed it too.
const exitBtn = el('button', {
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
padding: '0', boxSizing: 'border-box',
width: '24px', height: '24px', borderRadius: '6px',
flexShrink: '0',
border: 'none', background: 'transparent',
color: P.textDim, fontFamily: FONT, fontSize: '0', lineHeight: '0',
cursor: 'pointer', transition: 'color 0.12s ease, background 0.12s ease',
});
exitBtn.id = PREFIX + '-exit';
exitBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="3" y1="3" x2="11" y2="11"/><line x1="11" y1="3" x2="3" y2="11"/></svg>';
exitBtn.title = 'Exit live mode';
exitBtn.addEventListener('mouseenter', () => { exitBtn.style.color = 'oklch(58% 0.15 35)'; exitBtn.style.background = P.exitHover; });
exitBtn.addEventListener('mouseleave', () => { exitBtn.style.color = P.textDim; exitBtn.style.background = 'transparent'; });
exitBtn.addEventListener('click', () => { sendEvent({ type: 'exit' }); teardown(); });
inner.appendChild(exitBtn);
// Bar-level hover: expand mode labels unless Steer is using the space.
// Buttons with dataset.active="true" ignore collapse (their label stays).
globalBarEl.addEventListener('mouseenter', () => {
syncGlobalBarExpandedLabels(true);
syncPageChatExpandedWidth();
schedulePendingDockPosition();
setTimeout(schedulePendingDockPosition, 260);
});
globalBarEl.addEventListener('mouseleave', () => {
syncGlobalBarExpandedLabels(false);
schedulePendingDockPosition();
setTimeout(schedulePendingDockPosition, 260);
});
globalBarEl.addEventListener('pointerdown', () => {
try { window.focus(); } catch { /* in-app preview may block */ }
}, true);
uiAppend(pendingDockEl);
uiAppend(globalBarEl);
defangOutsideHandlers(pendingDockEl);
defangOutsideHandlers(globalBarEl);
if (window.ResizeObserver) {
pendingDockResizeObserver = new ResizeObserver(schedulePendingDockPosition);
pendingDockResizeObserver.observe(globalBarEl);
}
window.addEventListener('resize', positionPendingDock);
window.addEventListener('resize', syncPageChatExpandedWidth);
requestAnimationFrame(() => {
globalBarEl.style.opacity = '1';
globalBarEl.style.transform = 'translateX(-50%) translateY(0)';
syncPageChatFocus('global-bar-visible');
});
// Listen for detection results AND ready signal
window.addEventListener('message', onDetectMessage);
updateGlobalBarState();
}
function updateGlobalBarState() {
const detectToggle = uiGetById(PREFIX + '-detect-toggle');
const detectBadge = uiGetById(PREFIX + '-detect-badge');
const pickToggle = uiGetById(PREFIX + '-pick-toggle');
const insertToggle = uiGetById(PREFIX + '-insert-toggle');
const designToggle = uiGetById(PREFIX + '-design-toggle');
const theme = globalBarEl?.dataset.theme || 'light';
const P = barPaletteForTheme(theme);
// Sync one toggle's active state, colors, and slide-label visibility.
function sync(btn, active) {
if (!btn) return;
btn.style.background = active ? P.toggleActive : 'transparent';
btn.style.color = active ? P.accent : P.textDim;
btn.dataset.active = active ? 'true' : 'false';
if (active && btn._expandLabel) btn._expandLabel();
else if (!active && btn._collapseLabel) btn._collapseLabel();
}
sync(pickToggle, pickActive);
sync(insertToggle, insertActive);
sync(detectToggle, detectActive);
sync(designToggle, designState.open);
const controlsLocked = pendingApplyInFlight === true;
[pickToggle, insertToggle, detectToggle, designToggle].forEach((btn) => {
if (!btn) return;
btn.disabled = controlsLocked;
btn.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
btn.style.opacity = controlsLocked ? '0.55' : '1';
});
// If the bar is currently under the cursor, keep all labels expanded -
// otherwise clicking a toggle that deactivates (e.g. closing DESIGN.md)
// would collapse its label while the user's mouse is still on the bar.
syncGlobalBarExpandedLabels(globalBarEl && globalBarEl.matches(':hover'));
if (detectBadge) {
detectBadge.style.display = (detectActive && detectCount > 0) ? 'inline' : 'none';
detectBadge.textContent = detectCount;
}
// When pick/insert is active, make detect overlays click-through
document.querySelectorAll('.impeccable-overlay').forEach(o => {
o.style.pointerEvents = (pickActive || insertActive) ? 'none' : '';
});
syncPageInteractionCursor();
}
let detectReady = false; // true once detect script posts 'impeccable-ready'
let detectPendingScan = false; // scan requested before script was ready
function requestDetectScan() {
const scanId = String(++detectScanSeq);
activeDetectScanId = scanId;
pendingDetectScanId = scanId;
window.postMessage({
source: 'impeccable-command',
action: 'scan',
config: { scanId },
}, '*');
}
function toggleDetect() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
detectActive = !detectActive;
updateGlobalBarState();
if (detectActive) {
if (!detectScriptLoaded) {
detectPendingScan = true;
loadDetectScript();
} else if (detectReady) {
requestDetectScan();
} else {
detectPendingScan = true;
}
} else {
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
activeDetectScanId = null;
pendingDetectScanId = null;
detectCount = 0;
updateGlobalBarState();
}
}
function togglePick() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
pickActive = !pickActive;
if (pickActive) {
insertActive = false;
clearInsertPicking();
}
saveInteractionPrefs();
updateGlobalBarState();
if (!pickActive) {
if (configureKind === 'insert' && state === 'CONFIGURING') {
cancelInsertConfigure();
return;
}
teardownConfigureChrome();
hideHighlight();
hideActionPicker();
selectedElement = null;
hoveredElement = null;
configureKind = 'replace';
if (state === 'PICKING' || state === 'CONFIGURING') setLiveState('IDLE');
} else {
if (state === 'IDLE') setLiveState('PICKING');
}
syncPageChatFocus('toggle-pick');
}
function toggleInsert() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
insertActive = !insertActive;
if (insertActive) {
pickActive = false;
hideHighlight();
hideBar();
hideActionPicker();
selectedElement = null;
configureKind = 'replace';
if (state === 'CONFIGURING') cancelInsertConfigure();
else if (state === 'IDLE' || state === 'PICKING') setLiveState('PICKING');
} else {
clearInsertPicking();
if (state === 'PICKING' && !pickActive) setLiveState('IDLE');
}
saveInteractionPrefs();
updateGlobalBarState();
syncPageChatFocus('toggle-insert');
}
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;
requestDetectScan();
}
}
// Scan results arrived
if (e.data.source === 'impeccable-results') {
if (!detectActive) return;
if (activeDetectScanId && e.data.scanId !== activeDetectScanId) return;
detectCount = e.data.count || 0;
if (detectActive && pendingDetectScanId && detectCount === 0) {
showToast(DETECT_EMPTY_MESSAGE, 3200);
}
pendingDetectScanId = null;
updateGlobalBarState();
}
}
/** Full teardown: remove all UI, disconnect SSE, clean up. */
function teardown() {
stopAgentStatusPoll();
hideAgentPollTooltip();
if (agentPollTooltipEl) {
agentPollTooltipEl.remove();
agentPollTooltipEl = null;
}
stopVoice({ suppressSubmit: true });
clearSteerFocusRecoverTimer();
steerFocusSuspended = false;
steerFocusPauseUntil = 0;
pagePointerGesture = null;
pagePickSkipClick = false;
cleanup();
hideBar();
if (pendingDockResizeObserver) { pendingDockResizeObserver.disconnect(); pendingDockResizeObserver = null; }
window.removeEventListener('resize', positionPendingDock);
if (pendingIntroAnimation) { pendingIntroAnimation.cancel(); pendingIntroAnimation = null; }
if (pendingDockEl) {
pendingDockEl.remove();
pendingDockEl = null;
pendingPillEl = null;
pendingPillSpinnerEl = null;
pendingPillLabelEl = null;
pendingPillCountEl = null;
pendingTrashBtn = null;
pendingKeepFixingBtn = null;
pendingRollbackBtn = null;
pendingApplyInFlight = false;
}
if (globalBarEl) {
globalBarEl.style.transition = 'none';
globalBarEl.remove();
globalBarEl = null;
}
pageChatEl = null;
pageChatInput = null;
pageChatHint = null;
pageChatVoiceBtn = null;
pageChatExpanded = false;
if (insertCreateTooltipEl) { insertCreateTooltipEl.remove(); insertCreateTooltipEl = null; }
if (configureBarTooltipEl) { configureBarTooltipEl.remove(); configureBarTooltipEl = null; }
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 (paramsPanelEl) { paramsPanelEl.remove(); paramsPanelEl = null; paramsPanelInner = null; paramsPanelBody = null; }
if (editBadgeProxyRoot) { editBadgeProxyRoot.remove(); editBadgeProxyRoot = null; editBadgeProxyByTarget = new Map(); }
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' }, '*');
setLiveState('IDLE');
document.getElementById(PICK_CURSOR_STYLE_ID)?.remove();
removeVariantStateStylesheet();
window.__IMPECCABLE_LIVE_INIT__ = false;
console.log('[impeccable] Live mode exited.');
}
//
// Design System Panel - visualizes the project's .impeccable/design.json sidecar
//
const DESIGN_PREFS_KEY = 'impeccable-live-design-panel';
const DESIGN_PANEL_WIDTH = 440;
let designHost = null;
let designShadow = null;
let designState = {
open: false,
tab: 'visual', // 'visual' | 'raw'
parsed: null, // parseDesignMd output (frontmatter + body sections)
sidecar: null, // .impeccable/design.json v2 payload (extensions + components + narrative)
hasMd: false,
hasSidecar: false,
present: null, // true/false once fetch resolves
raw: null, // raw DESIGN.md for the raw tab
mdNewerThanJson: false, // stale-hint flag
loading: false,
error: null,
collapsed: { // narrative-section accordion state
rules: true, dosdonts: true, overview: true,
},
};
function loadDesignPrefs() {
// `open` is intentionally NOT persisted - the panel always starts closed
// so live mode doesn't auto-slide a big panel over the page on startup.
try {
const raw = localStorage.getItem(DESIGN_PREFS_KEY);
if (!raw) return;
const prefs = JSON.parse(raw);
if (prefs.tab === 'visual' || prefs.tab === 'raw') designState.tab = prefs.tab;
if (prefs.collapsed && typeof prefs.collapsed === 'object') {
Object.assign(designState.collapsed, prefs.collapsed);
}
} catch { /* ignore */ }
}
function saveDesignPrefs() {
try {
localStorage.setItem(DESIGN_PREFS_KEY, JSON.stringify({
tab: designState.tab,
collapsed: designState.collapsed,
}));
} catch { /* ignore */ }
}
function initDesignPanel() {
designHost = document.createElement('div');
designHost.id = PREFIX + '-design-host';
Object.assign(designHost.style, {
position: 'fixed', top: '0', left: '0',
width: '0', height: '0',
zIndex: String(Z.bar + 10),
pointerEvents: 'none',
});
designShadow = designHost.attachShadow({ mode: 'open' });
const style = document.createElement('style');
// Theme-match the bar: dark chrome on light pages, light chrome on dark pages.
const theme = detectPageTheme();
style.textContent = designPanelCss(barPaletteForTheme(theme));
designShadow.appendChild(style);
const root = document.createElement('div');
root.className = 'root';
designShadow.appendChild(root);
uiAppend(designHost);
// The host is pointer-events: none; the panel inside the shadow DOM
// manages its own auto/none. Events bubble through the shadow boundary,
// so attaching here silences host-page outside-interaction handlers
// without touching the host's click-through behavior.
defangOutsideHandlers(designHost, { setPointerEvents: false });
loadDesignPrefs();
renderDesignChrome();
if (designState.open) {
fetchDesignSystem();
}
}
// Neutral panel palette - deliberately NOT Impeccable-branded. The panel is
// a viewer of the project's design system, not an Impeccable surface.
const DP = {
canvas: 'oklch(94% 0 0)', // panel background
tile: 'oklch(98.5% 0 0)', // card-on-canvas
tileAlt: 'oklch(96% 0 0)', // subtler tile for inner surfaces
ink: 'oklch(15% 0 0)',
ink2: 'oklch(35% 0 0)',
meta: 'oklch(55% 0 0)',
hairline: 'oklch(88% 0 0)',
hairlineSoft: 'oklch(92% 0 0)',
amber: 'oklch(77% 0.13 82)', // stale-hint accent
amberBg: 'oklch(89% 0.055 84)',
};
function designPanelCss(BP) {
// BP = bar palette (theme-aware, matches the global bar).
// DP = internal content palette (neutral, so tiles render colors true).
return `
:host, .root { all: initial; }
.root {
font-family: ${FONT};
color: ${DP.ink};
pointer-events: none;
}
.root * { box-sizing: border-box; }
button { font: inherit; color: inherit; }
/* Panel shell: chrome matches the bar; body canvas stays neutral */
.panel {
position: fixed; top: 12px; bottom: 72px; right: 12px;
width: ${DESIGN_PANEL_WIDTH}px; max-width: calc(100vw - 24px);
background: ${BP.surface};
border: 1.5px solid ${BP.border};
border-radius: 14px;
box-shadow: ${BP.shadow};
display: flex; flex-direction: column;
transform: translateX(calc(100% + 24px));
opacity: 0;
transition: transform 0.35s ${EASE}, opacity 0.25s ${EASE};
pointer-events: none;
overflow: hidden;
}
.panel[data-open="true"] { transform: translateX(0); opacity: 1; pointer-events: auto; }
.panel-header {
display: flex; align-items: center; gap: 10px;
padding: 10px 10px 10px 14px;
background: transparent;
border-bottom: 1px solid ${BP.hairline};
}
.panel-title {
flex: 1; min-width: 0;
font-family: ${MONO};
font-size: 11.5px; font-weight: 600;
letter-spacing: 0.02em;
color: ${BP.text};
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.panel-close {
border: none; background: transparent; color: ${BP.textDim};
width: 26px; height: 26px; border-radius: 7px;
display: inline-flex; align-items: center; justify-content: center;
cursor: pointer; transition: background 0.15s ease, color 0.15s ease;
}
.panel-close:hover { background: ${BP.hairline}; color: ${BP.text}; }
.tabs {
display: inline-flex; padding: 2px;
background: ${BP.hairline};
border-radius: 7px;
gap: 2px;
}
.tab {
border: none; background: transparent;
padding: 4px 10px; border-radius: 5px;
font-family: ${MONO};
font-size: 10px; font-weight: 600; letter-spacing: 0.08em;
text-transform: uppercase;
color: ${BP.textDim}; cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.tab[data-active="true"] { background: ${BP.surface}; color: ${BP.text}; }
.panel-body {
flex: 1; overflow-y: auto;
padding: 12px 12px 20px;
background: ${DP.canvas};
scrollbar-width: thin;
scrollbar-color: ${DP.hairline} transparent;
}
.panel-body::-webkit-scrollbar { width: 8px; }
.panel-body::-webkit-scrollbar-thumb { background: ${DP.hairline}; border-radius: 8px; border: 2px solid transparent; background-clip: padding-box; }
/* States */
.empty, .loading, .error {
margin: 16px 4px;
padding: 28px 20px; text-align: center;
background: ${DP.tile}; border-radius: 14px;
color: ${DP.ink2}; font-size: 13px; line-height: 1.55;
}
.empty strong { color: ${DP.ink}; display: block; margin-bottom: 6px; font-size: 14px; }
.empty code { font-family: ${MONO}; background: ${DP.canvas}; padding: 1px 6px; border-radius: 4px; font-size: 12px; color: ${DP.ink}; }
.error { color: oklch(58% 0.15 35); }
/* Stale hint */
.stale {
display: flex; align-items: center; gap: 8px;
margin: 8px 4px 12px;
padding: 8px 12px;
background: ${DP.amberBg};
border-radius: 10px;
font-size: 11.5px; color: ${DP.ink2};
}
.stale-dot { width: 8px; height: 8px; border-radius: 50%; background: ${DP.amber}; flex-shrink: 0; }
.stale-text { flex: 1; min-width: 0; }
.stale-text strong { color: ${DP.ink}; font-weight: 600; }
/* Parsed-md fallback banner */
.parsed-md-cta {
margin: 8px 4px 14px;
padding: 14px 16px;
background: ${DP.tile};
border: 1px dashed ${DP.hairline};
border-radius: 12px;
font-size: 12px; color: ${DP.ink2}; line-height: 1.55;
}
.parsed-md-cta strong { color: ${DP.ink}; display: block; margin-bottom: 4px; font-size: 13px; font-weight: 600; }
.parsed-md-cta code { font-family: ${MONO}; background: ${DP.canvas}; padding: 1px 5px; border-radius: 4px; font-size: 11.5px; color: ${DP.ink}; }
/* Tile primitives */
.tile {
position: relative;
background: ${DP.tile};
border-radius: 16px;
padding: 16px;
margin: 0 4px 10px;
}
.tile-row { margin: 0 4px 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.tile-row .tile { margin: 0; }
.tile-meta {
display: flex; align-items: baseline; justify-content: space-between;
gap: 10px;
font-family: ${MONO};
font-size: 10px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase;
color: ${DP.meta};
}
.tile-meta .name { color: ${DP.ink}; font-weight: 600; letter-spacing: 0.05em; text-transform: none; font-family: ${FONT}; font-size: 12.5px; }
/* Color tile */
.c-tile { cursor: pointer; transition: transform 0.2s ${EASE}; }
.c-tile:hover { transform: translateY(-1px); }
.c-hero {
height: 72px; border-radius: 10px; margin-top: 10px;
box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.05);
}
.c-ramp {
display: flex; gap: 0; height: 14px; border-radius: 4px; overflow: hidden;
margin-top: 8px;
box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.04);
}
.c-ramp > span { flex: 1; }
.c-desc { margin-top: 8px; font-size: 11.5px; line-height: 1.45; color: ${DP.ink2}; }
/* Type tile */
.t-tile { }
.t-specimen {
margin: 4px 0 6px;
color: ${DP.ink};
line-height: 0.9;
}
.t-family { margin-top: 4px; font-size: 12px; font-weight: 600; color: ${DP.ink}; }
.t-purpose { margin-top: 4px; font-size: 11px; line-height: 1.45; color: ${DP.ink2}; }
/* Shadow tile */
.s-tile { }
.s-surface {
height: 60px; margin: 8px 2px 10px;
background: ${DP.tile};
border-radius: 10px;
}
.s-value { font-family: ${MONO}; font-size: 10px; color: ${DP.meta}; word-break: break-all; line-height: 1.4; }
.s-purpose { margin-top: 4px; font-size: 11px; color: ${DP.ink2}; line-height: 1.45; }
/* Radii strip */
.r-strip { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 10px; }
.r-item { display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 1; min-width: 60px; }
.r-sample { width: 44px; height: 44px; background: ${DP.canvas}; box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.08); }
.r-label { font-family: ${MONO}; font-size: 10px; color: ${DP.meta}; letter-spacing: 0.05em; text-transform: uppercase; }
.r-val { font-family: ${MONO}; font-size: 10px; color: ${DP.ink}; }
/* Component tile (hosts live primitives) */
.cmp-tile { }
.cmp-stage {
margin: 12px -4px 0;
padding: 18px 16px 10px;
border-top: 1px solid ${DP.hairlineSoft};
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 14px;
min-height: 68px;
}
.cmp-stage + .cmp-stage { border-top: 1px dashed ${DP.hairlineSoft}; }
.cmp-sublabel { font-family: ${MONO}; font-size: 10px; color: ${DP.meta}; letter-spacing: 0.06em; }
.cmp-kind { font-family: ${MONO}; font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: ${DP.meta}; }
/* Collapsible */
.coll {
margin: 0 4px 8px;
background: ${DP.tile};
border-radius: 12px;
overflow: hidden;
}
.coll-head {
display: flex; align-items: center; gap: 10px;
width: 100%;
padding: 12px 14px;
background: transparent; border: none;
cursor: pointer; text-align: left;
font-family: ${FONT}; font-size: 12.5px; font-weight: 600; color: ${DP.ink};
transition: background 0.12s ease;
}
.coll-head:hover { background: ${DP.tileAlt}; }
.coll-chev {
width: 12px; height: 12px; flex-shrink: 0;
color: ${DP.meta};
transition: transform 0.2s ${EASE};
}
.coll[data-open="true"] .coll-chev { transform: rotate(90deg); }
.coll-count { margin-left: auto; font-family: ${MONO}; font-size: 10px; color: ${DP.meta}; letter-spacing: 0.05em; }
.coll-body { padding: 0 14px 14px; display: none; }
.coll[data-open="true"] .coll-body { display: block; }
.rule-card {
padding: 10px 0;
border-top: 1px solid ${DP.hairlineSoft};
}
.rule-card:first-child { border-top: none; padding-top: 2px; }
.rule-card .name { font-size: 11.5px; font-weight: 700; color: ${DP.ink}; margin-bottom: 3px; }
.rule-card .name .section { font-family: ${MONO}; font-size: 9px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: ${DP.meta}; margin-left: 8px; }
.rule-card .body { font-size: 11.5px; color: ${DP.ink2}; line-height: 1.5; }
.coll .dos { display: grid; gap: 0; margin-top: 2px; }
.coll .do, .coll .dont {
position: relative;
padding: 8px 0 8px 22px;
font-size: 11.5px; line-height: 1.5; color: ${DP.ink2};
border-top: 1px solid ${DP.hairlineSoft};
}
.coll .do:first-child, .coll .dont:first-child,
.coll .do:first-of-type { border-top: none; }
.coll .do + .dont { border-top: 1px solid ${DP.hairlineSoft}; }
.coll .do::before, .coll .dont::before {
content: ''; position: absolute; left: 4px; top: 13px;
width: 8px; height: 8px; border-radius: 50%;
}
.coll .do::before { background: oklch(45% 0.18 145); }
.coll .dont::before { background: oklch(58% 0.15 35); }
.coll .overview-body {
font-size: 12px; line-height: 1.55; color: ${DP.ink2};
}
.coll .overview-body .north-star {
display: block; font-family: ${FONT}; font-style: italic;
font-size: 15px; line-height: 1.3; color: ${DP.ink};
margin-bottom: 8px;
}
.coll .overview-body p { margin: 0 0 8px; }
.coll .overview-body ul { margin: 6px 0 0; padding-left: 16px; font-size: 11.5px; }
.coll .overview-body li { margin-bottom: 3px; }
/* raw tab markdown (unchanged layout, neutralized palette) */
.md { padding: 4px 10px 20px; font-size: 13px; line-height: 1.6; color: ${DP.ink}; }
.md h1, .md h2, .md h3, .md h4 { margin: 20px 0 8px; color: ${DP.ink}; font-weight: 600; }
.md h1 { font-size: 18px; }
.md h2 { font-size: 15px; padding-bottom: 4px; border-bottom: 1px solid ${DP.hairlineSoft}; }
.md h3 { font-size: 13px; }
.md h4 { font-size: 12px; color: ${DP.meta}; }
.md p { margin: 0 0 10px; }
.md ul, .md ol { margin: 0 0 10px; padding-left: 20px; }
.md li { margin-bottom: 4px; }
.md code { font-family: ${MONO}; font-size: 12px; background: ${DP.canvas}; padding: 1px 5px; border-radius: 4px; }
.md pre { font-family: ${MONO}; font-size: 12px; background: ${DP.canvas}; padding: 10px 12px; border-radius: 8px; overflow-x: auto; margin: 0 0 10px; }
.md pre code { background: none; padding: 0; }
.md strong { font-weight: 700; }
.md em { font-style: italic; }
.md a { color: ${DP.ink}; text-decoration: underline; }
.md hr { border: none; border-top: 1px solid ${DP.hairlineSoft}; margin: 16px 0; }
`;
}
function renderDesignChrome() {
const root = designShadow.querySelector('.root');
root.innerHTML = '';
// (Panel toggle lives in the global bar - no floating FAB.)
// Panel
const panel = document.createElement('aside');
panel.className = 'panel';
panel.setAttribute('data-open', designState.open ? 'true' : 'false');
panel.appendChild(buildDesignHeader());
const body = document.createElement('div');
body.className = 'panel-body';
body.id = 'panel-body';
panel.appendChild(body);
root.appendChild(panel);
renderDesignBody();
}
function buildDesignHeader() {
const header = document.createElement('div');
header.className = 'panel-header';
const title = document.createElement('div');
title.className = 'panel-title';
title.textContent = 'DESIGN.md';
header.appendChild(title);
const tabs = document.createElement('div');
tabs.className = 'tabs';
for (const t of [['visual', 'Visual'], ['raw', 'Raw']]) {
const btn = document.createElement('button');
btn.className = 'tab';
btn.textContent = t[1];
btn.setAttribute('data-active', designState.tab === t[0] ? 'true' : 'false');
btn.addEventListener('click', () => {
if (designState.tab === t[0]) return;
designState.tab = t[0];
saveDesignPrefs();
renderDesignChrome();
if (t[0] === 'raw' && designState.raw === null && !designState.loading) {
fetchDesignSystem(); // raw is part of the same fetch pair
}
});
tabs.appendChild(btn);
}
header.appendChild(tabs);
const close = document.createElement('button');
close.className = 'panel-close';
close.innerHTML = '&#x2715;';
close.setAttribute('aria-label', 'Close panel');
close.addEventListener('click', toggleDesignPanel);
header.appendChild(close);
return header;
}
function toggleDesignPanel() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
designState.open = !designState.open;
renderDesignChrome();
updateGlobalBarState();
if (designState.open && designState.present === null && !designState.loading) {
fetchDesignSystem();
}
}
async function fetchDesignSystem() {
designState.loading = true;
designState.error = null;
renderDesignBody();
try {
const [jsonRes, rawRes] = await Promise.all([
fetch(`http://localhost:${PORT}/design-system.json?token=${TOKEN}`, { cache: 'no-store' }),
fetch(`http://localhost:${PORT}/design-system/raw?token=${TOKEN}`, { cache: 'no-store' }),
]);
const jsonData = await jsonRes.json();
designState.present = jsonData.present === true;
designState.parsed = jsonData.parsed || null;
designState.sidecar = jsonData.sidecar || null;
designState.hasMd = !!jsonData.hasMd;
designState.hasSidecar = !!jsonData.hasSidecar;
designState.mdNewerThanJson = !!jsonData.mdNewerThanJson;
designState.raw = designState.present && rawRes.ok ? await rawRes.text() : null;
designState.error = jsonData.parseError || jsonData.sidecarError || null;
} catch (err) {
designState.error = err?.message || 'Failed to load design system.';
} finally {
designState.loading = false;
renderDesignChrome(); // refresh title from data
}
}
function renderDesignBody() {
const body = designShadow.querySelector('#panel-body');
if (!body) return;
body.innerHTML = '';
if (designState.loading) {
body.appendChild(msgDiv('loading', 'Loading design system…'));
return;
}
if (designState.error) {
body.appendChild(msgDiv('error', designState.error));
return;
}
if (designState.present === false) {
const empty = document.createElement('div');
empty.className = 'empty';
empty.innerHTML = `<strong>No DESIGN.md yet</strong>Create one by running <code>${IMPECCABLE_COMMAND} document</code> in your terminal, then re-open this panel.`;
body.appendChild(empty);
return;
}
if (designState.tab === 'raw') {
renderRawTab(body, designState.raw || '');
return;
}
// Visual tab - single unified render path.
if (designState.mdNewerThanJson) body.appendChild(renderStaleHint());
if (designState.hasMd && !designState.hasSidecar) {
body.appendChild(renderParsedMdCta());
}
renderDesignVisual(body, designState.parsed, designState.sidecar);
}
function msgDiv(cls, text) {
const d = document.createElement('div');
d.className = cls;
d.textContent = text;
return d;
}
function renderStaleHint() {
const box = document.createElement('div');
box.className = 'stale';
box.innerHTML = `
<span class="stale-dot"></span>
<span class="stale-text"><strong>DESIGN.md is newer than .impeccable/design.json.</strong> Run <code>${IMPECCABLE_COMMAND} document</code> to refresh the sidecar.</span>
`;
return box;
}
function renderParsedMdCta() {
const box = document.createElement('div');
box.className = 'parsed-md-cta';
box.innerHTML = `<strong>Basic view</strong>This panel reads the tokens in your <code>DESIGN.md</code> frontmatter. Running <code>${IMPECCABLE_COMMAND} document</code> also generates a <code>.impeccable/design.json</code> sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`;
return box;
}
// Unified render: merge parsed DESIGN.md frontmatter with sidecar v2
/**
* The empty state has to say which emptiness it is. `present:false` (no
* DESIGN.md at all) is handled upstream in renderDesignBody; everything here
* means the helper found a design system and this panel found nothing in it
* worth drawing. Telling that user "no design system data" reads as "your
* DESIGN.md is missing" and sends them to write a file they already have.
*/
function designEmptyMessage() {
if (designState.hasMd && !designState.hasSidecar) {
return 'DESIGN.md found, no structured tokens to display. Run ' + IMPECCABLE_COMMAND + ' document to generate the .impeccable/design.json sidecar.';
}
if (designState.hasMd) {
return 'DESIGN.md and its sidecar were found, but neither carries colors, type, radii, or components to display.';
}
return 'No design system data available.';
}
function renderDesignVisual(body, parsed, sidecar) {
// Count only what this function draws: renderDesignBody may already have
// appended a stale-sidecar hint or the basic-view CTA, and those must not
// pass for token content.
const beforeCount = body.childElementCount;
const frontmatter = parsed?.frontmatter || {};
const extensions = sidecar?.extensions || {};
const proseColors = parsed?.colors || null;
const colors = buildColorModels(frontmatter.colors, extensions.colorMeta, proseColors);
if (colors.length) renderColorTiles(body, colors);
const types = buildTypographyModels(frontmatter.typography, extensions.typographyMeta);
if (types.length) renderTypeTiles(body, types);
const radii = buildRadiiModels(frontmatter.rounded);
if (radii.length) renderRadiiTile(body, radii);
if (extensions.shadows?.length) renderShadowTiles(body, extensions.shadows);
const components = sidecar?.components || [];
if (components.length) renderComponentTiles(body, components);
// Narrative: sidecar wins if present (richer, agent-curated). Otherwise
// synthesize from prose sections.
const narrative = sidecar?.narrative || synthesizeNarrative(parsed);
if (narrative.rules?.length) body.appendChild(renderRulesCollapsible(narrative.rules));
if ((narrative.dos?.length || narrative.donts?.length)) body.appendChild(renderDosDontsCollapsible(narrative));
if (narrative.overview || narrative.northStar || narrative.keyCharacteristics?.length) {
body.appendChild(renderOverviewCollapsible(narrative));
}
if (body.childElementCount === beforeCount) {
body.appendChild(msgDiv('empty', designEmptyMessage()));
}
}
// Frontmatter primitives + sidecar colorMeta → tile-ready color models.
// A matching prose bullet (when the slug sits in the bullet text) supplies
// description as a last-resort fallback.
function buildColorModels(fmColors, colorMeta, proseColors) {
if (!fmColors) return [];
const meta = colorMeta || {};
return Object.entries(fmColors).map(([key, value]) => {
const m = meta[key] || {};
return {
role: m.role || humanizeKey(key),
name: m.displayName || humanizeKey(key),
value: normalizeCssColor(m.canonical || value),
canonical: m.canonical || null,
description: m.description || findProseDescription(proseColors, key, m.displayName),
tonalRamp: m.tonalRamp || null,
};
});
}
function buildTypographyModels(fmTypography, typographyMeta) {
if (!fmTypography) return [];
const meta = typographyMeta || {};
return Object.entries(fmTypography).map(([key, spec]) => {
const m = meta[key] || {};
const { family, fallback } = splitFontFamily(spec?.fontFamily);
return {
role: key,
name: m.displayName || humanizeKey(key),
family,
fallback,
weight: spec?.fontWeight ?? 400,
// fontStyle isn't in Stitch's frontmatter schema; the sidecar carries
// it when a role is rendered in italic (e.g. display italic).
style: m.style || 'normal',
sampleSize: spec?.fontSize || '1rem',
lineHeight: spec?.lineHeight != null ? String(spec.lineHeight) : '',
letterSpacing: spec?.letterSpacing,
purpose: m.purpose,
};
});
}
function buildRadiiModels(fmRounded) {
if (!fmRounded) return [];
return Object.entries(fmRounded).map(([name, value]) => ({ name, value }));
}
function splitFontFamily(stack) {
if (!stack || typeof stack !== 'string') return { family: '', fallback: '' };
const parts = stack.split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, ''));
return { family: parts[0] || '', fallback: parts.slice(1).join(', ') };
}
function humanizeKey(k) {
return String(k || '').replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
}
function findProseDescription(proseColors, key, displayName) {
if (!proseColors || !proseColors.groups) return null;
const needles = [key, displayName].filter(Boolean).map((s) => s.toLowerCase());
for (const g of proseColors.groups) {
for (const c of g.colors || []) {
const hay = String(c.name || '').toLowerCase();
if (hay && needles.some((n) => hay.includes(n) || n.includes(hay))) {
return c.description || null;
}
}
}
return null;
}
function synthesizeNarrative(parsed) {
if (!parsed) return {};
const md = parsed;
return {
northStar: md.overview?.creativeNorthStar,
overview: (md.overview?.philosophy || []).join(' '),
keyCharacteristics: md.overview?.keyCharacteristics || [],
rules: [
...(md.colors?.rules || []).map((r) => ({ ...r, section: 'colors' })),
...(md.typography?.rules || []).map((r) => ({ ...r, section: 'typography' })),
...(md.elevation?.rules || []).map((r) => ({ ...r, section: 'elevation' })),
],
dos: md.dosDonts?.dos || [],
donts: md.dosDonts?.donts || [],
};
}
function renderColorTiles(body, colors) {
for (const c of colors) {
const tile = document.createElement('div');
tile.className = 'tile c-tile';
tile.title = 'Click to copy';
tile.addEventListener('click', () => copyToClipboard(c.value));
const meta = document.createElement('div');
meta.className = 'tile-meta';
meta.innerHTML = `<span class="name">${escapeHtml(c.name || c.role || 'Color')}</span><span>${escapeHtml(c.value || '')}</span>`;
tile.appendChild(meta);
const hero = document.createElement('div');
hero.className = 'c-hero';
hero.style.background = cssSafe(c.value || '');
tile.appendChild(hero);
const ramp = synthesizeRamp(c);
if (ramp.length) {
const r = document.createElement('div');
r.className = 'c-ramp';
r.innerHTML = ramp.map((v) => `<span style="background:${cssSafe(v)}"></span>`).join('');
tile.appendChild(r);
}
if (c.description) {
const d = document.createElement('div');
d.className = 'c-desc';
d.textContent = c.description;
tile.appendChild(d);
}
body.appendChild(tile);
}
}
function synthesizeRamp(c) {
if (c.tonalRamp?.length) return c.tonalRamp;
// If base value is OKLCH, synthesize an 8-step ramp across lightness.
const m = typeof c.value === 'string' && c.value.match(/^oklch\(\s*([\d.]+)%\s+([\d.]+)\s+([\d.]+)\s*(?:\/\s*([\d.]+))?\s*\)$/i);
if (!m) return [];
const [, , chroma, hue] = m;
const steps = [20, 32, 44, 56, 68, 80, 90, 96];
return steps.map((l) => `oklch(${l}% ${chroma} ${hue})`);
}
function renderTypeTiles(body, types) {
for (const t of types) {
const tile = document.createElement('div');
tile.className = 'tile t-tile';
const meta = document.createElement('div');
meta.className = 'tile-meta';
meta.innerHTML = `<span>${escapeHtml(t.role || '')}</span><span>${escapeHtml(t.weight || '')} ${escapeHtml(t.style === 'italic' ? 'italic' : '')}</span>`;
tile.appendChild(meta);
const specimen = document.createElement('div');
specimen.className = 't-specimen';
specimen.textContent = 'Aa';
specimen.style.fontFamily = fontStack(t);
specimen.style.fontWeight = String(t.weight || 400);
specimen.style.fontStyle = t.style || 'normal';
specimen.style.fontSize = '56px'; // Fixed specimen size - compare faces, not scales.
specimen.style.letterSpacing = 'normal';
specimen.style.textTransform = 'none';
tile.appendChild(specimen);
// The system's actual sample size for this role, shown as small mono meta below.
if (t.sampleSize) {
const scale = document.createElement('div');
scale.style.cssText = 'font-family:' + MONO + '; font-size: 10px; color:' + DP.meta + '; margin-top: 2px;';
scale.textContent = t.sampleSize;
tile.appendChild(scale);
}
const family = document.createElement('div');
family.className = 't-family';
family.textContent = t.family || t.name || '';
tile.appendChild(family);
if (t.purpose) {
const p = document.createElement('div');
p.className = 't-purpose';
p.textContent = t.purpose;
tile.appendChild(p);
}
body.appendChild(tile);
}
}
function fontStack(t) {
const fam = t.family || '';
const fb = t.fallback || '';
if (fam && /[,\s]/.test(fam) && !fam.includes("'") && !fam.includes('"')) {
return `"${fam}", ${fb}`;
}
return fam && fb ? `"${fam}", ${fb}` : (fam || fb);
}
function renderRadiiTile(body, radii) {
const tile = document.createElement('div');
tile.className = 'tile';
const meta = document.createElement('div');
meta.className = 'tile-meta';
meta.innerHTML = `<span class="name">Corner Radii</span><span>${radii.length}</span>`;
tile.appendChild(meta);
const strip = document.createElement('div');
strip.className = 'r-strip';
for (const r of radii) {
const item = document.createElement('div');
item.className = 'r-item';
const s = document.createElement('div');
s.className = 'r-sample';
s.style.borderRadius = r.value || '0';
item.appendChild(s);
const lbl = document.createElement('div');
lbl.className = 'r-label';
lbl.textContent = r.name || '';
item.appendChild(lbl);
const val = document.createElement('div');
val.className = 'r-val';
val.textContent = r.value || '';
item.appendChild(val);
strip.appendChild(item);
}
tile.appendChild(strip);
body.appendChild(tile);
}
function renderShadowTiles(body, shadows) {
for (const sh of shadows) {
const tile = document.createElement('div');
tile.className = 'tile s-tile';
const meta = document.createElement('div');
meta.className = 'tile-meta';
meta.innerHTML = `<span class="name">${escapeHtml(sh.name || 'Shadow')}</span><span>Elevation</span>`;
tile.appendChild(meta);
const surface = document.createElement('div');
surface.className = 's-surface';
surface.style.boxShadow = sh.value || 'none';
tile.appendChild(surface);
const val = document.createElement('div');
val.className = 's-value';
val.textContent = sh.value || '';
tile.appendChild(val);
if (sh.purpose) {
const p = document.createElement('div');
p.className = 's-purpose';
p.textContent = sh.purpose;
tile.appendChild(p);
}
body.appendChild(tile);
}
}
function renderComponentTiles(body, components) {
// Group consecutive components that share a kind into one tile. This avoids
// a pile of one-component tiles (e.g., three button variants = three tiles)
// and reads more like a proper category.
const groups = groupByKind(components);
for (const group of groups) {
const tile = document.createElement('div');
tile.className = 'tile cmp-tile';
const meta = document.createElement('div');
meta.className = 'tile-meta';
const groupTitle = group.length === 1
? (group[0].name || group[0].kind || 'Component')
: titleForKind(group[0].kind, group.length);
meta.innerHTML = `<span class="name">${escapeHtml(groupTitle)}</span><span class="cmp-kind">${escapeHtml(group[0].kind || '')}</span>`;
tile.appendChild(meta);
for (const c of group) {
const stage = document.createElement('div');
stage.className = 'cmp-stage';
// Render the component in its own shadow root so its CSS can't bleed.
const host = document.createElement('div');
const sub = host.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = c.css || '';
sub.appendChild(style);
const container = document.createElement('div');
container.innerHTML = c.html || '';
sub.appendChild(container);
stage.appendChild(host);
// Show component name as a sublabel only when the tile groups >1 item,
// or when the component's display name differs from its kind.
const showSublabel = group.length > 1;
if (showSublabel) {
const lbl = document.createElement('div');
lbl.className = 'cmp-sublabel';
lbl.textContent = c.name || '';
stage.appendChild(lbl);
}
tile.appendChild(stage);
}
// Single shared description if all items carry the same one; otherwise
// skip - per-item descriptions clutter a grouped tile.
if (group.length === 1 && group[0].description) {
const d = document.createElement('div');
d.className = 'c-desc';
d.textContent = group[0].description;
tile.appendChild(d);
}
body.appendChild(tile);
}
}
function groupByKind(components) {
const groups = [];
for (const c of components) {
const last = groups[groups.length - 1];
if (last && last[0].kind && c.kind === last[0].kind) {
last.push(c);
} else {
groups.push([c]);
}
}
return groups;
}
function titleForKind(kind, count) {
const labels = {
button: 'Buttons',
input: 'Inputs',
nav: 'Navigation',
chip: 'Chips',
card: 'Cards',
custom: 'Components',
};
return labels[kind] || (kind ? kind.charAt(0).toUpperCase() + kind.slice(1) + 's' : 'Components');
}
// Collapsibles.
function buildCollapsible(key, label, count) {
const wrap = document.createElement('div');
wrap.className = 'coll';
wrap.setAttribute('data-open', designState.collapsed[key] ? 'false' : 'true');
const head = document.createElement('button');
head.className = 'coll-head';
head.innerHTML = `
<svg class="coll-chev" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2.5L8 6 4 9.5"/></svg>
<span>${escapeHtml(label)}</span>
${count != null ? `<span class="coll-count">${escapeHtml(String(count))}</span>` : ''}
`;
head.addEventListener('click', () => {
designState.collapsed[key] = !designState.collapsed[key];
saveDesignPrefs();
renderDesignBody();
});
wrap.appendChild(head);
const body = document.createElement('div');
body.className = 'coll-body';
wrap.appendChild(body);
return { wrap, body };
}
function renderRulesCollapsible(rules) {
const { wrap, body } = buildCollapsible('rules', 'Named Rules', rules.length);
for (const r of rules) {
const card = document.createElement('div');
card.className = 'rule-card';
const name = document.createElement('div');
name.className = 'name';
name.innerHTML = `${escapeHtml(r.name)}${r.section ? `<span class="section">${escapeHtml(r.section)}</span>` : ''}`;
card.appendChild(name);
const b = document.createElement('div');
b.className = 'body';
b.textContent = r.body || '';
card.appendChild(b);
body.appendChild(card);
}
return wrap;
}
function renderDosDontsCollapsible(n) {
const total = (n.dos?.length || 0) + (n.donts?.length || 0);
const { wrap, body } = buildCollapsible('dosdonts', "Do's and Don'ts", total);
const grid = document.createElement('div');
grid.className = 'dos';
for (const d of n.dos || []) {
const el = document.createElement('div');
el.className = 'do';
el.innerHTML = inlineMd(d);
grid.appendChild(el);
}
for (const d of n.donts || []) {
const el = document.createElement('div');
el.className = 'dont';
el.innerHTML = inlineMd(d);
grid.appendChild(el);
}
body.appendChild(grid);
return wrap;
}
function renderOverviewCollapsible(n) {
const { wrap, body } = buildCollapsible('overview', 'Overview', null);
const ov = document.createElement('div');
ov.className = 'overview-body';
if (n.northStar) {
const star = document.createElement('span');
star.className = 'north-star';
star.textContent = '“' + n.northStar + '”';
ov.appendChild(star);
}
if (n.overview) {
const p = document.createElement('p');
p.innerHTML = inlineMd(n.overview);
ov.appendChild(p);
}
if (n.keyCharacteristics?.length) {
const ul = document.createElement('ul');
ul.innerHTML = n.keyCharacteristics.map((k) => `<li>${inlineMd(k)}</li>`).join('');
ov.appendChild(ul);
}
body.appendChild(ov);
return wrap;
}
function cssSafe(v) {
// Strip anything outside valid CSS value chars to prevent injection via
// .impeccable/design.json values rendered into inline style strings.
return String(v).replace(/[<>"'`\n]/g, '');
}
function normalizeCssColor(v) {
if (!v || typeof v !== 'string') return v;
const s = v.trim();
const oklch = s.match(/oklch\([^)]+\)/i);
if (oklch) return oklch[0];
const hex = s.match(/#[0-9a-fA-F]{3,8}\b/);
if (hex) return hex[0];
const rgb = s.match(/rgba?\([^)]+\)/i);
if (rgb) return rgb[0];
return s.replace(/\s+#.*$/, '').trim();
}
// Raw tab: minimal markdown renderer (subset)
function renderRawTab(body, md) {
const wrap = document.createElement('div');
wrap.className = 'md';
wrap.innerHTML = renderMarkdown(md);
body.appendChild(wrap);
}
function renderMarkdown(md) {
const lines = md.split(/\r?\n/);
const out = [];
let i = 0;
let inCode = false;
let codeBuf = [];
let paraBuf = [];
let listBuf = []; // array of { indent, html }
let listType = null; // 'ul' | 'ol'
const flushPara = () => {
if (paraBuf.length) {
out.push(`<p>${inlineMd(paraBuf.join(' '))}</p>`);
paraBuf = [];
}
};
const flushList = () => {
if (listBuf.length) {
out.push(buildListHtml(listBuf, listType));
listBuf = [];
listType = null;
}
};
const flushAll = () => { flushPara(); flushList(); };
for (; i < lines.length; i++) {
const line = lines[i];
// Code fence
const fence = line.match(/^```(\w*)\s*$/);
if (fence) {
if (!inCode) { flushAll(); inCode = true; codeBuf = []; }
else {
out.push(`<pre><code>${escapeHtml(codeBuf.join('\n'))}</code></pre>`);
inCode = false;
}
continue;
}
if (inCode) { codeBuf.push(line); continue; }
if (line.trim() === '') { flushAll(); continue; }
const hr = line.match(/^\s*(?:---+|\*\*\*+)\s*$/);
if (hr) { flushAll(); out.push('<hr />'); continue; }
const heading = line.match(/^(#{1,4})\s+(.+)$/);
if (heading) {
flushAll();
const lvl = heading[1].length;
out.push(`<h${lvl}>${inlineMd(heading[2])}</h${lvl}>`);
continue;
}
const bullet = line.match(/^(\s*)([-*])\s+(.+)$/);
const ordered = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
if (bullet || ordered) {
flushPara();
const m = bullet || ordered;
const indent = Math.floor(m[1].length / 2);
const t = bullet ? 'ul' : 'ol';
if (listType && listType !== t) flushList();
listType = t;
listBuf.push({ indent, html: inlineMd(m[3]) });
continue;
}
paraBuf.push(line);
}
flushAll();
if (inCode && codeBuf.length) {
out.push(`<pre><code>${escapeHtml(codeBuf.join('\n'))}</code></pre>`);
}
return out.join('\n');
}
function buildListHtml(items, type) {
// Nest by indent (one level deep is plenty for DESIGN.md).
let html = `<${type}>`;
let lastIndent = 0;
for (const it of items) {
if (it.indent > lastIndent) html += `<${type}>`;
else if (it.indent < lastIndent) html += `</${type}>`.repeat(lastIndent - it.indent);
html += `<li>${it.html}</li>`;
lastIndent = it.indent;
}
html += `</${type}>`.repeat(lastIndent + 1);
return html;
}
function inlineMd(text) {
// Order matters: escape first, then re-inject tags.
let s = escapeHtml(text);
// Code spans
s = s.replace(/`([^`]+)`/g, (_, code) => `<code>${code}</code>`);
// Links [text](url)
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, t, u) => `<a href="${u}" target="_blank" rel="noopener noreferrer">${t}</a>`);
// Bold
s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
// Italic (only single *…*, skip if inside bold already handled)
s = s.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1<em>$2</em>');
return s;
}
function highlightBold(text) {
return inlineMd(text);
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function copyToClipboard(text) {
if (!text) return;
try {
navigator.clipboard.writeText(text);
showToast('Copied: ' + text);
} catch { /* ignore */ }
}
//
// Init
//
function init() {
try { history.scrollRestoration = 'manual'; } catch {}
initHighlight();
initEditBadge();
initAnnotOverlay();
initBar();
initActionPicker();
initParamsPanel();
initGlobalBar();
attachSteerFocusDebug();
attachSteerFocusGuard();
initDesignPanel();
fetchPendingCount();
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.');
// SvelteKit (and any framework that hydrates after HTML parse) may add
// the variant wrapper AFTER init runs. Watch for it and retry resume
// once it appears. Disconnect on first hit.
const scout = new MutationObserver(() => {
const wrapper = document.querySelector('[data-impeccable-variants]');
if (!wrapper) return;
scout.disconnect();
if (resumeSession()) {
console.log('[impeccable] Resumed deferred session ' + currentSessionId + ' (post-hydration).');
}
});
scout.observe(document.body, { childList: true, subtree: true });
} else {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
syncPageInteractionCursor();
syncPageChatFocus('init-complete');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();