From 92d6141cdf61f9943dfc8e2e46870e54e46d8641 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Thu, 11 Jun 2026 05:19:15 +0000
Subject: [PATCH] Sync generated provider output
---
.../skills/impeccable/scripts/live-browser.js | 1439 ++++++++++++-----
.../impeccable/scripts/live/ui-core.mjs | 3 +-
.../skills/impeccable/scripts/live-browser.js | 1439 ++++++++++++-----
.../impeccable/scripts/live/ui-core.mjs | 3 +-
.../skills/impeccable/scripts/live-browser.js | 1439 ++++++++++++-----
.../impeccable/scripts/live/ui-core.mjs | 3 +-
.../skills/impeccable/scripts/live-browser.js | 1439 ++++++++++++-----
.../impeccable/scripts/live/ui-core.mjs | 3 +-
.pi/skills/impeccable/scripts/live-browser.js | 1439 ++++++++++++-----
.../impeccable/scripts/live/ui-core.mjs | 3 +-
.../skills/impeccable/scripts/live-browser.js | 1439 ++++++++++++-----
.../impeccable/scripts/live/ui-core.mjs | 3 +-
.../skills/impeccable/scripts/live-browser.js | 1439 ++++++++++++-----
.../impeccable/scripts/live/ui-core.mjs | 3 +-
.../skills/impeccable/scripts/live-browser.js | 1439 ++++++++++++-----
.../impeccable/scripts/live/ui-core.mjs | 3 +-
.../skills/impeccable/scripts/live-browser.js | 1439 ++++++++++++-----
.../impeccable/scripts/live/ui-core.mjs | 3 +-
.../skills/impeccable/scripts/live-browser.js | 1439 ++++++++++++-----
.../impeccable/scripts/live/ui-core.mjs | 3 +-
20 files changed, 10570 insertions(+), 3850 deletions(-)
diff --git a/.gemini/skills/impeccable/scripts/live-browser.js b/.gemini/skills/impeccable/scripts/live-browser.js
index 9092338eb..539cd07d9 100644
--- a/.gemini/skills/impeccable/scripts/live-browser.js
+++ b/.gemini/skills/impeccable/scripts/live-browser.js
@@ -57,6 +57,7 @@
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 PICK_CURSOR_CLASS = PREFIX + '-pick-cursor';
const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;
const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
prefix: PREFIX,
@@ -98,7 +99,7 @@
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'] },
{ key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] },
- { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-configure-input-wrap', PREFIX + '-input', PREFIX + '-configure-voice'] },
+ { 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'] },
@@ -131,6 +132,8 @@
let currentPreviewFile = null;
let currentPreviewMode = null;
let recoveryWaitingForAnchor = false;
+ let pickedAnchorSnapshot = null;
+ let pendingVariantAnchorRetryObserver = null;
let pendingAcceptedSession = null;
let variantObserver = null;
let variantSelectionInFlight = false;
@@ -283,31 +286,53 @@
uiAppend(tooltipEl);
}
+ function shouldShowHighlightTagTooltip() {
+ // Configure/edit carry the tag in the bar selection pill — 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 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);
+ 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' });
- tooltipEl.style.transition = 'none';
- Object.assign(tooltipEl.style, { top: tipY, left: tipX, 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(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });
}
}
@@ -1010,8 +1035,8 @@
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: '6px',
- maxWidth: '520px', minWidth: '320px',
+ padding: '5px',
+ maxWidth: '560px', minWidth: '340px',
});
uiAppend(barEl);
defangOutsideHandlers(barEl);
@@ -1019,14 +1044,29 @@
function positionBar() {
if (!barEl) return;
- const anchor = resolveBarAnchor();
- if (!anchor) return;
- const r = anchor.getBoundingClientRect();
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.
@@ -1054,8 +1094,14 @@
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
+ 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(() => {
@@ -1075,6 +1121,7 @@
setTimeout(() => { if (barEl && hideSeq === barHideSeq) barEl.style.display = 'none'; }, 250);
hideActionPicker();
closeTunePopover();
+ hideConfigureBarTooltip();
if (state === 'EDITING') restoreInlineEditDrafts();
disableInlineEdit();
}
@@ -1083,36 +1130,472 @@
if (!barEl || barEl.style.display === 'none') return;
if (mode === 'cycling' && !ensureCyclingRenderable('update-bar')) return;
barEl.innerHTML = '';
- // Reset bar styling to the kinpaku picker palette
- barEl.style.background = BP.surface;
- barEl.style.border = '1px solid ' + BP.border;
- barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
- else if (mode === 'saving') barEl.appendChild(buildSavingRow());
- else if (mode === 'confirmed') {
- barEl.appendChild(buildConfirmedRow());
- barEl.style.background = 'oklch(95% 0.05 145)';
- barEl.style.border = '1px solid oklch(75% 0.12 145 / 0.4)';
+ 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
+ // 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 =
+ '';
+
+ 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 wrap = uiGetById(PREFIX + '-configure-input-wrap');
- const input = uiGetById(PREFIX + '-input');
- if (!wrap || !input) return;
+ 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;
- wrap.dataset.inputFocused = focused ? 'true' : 'false';
- wrap.dataset.voiceListening = (voiceListening && voiceCtx?.mode === 'configure') ? 'true' : 'false';
- wrap.style.borderColor = (voiceListening && voiceCtx?.mode === 'configure')
+ 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.hairline);
+ : (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.02em',
+ }));
+ 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)
@@ -1434,14 +1917,38 @@
let pageInteractionCursorActive = false;
- /** Page-level cursor while insert mode is choosing a before/after edge. */
+ function ensurePickCursorStyle() {
+ if (document.getElementById(PREFIX + '-pick-cursor-style')) return;
+ const style = document.createElement('style');
+ style.id = PREFIX + '-pick-cursor-style';
+ style.textContent =
+ 'html.' + PICK_CURSOR_CLASS + ' * { cursor: crosshair !important; }\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"],\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"] * { cursor: revert !important; }';
+ // Styles the host page, not the chrome - inside the adapter's shadow UI
+ // root (uiAppendStyle's target) these selectors would match nothing.
+ document.head.appendChild(style);
+ }
+
+ /** Page-level cursor while pick or insert mode is targeting page elements. */
function syncPageInteractionCursor() {
- let next = '';
+ const pickCursor = state === 'PICKING' && pickActive && !insertActive;
+ let axisCursor = '';
if (state === 'PICKING' && insertActive) {
- next = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
+ axisCursor = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
}
- if (next) {
- document.documentElement.style.cursor = next;
+
+ if (pickCursor) {
+ ensurePickCursorStyle();
+ document.documentElement.classList.add(PICK_CURSOR_CLASS);
+ document.documentElement.style.cursor = '';
+ pageInteractionCursorActive = true;
+ return;
+ }
+
+ document.documentElement.classList.remove(PICK_CURSOR_CLASS);
+ if (axisCursor) {
+ document.documentElement.style.cursor = axisCursor;
pageInteractionCursorActive = true;
} else if (pageInteractionCursorActive) {
document.documentElement.style.cursor = '';
@@ -1449,6 +1956,16 @@
}
}
+ /**
+ * 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;
+ syncPageInteractionCursor();
+ }
+
/** Element used to position the floating bar / shader during a session. */
function resolveBarAnchor() {
if (svelteComponentSession?.sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
@@ -1767,69 +2284,35 @@
}
}
+ /** 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: 'center', gap: '6px',
+ display: 'flex', alignItems: 'stretch', width: '100%', height: CONFIGURE_BAR_H,
});
- // Action pill - dark graphite chip (matches kinpaku-kit .live-demo-ctx-pill)
- const pill = el('button', {
- display: 'inline-flex', alignItems: 'center', gap: '4px',
- padding: '5px 10px', borderRadius: '6px',
- background: BP.chatSurface, color: BP.text,
- fontFamily: FONT, fontSize: '12px', fontWeight: '500',
- border: '1px solid ' + BP.hairline, cursor: 'pointer',
- transition: 'background 0.12s ease, border-color 0.12s ease, transform 0.1s ease',
- whiteSpace: 'nowrap', flexShrink: '0',
- });
- pill.textContent = actionLabel() + ' \u25BE';
- pill.disabled = controlsLocked;
- pill.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- pill.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) pill.title = 'Apply is still running';
- pill.addEventListener('mouseenter', () => {
- if (controlsLocked) return;
- pill.style.background = BP.accentSoft;
- pill.style.borderColor = BP.accent;
- });
- pill.addEventListener('mouseleave', () => {
- if (controlsLocked) return;
- pill.style.background = BP.chatSurface;
- pill.style.borderColor = BP.hairline;
- });
- pill.addEventListener('mousedown', () => { if (!controlsLocked) pill.style.transform = 'scale(0.97)'; });
- pill.addEventListener('mouseup', () => pill.style.transform = 'scale(1)');
- pill.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleActionPicker();
- });
- row.appendChild(pill);
-
- // Prompt field - same chat-surface chrome as the bottom Steer bar
- const inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-configure-input-wrap';
+ const inputShell = el('div', configureInputShellStyle());
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
- input.placeholder = selectedAction === 'impeccable' ? 'describe what you want…' : 'refine further (optional)…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the change');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1837,33 +2320,31 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, background 0.12s ease',
+ const action = buildConfigureActionControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleActionPicker();
+ },
});
- voiceBtn.id = PREFIX + '-configure-voice';
- 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';
- if (!uiGetById(PREFIX + '-configure-input-style')) {
- 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::placeholder { color: ' + BP.textDim + '; opacity: 1; }' +
- '#' + PREFIX + '-configure-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 { animation: none; opacity: 1; } }' +
- '#' + PREFIX + '-configure-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
- uiAppendStyle(s);
- }
+ 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());
@@ -1873,81 +2354,33 @@
e.stopPropagation();
e.preventDefault();
input.blur();
- disableInlineEdit();
- hideBar();
- renderEditBadge('hidden');
- state = 'PICKING';
- syncPageChatFocus('configure-input-escape');
+ exitConfigureToPicking('configure-input-escape');
return;
}
- // Let arrow keys pass through to the element picker when the input is empty
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !input.value) return;
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ const voiceBtn = buildConfigureVoiceButton({
+ id: PREFIX + '-configure-voice',
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleConfigureVoice();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
+ const go = buildConfigureSubmitButton({
+ controlsLocked,
+ ariaLabel: 'Generate variants',
+ onClick: (e) => { e.stopPropagation(); handleGo(); },
+ });
+
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([action, count], voiceBtn, go));
syncConfigureInputChrome();
- // Variant count toggle
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, border-color 0.12s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.title = 'Variants: click to change';
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) count.title = 'Apply is still running';
- count.addEventListener('mouseenter', () => { if (!controlsLocked) { count.style.color = BP.text; count.style.borderColor = BP.text; } });
- count.addEventListener('mouseleave', () => { if (!controlsLocked) { count.style.color = BP.textDim; count.style.borderColor = BP.hairline; } });
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- // Go button
- const go = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- cursor: 'pointer',
- transition: 'filter 0.12s ease, transform 0.1s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- go.textContent = 'Go \u2192';
- go.disabled = controlsLocked;
- go.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- go.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) go.title = 'Apply is still running';
- go.addEventListener('mouseenter', () => { if (!controlsLocked) go.style.filter = 'brightness(1.1)'; });
- go.addEventListener('mouseleave', () => go.style.filter = 'none');
- go.addEventListener('mousedown', () => { if (!controlsLocked) go.style.transform = 'scale(0.97)'; });
- go.addEventListener('mouseup', () => go.style.transform = 'scale(1)');
- go.addEventListener('click', (e) => { e.stopPropagation(); handleGo(); });
- row.appendChild(go);
-
- // Auto-focus input after a beat
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
@@ -1956,34 +2389,20 @@
function buildInsertConfigureRow() {
const controlsLocked = pendingApplyInFlight === true;
const row = el('div', {
- display: 'flex', alignItems: 'center', gap: '6px',
+ 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 inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-insert-input-wrap';
- inputWrap.addEventListener('pointerdown', (e) => e.stopPropagation());
- inputWrap.addEventListener('mousedown', (e) => e.stopPropagation());
- inputWrap.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 = 'describe what to insert…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the new element');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1991,20 +2410,22 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
+ const count = buildConfigureCountControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ count.textContent = '\u00D7' + cycleSelectedCount();
+ if (count.matches(':hover')) {
+ showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));
+ }
+ },
});
- voiceBtn.id = PREFIX + '-insert-voice';
- 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';
+
+ inputShell.appendChild(buildSelectionPill({ el: selectedElement, controlsLocked }));
+ inputShell.appendChild(input);
+
+ ensureConfigureInputStyle();
input.addEventListener('input', () => syncInsertCreateButton());
input.addEventListener('pointerdown', (e) => e.stopPropagation());
@@ -2026,48 +2447,31 @@
}
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ 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();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
-
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer', flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- const create = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- flexShrink: '0', whiteSpace: 'nowrap',
+ 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.textContent = 'Create \u2192';
- create.disabled = controlsLocked;
create.addEventListener('mouseenter', () => {
if (controlsLocked) return;
if (isInsertCreateEnabled(create)) {
@@ -2077,15 +2481,10 @@
showInsertCreateTooltip(create, insertCreateDisabledReason(insertCreateGateState(input)));
});
create.addEventListener('mouseleave', hideInsertCreateTooltip);
- create.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- if (!isInsertCreateEnabled(create)) return;
- handleInsertCreate();
- });
- row.appendChild(create);
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([count], voiceBtn, create));
syncInsertCreateButton(create, input);
+ syncConfigureInputChrome();
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
}
@@ -2375,7 +2774,7 @@
position: 'fixed', zIndex: Z.picker,
display: 'none', opacity: '0',
transform: 'scale(0.96) translateY(4px)',
- transformOrigin: 'bottom left',
+ transformOrigin: 'bottom right',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1px solid ' + P.border,
@@ -2451,14 +2850,18 @@
chip.style.background = isActive ? P.accentSoft : 'transparent';
chip.style.color = isActive ? P.accent : P.text;
});
- // Position above the bar
+ // 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: barRect.left + 'px',
- display: 'block',
+ top: top + 'px',
+ left: left + 'px',
});
requestAnimationFrame(() => {
pickerEl.style.opacity = '1';
@@ -2941,7 +3344,7 @@
function enterEditingMode() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
- state = 'EDITING';
+ setLiveState('EDITING');
hideBar();
hideAnnotOverlay();
renderEditBadge('editing');
@@ -2974,7 +3377,7 @@
function cancelEditing() {
restoreInlineEditDrafts();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -2988,12 +3391,33 @@
hideAnnotOverlay();
clearAnnotations();
renderEditBadge('hidden');
- state = 'PICKING';
+ 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 ),
// climb to the nearest ancestor with one. The CLI uses tag+class together,
// so tag must come from the same node as the locator.
@@ -3215,7 +3639,7 @@
updatePendingCounter(stashResult.pendingCount || 0);
maybeShowFirstSaveToast();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -3895,6 +4319,12 @@
// Edit content badge - floating button at element top-right to enter EDITING mode
//
+ const EDIT_COPY_LABEL = 'Edit copy';
+ const EDIT_COPY_ICON =
+ '';
+
function usesShadowChromeRoot() {
const root = liveUiRoot();
return root && root !== document.body && root.host && root.host.id === PREFIX + '-root';
@@ -4045,7 +4475,7 @@
editBadgeProxyRoot.appendChild(proxy);
editBadgeProxyByTarget.set(target, proxy);
}
- proxy.title = target.title || target.textContent || 'Edit copy';
+ proxy.title = target.title || target.getAttribute('aria-label') || target.textContent || EDIT_COPY_LABEL;
styleEditBadgeProxy(proxy, target);
}
}
@@ -4085,13 +4515,16 @@
}
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, r.right - bw) + '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;
@@ -4129,16 +4562,36 @@
const disabled = mode === 'idle-disabled';
editBadgeEl.innerHTML = '';
const btn = document.createElement('button');
- btn.textContent = 'Edit copy';
- Object.assign(btn.style, calloutStyle(disabled ? MUTED : ACCENT, disabled ? HAIRLINE : ACCENT));
+ 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;
- btn.title = 'Edit copy is disabled while the current copy edit is applying';
+ 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', () => { btn.style.background = ACCENT; btn.style.color = PRIMARY_TEXT; });
- btn.addEventListener('mouseleave', () => { btn.style.background = SURFACE; btn.style.color = ACCENT; });
+ btn.addEventListener('mouseenter', () => showConfigureBarTooltip(btn, EDIT_COPY_LABEL));
+ btn.addEventListener('mouseleave', hideConfigureBarTooltip);
btn.onclick = enterEditingMode;
}
editBadgeEl.appendChild(btn);
@@ -4154,9 +4607,8 @@
cancel.onclick = cancelEditing;
const save = document.createElement('button');
save.textContent = 'Save';
- Object.assign(save.style, calloutStyle(ACCENT));
- save.addEventListener('mouseenter', () => { save.style.background = ACCENT; save.style.color = PRIMARY_TEXT; });
- save.addEventListener('mouseleave', () => { save.style.background = SURFACE; save.style.color = ACCENT; });
+ Object.assign(save.style, calloutStyle(PRIMARY_TEXT, ACCENT));
+ save.style.background = ACCENT;
save.onclick = applyEditing;
editBadgeEl.append(cancel, save);
}
@@ -4392,31 +4844,142 @@
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 = origContent.className;
- let liveEl = null;
+ const cls = normalizeElementClassName(origContent);
+ const candidates = [...document.getElementsByTagName(tag)];
+
if (origContent.id) {
- liveEl = document.getElementById(origContent.id);
- } else if (cls) {
- const candidates = [...document.getElementsByTagName(tag)];
- for (const c of candidates) {
- if (c.className === cls && !own(c)) { liveEl = c; break; }
- }
- if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
- if (expectedClasses.length > 0) {
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
- }
+ 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;
}
}
}
- return liveEl;
+
+ 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) {
@@ -4428,7 +4991,45 @@
const anchor = findInsertAnchorInDom();
if (anchor?.parentElement) return anchor;
}
- return findLiveElementForOriginalMarkup(manifest?.originalMarkup || manifest?.anchorMarkup || '');
+ 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);
+ }
}
function loadSvelteRuntime(runtimeModule) {
@@ -4595,7 +5196,7 @@
previewFile: manifestPath,
previewMode: 'svelte-component',
});
- if (state !== 'CYCLING') state = 'GENERATING';
+ if (state !== 'CYCLING') setLiveState('GENERATING');
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper && svelteComponentSession?.sessionId === sessionId) {
@@ -4605,7 +5206,7 @@
expectedVariants = arrivedVariants;
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants ? visibleVariant : 1;
await mountSvelteComponentVariant(visibleVariant || 1);
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
saveSession();
return;
@@ -4621,15 +5222,8 @@
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants
? visibleVariant
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
- showBar('generating');
- startScrollTracking();
- saveSession();
- queueCheckpoint('svelte_component_anchor_missing');
+ enterRecoveryWaitingForAnchor({ checkpointReason: 'svelte_component_anchor_missing', trackScroll: true });
waitForSvelteComponentTargetAndRetry({ manifestPath, sessionId, manifest });
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return;
}
@@ -4692,7 +5286,7 @@
}
selectedElement = mountTarget.firstElementChild || mountTarget;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -4741,6 +5335,7 @@
hideShaderOverlay();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
if (pendingSvelteComponentRetryObserver) { pendingSvelteComponentRetryObserver.disconnect(); pendingSvelteComponentRetryObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearSession();
clearHandled();
@@ -4750,7 +5345,7 @@
arrivedVariants = 0;
visibleVariant = 0;
selectedElement = null;
- state = 'PICKING';
+ setLiveState('PICKING');
hideBar();
if (message) showToast(message, 5000);
}
@@ -4799,21 +5394,26 @@
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
- const liveEl = findLiveElementForOriginalMarkup(origContent.outerHTML);
+ const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
- selectedElement = document.body;
- recoveryWaitingForAnchor = true;
- state = 'GENERATING';
- showBar('generating');
- saveSession();
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
+ 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.
@@ -4834,7 +5434,7 @@
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -5240,7 +5840,7 @@
if (expected > 0) expectedVariants = expected;
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
if (wrapper.dataset.impeccableMode === 'insert') finalizeInsertSession();
@@ -5326,7 +5926,8 @@
syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll();
restoreFromActiveSessions(msg.activeSessions, 'sse_connected');
- if (state === 'IDLE' && (pickActive || insertActive)) state = 'PICKING';
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('sse-connected');
break;
case 'agent_polling':
@@ -5352,7 +5953,7 @@
// Variants already arrived via HMR → normal transition.
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
if (state === 'GENERATING') {
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
disableInlineEdit();
refreshParamsPanel();
@@ -5402,7 +6003,7 @@
case 'error':
if (pendingAcceptedSession?.id && msg.id === pendingAcceptedSession.id) {
pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
updateBarContent('cycling');
showToast('Could not complete accept cleanup. Try Accept again.', 5000);
break;
@@ -5412,7 +6013,7 @@
showToast('Error: ' + msg.message, 5000);
hideBar();
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
break;
}
};
@@ -5450,7 +6051,7 @@
// transient disconnect as an explicit discard.
selectedElement = null;
selectedAction = 'impeccable';
- state = recoveryState;
+ setLiveState(recoveryState);
if (currentSessionId) saveSession();
}
@@ -5592,15 +6193,7 @@
&& !selectedElement.contains(e.target)
) {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- hideBar();
- stopScrollTracking();
- hideAnnotOverlay();
- clearAnnotations();
- renderEditBadge('hidden');
- state = 'PICKING';
- hoveredElement = null;
- hideHighlight();
- syncPageChatFocus('configure-outside-click');
+ exitConfigureToPicking('configure-outside-click', { clearHover: true });
return;
}
if (state === 'PICKING' && insertActive) {
@@ -5617,13 +6210,12 @@
hideInsertLine();
configureKind = 'insert';
selectedElement = placeholder;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
hideHighlight();
clearAnnotations();
showAnnotOverlay(placeholder);
showBar('configure');
startScrollTracking();
- syncPageInteractionCursor();
return;
}
if (state !== 'PICKING' || !pickActive) return;
@@ -5636,7 +6228,7 @@
e.preventDefault();
e.stopPropagation();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5721,6 +6313,16 @@
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;
@@ -5729,6 +6331,7 @@
deepActive
&& own(deepActive)
&& /^(INPUT|TEXTAREA|SELECT)$/.test(deepActive.tagName || '')
+ && !shouldPassthroughElementNav(deepActive, e)
) {
return;
}
@@ -5767,14 +6370,15 @@
if (state === 'EDITING') { cancelEditing(); return; }
if (state === 'CONFIGURING') {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- disableInlineEdit(); hideBar(); stopScrollTracking(); hideAnnotOverlay(); clearAnnotations(); renderEditBadge('hidden'); state = 'PICKING'; syncPageChatFocus('escape-from-configure'); 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(); state = 'IDLE'; }
+ else { hideHighlight(); setLiveState('IDLE'); }
return;
}
}
@@ -5798,7 +6402,7 @@
} else if (e.key === 'Enter') {
e.preventDefault();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5859,6 +6463,7 @@
// screenshot is uploaded (or capture fails - we still emit, just without
// screenshotPath).
const elForCapture = selectedElement;
+ pickedAnchorSnapshot = buildPickedAnchorSnapshot(elForCapture);
const captureRect = elForCapture.getBoundingClientRect();
const snapshot = {
comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })),
@@ -5879,7 +6484,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ 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
@@ -5904,7 +6509,7 @@
clearInsertPicking();
configureKind = 'replace';
selectedElement = null;
- state = insertActive ? 'PICKING' : 'IDLE';
+ setLiveState(insertActive ? 'PICKING' : 'IDLE');
hideHighlight();
syncPageChatFocus('insert-configure-cancel');
}
@@ -5954,7 +6559,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ setLiveState('GENERATING');
showBar('generating');
startScrollTracking();
saveSession();
@@ -6712,7 +7317,7 @@ void main() {
|| acceptWrapper?.dataset?.impeccablePreview === 'svelte-component';
const acceptedSnapshot = snapshotAcceptedVariantDom(acceptedSessionId, acceptedVariant);
- state = 'SAVING';
+ setLiveState('SAVING');
updateBarContent('saving');
pendingAcceptedSession = {
id: acceptedSessionId,
@@ -6727,7 +7332,7 @@ void main() {
.then(() => {})
.catch(() => {
if (pendingAcceptedSession?.id === acceptedSessionId) pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
showToast('Could not confirm accept with the live server. Session kept for recovery; try Accept again.', 5000);
});
@@ -6746,7 +7351,7 @@ void main() {
if (pending.isSvelteComponent) {
commitAcceptedSvelteComponentToDom(pending.id);
}
- state = 'CONFIRMED';
+ setLiveState('CONFIRMED');
updateBarContent('confirmed');
scheduleAcceptCleanup(pending);
return true;
@@ -6870,7 +7475,7 @@ void main() {
selectedAction = 'impeccable';
pendingAcceptedSession = null;
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
function commitAcceptedVariantToDom(sessionId, variantId) {
@@ -6922,6 +7527,7 @@ void main() {
currentPreviewFile = null;
currentPreviewMode = null;
recoveryWaitingForAnchor = false;
+ pickedAnchorSnapshot = null;
}
function rememberSessionFileMeta(meta = {}) {
@@ -6946,6 +7552,7 @@ void main() {
if (!saved) return;
rememberSessionFileMeta(saved);
if (saved.insertPlaceholder) insertPlaceholderSnapshot = saved.insertPlaceholder;
+ if (saved.pickedAnchor) pickedAnchorSnapshot = saved.pickedAnchor;
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
if (saved.previewMode) currentPreviewMode = saved.previewMode;
@@ -7012,9 +7619,10 @@ void main() {
|| clampVariantIndex(serverSession?.visibleVariant, arrivedVariants || expectedVariants)
|| (arrivedVariants > 0 ? 1 : 0);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
+ const restoredAnchor = findLiveElementFromAnchorSnapshot(pickedAnchorSnapshot);
+ selectedElement = restoredAnchor || document.body;
+ setLiveState('GENERATING');
+ recoveryWaitingForAnchor = !restoredAnchor;
showBar('generating');
startScrollTracking();
if (variantObserver) variantObserver.disconnect();
@@ -7030,7 +7638,6 @@ void main() {
return true;
}
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return true;
}
@@ -7059,6 +7666,7 @@ void main() {
pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues },
insertPlaceholder: insertPlaceholderSnapshot || undefined,
+ pickedAnchor: pickedAnchorSnapshot || undefined,
});
}
@@ -7118,6 +7726,7 @@ void main() {
hideHighlight();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearScrollY();
finalizeInsertSession();
@@ -7127,15 +7736,21 @@ void main() {
currentSessionId = null;
selectedAction = 'impeccable';
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
//
// Toast
//
+ function dismissToast() {
+ if (!toastEl) return;
+ toastEl.remove();
+ toastEl = null;
+ }
+
function showToast(message, duration) {
- if (toastEl) toastEl.remove();
+ 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
@@ -7224,7 +7839,7 @@ void main() {
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
selectedElement = resolveSvelteComponentAnchor()
|| wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
hideShaderOverlay();
showBar('cycling');
startScrollTracking();
@@ -7260,7 +7875,7 @@ void main() {
const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
const origEl = pickVariantContent(wrapper, 'original');
- state = resumedState;
+ setLiveState(resumedState);
if (isInsert && resumedState === 'GENERATING' && arrivedVariants === 0) {
selectedElement = ensureInsertPlaceholder() || findInsertAnchorInDom() || wrapper;
} else {
@@ -7419,6 +8034,8 @@ void main() {
let voiceCtx = null;
const PAGE_CHAT_COLLAPSED_W = '88px';
const PAGE_CHAT_PROCESSING_W = '76px';
+ 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(56% 0.032 82 / 0.78)';
@@ -7509,13 +8126,16 @@ void main() {
function syncPageChatChrome() {
if (!pageChatEl) return;
const P = pageChatPalette();
+ const inputFocused = pageChatInput && activeElementDeep() === pageChatInput;
pageChatEl.style.background = P.chatSurface;
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
+ 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 : P.textDim;
+ 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';
@@ -7686,24 +8306,44 @@ void main() {
function syncPageChatFocusRing() {
if (!pageChatEl || !pageChatInput) return;
const focused = activeElementDeep() === pageChatInput;
+ const typingReady = focused && !steerLocked;
pageChatEl.dataset.inputFocused = focused ? 'true' : 'false';
- const P = pageChatPalette();
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
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.color = steerLocked
- ? P.patinaPale
- : ((!pageChatExpanded && focused) ? P.patinaPale : P.textDim);
- }
- if (!pageChatExpanded) {
- pageChatInput.style.width = '0';
- pageChatInput.style.padding = '0';
- pageChatInput.style.opacity = '0';
- pageChatInput.style.pointerEvents = focused ? 'auto' : 'none';
- if (pageChatHint) pageChatHint.style.visibility = '';
+ 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) {
@@ -7723,6 +8363,7 @@ void main() {
try { window.focus(); } catch { /* embed may block */ }
try { pageChatInput.focus({ preventScroll: true }); } catch { pageChatInput.focus(); }
syncPageChatFocusRing();
+ syncPageChatChrome();
steerFocusLog('focusSteerChat result', {
reason,
before: steerFocusTargetLabel(before),
@@ -7769,6 +8410,7 @@ void main() {
pageChatEl.dataset.expanded = 'true';
pageChatEl.style.width = PAGE_CHAT_EXPANDED_W;
pageChatEl.style.cursor = steerLocked ? 'default' : 'text';
+ pageChatInput.placeholder = PAGE_CHAT_PLACEHOLDER_EXPANDED;
if (pageChatHint) {
pageChatHint.style.display = 'none';
pageChatHint.style.opacity = '0';
@@ -7780,6 +8422,20 @@ void main() {
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(); }
@@ -7938,7 +8594,9 @@ void main() {
if (pageChatEl) pageChatEl.dataset.voiceListening = listening ? 'true' : 'false';
syncPageChatChrome();
} else if (voiceCtx?.mode === 'configure') {
- const voiceBtn = uiGetById(PREFIX + '-configure-voice');
+ // 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';
@@ -8180,12 +8838,12 @@ void main() {
height: '28px', margin: '0 4px 0 ' + (GLOBAL_BAR_SECTION_GAP - GLOBAL_BAR_INNER_GAP) + 'px',
borderRadius: '7px',
background: P.chatSurface,
- border: '1px solid ' + P.hairline,
+ border: '1px solid transparent',
overflow: 'hidden',
cursor: 'pointer',
flexShrink: '0',
width: PAGE_CHAT_COLLAPSED_W,
- transition: 'border-color 0.15s ease',
+ transition: 'width 0.18s ease, border-color 0.15s ease',
});
pageChatEl.id = PREFIX + '-page-chat';
pageChatEl.dataset.expanded = 'false';
@@ -8211,13 +8869,14 @@ void main() {
pageChatInput = document.createElement('input');
pageChatInput.id = PREFIX + '-page-chat-input';
pageChatInput.type = 'text';
- pageChatInput.placeholder = 'Steer the page…';
+ 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',
});
@@ -8252,18 +8911,23 @@ void main() {
'#' + 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(63% 0.024 82); 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.024 82); }' +
'#' + PREFIX + '-page-chat-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
uiAppendStyle(s);
}
- pageChatEl.addEventListener('pointerdown', keepSteerPointerInside);
+ pageChatEl.addEventListener('pointerdown', (e) => {
+ keepSteerPointerInside(e);
+ if (steerLocked || pageChatVoiceBtn.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)) return;
- expandPageChat({ focus: false });
- focusPageChatInput('page-chat-click');
+ armPageChatForTyping({ expand: true, focus: true });
});
pageChatVoiceBtn.addEventListener('pointerdown', keepSteerPointerInside);
@@ -8286,7 +8950,9 @@ void main() {
});
pageChatInput.addEventListener('focus', () => {
+ steerInputWasFocused = true;
syncPageChatFocusRing();
+ syncPageChatChrome();
});
pageChatInput.addEventListener('blur', () => {
@@ -8953,14 +9619,15 @@ void main() {
cancelInsertConfigure();
return;
}
+ teardownConfigureChrome();
hideHighlight();
- hideBar();
hideActionPicker();
selectedElement = null;
+ hoveredElement = null;
configureKind = 'replace';
- if (state === 'PICKING' || state === 'CONFIGURING') state = 'IDLE';
+ if (state === 'PICKING' || state === 'CONFIGURING') setLiveState('IDLE');
} else {
- if (state === 'IDLE') state = 'PICKING';
+ if (state === 'IDLE') setLiveState('PICKING');
}
syncPageChatFocus('toggle-pick');
}
@@ -8976,10 +9643,10 @@ void main() {
selectedElement = null;
configureKind = 'replace';
if (state === 'CONFIGURING') cancelInsertConfigure();
- else if (state === 'IDLE' || state === 'PICKING') state = 'PICKING';
+ else if (state === 'IDLE' || state === 'PICKING') setLiveState('PICKING');
} else {
clearInsertPicking();
- if (state === 'PICKING' && !pickActive) state = 'IDLE';
+ if (state === 'PICKING' && !pickActive) setLiveState('IDLE');
}
saveInteractionPrefs();
updateGlobalBarState();
@@ -9060,6 +9727,7 @@ void main() {
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; }
@@ -9073,7 +9741,8 @@ void main() {
window.removeEventListener('message', onDetectMessage);
// Remove detection overlays
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
- state = 'IDLE';
+ setLiveState('IDLE');
+ document.getElementById(PREFIX + '-pick-cursor-style')?.remove();
window.__IMPECCABLE_LIVE_INIT__ = false;
console.log('[impeccable] Live mode exited.');
}
@@ -10250,6 +10919,8 @@ void main() {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('init-complete');
}
diff --git a/.gemini/skills/impeccable/scripts/live/ui-core.mjs b/.gemini/skills/impeccable/scripts/live/ui-core.mjs
index 4b7adde8b..bbb2c5c35 100644
--- a/.gemini/skills/impeccable/scripts/live/ui-core.mjs
+++ b/.gemini/skills/impeccable/scripts/live/ui-core.mjs
@@ -42,9 +42,10 @@ export const LIVE_UI_SURFACES = Object.freeze([
'impeccable-live-highlight',
'impeccable-live-tooltip',
'impeccable-live-bar',
- 'impeccable-live-configure-input-wrap',
+ 'impeccable-live-selection-pill',
'impeccable-live-input',
'impeccable-live-configure-voice',
+ 'impeccable-live-configure-bar-tooltip',
],
states: ['rest', 'hover', 'focus-visible', 'pressed', 'disabled'],
},
diff --git a/.github/skills/impeccable/scripts/live-browser.js b/.github/skills/impeccable/scripts/live-browser.js
index 9092338eb..539cd07d9 100644
--- a/.github/skills/impeccable/scripts/live-browser.js
+++ b/.github/skills/impeccable/scripts/live-browser.js
@@ -57,6 +57,7 @@
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 PICK_CURSOR_CLASS = PREFIX + '-pick-cursor';
const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;
const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
prefix: PREFIX,
@@ -98,7 +99,7 @@
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'] },
{ key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] },
- { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-configure-input-wrap', PREFIX + '-input', PREFIX + '-configure-voice'] },
+ { 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'] },
@@ -131,6 +132,8 @@
let currentPreviewFile = null;
let currentPreviewMode = null;
let recoveryWaitingForAnchor = false;
+ let pickedAnchorSnapshot = null;
+ let pendingVariantAnchorRetryObserver = null;
let pendingAcceptedSession = null;
let variantObserver = null;
let variantSelectionInFlight = false;
@@ -283,31 +286,53 @@
uiAppend(tooltipEl);
}
+ function shouldShowHighlightTagTooltip() {
+ // Configure/edit carry the tag in the bar selection pill — 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 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);
+ 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' });
- tooltipEl.style.transition = 'none';
- Object.assign(tooltipEl.style, { top: tipY, left: tipX, 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(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });
}
}
@@ -1010,8 +1035,8 @@
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: '6px',
- maxWidth: '520px', minWidth: '320px',
+ padding: '5px',
+ maxWidth: '560px', minWidth: '340px',
});
uiAppend(barEl);
defangOutsideHandlers(barEl);
@@ -1019,14 +1044,29 @@
function positionBar() {
if (!barEl) return;
- const anchor = resolveBarAnchor();
- if (!anchor) return;
- const r = anchor.getBoundingClientRect();
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.
@@ -1054,8 +1094,14 @@
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
+ 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(() => {
@@ -1075,6 +1121,7 @@
setTimeout(() => { if (barEl && hideSeq === barHideSeq) barEl.style.display = 'none'; }, 250);
hideActionPicker();
closeTunePopover();
+ hideConfigureBarTooltip();
if (state === 'EDITING') restoreInlineEditDrafts();
disableInlineEdit();
}
@@ -1083,36 +1130,472 @@
if (!barEl || barEl.style.display === 'none') return;
if (mode === 'cycling' && !ensureCyclingRenderable('update-bar')) return;
barEl.innerHTML = '';
- // Reset bar styling to the kinpaku picker palette
- barEl.style.background = BP.surface;
- barEl.style.border = '1px solid ' + BP.border;
- barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
- else if (mode === 'saving') barEl.appendChild(buildSavingRow());
- else if (mode === 'confirmed') {
- barEl.appendChild(buildConfirmedRow());
- barEl.style.background = 'oklch(95% 0.05 145)';
- barEl.style.border = '1px solid oklch(75% 0.12 145 / 0.4)';
+ 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
+ // 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 =
+ '';
+
+ 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 wrap = uiGetById(PREFIX + '-configure-input-wrap');
- const input = uiGetById(PREFIX + '-input');
- if (!wrap || !input) return;
+ 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;
- wrap.dataset.inputFocused = focused ? 'true' : 'false';
- wrap.dataset.voiceListening = (voiceListening && voiceCtx?.mode === 'configure') ? 'true' : 'false';
- wrap.style.borderColor = (voiceListening && voiceCtx?.mode === 'configure')
+ 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.hairline);
+ : (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.02em',
+ }));
+ 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)
@@ -1434,14 +1917,38 @@
let pageInteractionCursorActive = false;
- /** Page-level cursor while insert mode is choosing a before/after edge. */
+ function ensurePickCursorStyle() {
+ if (document.getElementById(PREFIX + '-pick-cursor-style')) return;
+ const style = document.createElement('style');
+ style.id = PREFIX + '-pick-cursor-style';
+ style.textContent =
+ 'html.' + PICK_CURSOR_CLASS + ' * { cursor: crosshair !important; }\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"],\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"] * { cursor: revert !important; }';
+ // Styles the host page, not the chrome - inside the adapter's shadow UI
+ // root (uiAppendStyle's target) these selectors would match nothing.
+ document.head.appendChild(style);
+ }
+
+ /** Page-level cursor while pick or insert mode is targeting page elements. */
function syncPageInteractionCursor() {
- let next = '';
+ const pickCursor = state === 'PICKING' && pickActive && !insertActive;
+ let axisCursor = '';
if (state === 'PICKING' && insertActive) {
- next = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
+ axisCursor = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
}
- if (next) {
- document.documentElement.style.cursor = next;
+
+ if (pickCursor) {
+ ensurePickCursorStyle();
+ document.documentElement.classList.add(PICK_CURSOR_CLASS);
+ document.documentElement.style.cursor = '';
+ pageInteractionCursorActive = true;
+ return;
+ }
+
+ document.documentElement.classList.remove(PICK_CURSOR_CLASS);
+ if (axisCursor) {
+ document.documentElement.style.cursor = axisCursor;
pageInteractionCursorActive = true;
} else if (pageInteractionCursorActive) {
document.documentElement.style.cursor = '';
@@ -1449,6 +1956,16 @@
}
}
+ /**
+ * 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;
+ syncPageInteractionCursor();
+ }
+
/** Element used to position the floating bar / shader during a session. */
function resolveBarAnchor() {
if (svelteComponentSession?.sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
@@ -1767,69 +2284,35 @@
}
}
+ /** 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: 'center', gap: '6px',
+ display: 'flex', alignItems: 'stretch', width: '100%', height: CONFIGURE_BAR_H,
});
- // Action pill - dark graphite chip (matches kinpaku-kit .live-demo-ctx-pill)
- const pill = el('button', {
- display: 'inline-flex', alignItems: 'center', gap: '4px',
- padding: '5px 10px', borderRadius: '6px',
- background: BP.chatSurface, color: BP.text,
- fontFamily: FONT, fontSize: '12px', fontWeight: '500',
- border: '1px solid ' + BP.hairline, cursor: 'pointer',
- transition: 'background 0.12s ease, border-color 0.12s ease, transform 0.1s ease',
- whiteSpace: 'nowrap', flexShrink: '0',
- });
- pill.textContent = actionLabel() + ' \u25BE';
- pill.disabled = controlsLocked;
- pill.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- pill.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) pill.title = 'Apply is still running';
- pill.addEventListener('mouseenter', () => {
- if (controlsLocked) return;
- pill.style.background = BP.accentSoft;
- pill.style.borderColor = BP.accent;
- });
- pill.addEventListener('mouseleave', () => {
- if (controlsLocked) return;
- pill.style.background = BP.chatSurface;
- pill.style.borderColor = BP.hairline;
- });
- pill.addEventListener('mousedown', () => { if (!controlsLocked) pill.style.transform = 'scale(0.97)'; });
- pill.addEventListener('mouseup', () => pill.style.transform = 'scale(1)');
- pill.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleActionPicker();
- });
- row.appendChild(pill);
-
- // Prompt field - same chat-surface chrome as the bottom Steer bar
- const inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-configure-input-wrap';
+ const inputShell = el('div', configureInputShellStyle());
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
- input.placeholder = selectedAction === 'impeccable' ? 'describe what you want…' : 'refine further (optional)…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the change');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1837,33 +2320,31 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, background 0.12s ease',
+ const action = buildConfigureActionControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleActionPicker();
+ },
});
- voiceBtn.id = PREFIX + '-configure-voice';
- 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';
- if (!uiGetById(PREFIX + '-configure-input-style')) {
- 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::placeholder { color: ' + BP.textDim + '; opacity: 1; }' +
- '#' + PREFIX + '-configure-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 { animation: none; opacity: 1; } }' +
- '#' + PREFIX + '-configure-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
- uiAppendStyle(s);
- }
+ 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());
@@ -1873,81 +2354,33 @@
e.stopPropagation();
e.preventDefault();
input.blur();
- disableInlineEdit();
- hideBar();
- renderEditBadge('hidden');
- state = 'PICKING';
- syncPageChatFocus('configure-input-escape');
+ exitConfigureToPicking('configure-input-escape');
return;
}
- // Let arrow keys pass through to the element picker when the input is empty
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !input.value) return;
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ const voiceBtn = buildConfigureVoiceButton({
+ id: PREFIX + '-configure-voice',
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleConfigureVoice();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
+ const go = buildConfigureSubmitButton({
+ controlsLocked,
+ ariaLabel: 'Generate variants',
+ onClick: (e) => { e.stopPropagation(); handleGo(); },
+ });
+
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([action, count], voiceBtn, go));
syncConfigureInputChrome();
- // Variant count toggle
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, border-color 0.12s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.title = 'Variants: click to change';
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) count.title = 'Apply is still running';
- count.addEventListener('mouseenter', () => { if (!controlsLocked) { count.style.color = BP.text; count.style.borderColor = BP.text; } });
- count.addEventListener('mouseleave', () => { if (!controlsLocked) { count.style.color = BP.textDim; count.style.borderColor = BP.hairline; } });
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- // Go button
- const go = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- cursor: 'pointer',
- transition: 'filter 0.12s ease, transform 0.1s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- go.textContent = 'Go \u2192';
- go.disabled = controlsLocked;
- go.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- go.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) go.title = 'Apply is still running';
- go.addEventListener('mouseenter', () => { if (!controlsLocked) go.style.filter = 'brightness(1.1)'; });
- go.addEventListener('mouseleave', () => go.style.filter = 'none');
- go.addEventListener('mousedown', () => { if (!controlsLocked) go.style.transform = 'scale(0.97)'; });
- go.addEventListener('mouseup', () => go.style.transform = 'scale(1)');
- go.addEventListener('click', (e) => { e.stopPropagation(); handleGo(); });
- row.appendChild(go);
-
- // Auto-focus input after a beat
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
@@ -1956,34 +2389,20 @@
function buildInsertConfigureRow() {
const controlsLocked = pendingApplyInFlight === true;
const row = el('div', {
- display: 'flex', alignItems: 'center', gap: '6px',
+ 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 inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-insert-input-wrap';
- inputWrap.addEventListener('pointerdown', (e) => e.stopPropagation());
- inputWrap.addEventListener('mousedown', (e) => e.stopPropagation());
- inputWrap.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 = 'describe what to insert…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the new element');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1991,20 +2410,22 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
+ const count = buildConfigureCountControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ count.textContent = '\u00D7' + cycleSelectedCount();
+ if (count.matches(':hover')) {
+ showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));
+ }
+ },
});
- voiceBtn.id = PREFIX + '-insert-voice';
- 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';
+
+ inputShell.appendChild(buildSelectionPill({ el: selectedElement, controlsLocked }));
+ inputShell.appendChild(input);
+
+ ensureConfigureInputStyle();
input.addEventListener('input', () => syncInsertCreateButton());
input.addEventListener('pointerdown', (e) => e.stopPropagation());
@@ -2026,48 +2447,31 @@
}
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ 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();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
-
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer', flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- const create = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- flexShrink: '0', whiteSpace: 'nowrap',
+ 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.textContent = 'Create \u2192';
- create.disabled = controlsLocked;
create.addEventListener('mouseenter', () => {
if (controlsLocked) return;
if (isInsertCreateEnabled(create)) {
@@ -2077,15 +2481,10 @@
showInsertCreateTooltip(create, insertCreateDisabledReason(insertCreateGateState(input)));
});
create.addEventListener('mouseleave', hideInsertCreateTooltip);
- create.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- if (!isInsertCreateEnabled(create)) return;
- handleInsertCreate();
- });
- row.appendChild(create);
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([count], voiceBtn, create));
syncInsertCreateButton(create, input);
+ syncConfigureInputChrome();
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
}
@@ -2375,7 +2774,7 @@
position: 'fixed', zIndex: Z.picker,
display: 'none', opacity: '0',
transform: 'scale(0.96) translateY(4px)',
- transformOrigin: 'bottom left',
+ transformOrigin: 'bottom right',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1px solid ' + P.border,
@@ -2451,14 +2850,18 @@
chip.style.background = isActive ? P.accentSoft : 'transparent';
chip.style.color = isActive ? P.accent : P.text;
});
- // Position above the bar
+ // 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: barRect.left + 'px',
- display: 'block',
+ top: top + 'px',
+ left: left + 'px',
});
requestAnimationFrame(() => {
pickerEl.style.opacity = '1';
@@ -2941,7 +3344,7 @@
function enterEditingMode() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
- state = 'EDITING';
+ setLiveState('EDITING');
hideBar();
hideAnnotOverlay();
renderEditBadge('editing');
@@ -2974,7 +3377,7 @@
function cancelEditing() {
restoreInlineEditDrafts();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -2988,12 +3391,33 @@
hideAnnotOverlay();
clearAnnotations();
renderEditBadge('hidden');
- state = 'PICKING';
+ 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 ),
// climb to the nearest ancestor with one. The CLI uses tag+class together,
// so tag must come from the same node as the locator.
@@ -3215,7 +3639,7 @@
updatePendingCounter(stashResult.pendingCount || 0);
maybeShowFirstSaveToast();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -3895,6 +4319,12 @@
// Edit content badge - floating button at element top-right to enter EDITING mode
//
+ const EDIT_COPY_LABEL = 'Edit copy';
+ const EDIT_COPY_ICON =
+ '';
+
function usesShadowChromeRoot() {
const root = liveUiRoot();
return root && root !== document.body && root.host && root.host.id === PREFIX + '-root';
@@ -4045,7 +4475,7 @@
editBadgeProxyRoot.appendChild(proxy);
editBadgeProxyByTarget.set(target, proxy);
}
- proxy.title = target.title || target.textContent || 'Edit copy';
+ proxy.title = target.title || target.getAttribute('aria-label') || target.textContent || EDIT_COPY_LABEL;
styleEditBadgeProxy(proxy, target);
}
}
@@ -4085,13 +4515,16 @@
}
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, r.right - bw) + '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;
@@ -4129,16 +4562,36 @@
const disabled = mode === 'idle-disabled';
editBadgeEl.innerHTML = '';
const btn = document.createElement('button');
- btn.textContent = 'Edit copy';
- Object.assign(btn.style, calloutStyle(disabled ? MUTED : ACCENT, disabled ? HAIRLINE : ACCENT));
+ 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;
- btn.title = 'Edit copy is disabled while the current copy edit is applying';
+ 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', () => { btn.style.background = ACCENT; btn.style.color = PRIMARY_TEXT; });
- btn.addEventListener('mouseleave', () => { btn.style.background = SURFACE; btn.style.color = ACCENT; });
+ btn.addEventListener('mouseenter', () => showConfigureBarTooltip(btn, EDIT_COPY_LABEL));
+ btn.addEventListener('mouseleave', hideConfigureBarTooltip);
btn.onclick = enterEditingMode;
}
editBadgeEl.appendChild(btn);
@@ -4154,9 +4607,8 @@
cancel.onclick = cancelEditing;
const save = document.createElement('button');
save.textContent = 'Save';
- Object.assign(save.style, calloutStyle(ACCENT));
- save.addEventListener('mouseenter', () => { save.style.background = ACCENT; save.style.color = PRIMARY_TEXT; });
- save.addEventListener('mouseleave', () => { save.style.background = SURFACE; save.style.color = ACCENT; });
+ Object.assign(save.style, calloutStyle(PRIMARY_TEXT, ACCENT));
+ save.style.background = ACCENT;
save.onclick = applyEditing;
editBadgeEl.append(cancel, save);
}
@@ -4392,31 +4844,142 @@
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 = origContent.className;
- let liveEl = null;
+ const cls = normalizeElementClassName(origContent);
+ const candidates = [...document.getElementsByTagName(tag)];
+
if (origContent.id) {
- liveEl = document.getElementById(origContent.id);
- } else if (cls) {
- const candidates = [...document.getElementsByTagName(tag)];
- for (const c of candidates) {
- if (c.className === cls && !own(c)) { liveEl = c; break; }
- }
- if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
- if (expectedClasses.length > 0) {
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
- }
+ 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;
}
}
}
- return liveEl;
+
+ 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) {
@@ -4428,7 +4991,45 @@
const anchor = findInsertAnchorInDom();
if (anchor?.parentElement) return anchor;
}
- return findLiveElementForOriginalMarkup(manifest?.originalMarkup || manifest?.anchorMarkup || '');
+ 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);
+ }
}
function loadSvelteRuntime(runtimeModule) {
@@ -4595,7 +5196,7 @@
previewFile: manifestPath,
previewMode: 'svelte-component',
});
- if (state !== 'CYCLING') state = 'GENERATING';
+ if (state !== 'CYCLING') setLiveState('GENERATING');
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper && svelteComponentSession?.sessionId === sessionId) {
@@ -4605,7 +5206,7 @@
expectedVariants = arrivedVariants;
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants ? visibleVariant : 1;
await mountSvelteComponentVariant(visibleVariant || 1);
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
saveSession();
return;
@@ -4621,15 +5222,8 @@
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants
? visibleVariant
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
- showBar('generating');
- startScrollTracking();
- saveSession();
- queueCheckpoint('svelte_component_anchor_missing');
+ enterRecoveryWaitingForAnchor({ checkpointReason: 'svelte_component_anchor_missing', trackScroll: true });
waitForSvelteComponentTargetAndRetry({ manifestPath, sessionId, manifest });
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return;
}
@@ -4692,7 +5286,7 @@
}
selectedElement = mountTarget.firstElementChild || mountTarget;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -4741,6 +5335,7 @@
hideShaderOverlay();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
if (pendingSvelteComponentRetryObserver) { pendingSvelteComponentRetryObserver.disconnect(); pendingSvelteComponentRetryObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearSession();
clearHandled();
@@ -4750,7 +5345,7 @@
arrivedVariants = 0;
visibleVariant = 0;
selectedElement = null;
- state = 'PICKING';
+ setLiveState('PICKING');
hideBar();
if (message) showToast(message, 5000);
}
@@ -4799,21 +5394,26 @@
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
- const liveEl = findLiveElementForOriginalMarkup(origContent.outerHTML);
+ const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
- selectedElement = document.body;
- recoveryWaitingForAnchor = true;
- state = 'GENERATING';
- showBar('generating');
- saveSession();
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
+ 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.
@@ -4834,7 +5434,7 @@
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -5240,7 +5840,7 @@
if (expected > 0) expectedVariants = expected;
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
if (wrapper.dataset.impeccableMode === 'insert') finalizeInsertSession();
@@ -5326,7 +5926,8 @@
syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll();
restoreFromActiveSessions(msg.activeSessions, 'sse_connected');
- if (state === 'IDLE' && (pickActive || insertActive)) state = 'PICKING';
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('sse-connected');
break;
case 'agent_polling':
@@ -5352,7 +5953,7 @@
// Variants already arrived via HMR → normal transition.
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
if (state === 'GENERATING') {
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
disableInlineEdit();
refreshParamsPanel();
@@ -5402,7 +6003,7 @@
case 'error':
if (pendingAcceptedSession?.id && msg.id === pendingAcceptedSession.id) {
pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
updateBarContent('cycling');
showToast('Could not complete accept cleanup. Try Accept again.', 5000);
break;
@@ -5412,7 +6013,7 @@
showToast('Error: ' + msg.message, 5000);
hideBar();
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
break;
}
};
@@ -5450,7 +6051,7 @@
// transient disconnect as an explicit discard.
selectedElement = null;
selectedAction = 'impeccable';
- state = recoveryState;
+ setLiveState(recoveryState);
if (currentSessionId) saveSession();
}
@@ -5592,15 +6193,7 @@
&& !selectedElement.contains(e.target)
) {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- hideBar();
- stopScrollTracking();
- hideAnnotOverlay();
- clearAnnotations();
- renderEditBadge('hidden');
- state = 'PICKING';
- hoveredElement = null;
- hideHighlight();
- syncPageChatFocus('configure-outside-click');
+ exitConfigureToPicking('configure-outside-click', { clearHover: true });
return;
}
if (state === 'PICKING' && insertActive) {
@@ -5617,13 +6210,12 @@
hideInsertLine();
configureKind = 'insert';
selectedElement = placeholder;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
hideHighlight();
clearAnnotations();
showAnnotOverlay(placeholder);
showBar('configure');
startScrollTracking();
- syncPageInteractionCursor();
return;
}
if (state !== 'PICKING' || !pickActive) return;
@@ -5636,7 +6228,7 @@
e.preventDefault();
e.stopPropagation();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5721,6 +6313,16 @@
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;
@@ -5729,6 +6331,7 @@
deepActive
&& own(deepActive)
&& /^(INPUT|TEXTAREA|SELECT)$/.test(deepActive.tagName || '')
+ && !shouldPassthroughElementNav(deepActive, e)
) {
return;
}
@@ -5767,14 +6370,15 @@
if (state === 'EDITING') { cancelEditing(); return; }
if (state === 'CONFIGURING') {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- disableInlineEdit(); hideBar(); stopScrollTracking(); hideAnnotOverlay(); clearAnnotations(); renderEditBadge('hidden'); state = 'PICKING'; syncPageChatFocus('escape-from-configure'); 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(); state = 'IDLE'; }
+ else { hideHighlight(); setLiveState('IDLE'); }
return;
}
}
@@ -5798,7 +6402,7 @@
} else if (e.key === 'Enter') {
e.preventDefault();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5859,6 +6463,7 @@
// screenshot is uploaded (or capture fails - we still emit, just without
// screenshotPath).
const elForCapture = selectedElement;
+ pickedAnchorSnapshot = buildPickedAnchorSnapshot(elForCapture);
const captureRect = elForCapture.getBoundingClientRect();
const snapshot = {
comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })),
@@ -5879,7 +6484,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ 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
@@ -5904,7 +6509,7 @@
clearInsertPicking();
configureKind = 'replace';
selectedElement = null;
- state = insertActive ? 'PICKING' : 'IDLE';
+ setLiveState(insertActive ? 'PICKING' : 'IDLE');
hideHighlight();
syncPageChatFocus('insert-configure-cancel');
}
@@ -5954,7 +6559,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ setLiveState('GENERATING');
showBar('generating');
startScrollTracking();
saveSession();
@@ -6712,7 +7317,7 @@ void main() {
|| acceptWrapper?.dataset?.impeccablePreview === 'svelte-component';
const acceptedSnapshot = snapshotAcceptedVariantDom(acceptedSessionId, acceptedVariant);
- state = 'SAVING';
+ setLiveState('SAVING');
updateBarContent('saving');
pendingAcceptedSession = {
id: acceptedSessionId,
@@ -6727,7 +7332,7 @@ void main() {
.then(() => {})
.catch(() => {
if (pendingAcceptedSession?.id === acceptedSessionId) pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
showToast('Could not confirm accept with the live server. Session kept for recovery; try Accept again.', 5000);
});
@@ -6746,7 +7351,7 @@ void main() {
if (pending.isSvelteComponent) {
commitAcceptedSvelteComponentToDom(pending.id);
}
- state = 'CONFIRMED';
+ setLiveState('CONFIRMED');
updateBarContent('confirmed');
scheduleAcceptCleanup(pending);
return true;
@@ -6870,7 +7475,7 @@ void main() {
selectedAction = 'impeccable';
pendingAcceptedSession = null;
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
function commitAcceptedVariantToDom(sessionId, variantId) {
@@ -6922,6 +7527,7 @@ void main() {
currentPreviewFile = null;
currentPreviewMode = null;
recoveryWaitingForAnchor = false;
+ pickedAnchorSnapshot = null;
}
function rememberSessionFileMeta(meta = {}) {
@@ -6946,6 +7552,7 @@ void main() {
if (!saved) return;
rememberSessionFileMeta(saved);
if (saved.insertPlaceholder) insertPlaceholderSnapshot = saved.insertPlaceholder;
+ if (saved.pickedAnchor) pickedAnchorSnapshot = saved.pickedAnchor;
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
if (saved.previewMode) currentPreviewMode = saved.previewMode;
@@ -7012,9 +7619,10 @@ void main() {
|| clampVariantIndex(serverSession?.visibleVariant, arrivedVariants || expectedVariants)
|| (arrivedVariants > 0 ? 1 : 0);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
+ const restoredAnchor = findLiveElementFromAnchorSnapshot(pickedAnchorSnapshot);
+ selectedElement = restoredAnchor || document.body;
+ setLiveState('GENERATING');
+ recoveryWaitingForAnchor = !restoredAnchor;
showBar('generating');
startScrollTracking();
if (variantObserver) variantObserver.disconnect();
@@ -7030,7 +7638,6 @@ void main() {
return true;
}
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return true;
}
@@ -7059,6 +7666,7 @@ void main() {
pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues },
insertPlaceholder: insertPlaceholderSnapshot || undefined,
+ pickedAnchor: pickedAnchorSnapshot || undefined,
});
}
@@ -7118,6 +7726,7 @@ void main() {
hideHighlight();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearScrollY();
finalizeInsertSession();
@@ -7127,15 +7736,21 @@ void main() {
currentSessionId = null;
selectedAction = 'impeccable';
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
//
// Toast
//
+ function dismissToast() {
+ if (!toastEl) return;
+ toastEl.remove();
+ toastEl = null;
+ }
+
function showToast(message, duration) {
- if (toastEl) toastEl.remove();
+ 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
@@ -7224,7 +7839,7 @@ void main() {
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
selectedElement = resolveSvelteComponentAnchor()
|| wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
hideShaderOverlay();
showBar('cycling');
startScrollTracking();
@@ -7260,7 +7875,7 @@ void main() {
const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
const origEl = pickVariantContent(wrapper, 'original');
- state = resumedState;
+ setLiveState(resumedState);
if (isInsert && resumedState === 'GENERATING' && arrivedVariants === 0) {
selectedElement = ensureInsertPlaceholder() || findInsertAnchorInDom() || wrapper;
} else {
@@ -7419,6 +8034,8 @@ void main() {
let voiceCtx = null;
const PAGE_CHAT_COLLAPSED_W = '88px';
const PAGE_CHAT_PROCESSING_W = '76px';
+ 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(56% 0.032 82 / 0.78)';
@@ -7509,13 +8126,16 @@ void main() {
function syncPageChatChrome() {
if (!pageChatEl) return;
const P = pageChatPalette();
+ const inputFocused = pageChatInput && activeElementDeep() === pageChatInput;
pageChatEl.style.background = P.chatSurface;
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
+ 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 : P.textDim;
+ 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';
@@ -7686,24 +8306,44 @@ void main() {
function syncPageChatFocusRing() {
if (!pageChatEl || !pageChatInput) return;
const focused = activeElementDeep() === pageChatInput;
+ const typingReady = focused && !steerLocked;
pageChatEl.dataset.inputFocused = focused ? 'true' : 'false';
- const P = pageChatPalette();
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
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.color = steerLocked
- ? P.patinaPale
- : ((!pageChatExpanded && focused) ? P.patinaPale : P.textDim);
- }
- if (!pageChatExpanded) {
- pageChatInput.style.width = '0';
- pageChatInput.style.padding = '0';
- pageChatInput.style.opacity = '0';
- pageChatInput.style.pointerEvents = focused ? 'auto' : 'none';
- if (pageChatHint) pageChatHint.style.visibility = '';
+ 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) {
@@ -7723,6 +8363,7 @@ void main() {
try { window.focus(); } catch { /* embed may block */ }
try { pageChatInput.focus({ preventScroll: true }); } catch { pageChatInput.focus(); }
syncPageChatFocusRing();
+ syncPageChatChrome();
steerFocusLog('focusSteerChat result', {
reason,
before: steerFocusTargetLabel(before),
@@ -7769,6 +8410,7 @@ void main() {
pageChatEl.dataset.expanded = 'true';
pageChatEl.style.width = PAGE_CHAT_EXPANDED_W;
pageChatEl.style.cursor = steerLocked ? 'default' : 'text';
+ pageChatInput.placeholder = PAGE_CHAT_PLACEHOLDER_EXPANDED;
if (pageChatHint) {
pageChatHint.style.display = 'none';
pageChatHint.style.opacity = '0';
@@ -7780,6 +8422,20 @@ void main() {
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(); }
@@ -7938,7 +8594,9 @@ void main() {
if (pageChatEl) pageChatEl.dataset.voiceListening = listening ? 'true' : 'false';
syncPageChatChrome();
} else if (voiceCtx?.mode === 'configure') {
- const voiceBtn = uiGetById(PREFIX + '-configure-voice');
+ // 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';
@@ -8180,12 +8838,12 @@ void main() {
height: '28px', margin: '0 4px 0 ' + (GLOBAL_BAR_SECTION_GAP - GLOBAL_BAR_INNER_GAP) + 'px',
borderRadius: '7px',
background: P.chatSurface,
- border: '1px solid ' + P.hairline,
+ border: '1px solid transparent',
overflow: 'hidden',
cursor: 'pointer',
flexShrink: '0',
width: PAGE_CHAT_COLLAPSED_W,
- transition: 'border-color 0.15s ease',
+ transition: 'width 0.18s ease, border-color 0.15s ease',
});
pageChatEl.id = PREFIX + '-page-chat';
pageChatEl.dataset.expanded = 'false';
@@ -8211,13 +8869,14 @@ void main() {
pageChatInput = document.createElement('input');
pageChatInput.id = PREFIX + '-page-chat-input';
pageChatInput.type = 'text';
- pageChatInput.placeholder = 'Steer the page…';
+ 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',
});
@@ -8252,18 +8911,23 @@ void main() {
'#' + 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(63% 0.024 82); 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.024 82); }' +
'#' + PREFIX + '-page-chat-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
uiAppendStyle(s);
}
- pageChatEl.addEventListener('pointerdown', keepSteerPointerInside);
+ pageChatEl.addEventListener('pointerdown', (e) => {
+ keepSteerPointerInside(e);
+ if (steerLocked || pageChatVoiceBtn.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)) return;
- expandPageChat({ focus: false });
- focusPageChatInput('page-chat-click');
+ armPageChatForTyping({ expand: true, focus: true });
});
pageChatVoiceBtn.addEventListener('pointerdown', keepSteerPointerInside);
@@ -8286,7 +8950,9 @@ void main() {
});
pageChatInput.addEventListener('focus', () => {
+ steerInputWasFocused = true;
syncPageChatFocusRing();
+ syncPageChatChrome();
});
pageChatInput.addEventListener('blur', () => {
@@ -8953,14 +9619,15 @@ void main() {
cancelInsertConfigure();
return;
}
+ teardownConfigureChrome();
hideHighlight();
- hideBar();
hideActionPicker();
selectedElement = null;
+ hoveredElement = null;
configureKind = 'replace';
- if (state === 'PICKING' || state === 'CONFIGURING') state = 'IDLE';
+ if (state === 'PICKING' || state === 'CONFIGURING') setLiveState('IDLE');
} else {
- if (state === 'IDLE') state = 'PICKING';
+ if (state === 'IDLE') setLiveState('PICKING');
}
syncPageChatFocus('toggle-pick');
}
@@ -8976,10 +9643,10 @@ void main() {
selectedElement = null;
configureKind = 'replace';
if (state === 'CONFIGURING') cancelInsertConfigure();
- else if (state === 'IDLE' || state === 'PICKING') state = 'PICKING';
+ else if (state === 'IDLE' || state === 'PICKING') setLiveState('PICKING');
} else {
clearInsertPicking();
- if (state === 'PICKING' && !pickActive) state = 'IDLE';
+ if (state === 'PICKING' && !pickActive) setLiveState('IDLE');
}
saveInteractionPrefs();
updateGlobalBarState();
@@ -9060,6 +9727,7 @@ void main() {
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; }
@@ -9073,7 +9741,8 @@ void main() {
window.removeEventListener('message', onDetectMessage);
// Remove detection overlays
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
- state = 'IDLE';
+ setLiveState('IDLE');
+ document.getElementById(PREFIX + '-pick-cursor-style')?.remove();
window.__IMPECCABLE_LIVE_INIT__ = false;
console.log('[impeccable] Live mode exited.');
}
@@ -10250,6 +10919,8 @@ void main() {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('init-complete');
}
diff --git a/.github/skills/impeccable/scripts/live/ui-core.mjs b/.github/skills/impeccable/scripts/live/ui-core.mjs
index 4b7adde8b..bbb2c5c35 100644
--- a/.github/skills/impeccable/scripts/live/ui-core.mjs
+++ b/.github/skills/impeccable/scripts/live/ui-core.mjs
@@ -42,9 +42,10 @@ export const LIVE_UI_SURFACES = Object.freeze([
'impeccable-live-highlight',
'impeccable-live-tooltip',
'impeccable-live-bar',
- 'impeccable-live-configure-input-wrap',
+ 'impeccable-live-selection-pill',
'impeccable-live-input',
'impeccable-live-configure-voice',
+ 'impeccable-live-configure-bar-tooltip',
],
states: ['rest', 'hover', 'focus-visible', 'pressed', 'disabled'],
},
diff --git a/.kiro/skills/impeccable/scripts/live-browser.js b/.kiro/skills/impeccable/scripts/live-browser.js
index 9092338eb..539cd07d9 100644
--- a/.kiro/skills/impeccable/scripts/live-browser.js
+++ b/.kiro/skills/impeccable/scripts/live-browser.js
@@ -57,6 +57,7 @@
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 PICK_CURSOR_CLASS = PREFIX + '-pick-cursor';
const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;
const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
prefix: PREFIX,
@@ -98,7 +99,7 @@
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'] },
{ key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] },
- { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-configure-input-wrap', PREFIX + '-input', PREFIX + '-configure-voice'] },
+ { 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'] },
@@ -131,6 +132,8 @@
let currentPreviewFile = null;
let currentPreviewMode = null;
let recoveryWaitingForAnchor = false;
+ let pickedAnchorSnapshot = null;
+ let pendingVariantAnchorRetryObserver = null;
let pendingAcceptedSession = null;
let variantObserver = null;
let variantSelectionInFlight = false;
@@ -283,31 +286,53 @@
uiAppend(tooltipEl);
}
+ function shouldShowHighlightTagTooltip() {
+ // Configure/edit carry the tag in the bar selection pill — 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 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);
+ 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' });
- tooltipEl.style.transition = 'none';
- Object.assign(tooltipEl.style, { top: tipY, left: tipX, 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(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });
}
}
@@ -1010,8 +1035,8 @@
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: '6px',
- maxWidth: '520px', minWidth: '320px',
+ padding: '5px',
+ maxWidth: '560px', minWidth: '340px',
});
uiAppend(barEl);
defangOutsideHandlers(barEl);
@@ -1019,14 +1044,29 @@
function positionBar() {
if (!barEl) return;
- const anchor = resolveBarAnchor();
- if (!anchor) return;
- const r = anchor.getBoundingClientRect();
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.
@@ -1054,8 +1094,14 @@
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
+ 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(() => {
@@ -1075,6 +1121,7 @@
setTimeout(() => { if (barEl && hideSeq === barHideSeq) barEl.style.display = 'none'; }, 250);
hideActionPicker();
closeTunePopover();
+ hideConfigureBarTooltip();
if (state === 'EDITING') restoreInlineEditDrafts();
disableInlineEdit();
}
@@ -1083,36 +1130,472 @@
if (!barEl || barEl.style.display === 'none') return;
if (mode === 'cycling' && !ensureCyclingRenderable('update-bar')) return;
barEl.innerHTML = '';
- // Reset bar styling to the kinpaku picker palette
- barEl.style.background = BP.surface;
- barEl.style.border = '1px solid ' + BP.border;
- barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
- else if (mode === 'saving') barEl.appendChild(buildSavingRow());
- else if (mode === 'confirmed') {
- barEl.appendChild(buildConfirmedRow());
- barEl.style.background = 'oklch(95% 0.05 145)';
- barEl.style.border = '1px solid oklch(75% 0.12 145 / 0.4)';
+ 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
+ // 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 =
+ '';
+
+ 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 wrap = uiGetById(PREFIX + '-configure-input-wrap');
- const input = uiGetById(PREFIX + '-input');
- if (!wrap || !input) return;
+ 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;
- wrap.dataset.inputFocused = focused ? 'true' : 'false';
- wrap.dataset.voiceListening = (voiceListening && voiceCtx?.mode === 'configure') ? 'true' : 'false';
- wrap.style.borderColor = (voiceListening && voiceCtx?.mode === 'configure')
+ 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.hairline);
+ : (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.02em',
+ }));
+ 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)
@@ -1434,14 +1917,38 @@
let pageInteractionCursorActive = false;
- /** Page-level cursor while insert mode is choosing a before/after edge. */
+ function ensurePickCursorStyle() {
+ if (document.getElementById(PREFIX + '-pick-cursor-style')) return;
+ const style = document.createElement('style');
+ style.id = PREFIX + '-pick-cursor-style';
+ style.textContent =
+ 'html.' + PICK_CURSOR_CLASS + ' * { cursor: crosshair !important; }\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"],\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"] * { cursor: revert !important; }';
+ // Styles the host page, not the chrome - inside the adapter's shadow UI
+ // root (uiAppendStyle's target) these selectors would match nothing.
+ document.head.appendChild(style);
+ }
+
+ /** Page-level cursor while pick or insert mode is targeting page elements. */
function syncPageInteractionCursor() {
- let next = '';
+ const pickCursor = state === 'PICKING' && pickActive && !insertActive;
+ let axisCursor = '';
if (state === 'PICKING' && insertActive) {
- next = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
+ axisCursor = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
}
- if (next) {
- document.documentElement.style.cursor = next;
+
+ if (pickCursor) {
+ ensurePickCursorStyle();
+ document.documentElement.classList.add(PICK_CURSOR_CLASS);
+ document.documentElement.style.cursor = '';
+ pageInteractionCursorActive = true;
+ return;
+ }
+
+ document.documentElement.classList.remove(PICK_CURSOR_CLASS);
+ if (axisCursor) {
+ document.documentElement.style.cursor = axisCursor;
pageInteractionCursorActive = true;
} else if (pageInteractionCursorActive) {
document.documentElement.style.cursor = '';
@@ -1449,6 +1956,16 @@
}
}
+ /**
+ * 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;
+ syncPageInteractionCursor();
+ }
+
/** Element used to position the floating bar / shader during a session. */
function resolveBarAnchor() {
if (svelteComponentSession?.sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
@@ -1767,69 +2284,35 @@
}
}
+ /** 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: 'center', gap: '6px',
+ display: 'flex', alignItems: 'stretch', width: '100%', height: CONFIGURE_BAR_H,
});
- // Action pill - dark graphite chip (matches kinpaku-kit .live-demo-ctx-pill)
- const pill = el('button', {
- display: 'inline-flex', alignItems: 'center', gap: '4px',
- padding: '5px 10px', borderRadius: '6px',
- background: BP.chatSurface, color: BP.text,
- fontFamily: FONT, fontSize: '12px', fontWeight: '500',
- border: '1px solid ' + BP.hairline, cursor: 'pointer',
- transition: 'background 0.12s ease, border-color 0.12s ease, transform 0.1s ease',
- whiteSpace: 'nowrap', flexShrink: '0',
- });
- pill.textContent = actionLabel() + ' \u25BE';
- pill.disabled = controlsLocked;
- pill.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- pill.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) pill.title = 'Apply is still running';
- pill.addEventListener('mouseenter', () => {
- if (controlsLocked) return;
- pill.style.background = BP.accentSoft;
- pill.style.borderColor = BP.accent;
- });
- pill.addEventListener('mouseleave', () => {
- if (controlsLocked) return;
- pill.style.background = BP.chatSurface;
- pill.style.borderColor = BP.hairline;
- });
- pill.addEventListener('mousedown', () => { if (!controlsLocked) pill.style.transform = 'scale(0.97)'; });
- pill.addEventListener('mouseup', () => pill.style.transform = 'scale(1)');
- pill.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleActionPicker();
- });
- row.appendChild(pill);
-
- // Prompt field - same chat-surface chrome as the bottom Steer bar
- const inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-configure-input-wrap';
+ const inputShell = el('div', configureInputShellStyle());
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
- input.placeholder = selectedAction === 'impeccable' ? 'describe what you want…' : 'refine further (optional)…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the change');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1837,33 +2320,31 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, background 0.12s ease',
+ const action = buildConfigureActionControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleActionPicker();
+ },
});
- voiceBtn.id = PREFIX + '-configure-voice';
- 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';
- if (!uiGetById(PREFIX + '-configure-input-style')) {
- 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::placeholder { color: ' + BP.textDim + '; opacity: 1; }' +
- '#' + PREFIX + '-configure-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 { animation: none; opacity: 1; } }' +
- '#' + PREFIX + '-configure-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
- uiAppendStyle(s);
- }
+ 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());
@@ -1873,81 +2354,33 @@
e.stopPropagation();
e.preventDefault();
input.blur();
- disableInlineEdit();
- hideBar();
- renderEditBadge('hidden');
- state = 'PICKING';
- syncPageChatFocus('configure-input-escape');
+ exitConfigureToPicking('configure-input-escape');
return;
}
- // Let arrow keys pass through to the element picker when the input is empty
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !input.value) return;
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ const voiceBtn = buildConfigureVoiceButton({
+ id: PREFIX + '-configure-voice',
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleConfigureVoice();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
+ const go = buildConfigureSubmitButton({
+ controlsLocked,
+ ariaLabel: 'Generate variants',
+ onClick: (e) => { e.stopPropagation(); handleGo(); },
+ });
+
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([action, count], voiceBtn, go));
syncConfigureInputChrome();
- // Variant count toggle
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, border-color 0.12s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.title = 'Variants: click to change';
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) count.title = 'Apply is still running';
- count.addEventListener('mouseenter', () => { if (!controlsLocked) { count.style.color = BP.text; count.style.borderColor = BP.text; } });
- count.addEventListener('mouseleave', () => { if (!controlsLocked) { count.style.color = BP.textDim; count.style.borderColor = BP.hairline; } });
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- // Go button
- const go = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- cursor: 'pointer',
- transition: 'filter 0.12s ease, transform 0.1s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- go.textContent = 'Go \u2192';
- go.disabled = controlsLocked;
- go.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- go.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) go.title = 'Apply is still running';
- go.addEventListener('mouseenter', () => { if (!controlsLocked) go.style.filter = 'brightness(1.1)'; });
- go.addEventListener('mouseleave', () => go.style.filter = 'none');
- go.addEventListener('mousedown', () => { if (!controlsLocked) go.style.transform = 'scale(0.97)'; });
- go.addEventListener('mouseup', () => go.style.transform = 'scale(1)');
- go.addEventListener('click', (e) => { e.stopPropagation(); handleGo(); });
- row.appendChild(go);
-
- // Auto-focus input after a beat
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
@@ -1956,34 +2389,20 @@
function buildInsertConfigureRow() {
const controlsLocked = pendingApplyInFlight === true;
const row = el('div', {
- display: 'flex', alignItems: 'center', gap: '6px',
+ 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 inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-insert-input-wrap';
- inputWrap.addEventListener('pointerdown', (e) => e.stopPropagation());
- inputWrap.addEventListener('mousedown', (e) => e.stopPropagation());
- inputWrap.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 = 'describe what to insert…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the new element');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1991,20 +2410,22 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
+ const count = buildConfigureCountControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ count.textContent = '\u00D7' + cycleSelectedCount();
+ if (count.matches(':hover')) {
+ showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));
+ }
+ },
});
- voiceBtn.id = PREFIX + '-insert-voice';
- 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';
+
+ inputShell.appendChild(buildSelectionPill({ el: selectedElement, controlsLocked }));
+ inputShell.appendChild(input);
+
+ ensureConfigureInputStyle();
input.addEventListener('input', () => syncInsertCreateButton());
input.addEventListener('pointerdown', (e) => e.stopPropagation());
@@ -2026,48 +2447,31 @@
}
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ 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();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
-
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer', flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- const create = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- flexShrink: '0', whiteSpace: 'nowrap',
+ 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.textContent = 'Create \u2192';
- create.disabled = controlsLocked;
create.addEventListener('mouseenter', () => {
if (controlsLocked) return;
if (isInsertCreateEnabled(create)) {
@@ -2077,15 +2481,10 @@
showInsertCreateTooltip(create, insertCreateDisabledReason(insertCreateGateState(input)));
});
create.addEventListener('mouseleave', hideInsertCreateTooltip);
- create.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- if (!isInsertCreateEnabled(create)) return;
- handleInsertCreate();
- });
- row.appendChild(create);
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([count], voiceBtn, create));
syncInsertCreateButton(create, input);
+ syncConfigureInputChrome();
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
}
@@ -2375,7 +2774,7 @@
position: 'fixed', zIndex: Z.picker,
display: 'none', opacity: '0',
transform: 'scale(0.96) translateY(4px)',
- transformOrigin: 'bottom left',
+ transformOrigin: 'bottom right',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1px solid ' + P.border,
@@ -2451,14 +2850,18 @@
chip.style.background = isActive ? P.accentSoft : 'transparent';
chip.style.color = isActive ? P.accent : P.text;
});
- // Position above the bar
+ // 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: barRect.left + 'px',
- display: 'block',
+ top: top + 'px',
+ left: left + 'px',
});
requestAnimationFrame(() => {
pickerEl.style.opacity = '1';
@@ -2941,7 +3344,7 @@
function enterEditingMode() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
- state = 'EDITING';
+ setLiveState('EDITING');
hideBar();
hideAnnotOverlay();
renderEditBadge('editing');
@@ -2974,7 +3377,7 @@
function cancelEditing() {
restoreInlineEditDrafts();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -2988,12 +3391,33 @@
hideAnnotOverlay();
clearAnnotations();
renderEditBadge('hidden');
- state = 'PICKING';
+ 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 ),
// climb to the nearest ancestor with one. The CLI uses tag+class together,
// so tag must come from the same node as the locator.
@@ -3215,7 +3639,7 @@
updatePendingCounter(stashResult.pendingCount || 0);
maybeShowFirstSaveToast();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -3895,6 +4319,12 @@
// Edit content badge - floating button at element top-right to enter EDITING mode
//
+ const EDIT_COPY_LABEL = 'Edit copy';
+ const EDIT_COPY_ICON =
+ '';
+
function usesShadowChromeRoot() {
const root = liveUiRoot();
return root && root !== document.body && root.host && root.host.id === PREFIX + '-root';
@@ -4045,7 +4475,7 @@
editBadgeProxyRoot.appendChild(proxy);
editBadgeProxyByTarget.set(target, proxy);
}
- proxy.title = target.title || target.textContent || 'Edit copy';
+ proxy.title = target.title || target.getAttribute('aria-label') || target.textContent || EDIT_COPY_LABEL;
styleEditBadgeProxy(proxy, target);
}
}
@@ -4085,13 +4515,16 @@
}
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, r.right - bw) + '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;
@@ -4129,16 +4562,36 @@
const disabled = mode === 'idle-disabled';
editBadgeEl.innerHTML = '';
const btn = document.createElement('button');
- btn.textContent = 'Edit copy';
- Object.assign(btn.style, calloutStyle(disabled ? MUTED : ACCENT, disabled ? HAIRLINE : ACCENT));
+ 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;
- btn.title = 'Edit copy is disabled while the current copy edit is applying';
+ 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', () => { btn.style.background = ACCENT; btn.style.color = PRIMARY_TEXT; });
- btn.addEventListener('mouseleave', () => { btn.style.background = SURFACE; btn.style.color = ACCENT; });
+ btn.addEventListener('mouseenter', () => showConfigureBarTooltip(btn, EDIT_COPY_LABEL));
+ btn.addEventListener('mouseleave', hideConfigureBarTooltip);
btn.onclick = enterEditingMode;
}
editBadgeEl.appendChild(btn);
@@ -4154,9 +4607,8 @@
cancel.onclick = cancelEditing;
const save = document.createElement('button');
save.textContent = 'Save';
- Object.assign(save.style, calloutStyle(ACCENT));
- save.addEventListener('mouseenter', () => { save.style.background = ACCENT; save.style.color = PRIMARY_TEXT; });
- save.addEventListener('mouseleave', () => { save.style.background = SURFACE; save.style.color = ACCENT; });
+ Object.assign(save.style, calloutStyle(PRIMARY_TEXT, ACCENT));
+ save.style.background = ACCENT;
save.onclick = applyEditing;
editBadgeEl.append(cancel, save);
}
@@ -4392,31 +4844,142 @@
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 = origContent.className;
- let liveEl = null;
+ const cls = normalizeElementClassName(origContent);
+ const candidates = [...document.getElementsByTagName(tag)];
+
if (origContent.id) {
- liveEl = document.getElementById(origContent.id);
- } else if (cls) {
- const candidates = [...document.getElementsByTagName(tag)];
- for (const c of candidates) {
- if (c.className === cls && !own(c)) { liveEl = c; break; }
- }
- if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
- if (expectedClasses.length > 0) {
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
- }
+ 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;
}
}
}
- return liveEl;
+
+ 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) {
@@ -4428,7 +4991,45 @@
const anchor = findInsertAnchorInDom();
if (anchor?.parentElement) return anchor;
}
- return findLiveElementForOriginalMarkup(manifest?.originalMarkup || manifest?.anchorMarkup || '');
+ 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);
+ }
}
function loadSvelteRuntime(runtimeModule) {
@@ -4595,7 +5196,7 @@
previewFile: manifestPath,
previewMode: 'svelte-component',
});
- if (state !== 'CYCLING') state = 'GENERATING';
+ if (state !== 'CYCLING') setLiveState('GENERATING');
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper && svelteComponentSession?.sessionId === sessionId) {
@@ -4605,7 +5206,7 @@
expectedVariants = arrivedVariants;
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants ? visibleVariant : 1;
await mountSvelteComponentVariant(visibleVariant || 1);
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
saveSession();
return;
@@ -4621,15 +5222,8 @@
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants
? visibleVariant
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
- showBar('generating');
- startScrollTracking();
- saveSession();
- queueCheckpoint('svelte_component_anchor_missing');
+ enterRecoveryWaitingForAnchor({ checkpointReason: 'svelte_component_anchor_missing', trackScroll: true });
waitForSvelteComponentTargetAndRetry({ manifestPath, sessionId, manifest });
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return;
}
@@ -4692,7 +5286,7 @@
}
selectedElement = mountTarget.firstElementChild || mountTarget;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -4741,6 +5335,7 @@
hideShaderOverlay();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
if (pendingSvelteComponentRetryObserver) { pendingSvelteComponentRetryObserver.disconnect(); pendingSvelteComponentRetryObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearSession();
clearHandled();
@@ -4750,7 +5345,7 @@
arrivedVariants = 0;
visibleVariant = 0;
selectedElement = null;
- state = 'PICKING';
+ setLiveState('PICKING');
hideBar();
if (message) showToast(message, 5000);
}
@@ -4799,21 +5394,26 @@
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
- const liveEl = findLiveElementForOriginalMarkup(origContent.outerHTML);
+ const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
- selectedElement = document.body;
- recoveryWaitingForAnchor = true;
- state = 'GENERATING';
- showBar('generating');
- saveSession();
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
+ 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.
@@ -4834,7 +5434,7 @@
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -5240,7 +5840,7 @@
if (expected > 0) expectedVariants = expected;
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
if (wrapper.dataset.impeccableMode === 'insert') finalizeInsertSession();
@@ -5326,7 +5926,8 @@
syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll();
restoreFromActiveSessions(msg.activeSessions, 'sse_connected');
- if (state === 'IDLE' && (pickActive || insertActive)) state = 'PICKING';
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('sse-connected');
break;
case 'agent_polling':
@@ -5352,7 +5953,7 @@
// Variants already arrived via HMR → normal transition.
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
if (state === 'GENERATING') {
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
disableInlineEdit();
refreshParamsPanel();
@@ -5402,7 +6003,7 @@
case 'error':
if (pendingAcceptedSession?.id && msg.id === pendingAcceptedSession.id) {
pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
updateBarContent('cycling');
showToast('Could not complete accept cleanup. Try Accept again.', 5000);
break;
@@ -5412,7 +6013,7 @@
showToast('Error: ' + msg.message, 5000);
hideBar();
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
break;
}
};
@@ -5450,7 +6051,7 @@
// transient disconnect as an explicit discard.
selectedElement = null;
selectedAction = 'impeccable';
- state = recoveryState;
+ setLiveState(recoveryState);
if (currentSessionId) saveSession();
}
@@ -5592,15 +6193,7 @@
&& !selectedElement.contains(e.target)
) {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- hideBar();
- stopScrollTracking();
- hideAnnotOverlay();
- clearAnnotations();
- renderEditBadge('hidden');
- state = 'PICKING';
- hoveredElement = null;
- hideHighlight();
- syncPageChatFocus('configure-outside-click');
+ exitConfigureToPicking('configure-outside-click', { clearHover: true });
return;
}
if (state === 'PICKING' && insertActive) {
@@ -5617,13 +6210,12 @@
hideInsertLine();
configureKind = 'insert';
selectedElement = placeholder;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
hideHighlight();
clearAnnotations();
showAnnotOverlay(placeholder);
showBar('configure');
startScrollTracking();
- syncPageInteractionCursor();
return;
}
if (state !== 'PICKING' || !pickActive) return;
@@ -5636,7 +6228,7 @@
e.preventDefault();
e.stopPropagation();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5721,6 +6313,16 @@
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;
@@ -5729,6 +6331,7 @@
deepActive
&& own(deepActive)
&& /^(INPUT|TEXTAREA|SELECT)$/.test(deepActive.tagName || '')
+ && !shouldPassthroughElementNav(deepActive, e)
) {
return;
}
@@ -5767,14 +6370,15 @@
if (state === 'EDITING') { cancelEditing(); return; }
if (state === 'CONFIGURING') {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- disableInlineEdit(); hideBar(); stopScrollTracking(); hideAnnotOverlay(); clearAnnotations(); renderEditBadge('hidden'); state = 'PICKING'; syncPageChatFocus('escape-from-configure'); 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(); state = 'IDLE'; }
+ else { hideHighlight(); setLiveState('IDLE'); }
return;
}
}
@@ -5798,7 +6402,7 @@
} else if (e.key === 'Enter') {
e.preventDefault();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5859,6 +6463,7 @@
// screenshot is uploaded (or capture fails - we still emit, just without
// screenshotPath).
const elForCapture = selectedElement;
+ pickedAnchorSnapshot = buildPickedAnchorSnapshot(elForCapture);
const captureRect = elForCapture.getBoundingClientRect();
const snapshot = {
comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })),
@@ -5879,7 +6484,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ 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
@@ -5904,7 +6509,7 @@
clearInsertPicking();
configureKind = 'replace';
selectedElement = null;
- state = insertActive ? 'PICKING' : 'IDLE';
+ setLiveState(insertActive ? 'PICKING' : 'IDLE');
hideHighlight();
syncPageChatFocus('insert-configure-cancel');
}
@@ -5954,7 +6559,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ setLiveState('GENERATING');
showBar('generating');
startScrollTracking();
saveSession();
@@ -6712,7 +7317,7 @@ void main() {
|| acceptWrapper?.dataset?.impeccablePreview === 'svelte-component';
const acceptedSnapshot = snapshotAcceptedVariantDom(acceptedSessionId, acceptedVariant);
- state = 'SAVING';
+ setLiveState('SAVING');
updateBarContent('saving');
pendingAcceptedSession = {
id: acceptedSessionId,
@@ -6727,7 +7332,7 @@ void main() {
.then(() => {})
.catch(() => {
if (pendingAcceptedSession?.id === acceptedSessionId) pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
showToast('Could not confirm accept with the live server. Session kept for recovery; try Accept again.', 5000);
});
@@ -6746,7 +7351,7 @@ void main() {
if (pending.isSvelteComponent) {
commitAcceptedSvelteComponentToDom(pending.id);
}
- state = 'CONFIRMED';
+ setLiveState('CONFIRMED');
updateBarContent('confirmed');
scheduleAcceptCleanup(pending);
return true;
@@ -6870,7 +7475,7 @@ void main() {
selectedAction = 'impeccable';
pendingAcceptedSession = null;
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
function commitAcceptedVariantToDom(sessionId, variantId) {
@@ -6922,6 +7527,7 @@ void main() {
currentPreviewFile = null;
currentPreviewMode = null;
recoveryWaitingForAnchor = false;
+ pickedAnchorSnapshot = null;
}
function rememberSessionFileMeta(meta = {}) {
@@ -6946,6 +7552,7 @@ void main() {
if (!saved) return;
rememberSessionFileMeta(saved);
if (saved.insertPlaceholder) insertPlaceholderSnapshot = saved.insertPlaceholder;
+ if (saved.pickedAnchor) pickedAnchorSnapshot = saved.pickedAnchor;
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
if (saved.previewMode) currentPreviewMode = saved.previewMode;
@@ -7012,9 +7619,10 @@ void main() {
|| clampVariantIndex(serverSession?.visibleVariant, arrivedVariants || expectedVariants)
|| (arrivedVariants > 0 ? 1 : 0);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
+ const restoredAnchor = findLiveElementFromAnchorSnapshot(pickedAnchorSnapshot);
+ selectedElement = restoredAnchor || document.body;
+ setLiveState('GENERATING');
+ recoveryWaitingForAnchor = !restoredAnchor;
showBar('generating');
startScrollTracking();
if (variantObserver) variantObserver.disconnect();
@@ -7030,7 +7638,6 @@ void main() {
return true;
}
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return true;
}
@@ -7059,6 +7666,7 @@ void main() {
pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues },
insertPlaceholder: insertPlaceholderSnapshot || undefined,
+ pickedAnchor: pickedAnchorSnapshot || undefined,
});
}
@@ -7118,6 +7726,7 @@ void main() {
hideHighlight();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearScrollY();
finalizeInsertSession();
@@ -7127,15 +7736,21 @@ void main() {
currentSessionId = null;
selectedAction = 'impeccable';
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
//
// Toast
//
+ function dismissToast() {
+ if (!toastEl) return;
+ toastEl.remove();
+ toastEl = null;
+ }
+
function showToast(message, duration) {
- if (toastEl) toastEl.remove();
+ 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
@@ -7224,7 +7839,7 @@ void main() {
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
selectedElement = resolveSvelteComponentAnchor()
|| wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
hideShaderOverlay();
showBar('cycling');
startScrollTracking();
@@ -7260,7 +7875,7 @@ void main() {
const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
const origEl = pickVariantContent(wrapper, 'original');
- state = resumedState;
+ setLiveState(resumedState);
if (isInsert && resumedState === 'GENERATING' && arrivedVariants === 0) {
selectedElement = ensureInsertPlaceholder() || findInsertAnchorInDom() || wrapper;
} else {
@@ -7419,6 +8034,8 @@ void main() {
let voiceCtx = null;
const PAGE_CHAT_COLLAPSED_W = '88px';
const PAGE_CHAT_PROCESSING_W = '76px';
+ 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(56% 0.032 82 / 0.78)';
@@ -7509,13 +8126,16 @@ void main() {
function syncPageChatChrome() {
if (!pageChatEl) return;
const P = pageChatPalette();
+ const inputFocused = pageChatInput && activeElementDeep() === pageChatInput;
pageChatEl.style.background = P.chatSurface;
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
+ 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 : P.textDim;
+ 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';
@@ -7686,24 +8306,44 @@ void main() {
function syncPageChatFocusRing() {
if (!pageChatEl || !pageChatInput) return;
const focused = activeElementDeep() === pageChatInput;
+ const typingReady = focused && !steerLocked;
pageChatEl.dataset.inputFocused = focused ? 'true' : 'false';
- const P = pageChatPalette();
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
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.color = steerLocked
- ? P.patinaPale
- : ((!pageChatExpanded && focused) ? P.patinaPale : P.textDim);
- }
- if (!pageChatExpanded) {
- pageChatInput.style.width = '0';
- pageChatInput.style.padding = '0';
- pageChatInput.style.opacity = '0';
- pageChatInput.style.pointerEvents = focused ? 'auto' : 'none';
- if (pageChatHint) pageChatHint.style.visibility = '';
+ 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) {
@@ -7723,6 +8363,7 @@ void main() {
try { window.focus(); } catch { /* embed may block */ }
try { pageChatInput.focus({ preventScroll: true }); } catch { pageChatInput.focus(); }
syncPageChatFocusRing();
+ syncPageChatChrome();
steerFocusLog('focusSteerChat result', {
reason,
before: steerFocusTargetLabel(before),
@@ -7769,6 +8410,7 @@ void main() {
pageChatEl.dataset.expanded = 'true';
pageChatEl.style.width = PAGE_CHAT_EXPANDED_W;
pageChatEl.style.cursor = steerLocked ? 'default' : 'text';
+ pageChatInput.placeholder = PAGE_CHAT_PLACEHOLDER_EXPANDED;
if (pageChatHint) {
pageChatHint.style.display = 'none';
pageChatHint.style.opacity = '0';
@@ -7780,6 +8422,20 @@ void main() {
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(); }
@@ -7938,7 +8594,9 @@ void main() {
if (pageChatEl) pageChatEl.dataset.voiceListening = listening ? 'true' : 'false';
syncPageChatChrome();
} else if (voiceCtx?.mode === 'configure') {
- const voiceBtn = uiGetById(PREFIX + '-configure-voice');
+ // 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';
@@ -8180,12 +8838,12 @@ void main() {
height: '28px', margin: '0 4px 0 ' + (GLOBAL_BAR_SECTION_GAP - GLOBAL_BAR_INNER_GAP) + 'px',
borderRadius: '7px',
background: P.chatSurface,
- border: '1px solid ' + P.hairline,
+ border: '1px solid transparent',
overflow: 'hidden',
cursor: 'pointer',
flexShrink: '0',
width: PAGE_CHAT_COLLAPSED_W,
- transition: 'border-color 0.15s ease',
+ transition: 'width 0.18s ease, border-color 0.15s ease',
});
pageChatEl.id = PREFIX + '-page-chat';
pageChatEl.dataset.expanded = 'false';
@@ -8211,13 +8869,14 @@ void main() {
pageChatInput = document.createElement('input');
pageChatInput.id = PREFIX + '-page-chat-input';
pageChatInput.type = 'text';
- pageChatInput.placeholder = 'Steer the page…';
+ 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',
});
@@ -8252,18 +8911,23 @@ void main() {
'#' + 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(63% 0.024 82); 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.024 82); }' +
'#' + PREFIX + '-page-chat-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
uiAppendStyle(s);
}
- pageChatEl.addEventListener('pointerdown', keepSteerPointerInside);
+ pageChatEl.addEventListener('pointerdown', (e) => {
+ keepSteerPointerInside(e);
+ if (steerLocked || pageChatVoiceBtn.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)) return;
- expandPageChat({ focus: false });
- focusPageChatInput('page-chat-click');
+ armPageChatForTyping({ expand: true, focus: true });
});
pageChatVoiceBtn.addEventListener('pointerdown', keepSteerPointerInside);
@@ -8286,7 +8950,9 @@ void main() {
});
pageChatInput.addEventListener('focus', () => {
+ steerInputWasFocused = true;
syncPageChatFocusRing();
+ syncPageChatChrome();
});
pageChatInput.addEventListener('blur', () => {
@@ -8953,14 +9619,15 @@ void main() {
cancelInsertConfigure();
return;
}
+ teardownConfigureChrome();
hideHighlight();
- hideBar();
hideActionPicker();
selectedElement = null;
+ hoveredElement = null;
configureKind = 'replace';
- if (state === 'PICKING' || state === 'CONFIGURING') state = 'IDLE';
+ if (state === 'PICKING' || state === 'CONFIGURING') setLiveState('IDLE');
} else {
- if (state === 'IDLE') state = 'PICKING';
+ if (state === 'IDLE') setLiveState('PICKING');
}
syncPageChatFocus('toggle-pick');
}
@@ -8976,10 +9643,10 @@ void main() {
selectedElement = null;
configureKind = 'replace';
if (state === 'CONFIGURING') cancelInsertConfigure();
- else if (state === 'IDLE' || state === 'PICKING') state = 'PICKING';
+ else if (state === 'IDLE' || state === 'PICKING') setLiveState('PICKING');
} else {
clearInsertPicking();
- if (state === 'PICKING' && !pickActive) state = 'IDLE';
+ if (state === 'PICKING' && !pickActive) setLiveState('IDLE');
}
saveInteractionPrefs();
updateGlobalBarState();
@@ -9060,6 +9727,7 @@ void main() {
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; }
@@ -9073,7 +9741,8 @@ void main() {
window.removeEventListener('message', onDetectMessage);
// Remove detection overlays
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
- state = 'IDLE';
+ setLiveState('IDLE');
+ document.getElementById(PREFIX + '-pick-cursor-style')?.remove();
window.__IMPECCABLE_LIVE_INIT__ = false;
console.log('[impeccable] Live mode exited.');
}
@@ -10250,6 +10919,8 @@ void main() {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('init-complete');
}
diff --git a/.kiro/skills/impeccable/scripts/live/ui-core.mjs b/.kiro/skills/impeccable/scripts/live/ui-core.mjs
index 4b7adde8b..bbb2c5c35 100644
--- a/.kiro/skills/impeccable/scripts/live/ui-core.mjs
+++ b/.kiro/skills/impeccable/scripts/live/ui-core.mjs
@@ -42,9 +42,10 @@ export const LIVE_UI_SURFACES = Object.freeze([
'impeccable-live-highlight',
'impeccable-live-tooltip',
'impeccable-live-bar',
- 'impeccable-live-configure-input-wrap',
+ 'impeccable-live-selection-pill',
'impeccable-live-input',
'impeccable-live-configure-voice',
+ 'impeccable-live-configure-bar-tooltip',
],
states: ['rest', 'hover', 'focus-visible', 'pressed', 'disabled'],
},
diff --git a/.opencode/skills/impeccable/scripts/live-browser.js b/.opencode/skills/impeccable/scripts/live-browser.js
index 9092338eb..539cd07d9 100644
--- a/.opencode/skills/impeccable/scripts/live-browser.js
+++ b/.opencode/skills/impeccable/scripts/live-browser.js
@@ -57,6 +57,7 @@
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 PICK_CURSOR_CLASS = PREFIX + '-pick-cursor';
const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;
const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
prefix: PREFIX,
@@ -98,7 +99,7 @@
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'] },
{ key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] },
- { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-configure-input-wrap', PREFIX + '-input', PREFIX + '-configure-voice'] },
+ { 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'] },
@@ -131,6 +132,8 @@
let currentPreviewFile = null;
let currentPreviewMode = null;
let recoveryWaitingForAnchor = false;
+ let pickedAnchorSnapshot = null;
+ let pendingVariantAnchorRetryObserver = null;
let pendingAcceptedSession = null;
let variantObserver = null;
let variantSelectionInFlight = false;
@@ -283,31 +286,53 @@
uiAppend(tooltipEl);
}
+ function shouldShowHighlightTagTooltip() {
+ // Configure/edit carry the tag in the bar selection pill — 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 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);
+ 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' });
- tooltipEl.style.transition = 'none';
- Object.assign(tooltipEl.style, { top: tipY, left: tipX, 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(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });
}
}
@@ -1010,8 +1035,8 @@
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: '6px',
- maxWidth: '520px', minWidth: '320px',
+ padding: '5px',
+ maxWidth: '560px', minWidth: '340px',
});
uiAppend(barEl);
defangOutsideHandlers(barEl);
@@ -1019,14 +1044,29 @@
function positionBar() {
if (!barEl) return;
- const anchor = resolveBarAnchor();
- if (!anchor) return;
- const r = anchor.getBoundingClientRect();
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.
@@ -1054,8 +1094,14 @@
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
+ 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(() => {
@@ -1075,6 +1121,7 @@
setTimeout(() => { if (barEl && hideSeq === barHideSeq) barEl.style.display = 'none'; }, 250);
hideActionPicker();
closeTunePopover();
+ hideConfigureBarTooltip();
if (state === 'EDITING') restoreInlineEditDrafts();
disableInlineEdit();
}
@@ -1083,36 +1130,472 @@
if (!barEl || barEl.style.display === 'none') return;
if (mode === 'cycling' && !ensureCyclingRenderable('update-bar')) return;
barEl.innerHTML = '';
- // Reset bar styling to the kinpaku picker palette
- barEl.style.background = BP.surface;
- barEl.style.border = '1px solid ' + BP.border;
- barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
- else if (mode === 'saving') barEl.appendChild(buildSavingRow());
- else if (mode === 'confirmed') {
- barEl.appendChild(buildConfirmedRow());
- barEl.style.background = 'oklch(95% 0.05 145)';
- barEl.style.border = '1px solid oklch(75% 0.12 145 / 0.4)';
+ 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
+ // 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 =
+ '';
+
+ 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 wrap = uiGetById(PREFIX + '-configure-input-wrap');
- const input = uiGetById(PREFIX + '-input');
- if (!wrap || !input) return;
+ 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;
- wrap.dataset.inputFocused = focused ? 'true' : 'false';
- wrap.dataset.voiceListening = (voiceListening && voiceCtx?.mode === 'configure') ? 'true' : 'false';
- wrap.style.borderColor = (voiceListening && voiceCtx?.mode === 'configure')
+ 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.hairline);
+ : (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.02em',
+ }));
+ 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)
@@ -1434,14 +1917,38 @@
let pageInteractionCursorActive = false;
- /** Page-level cursor while insert mode is choosing a before/after edge. */
+ function ensurePickCursorStyle() {
+ if (document.getElementById(PREFIX + '-pick-cursor-style')) return;
+ const style = document.createElement('style');
+ style.id = PREFIX + '-pick-cursor-style';
+ style.textContent =
+ 'html.' + PICK_CURSOR_CLASS + ' * { cursor: crosshair !important; }\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"],\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"] * { cursor: revert !important; }';
+ // Styles the host page, not the chrome - inside the adapter's shadow UI
+ // root (uiAppendStyle's target) these selectors would match nothing.
+ document.head.appendChild(style);
+ }
+
+ /** Page-level cursor while pick or insert mode is targeting page elements. */
function syncPageInteractionCursor() {
- let next = '';
+ const pickCursor = state === 'PICKING' && pickActive && !insertActive;
+ let axisCursor = '';
if (state === 'PICKING' && insertActive) {
- next = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
+ axisCursor = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
}
- if (next) {
- document.documentElement.style.cursor = next;
+
+ if (pickCursor) {
+ ensurePickCursorStyle();
+ document.documentElement.classList.add(PICK_CURSOR_CLASS);
+ document.documentElement.style.cursor = '';
+ pageInteractionCursorActive = true;
+ return;
+ }
+
+ document.documentElement.classList.remove(PICK_CURSOR_CLASS);
+ if (axisCursor) {
+ document.documentElement.style.cursor = axisCursor;
pageInteractionCursorActive = true;
} else if (pageInteractionCursorActive) {
document.documentElement.style.cursor = '';
@@ -1449,6 +1956,16 @@
}
}
+ /**
+ * 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;
+ syncPageInteractionCursor();
+ }
+
/** Element used to position the floating bar / shader during a session. */
function resolveBarAnchor() {
if (svelteComponentSession?.sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
@@ -1767,69 +2284,35 @@
}
}
+ /** 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: 'center', gap: '6px',
+ display: 'flex', alignItems: 'stretch', width: '100%', height: CONFIGURE_BAR_H,
});
- // Action pill - dark graphite chip (matches kinpaku-kit .live-demo-ctx-pill)
- const pill = el('button', {
- display: 'inline-flex', alignItems: 'center', gap: '4px',
- padding: '5px 10px', borderRadius: '6px',
- background: BP.chatSurface, color: BP.text,
- fontFamily: FONT, fontSize: '12px', fontWeight: '500',
- border: '1px solid ' + BP.hairline, cursor: 'pointer',
- transition: 'background 0.12s ease, border-color 0.12s ease, transform 0.1s ease',
- whiteSpace: 'nowrap', flexShrink: '0',
- });
- pill.textContent = actionLabel() + ' \u25BE';
- pill.disabled = controlsLocked;
- pill.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- pill.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) pill.title = 'Apply is still running';
- pill.addEventListener('mouseenter', () => {
- if (controlsLocked) return;
- pill.style.background = BP.accentSoft;
- pill.style.borderColor = BP.accent;
- });
- pill.addEventListener('mouseleave', () => {
- if (controlsLocked) return;
- pill.style.background = BP.chatSurface;
- pill.style.borderColor = BP.hairline;
- });
- pill.addEventListener('mousedown', () => { if (!controlsLocked) pill.style.transform = 'scale(0.97)'; });
- pill.addEventListener('mouseup', () => pill.style.transform = 'scale(1)');
- pill.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleActionPicker();
- });
- row.appendChild(pill);
-
- // Prompt field - same chat-surface chrome as the bottom Steer bar
- const inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-configure-input-wrap';
+ const inputShell = el('div', configureInputShellStyle());
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
- input.placeholder = selectedAction === 'impeccable' ? 'describe what you want…' : 'refine further (optional)…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the change');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1837,33 +2320,31 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, background 0.12s ease',
+ const action = buildConfigureActionControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleActionPicker();
+ },
});
- voiceBtn.id = PREFIX + '-configure-voice';
- 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';
- if (!uiGetById(PREFIX + '-configure-input-style')) {
- 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::placeholder { color: ' + BP.textDim + '; opacity: 1; }' +
- '#' + PREFIX + '-configure-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 { animation: none; opacity: 1; } }' +
- '#' + PREFIX + '-configure-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
- uiAppendStyle(s);
- }
+ 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());
@@ -1873,81 +2354,33 @@
e.stopPropagation();
e.preventDefault();
input.blur();
- disableInlineEdit();
- hideBar();
- renderEditBadge('hidden');
- state = 'PICKING';
- syncPageChatFocus('configure-input-escape');
+ exitConfigureToPicking('configure-input-escape');
return;
}
- // Let arrow keys pass through to the element picker when the input is empty
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !input.value) return;
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ const voiceBtn = buildConfigureVoiceButton({
+ id: PREFIX + '-configure-voice',
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleConfigureVoice();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
+ const go = buildConfigureSubmitButton({
+ controlsLocked,
+ ariaLabel: 'Generate variants',
+ onClick: (e) => { e.stopPropagation(); handleGo(); },
+ });
+
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([action, count], voiceBtn, go));
syncConfigureInputChrome();
- // Variant count toggle
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, border-color 0.12s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.title = 'Variants: click to change';
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) count.title = 'Apply is still running';
- count.addEventListener('mouseenter', () => { if (!controlsLocked) { count.style.color = BP.text; count.style.borderColor = BP.text; } });
- count.addEventListener('mouseleave', () => { if (!controlsLocked) { count.style.color = BP.textDim; count.style.borderColor = BP.hairline; } });
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- // Go button
- const go = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- cursor: 'pointer',
- transition: 'filter 0.12s ease, transform 0.1s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- go.textContent = 'Go \u2192';
- go.disabled = controlsLocked;
- go.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- go.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) go.title = 'Apply is still running';
- go.addEventListener('mouseenter', () => { if (!controlsLocked) go.style.filter = 'brightness(1.1)'; });
- go.addEventListener('mouseleave', () => go.style.filter = 'none');
- go.addEventListener('mousedown', () => { if (!controlsLocked) go.style.transform = 'scale(0.97)'; });
- go.addEventListener('mouseup', () => go.style.transform = 'scale(1)');
- go.addEventListener('click', (e) => { e.stopPropagation(); handleGo(); });
- row.appendChild(go);
-
- // Auto-focus input after a beat
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
@@ -1956,34 +2389,20 @@
function buildInsertConfigureRow() {
const controlsLocked = pendingApplyInFlight === true;
const row = el('div', {
- display: 'flex', alignItems: 'center', gap: '6px',
+ 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 inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-insert-input-wrap';
- inputWrap.addEventListener('pointerdown', (e) => e.stopPropagation());
- inputWrap.addEventListener('mousedown', (e) => e.stopPropagation());
- inputWrap.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 = 'describe what to insert…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the new element');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1991,20 +2410,22 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
+ const count = buildConfigureCountControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ count.textContent = '\u00D7' + cycleSelectedCount();
+ if (count.matches(':hover')) {
+ showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));
+ }
+ },
});
- voiceBtn.id = PREFIX + '-insert-voice';
- 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';
+
+ inputShell.appendChild(buildSelectionPill({ el: selectedElement, controlsLocked }));
+ inputShell.appendChild(input);
+
+ ensureConfigureInputStyle();
input.addEventListener('input', () => syncInsertCreateButton());
input.addEventListener('pointerdown', (e) => e.stopPropagation());
@@ -2026,48 +2447,31 @@
}
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ 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();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
-
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer', flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- const create = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- flexShrink: '0', whiteSpace: 'nowrap',
+ 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.textContent = 'Create \u2192';
- create.disabled = controlsLocked;
create.addEventListener('mouseenter', () => {
if (controlsLocked) return;
if (isInsertCreateEnabled(create)) {
@@ -2077,15 +2481,10 @@
showInsertCreateTooltip(create, insertCreateDisabledReason(insertCreateGateState(input)));
});
create.addEventListener('mouseleave', hideInsertCreateTooltip);
- create.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- if (!isInsertCreateEnabled(create)) return;
- handleInsertCreate();
- });
- row.appendChild(create);
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([count], voiceBtn, create));
syncInsertCreateButton(create, input);
+ syncConfigureInputChrome();
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
}
@@ -2375,7 +2774,7 @@
position: 'fixed', zIndex: Z.picker,
display: 'none', opacity: '0',
transform: 'scale(0.96) translateY(4px)',
- transformOrigin: 'bottom left',
+ transformOrigin: 'bottom right',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1px solid ' + P.border,
@@ -2451,14 +2850,18 @@
chip.style.background = isActive ? P.accentSoft : 'transparent';
chip.style.color = isActive ? P.accent : P.text;
});
- // Position above the bar
+ // 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: barRect.left + 'px',
- display: 'block',
+ top: top + 'px',
+ left: left + 'px',
});
requestAnimationFrame(() => {
pickerEl.style.opacity = '1';
@@ -2941,7 +3344,7 @@
function enterEditingMode() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
- state = 'EDITING';
+ setLiveState('EDITING');
hideBar();
hideAnnotOverlay();
renderEditBadge('editing');
@@ -2974,7 +3377,7 @@
function cancelEditing() {
restoreInlineEditDrafts();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -2988,12 +3391,33 @@
hideAnnotOverlay();
clearAnnotations();
renderEditBadge('hidden');
- state = 'PICKING';
+ 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 ),
// climb to the nearest ancestor with one. The CLI uses tag+class together,
// so tag must come from the same node as the locator.
@@ -3215,7 +3639,7 @@
updatePendingCounter(stashResult.pendingCount || 0);
maybeShowFirstSaveToast();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -3895,6 +4319,12 @@
// Edit content badge - floating button at element top-right to enter EDITING mode
//
+ const EDIT_COPY_LABEL = 'Edit copy';
+ const EDIT_COPY_ICON =
+ '';
+
function usesShadowChromeRoot() {
const root = liveUiRoot();
return root && root !== document.body && root.host && root.host.id === PREFIX + '-root';
@@ -4045,7 +4475,7 @@
editBadgeProxyRoot.appendChild(proxy);
editBadgeProxyByTarget.set(target, proxy);
}
- proxy.title = target.title || target.textContent || 'Edit copy';
+ proxy.title = target.title || target.getAttribute('aria-label') || target.textContent || EDIT_COPY_LABEL;
styleEditBadgeProxy(proxy, target);
}
}
@@ -4085,13 +4515,16 @@
}
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, r.right - bw) + '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;
@@ -4129,16 +4562,36 @@
const disabled = mode === 'idle-disabled';
editBadgeEl.innerHTML = '';
const btn = document.createElement('button');
- btn.textContent = 'Edit copy';
- Object.assign(btn.style, calloutStyle(disabled ? MUTED : ACCENT, disabled ? HAIRLINE : ACCENT));
+ 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;
- btn.title = 'Edit copy is disabled while the current copy edit is applying';
+ 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', () => { btn.style.background = ACCENT; btn.style.color = PRIMARY_TEXT; });
- btn.addEventListener('mouseleave', () => { btn.style.background = SURFACE; btn.style.color = ACCENT; });
+ btn.addEventListener('mouseenter', () => showConfigureBarTooltip(btn, EDIT_COPY_LABEL));
+ btn.addEventListener('mouseleave', hideConfigureBarTooltip);
btn.onclick = enterEditingMode;
}
editBadgeEl.appendChild(btn);
@@ -4154,9 +4607,8 @@
cancel.onclick = cancelEditing;
const save = document.createElement('button');
save.textContent = 'Save';
- Object.assign(save.style, calloutStyle(ACCENT));
- save.addEventListener('mouseenter', () => { save.style.background = ACCENT; save.style.color = PRIMARY_TEXT; });
- save.addEventListener('mouseleave', () => { save.style.background = SURFACE; save.style.color = ACCENT; });
+ Object.assign(save.style, calloutStyle(PRIMARY_TEXT, ACCENT));
+ save.style.background = ACCENT;
save.onclick = applyEditing;
editBadgeEl.append(cancel, save);
}
@@ -4392,31 +4844,142 @@
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 = origContent.className;
- let liveEl = null;
+ const cls = normalizeElementClassName(origContent);
+ const candidates = [...document.getElementsByTagName(tag)];
+
if (origContent.id) {
- liveEl = document.getElementById(origContent.id);
- } else if (cls) {
- const candidates = [...document.getElementsByTagName(tag)];
- for (const c of candidates) {
- if (c.className === cls && !own(c)) { liveEl = c; break; }
- }
- if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
- if (expectedClasses.length > 0) {
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
- }
+ 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;
}
}
}
- return liveEl;
+
+ 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) {
@@ -4428,7 +4991,45 @@
const anchor = findInsertAnchorInDom();
if (anchor?.parentElement) return anchor;
}
- return findLiveElementForOriginalMarkup(manifest?.originalMarkup || manifest?.anchorMarkup || '');
+ 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);
+ }
}
function loadSvelteRuntime(runtimeModule) {
@@ -4595,7 +5196,7 @@
previewFile: manifestPath,
previewMode: 'svelte-component',
});
- if (state !== 'CYCLING') state = 'GENERATING';
+ if (state !== 'CYCLING') setLiveState('GENERATING');
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper && svelteComponentSession?.sessionId === sessionId) {
@@ -4605,7 +5206,7 @@
expectedVariants = arrivedVariants;
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants ? visibleVariant : 1;
await mountSvelteComponentVariant(visibleVariant || 1);
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
saveSession();
return;
@@ -4621,15 +5222,8 @@
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants
? visibleVariant
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
- showBar('generating');
- startScrollTracking();
- saveSession();
- queueCheckpoint('svelte_component_anchor_missing');
+ enterRecoveryWaitingForAnchor({ checkpointReason: 'svelte_component_anchor_missing', trackScroll: true });
waitForSvelteComponentTargetAndRetry({ manifestPath, sessionId, manifest });
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return;
}
@@ -4692,7 +5286,7 @@
}
selectedElement = mountTarget.firstElementChild || mountTarget;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -4741,6 +5335,7 @@
hideShaderOverlay();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
if (pendingSvelteComponentRetryObserver) { pendingSvelteComponentRetryObserver.disconnect(); pendingSvelteComponentRetryObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearSession();
clearHandled();
@@ -4750,7 +5345,7 @@
arrivedVariants = 0;
visibleVariant = 0;
selectedElement = null;
- state = 'PICKING';
+ setLiveState('PICKING');
hideBar();
if (message) showToast(message, 5000);
}
@@ -4799,21 +5394,26 @@
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
- const liveEl = findLiveElementForOriginalMarkup(origContent.outerHTML);
+ const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
- selectedElement = document.body;
- recoveryWaitingForAnchor = true;
- state = 'GENERATING';
- showBar('generating');
- saveSession();
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
+ 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.
@@ -4834,7 +5434,7 @@
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -5240,7 +5840,7 @@
if (expected > 0) expectedVariants = expected;
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
if (wrapper.dataset.impeccableMode === 'insert') finalizeInsertSession();
@@ -5326,7 +5926,8 @@
syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll();
restoreFromActiveSessions(msg.activeSessions, 'sse_connected');
- if (state === 'IDLE' && (pickActive || insertActive)) state = 'PICKING';
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('sse-connected');
break;
case 'agent_polling':
@@ -5352,7 +5953,7 @@
// Variants already arrived via HMR → normal transition.
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
if (state === 'GENERATING') {
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
disableInlineEdit();
refreshParamsPanel();
@@ -5402,7 +6003,7 @@
case 'error':
if (pendingAcceptedSession?.id && msg.id === pendingAcceptedSession.id) {
pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
updateBarContent('cycling');
showToast('Could not complete accept cleanup. Try Accept again.', 5000);
break;
@@ -5412,7 +6013,7 @@
showToast('Error: ' + msg.message, 5000);
hideBar();
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
break;
}
};
@@ -5450,7 +6051,7 @@
// transient disconnect as an explicit discard.
selectedElement = null;
selectedAction = 'impeccable';
- state = recoveryState;
+ setLiveState(recoveryState);
if (currentSessionId) saveSession();
}
@@ -5592,15 +6193,7 @@
&& !selectedElement.contains(e.target)
) {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- hideBar();
- stopScrollTracking();
- hideAnnotOverlay();
- clearAnnotations();
- renderEditBadge('hidden');
- state = 'PICKING';
- hoveredElement = null;
- hideHighlight();
- syncPageChatFocus('configure-outside-click');
+ exitConfigureToPicking('configure-outside-click', { clearHover: true });
return;
}
if (state === 'PICKING' && insertActive) {
@@ -5617,13 +6210,12 @@
hideInsertLine();
configureKind = 'insert';
selectedElement = placeholder;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
hideHighlight();
clearAnnotations();
showAnnotOverlay(placeholder);
showBar('configure');
startScrollTracking();
- syncPageInteractionCursor();
return;
}
if (state !== 'PICKING' || !pickActive) return;
@@ -5636,7 +6228,7 @@
e.preventDefault();
e.stopPropagation();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5721,6 +6313,16 @@
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;
@@ -5729,6 +6331,7 @@
deepActive
&& own(deepActive)
&& /^(INPUT|TEXTAREA|SELECT)$/.test(deepActive.tagName || '')
+ && !shouldPassthroughElementNav(deepActive, e)
) {
return;
}
@@ -5767,14 +6370,15 @@
if (state === 'EDITING') { cancelEditing(); return; }
if (state === 'CONFIGURING') {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- disableInlineEdit(); hideBar(); stopScrollTracking(); hideAnnotOverlay(); clearAnnotations(); renderEditBadge('hidden'); state = 'PICKING'; syncPageChatFocus('escape-from-configure'); 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(); state = 'IDLE'; }
+ else { hideHighlight(); setLiveState('IDLE'); }
return;
}
}
@@ -5798,7 +6402,7 @@
} else if (e.key === 'Enter') {
e.preventDefault();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5859,6 +6463,7 @@
// screenshot is uploaded (or capture fails - we still emit, just without
// screenshotPath).
const elForCapture = selectedElement;
+ pickedAnchorSnapshot = buildPickedAnchorSnapshot(elForCapture);
const captureRect = elForCapture.getBoundingClientRect();
const snapshot = {
comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })),
@@ -5879,7 +6484,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ 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
@@ -5904,7 +6509,7 @@
clearInsertPicking();
configureKind = 'replace';
selectedElement = null;
- state = insertActive ? 'PICKING' : 'IDLE';
+ setLiveState(insertActive ? 'PICKING' : 'IDLE');
hideHighlight();
syncPageChatFocus('insert-configure-cancel');
}
@@ -5954,7 +6559,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ setLiveState('GENERATING');
showBar('generating');
startScrollTracking();
saveSession();
@@ -6712,7 +7317,7 @@ void main() {
|| acceptWrapper?.dataset?.impeccablePreview === 'svelte-component';
const acceptedSnapshot = snapshotAcceptedVariantDom(acceptedSessionId, acceptedVariant);
- state = 'SAVING';
+ setLiveState('SAVING');
updateBarContent('saving');
pendingAcceptedSession = {
id: acceptedSessionId,
@@ -6727,7 +7332,7 @@ void main() {
.then(() => {})
.catch(() => {
if (pendingAcceptedSession?.id === acceptedSessionId) pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
showToast('Could not confirm accept with the live server. Session kept for recovery; try Accept again.', 5000);
});
@@ -6746,7 +7351,7 @@ void main() {
if (pending.isSvelteComponent) {
commitAcceptedSvelteComponentToDom(pending.id);
}
- state = 'CONFIRMED';
+ setLiveState('CONFIRMED');
updateBarContent('confirmed');
scheduleAcceptCleanup(pending);
return true;
@@ -6870,7 +7475,7 @@ void main() {
selectedAction = 'impeccable';
pendingAcceptedSession = null;
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
function commitAcceptedVariantToDom(sessionId, variantId) {
@@ -6922,6 +7527,7 @@ void main() {
currentPreviewFile = null;
currentPreviewMode = null;
recoveryWaitingForAnchor = false;
+ pickedAnchorSnapshot = null;
}
function rememberSessionFileMeta(meta = {}) {
@@ -6946,6 +7552,7 @@ void main() {
if (!saved) return;
rememberSessionFileMeta(saved);
if (saved.insertPlaceholder) insertPlaceholderSnapshot = saved.insertPlaceholder;
+ if (saved.pickedAnchor) pickedAnchorSnapshot = saved.pickedAnchor;
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
if (saved.previewMode) currentPreviewMode = saved.previewMode;
@@ -7012,9 +7619,10 @@ void main() {
|| clampVariantIndex(serverSession?.visibleVariant, arrivedVariants || expectedVariants)
|| (arrivedVariants > 0 ? 1 : 0);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
+ const restoredAnchor = findLiveElementFromAnchorSnapshot(pickedAnchorSnapshot);
+ selectedElement = restoredAnchor || document.body;
+ setLiveState('GENERATING');
+ recoveryWaitingForAnchor = !restoredAnchor;
showBar('generating');
startScrollTracking();
if (variantObserver) variantObserver.disconnect();
@@ -7030,7 +7638,6 @@ void main() {
return true;
}
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return true;
}
@@ -7059,6 +7666,7 @@ void main() {
pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues },
insertPlaceholder: insertPlaceholderSnapshot || undefined,
+ pickedAnchor: pickedAnchorSnapshot || undefined,
});
}
@@ -7118,6 +7726,7 @@ void main() {
hideHighlight();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearScrollY();
finalizeInsertSession();
@@ -7127,15 +7736,21 @@ void main() {
currentSessionId = null;
selectedAction = 'impeccable';
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
//
// Toast
//
+ function dismissToast() {
+ if (!toastEl) return;
+ toastEl.remove();
+ toastEl = null;
+ }
+
function showToast(message, duration) {
- if (toastEl) toastEl.remove();
+ 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
@@ -7224,7 +7839,7 @@ void main() {
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
selectedElement = resolveSvelteComponentAnchor()
|| wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
hideShaderOverlay();
showBar('cycling');
startScrollTracking();
@@ -7260,7 +7875,7 @@ void main() {
const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
const origEl = pickVariantContent(wrapper, 'original');
- state = resumedState;
+ setLiveState(resumedState);
if (isInsert && resumedState === 'GENERATING' && arrivedVariants === 0) {
selectedElement = ensureInsertPlaceholder() || findInsertAnchorInDom() || wrapper;
} else {
@@ -7419,6 +8034,8 @@ void main() {
let voiceCtx = null;
const PAGE_CHAT_COLLAPSED_W = '88px';
const PAGE_CHAT_PROCESSING_W = '76px';
+ 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(56% 0.032 82 / 0.78)';
@@ -7509,13 +8126,16 @@ void main() {
function syncPageChatChrome() {
if (!pageChatEl) return;
const P = pageChatPalette();
+ const inputFocused = pageChatInput && activeElementDeep() === pageChatInput;
pageChatEl.style.background = P.chatSurface;
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
+ 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 : P.textDim;
+ 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';
@@ -7686,24 +8306,44 @@ void main() {
function syncPageChatFocusRing() {
if (!pageChatEl || !pageChatInput) return;
const focused = activeElementDeep() === pageChatInput;
+ const typingReady = focused && !steerLocked;
pageChatEl.dataset.inputFocused = focused ? 'true' : 'false';
- const P = pageChatPalette();
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
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.color = steerLocked
- ? P.patinaPale
- : ((!pageChatExpanded && focused) ? P.patinaPale : P.textDim);
- }
- if (!pageChatExpanded) {
- pageChatInput.style.width = '0';
- pageChatInput.style.padding = '0';
- pageChatInput.style.opacity = '0';
- pageChatInput.style.pointerEvents = focused ? 'auto' : 'none';
- if (pageChatHint) pageChatHint.style.visibility = '';
+ 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) {
@@ -7723,6 +8363,7 @@ void main() {
try { window.focus(); } catch { /* embed may block */ }
try { pageChatInput.focus({ preventScroll: true }); } catch { pageChatInput.focus(); }
syncPageChatFocusRing();
+ syncPageChatChrome();
steerFocusLog('focusSteerChat result', {
reason,
before: steerFocusTargetLabel(before),
@@ -7769,6 +8410,7 @@ void main() {
pageChatEl.dataset.expanded = 'true';
pageChatEl.style.width = PAGE_CHAT_EXPANDED_W;
pageChatEl.style.cursor = steerLocked ? 'default' : 'text';
+ pageChatInput.placeholder = PAGE_CHAT_PLACEHOLDER_EXPANDED;
if (pageChatHint) {
pageChatHint.style.display = 'none';
pageChatHint.style.opacity = '0';
@@ -7780,6 +8422,20 @@ void main() {
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(); }
@@ -7938,7 +8594,9 @@ void main() {
if (pageChatEl) pageChatEl.dataset.voiceListening = listening ? 'true' : 'false';
syncPageChatChrome();
} else if (voiceCtx?.mode === 'configure') {
- const voiceBtn = uiGetById(PREFIX + '-configure-voice');
+ // 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';
@@ -8180,12 +8838,12 @@ void main() {
height: '28px', margin: '0 4px 0 ' + (GLOBAL_BAR_SECTION_GAP - GLOBAL_BAR_INNER_GAP) + 'px',
borderRadius: '7px',
background: P.chatSurface,
- border: '1px solid ' + P.hairline,
+ border: '1px solid transparent',
overflow: 'hidden',
cursor: 'pointer',
flexShrink: '0',
width: PAGE_CHAT_COLLAPSED_W,
- transition: 'border-color 0.15s ease',
+ transition: 'width 0.18s ease, border-color 0.15s ease',
});
pageChatEl.id = PREFIX + '-page-chat';
pageChatEl.dataset.expanded = 'false';
@@ -8211,13 +8869,14 @@ void main() {
pageChatInput = document.createElement('input');
pageChatInput.id = PREFIX + '-page-chat-input';
pageChatInput.type = 'text';
- pageChatInput.placeholder = 'Steer the page…';
+ 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',
});
@@ -8252,18 +8911,23 @@ void main() {
'#' + 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(63% 0.024 82); 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.024 82); }' +
'#' + PREFIX + '-page-chat-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
uiAppendStyle(s);
}
- pageChatEl.addEventListener('pointerdown', keepSteerPointerInside);
+ pageChatEl.addEventListener('pointerdown', (e) => {
+ keepSteerPointerInside(e);
+ if (steerLocked || pageChatVoiceBtn.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)) return;
- expandPageChat({ focus: false });
- focusPageChatInput('page-chat-click');
+ armPageChatForTyping({ expand: true, focus: true });
});
pageChatVoiceBtn.addEventListener('pointerdown', keepSteerPointerInside);
@@ -8286,7 +8950,9 @@ void main() {
});
pageChatInput.addEventListener('focus', () => {
+ steerInputWasFocused = true;
syncPageChatFocusRing();
+ syncPageChatChrome();
});
pageChatInput.addEventListener('blur', () => {
@@ -8953,14 +9619,15 @@ void main() {
cancelInsertConfigure();
return;
}
+ teardownConfigureChrome();
hideHighlight();
- hideBar();
hideActionPicker();
selectedElement = null;
+ hoveredElement = null;
configureKind = 'replace';
- if (state === 'PICKING' || state === 'CONFIGURING') state = 'IDLE';
+ if (state === 'PICKING' || state === 'CONFIGURING') setLiveState('IDLE');
} else {
- if (state === 'IDLE') state = 'PICKING';
+ if (state === 'IDLE') setLiveState('PICKING');
}
syncPageChatFocus('toggle-pick');
}
@@ -8976,10 +9643,10 @@ void main() {
selectedElement = null;
configureKind = 'replace';
if (state === 'CONFIGURING') cancelInsertConfigure();
- else if (state === 'IDLE' || state === 'PICKING') state = 'PICKING';
+ else if (state === 'IDLE' || state === 'PICKING') setLiveState('PICKING');
} else {
clearInsertPicking();
- if (state === 'PICKING' && !pickActive) state = 'IDLE';
+ if (state === 'PICKING' && !pickActive) setLiveState('IDLE');
}
saveInteractionPrefs();
updateGlobalBarState();
@@ -9060,6 +9727,7 @@ void main() {
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; }
@@ -9073,7 +9741,8 @@ void main() {
window.removeEventListener('message', onDetectMessage);
// Remove detection overlays
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
- state = 'IDLE';
+ setLiveState('IDLE');
+ document.getElementById(PREFIX + '-pick-cursor-style')?.remove();
window.__IMPECCABLE_LIVE_INIT__ = false;
console.log('[impeccable] Live mode exited.');
}
@@ -10250,6 +10919,8 @@ void main() {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('init-complete');
}
diff --git a/.opencode/skills/impeccable/scripts/live/ui-core.mjs b/.opencode/skills/impeccable/scripts/live/ui-core.mjs
index 4b7adde8b..bbb2c5c35 100644
--- a/.opencode/skills/impeccable/scripts/live/ui-core.mjs
+++ b/.opencode/skills/impeccable/scripts/live/ui-core.mjs
@@ -42,9 +42,10 @@ export const LIVE_UI_SURFACES = Object.freeze([
'impeccable-live-highlight',
'impeccable-live-tooltip',
'impeccable-live-bar',
- 'impeccable-live-configure-input-wrap',
+ 'impeccable-live-selection-pill',
'impeccable-live-input',
'impeccable-live-configure-voice',
+ 'impeccable-live-configure-bar-tooltip',
],
states: ['rest', 'hover', 'focus-visible', 'pressed', 'disabled'],
},
diff --git a/.pi/skills/impeccable/scripts/live-browser.js b/.pi/skills/impeccable/scripts/live-browser.js
index 9092338eb..539cd07d9 100644
--- a/.pi/skills/impeccable/scripts/live-browser.js
+++ b/.pi/skills/impeccable/scripts/live-browser.js
@@ -57,6 +57,7 @@
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 PICK_CURSOR_CLASS = PREFIX + '-pick-cursor';
const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;
const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
prefix: PREFIX,
@@ -98,7 +99,7 @@
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'] },
{ key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] },
- { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-configure-input-wrap', PREFIX + '-input', PREFIX + '-configure-voice'] },
+ { 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'] },
@@ -131,6 +132,8 @@
let currentPreviewFile = null;
let currentPreviewMode = null;
let recoveryWaitingForAnchor = false;
+ let pickedAnchorSnapshot = null;
+ let pendingVariantAnchorRetryObserver = null;
let pendingAcceptedSession = null;
let variantObserver = null;
let variantSelectionInFlight = false;
@@ -283,31 +286,53 @@
uiAppend(tooltipEl);
}
+ function shouldShowHighlightTagTooltip() {
+ // Configure/edit carry the tag in the bar selection pill — 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 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);
+ 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' });
- tooltipEl.style.transition = 'none';
- Object.assign(tooltipEl.style, { top: tipY, left: tipX, 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(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });
}
}
@@ -1010,8 +1035,8 @@
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: '6px',
- maxWidth: '520px', minWidth: '320px',
+ padding: '5px',
+ maxWidth: '560px', minWidth: '340px',
});
uiAppend(barEl);
defangOutsideHandlers(barEl);
@@ -1019,14 +1044,29 @@
function positionBar() {
if (!barEl) return;
- const anchor = resolveBarAnchor();
- if (!anchor) return;
- const r = anchor.getBoundingClientRect();
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.
@@ -1054,8 +1094,14 @@
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
+ 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(() => {
@@ -1075,6 +1121,7 @@
setTimeout(() => { if (barEl && hideSeq === barHideSeq) barEl.style.display = 'none'; }, 250);
hideActionPicker();
closeTunePopover();
+ hideConfigureBarTooltip();
if (state === 'EDITING') restoreInlineEditDrafts();
disableInlineEdit();
}
@@ -1083,36 +1130,472 @@
if (!barEl || barEl.style.display === 'none') return;
if (mode === 'cycling' && !ensureCyclingRenderable('update-bar')) return;
barEl.innerHTML = '';
- // Reset bar styling to the kinpaku picker palette
- barEl.style.background = BP.surface;
- barEl.style.border = '1px solid ' + BP.border;
- barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
- else if (mode === 'saving') barEl.appendChild(buildSavingRow());
- else if (mode === 'confirmed') {
- barEl.appendChild(buildConfirmedRow());
- barEl.style.background = 'oklch(95% 0.05 145)';
- barEl.style.border = '1px solid oklch(75% 0.12 145 / 0.4)';
+ 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
+ // 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 =
+ '';
+
+ 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 wrap = uiGetById(PREFIX + '-configure-input-wrap');
- const input = uiGetById(PREFIX + '-input');
- if (!wrap || !input) return;
+ 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;
- wrap.dataset.inputFocused = focused ? 'true' : 'false';
- wrap.dataset.voiceListening = (voiceListening && voiceCtx?.mode === 'configure') ? 'true' : 'false';
- wrap.style.borderColor = (voiceListening && voiceCtx?.mode === 'configure')
+ 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.hairline);
+ : (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.02em',
+ }));
+ 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)
@@ -1434,14 +1917,38 @@
let pageInteractionCursorActive = false;
- /** Page-level cursor while insert mode is choosing a before/after edge. */
+ function ensurePickCursorStyle() {
+ if (document.getElementById(PREFIX + '-pick-cursor-style')) return;
+ const style = document.createElement('style');
+ style.id = PREFIX + '-pick-cursor-style';
+ style.textContent =
+ 'html.' + PICK_CURSOR_CLASS + ' * { cursor: crosshair !important; }\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"],\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"] * { cursor: revert !important; }';
+ // Styles the host page, not the chrome - inside the adapter's shadow UI
+ // root (uiAppendStyle's target) these selectors would match nothing.
+ document.head.appendChild(style);
+ }
+
+ /** Page-level cursor while pick or insert mode is targeting page elements. */
function syncPageInteractionCursor() {
- let next = '';
+ const pickCursor = state === 'PICKING' && pickActive && !insertActive;
+ let axisCursor = '';
if (state === 'PICKING' && insertActive) {
- next = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
+ axisCursor = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
}
- if (next) {
- document.documentElement.style.cursor = next;
+
+ if (pickCursor) {
+ ensurePickCursorStyle();
+ document.documentElement.classList.add(PICK_CURSOR_CLASS);
+ document.documentElement.style.cursor = '';
+ pageInteractionCursorActive = true;
+ return;
+ }
+
+ document.documentElement.classList.remove(PICK_CURSOR_CLASS);
+ if (axisCursor) {
+ document.documentElement.style.cursor = axisCursor;
pageInteractionCursorActive = true;
} else if (pageInteractionCursorActive) {
document.documentElement.style.cursor = '';
@@ -1449,6 +1956,16 @@
}
}
+ /**
+ * 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;
+ syncPageInteractionCursor();
+ }
+
/** Element used to position the floating bar / shader during a session. */
function resolveBarAnchor() {
if (svelteComponentSession?.sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
@@ -1767,69 +2284,35 @@
}
}
+ /** 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: 'center', gap: '6px',
+ display: 'flex', alignItems: 'stretch', width: '100%', height: CONFIGURE_BAR_H,
});
- // Action pill - dark graphite chip (matches kinpaku-kit .live-demo-ctx-pill)
- const pill = el('button', {
- display: 'inline-flex', alignItems: 'center', gap: '4px',
- padding: '5px 10px', borderRadius: '6px',
- background: BP.chatSurface, color: BP.text,
- fontFamily: FONT, fontSize: '12px', fontWeight: '500',
- border: '1px solid ' + BP.hairline, cursor: 'pointer',
- transition: 'background 0.12s ease, border-color 0.12s ease, transform 0.1s ease',
- whiteSpace: 'nowrap', flexShrink: '0',
- });
- pill.textContent = actionLabel() + ' \u25BE';
- pill.disabled = controlsLocked;
- pill.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- pill.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) pill.title = 'Apply is still running';
- pill.addEventListener('mouseenter', () => {
- if (controlsLocked) return;
- pill.style.background = BP.accentSoft;
- pill.style.borderColor = BP.accent;
- });
- pill.addEventListener('mouseleave', () => {
- if (controlsLocked) return;
- pill.style.background = BP.chatSurface;
- pill.style.borderColor = BP.hairline;
- });
- pill.addEventListener('mousedown', () => { if (!controlsLocked) pill.style.transform = 'scale(0.97)'; });
- pill.addEventListener('mouseup', () => pill.style.transform = 'scale(1)');
- pill.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleActionPicker();
- });
- row.appendChild(pill);
-
- // Prompt field - same chat-surface chrome as the bottom Steer bar
- const inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-configure-input-wrap';
+ const inputShell = el('div', configureInputShellStyle());
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
- input.placeholder = selectedAction === 'impeccable' ? 'describe what you want…' : 'refine further (optional)…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the change');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1837,33 +2320,31 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, background 0.12s ease',
+ const action = buildConfigureActionControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleActionPicker();
+ },
});
- voiceBtn.id = PREFIX + '-configure-voice';
- 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';
- if (!uiGetById(PREFIX + '-configure-input-style')) {
- 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::placeholder { color: ' + BP.textDim + '; opacity: 1; }' +
- '#' + PREFIX + '-configure-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 { animation: none; opacity: 1; } }' +
- '#' + PREFIX + '-configure-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
- uiAppendStyle(s);
- }
+ 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());
@@ -1873,81 +2354,33 @@
e.stopPropagation();
e.preventDefault();
input.blur();
- disableInlineEdit();
- hideBar();
- renderEditBadge('hidden');
- state = 'PICKING';
- syncPageChatFocus('configure-input-escape');
+ exitConfigureToPicking('configure-input-escape');
return;
}
- // Let arrow keys pass through to the element picker when the input is empty
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !input.value) return;
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ const voiceBtn = buildConfigureVoiceButton({
+ id: PREFIX + '-configure-voice',
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleConfigureVoice();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
+ const go = buildConfigureSubmitButton({
+ controlsLocked,
+ ariaLabel: 'Generate variants',
+ onClick: (e) => { e.stopPropagation(); handleGo(); },
+ });
+
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([action, count], voiceBtn, go));
syncConfigureInputChrome();
- // Variant count toggle
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, border-color 0.12s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.title = 'Variants: click to change';
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) count.title = 'Apply is still running';
- count.addEventListener('mouseenter', () => { if (!controlsLocked) { count.style.color = BP.text; count.style.borderColor = BP.text; } });
- count.addEventListener('mouseleave', () => { if (!controlsLocked) { count.style.color = BP.textDim; count.style.borderColor = BP.hairline; } });
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- // Go button
- const go = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- cursor: 'pointer',
- transition: 'filter 0.12s ease, transform 0.1s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- go.textContent = 'Go \u2192';
- go.disabled = controlsLocked;
- go.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- go.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) go.title = 'Apply is still running';
- go.addEventListener('mouseenter', () => { if (!controlsLocked) go.style.filter = 'brightness(1.1)'; });
- go.addEventListener('mouseleave', () => go.style.filter = 'none');
- go.addEventListener('mousedown', () => { if (!controlsLocked) go.style.transform = 'scale(0.97)'; });
- go.addEventListener('mouseup', () => go.style.transform = 'scale(1)');
- go.addEventListener('click', (e) => { e.stopPropagation(); handleGo(); });
- row.appendChild(go);
-
- // Auto-focus input after a beat
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
@@ -1956,34 +2389,20 @@
function buildInsertConfigureRow() {
const controlsLocked = pendingApplyInFlight === true;
const row = el('div', {
- display: 'flex', alignItems: 'center', gap: '6px',
+ 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 inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-insert-input-wrap';
- inputWrap.addEventListener('pointerdown', (e) => e.stopPropagation());
- inputWrap.addEventListener('mousedown', (e) => e.stopPropagation());
- inputWrap.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 = 'describe what to insert…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the new element');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1991,20 +2410,22 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
+ const count = buildConfigureCountControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ count.textContent = '\u00D7' + cycleSelectedCount();
+ if (count.matches(':hover')) {
+ showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));
+ }
+ },
});
- voiceBtn.id = PREFIX + '-insert-voice';
- 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';
+
+ inputShell.appendChild(buildSelectionPill({ el: selectedElement, controlsLocked }));
+ inputShell.appendChild(input);
+
+ ensureConfigureInputStyle();
input.addEventListener('input', () => syncInsertCreateButton());
input.addEventListener('pointerdown', (e) => e.stopPropagation());
@@ -2026,48 +2447,31 @@
}
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ 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();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
-
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer', flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- const create = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- flexShrink: '0', whiteSpace: 'nowrap',
+ 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.textContent = 'Create \u2192';
- create.disabled = controlsLocked;
create.addEventListener('mouseenter', () => {
if (controlsLocked) return;
if (isInsertCreateEnabled(create)) {
@@ -2077,15 +2481,10 @@
showInsertCreateTooltip(create, insertCreateDisabledReason(insertCreateGateState(input)));
});
create.addEventListener('mouseleave', hideInsertCreateTooltip);
- create.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- if (!isInsertCreateEnabled(create)) return;
- handleInsertCreate();
- });
- row.appendChild(create);
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([count], voiceBtn, create));
syncInsertCreateButton(create, input);
+ syncConfigureInputChrome();
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
}
@@ -2375,7 +2774,7 @@
position: 'fixed', zIndex: Z.picker,
display: 'none', opacity: '0',
transform: 'scale(0.96) translateY(4px)',
- transformOrigin: 'bottom left',
+ transformOrigin: 'bottom right',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1px solid ' + P.border,
@@ -2451,14 +2850,18 @@
chip.style.background = isActive ? P.accentSoft : 'transparent';
chip.style.color = isActive ? P.accent : P.text;
});
- // Position above the bar
+ // 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: barRect.left + 'px',
- display: 'block',
+ top: top + 'px',
+ left: left + 'px',
});
requestAnimationFrame(() => {
pickerEl.style.opacity = '1';
@@ -2941,7 +3344,7 @@
function enterEditingMode() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
- state = 'EDITING';
+ setLiveState('EDITING');
hideBar();
hideAnnotOverlay();
renderEditBadge('editing');
@@ -2974,7 +3377,7 @@
function cancelEditing() {
restoreInlineEditDrafts();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -2988,12 +3391,33 @@
hideAnnotOverlay();
clearAnnotations();
renderEditBadge('hidden');
- state = 'PICKING';
+ 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 ),
// climb to the nearest ancestor with one. The CLI uses tag+class together,
// so tag must come from the same node as the locator.
@@ -3215,7 +3639,7 @@
updatePendingCounter(stashResult.pendingCount || 0);
maybeShowFirstSaveToast();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -3895,6 +4319,12 @@
// Edit content badge - floating button at element top-right to enter EDITING mode
//
+ const EDIT_COPY_LABEL = 'Edit copy';
+ const EDIT_COPY_ICON =
+ '';
+
function usesShadowChromeRoot() {
const root = liveUiRoot();
return root && root !== document.body && root.host && root.host.id === PREFIX + '-root';
@@ -4045,7 +4475,7 @@
editBadgeProxyRoot.appendChild(proxy);
editBadgeProxyByTarget.set(target, proxy);
}
- proxy.title = target.title || target.textContent || 'Edit copy';
+ proxy.title = target.title || target.getAttribute('aria-label') || target.textContent || EDIT_COPY_LABEL;
styleEditBadgeProxy(proxy, target);
}
}
@@ -4085,13 +4515,16 @@
}
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, r.right - bw) + '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;
@@ -4129,16 +4562,36 @@
const disabled = mode === 'idle-disabled';
editBadgeEl.innerHTML = '';
const btn = document.createElement('button');
- btn.textContent = 'Edit copy';
- Object.assign(btn.style, calloutStyle(disabled ? MUTED : ACCENT, disabled ? HAIRLINE : ACCENT));
+ 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;
- btn.title = 'Edit copy is disabled while the current copy edit is applying';
+ 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', () => { btn.style.background = ACCENT; btn.style.color = PRIMARY_TEXT; });
- btn.addEventListener('mouseleave', () => { btn.style.background = SURFACE; btn.style.color = ACCENT; });
+ btn.addEventListener('mouseenter', () => showConfigureBarTooltip(btn, EDIT_COPY_LABEL));
+ btn.addEventListener('mouseleave', hideConfigureBarTooltip);
btn.onclick = enterEditingMode;
}
editBadgeEl.appendChild(btn);
@@ -4154,9 +4607,8 @@
cancel.onclick = cancelEditing;
const save = document.createElement('button');
save.textContent = 'Save';
- Object.assign(save.style, calloutStyle(ACCENT));
- save.addEventListener('mouseenter', () => { save.style.background = ACCENT; save.style.color = PRIMARY_TEXT; });
- save.addEventListener('mouseleave', () => { save.style.background = SURFACE; save.style.color = ACCENT; });
+ Object.assign(save.style, calloutStyle(PRIMARY_TEXT, ACCENT));
+ save.style.background = ACCENT;
save.onclick = applyEditing;
editBadgeEl.append(cancel, save);
}
@@ -4392,31 +4844,142 @@
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 = origContent.className;
- let liveEl = null;
+ const cls = normalizeElementClassName(origContent);
+ const candidates = [...document.getElementsByTagName(tag)];
+
if (origContent.id) {
- liveEl = document.getElementById(origContent.id);
- } else if (cls) {
- const candidates = [...document.getElementsByTagName(tag)];
- for (const c of candidates) {
- if (c.className === cls && !own(c)) { liveEl = c; break; }
- }
- if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
- if (expectedClasses.length > 0) {
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
- }
+ 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;
}
}
}
- return liveEl;
+
+ 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) {
@@ -4428,7 +4991,45 @@
const anchor = findInsertAnchorInDom();
if (anchor?.parentElement) return anchor;
}
- return findLiveElementForOriginalMarkup(manifest?.originalMarkup || manifest?.anchorMarkup || '');
+ 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);
+ }
}
function loadSvelteRuntime(runtimeModule) {
@@ -4595,7 +5196,7 @@
previewFile: manifestPath,
previewMode: 'svelte-component',
});
- if (state !== 'CYCLING') state = 'GENERATING';
+ if (state !== 'CYCLING') setLiveState('GENERATING');
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper && svelteComponentSession?.sessionId === sessionId) {
@@ -4605,7 +5206,7 @@
expectedVariants = arrivedVariants;
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants ? visibleVariant : 1;
await mountSvelteComponentVariant(visibleVariant || 1);
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
saveSession();
return;
@@ -4621,15 +5222,8 @@
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants
? visibleVariant
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
- showBar('generating');
- startScrollTracking();
- saveSession();
- queueCheckpoint('svelte_component_anchor_missing');
+ enterRecoveryWaitingForAnchor({ checkpointReason: 'svelte_component_anchor_missing', trackScroll: true });
waitForSvelteComponentTargetAndRetry({ manifestPath, sessionId, manifest });
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return;
}
@@ -4692,7 +5286,7 @@
}
selectedElement = mountTarget.firstElementChild || mountTarget;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -4741,6 +5335,7 @@
hideShaderOverlay();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
if (pendingSvelteComponentRetryObserver) { pendingSvelteComponentRetryObserver.disconnect(); pendingSvelteComponentRetryObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearSession();
clearHandled();
@@ -4750,7 +5345,7 @@
arrivedVariants = 0;
visibleVariant = 0;
selectedElement = null;
- state = 'PICKING';
+ setLiveState('PICKING');
hideBar();
if (message) showToast(message, 5000);
}
@@ -4799,21 +5394,26 @@
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
- const liveEl = findLiveElementForOriginalMarkup(origContent.outerHTML);
+ const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
- selectedElement = document.body;
- recoveryWaitingForAnchor = true;
- state = 'GENERATING';
- showBar('generating');
- saveSession();
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
+ 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.
@@ -4834,7 +5434,7 @@
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -5240,7 +5840,7 @@
if (expected > 0) expectedVariants = expected;
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
if (wrapper.dataset.impeccableMode === 'insert') finalizeInsertSession();
@@ -5326,7 +5926,8 @@
syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll();
restoreFromActiveSessions(msg.activeSessions, 'sse_connected');
- if (state === 'IDLE' && (pickActive || insertActive)) state = 'PICKING';
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('sse-connected');
break;
case 'agent_polling':
@@ -5352,7 +5953,7 @@
// Variants already arrived via HMR → normal transition.
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
if (state === 'GENERATING') {
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
disableInlineEdit();
refreshParamsPanel();
@@ -5402,7 +6003,7 @@
case 'error':
if (pendingAcceptedSession?.id && msg.id === pendingAcceptedSession.id) {
pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
updateBarContent('cycling');
showToast('Could not complete accept cleanup. Try Accept again.', 5000);
break;
@@ -5412,7 +6013,7 @@
showToast('Error: ' + msg.message, 5000);
hideBar();
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
break;
}
};
@@ -5450,7 +6051,7 @@
// transient disconnect as an explicit discard.
selectedElement = null;
selectedAction = 'impeccable';
- state = recoveryState;
+ setLiveState(recoveryState);
if (currentSessionId) saveSession();
}
@@ -5592,15 +6193,7 @@
&& !selectedElement.contains(e.target)
) {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- hideBar();
- stopScrollTracking();
- hideAnnotOverlay();
- clearAnnotations();
- renderEditBadge('hidden');
- state = 'PICKING';
- hoveredElement = null;
- hideHighlight();
- syncPageChatFocus('configure-outside-click');
+ exitConfigureToPicking('configure-outside-click', { clearHover: true });
return;
}
if (state === 'PICKING' && insertActive) {
@@ -5617,13 +6210,12 @@
hideInsertLine();
configureKind = 'insert';
selectedElement = placeholder;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
hideHighlight();
clearAnnotations();
showAnnotOverlay(placeholder);
showBar('configure');
startScrollTracking();
- syncPageInteractionCursor();
return;
}
if (state !== 'PICKING' || !pickActive) return;
@@ -5636,7 +6228,7 @@
e.preventDefault();
e.stopPropagation();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5721,6 +6313,16 @@
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;
@@ -5729,6 +6331,7 @@
deepActive
&& own(deepActive)
&& /^(INPUT|TEXTAREA|SELECT)$/.test(deepActive.tagName || '')
+ && !shouldPassthroughElementNav(deepActive, e)
) {
return;
}
@@ -5767,14 +6370,15 @@
if (state === 'EDITING') { cancelEditing(); return; }
if (state === 'CONFIGURING') {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- disableInlineEdit(); hideBar(); stopScrollTracking(); hideAnnotOverlay(); clearAnnotations(); renderEditBadge('hidden'); state = 'PICKING'; syncPageChatFocus('escape-from-configure'); 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(); state = 'IDLE'; }
+ else { hideHighlight(); setLiveState('IDLE'); }
return;
}
}
@@ -5798,7 +6402,7 @@
} else if (e.key === 'Enter') {
e.preventDefault();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5859,6 +6463,7 @@
// screenshot is uploaded (or capture fails - we still emit, just without
// screenshotPath).
const elForCapture = selectedElement;
+ pickedAnchorSnapshot = buildPickedAnchorSnapshot(elForCapture);
const captureRect = elForCapture.getBoundingClientRect();
const snapshot = {
comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })),
@@ -5879,7 +6484,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ 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
@@ -5904,7 +6509,7 @@
clearInsertPicking();
configureKind = 'replace';
selectedElement = null;
- state = insertActive ? 'PICKING' : 'IDLE';
+ setLiveState(insertActive ? 'PICKING' : 'IDLE');
hideHighlight();
syncPageChatFocus('insert-configure-cancel');
}
@@ -5954,7 +6559,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ setLiveState('GENERATING');
showBar('generating');
startScrollTracking();
saveSession();
@@ -6712,7 +7317,7 @@ void main() {
|| acceptWrapper?.dataset?.impeccablePreview === 'svelte-component';
const acceptedSnapshot = snapshotAcceptedVariantDom(acceptedSessionId, acceptedVariant);
- state = 'SAVING';
+ setLiveState('SAVING');
updateBarContent('saving');
pendingAcceptedSession = {
id: acceptedSessionId,
@@ -6727,7 +7332,7 @@ void main() {
.then(() => {})
.catch(() => {
if (pendingAcceptedSession?.id === acceptedSessionId) pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
showToast('Could not confirm accept with the live server. Session kept for recovery; try Accept again.', 5000);
});
@@ -6746,7 +7351,7 @@ void main() {
if (pending.isSvelteComponent) {
commitAcceptedSvelteComponentToDom(pending.id);
}
- state = 'CONFIRMED';
+ setLiveState('CONFIRMED');
updateBarContent('confirmed');
scheduleAcceptCleanup(pending);
return true;
@@ -6870,7 +7475,7 @@ void main() {
selectedAction = 'impeccable';
pendingAcceptedSession = null;
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
function commitAcceptedVariantToDom(sessionId, variantId) {
@@ -6922,6 +7527,7 @@ void main() {
currentPreviewFile = null;
currentPreviewMode = null;
recoveryWaitingForAnchor = false;
+ pickedAnchorSnapshot = null;
}
function rememberSessionFileMeta(meta = {}) {
@@ -6946,6 +7552,7 @@ void main() {
if (!saved) return;
rememberSessionFileMeta(saved);
if (saved.insertPlaceholder) insertPlaceholderSnapshot = saved.insertPlaceholder;
+ if (saved.pickedAnchor) pickedAnchorSnapshot = saved.pickedAnchor;
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
if (saved.previewMode) currentPreviewMode = saved.previewMode;
@@ -7012,9 +7619,10 @@ void main() {
|| clampVariantIndex(serverSession?.visibleVariant, arrivedVariants || expectedVariants)
|| (arrivedVariants > 0 ? 1 : 0);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
+ const restoredAnchor = findLiveElementFromAnchorSnapshot(pickedAnchorSnapshot);
+ selectedElement = restoredAnchor || document.body;
+ setLiveState('GENERATING');
+ recoveryWaitingForAnchor = !restoredAnchor;
showBar('generating');
startScrollTracking();
if (variantObserver) variantObserver.disconnect();
@@ -7030,7 +7638,6 @@ void main() {
return true;
}
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return true;
}
@@ -7059,6 +7666,7 @@ void main() {
pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues },
insertPlaceholder: insertPlaceholderSnapshot || undefined,
+ pickedAnchor: pickedAnchorSnapshot || undefined,
});
}
@@ -7118,6 +7726,7 @@ void main() {
hideHighlight();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearScrollY();
finalizeInsertSession();
@@ -7127,15 +7736,21 @@ void main() {
currentSessionId = null;
selectedAction = 'impeccable';
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
//
// Toast
//
+ function dismissToast() {
+ if (!toastEl) return;
+ toastEl.remove();
+ toastEl = null;
+ }
+
function showToast(message, duration) {
- if (toastEl) toastEl.remove();
+ 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
@@ -7224,7 +7839,7 @@ void main() {
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
selectedElement = resolveSvelteComponentAnchor()
|| wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
hideShaderOverlay();
showBar('cycling');
startScrollTracking();
@@ -7260,7 +7875,7 @@ void main() {
const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
const origEl = pickVariantContent(wrapper, 'original');
- state = resumedState;
+ setLiveState(resumedState);
if (isInsert && resumedState === 'GENERATING' && arrivedVariants === 0) {
selectedElement = ensureInsertPlaceholder() || findInsertAnchorInDom() || wrapper;
} else {
@@ -7419,6 +8034,8 @@ void main() {
let voiceCtx = null;
const PAGE_CHAT_COLLAPSED_W = '88px';
const PAGE_CHAT_PROCESSING_W = '76px';
+ 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(56% 0.032 82 / 0.78)';
@@ -7509,13 +8126,16 @@ void main() {
function syncPageChatChrome() {
if (!pageChatEl) return;
const P = pageChatPalette();
+ const inputFocused = pageChatInput && activeElementDeep() === pageChatInput;
pageChatEl.style.background = P.chatSurface;
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
+ 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 : P.textDim;
+ 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';
@@ -7686,24 +8306,44 @@ void main() {
function syncPageChatFocusRing() {
if (!pageChatEl || !pageChatInput) return;
const focused = activeElementDeep() === pageChatInput;
+ const typingReady = focused && !steerLocked;
pageChatEl.dataset.inputFocused = focused ? 'true' : 'false';
- const P = pageChatPalette();
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
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.color = steerLocked
- ? P.patinaPale
- : ((!pageChatExpanded && focused) ? P.patinaPale : P.textDim);
- }
- if (!pageChatExpanded) {
- pageChatInput.style.width = '0';
- pageChatInput.style.padding = '0';
- pageChatInput.style.opacity = '0';
- pageChatInput.style.pointerEvents = focused ? 'auto' : 'none';
- if (pageChatHint) pageChatHint.style.visibility = '';
+ 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) {
@@ -7723,6 +8363,7 @@ void main() {
try { window.focus(); } catch { /* embed may block */ }
try { pageChatInput.focus({ preventScroll: true }); } catch { pageChatInput.focus(); }
syncPageChatFocusRing();
+ syncPageChatChrome();
steerFocusLog('focusSteerChat result', {
reason,
before: steerFocusTargetLabel(before),
@@ -7769,6 +8410,7 @@ void main() {
pageChatEl.dataset.expanded = 'true';
pageChatEl.style.width = PAGE_CHAT_EXPANDED_W;
pageChatEl.style.cursor = steerLocked ? 'default' : 'text';
+ pageChatInput.placeholder = PAGE_CHAT_PLACEHOLDER_EXPANDED;
if (pageChatHint) {
pageChatHint.style.display = 'none';
pageChatHint.style.opacity = '0';
@@ -7780,6 +8422,20 @@ void main() {
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(); }
@@ -7938,7 +8594,9 @@ void main() {
if (pageChatEl) pageChatEl.dataset.voiceListening = listening ? 'true' : 'false';
syncPageChatChrome();
} else if (voiceCtx?.mode === 'configure') {
- const voiceBtn = uiGetById(PREFIX + '-configure-voice');
+ // 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';
@@ -8180,12 +8838,12 @@ void main() {
height: '28px', margin: '0 4px 0 ' + (GLOBAL_BAR_SECTION_GAP - GLOBAL_BAR_INNER_GAP) + 'px',
borderRadius: '7px',
background: P.chatSurface,
- border: '1px solid ' + P.hairline,
+ border: '1px solid transparent',
overflow: 'hidden',
cursor: 'pointer',
flexShrink: '0',
width: PAGE_CHAT_COLLAPSED_W,
- transition: 'border-color 0.15s ease',
+ transition: 'width 0.18s ease, border-color 0.15s ease',
});
pageChatEl.id = PREFIX + '-page-chat';
pageChatEl.dataset.expanded = 'false';
@@ -8211,13 +8869,14 @@ void main() {
pageChatInput = document.createElement('input');
pageChatInput.id = PREFIX + '-page-chat-input';
pageChatInput.type = 'text';
- pageChatInput.placeholder = 'Steer the page…';
+ 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',
});
@@ -8252,18 +8911,23 @@ void main() {
'#' + 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(63% 0.024 82); 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.024 82); }' +
'#' + PREFIX + '-page-chat-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
uiAppendStyle(s);
}
- pageChatEl.addEventListener('pointerdown', keepSteerPointerInside);
+ pageChatEl.addEventListener('pointerdown', (e) => {
+ keepSteerPointerInside(e);
+ if (steerLocked || pageChatVoiceBtn.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)) return;
- expandPageChat({ focus: false });
- focusPageChatInput('page-chat-click');
+ armPageChatForTyping({ expand: true, focus: true });
});
pageChatVoiceBtn.addEventListener('pointerdown', keepSteerPointerInside);
@@ -8286,7 +8950,9 @@ void main() {
});
pageChatInput.addEventListener('focus', () => {
+ steerInputWasFocused = true;
syncPageChatFocusRing();
+ syncPageChatChrome();
});
pageChatInput.addEventListener('blur', () => {
@@ -8953,14 +9619,15 @@ void main() {
cancelInsertConfigure();
return;
}
+ teardownConfigureChrome();
hideHighlight();
- hideBar();
hideActionPicker();
selectedElement = null;
+ hoveredElement = null;
configureKind = 'replace';
- if (state === 'PICKING' || state === 'CONFIGURING') state = 'IDLE';
+ if (state === 'PICKING' || state === 'CONFIGURING') setLiveState('IDLE');
} else {
- if (state === 'IDLE') state = 'PICKING';
+ if (state === 'IDLE') setLiveState('PICKING');
}
syncPageChatFocus('toggle-pick');
}
@@ -8976,10 +9643,10 @@ void main() {
selectedElement = null;
configureKind = 'replace';
if (state === 'CONFIGURING') cancelInsertConfigure();
- else if (state === 'IDLE' || state === 'PICKING') state = 'PICKING';
+ else if (state === 'IDLE' || state === 'PICKING') setLiveState('PICKING');
} else {
clearInsertPicking();
- if (state === 'PICKING' && !pickActive) state = 'IDLE';
+ if (state === 'PICKING' && !pickActive) setLiveState('IDLE');
}
saveInteractionPrefs();
updateGlobalBarState();
@@ -9060,6 +9727,7 @@ void main() {
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; }
@@ -9073,7 +9741,8 @@ void main() {
window.removeEventListener('message', onDetectMessage);
// Remove detection overlays
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
- state = 'IDLE';
+ setLiveState('IDLE');
+ document.getElementById(PREFIX + '-pick-cursor-style')?.remove();
window.__IMPECCABLE_LIVE_INIT__ = false;
console.log('[impeccable] Live mode exited.');
}
@@ -10250,6 +10919,8 @@ void main() {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('init-complete');
}
diff --git a/.pi/skills/impeccable/scripts/live/ui-core.mjs b/.pi/skills/impeccable/scripts/live/ui-core.mjs
index 4b7adde8b..bbb2c5c35 100644
--- a/.pi/skills/impeccable/scripts/live/ui-core.mjs
+++ b/.pi/skills/impeccable/scripts/live/ui-core.mjs
@@ -42,9 +42,10 @@ export const LIVE_UI_SURFACES = Object.freeze([
'impeccable-live-highlight',
'impeccable-live-tooltip',
'impeccable-live-bar',
- 'impeccable-live-configure-input-wrap',
+ 'impeccable-live-selection-pill',
'impeccable-live-input',
'impeccable-live-configure-voice',
+ 'impeccable-live-configure-bar-tooltip',
],
states: ['rest', 'hover', 'focus-visible', 'pressed', 'disabled'],
},
diff --git a/.qoder/skills/impeccable/scripts/live-browser.js b/.qoder/skills/impeccable/scripts/live-browser.js
index 9092338eb..539cd07d9 100644
--- a/.qoder/skills/impeccable/scripts/live-browser.js
+++ b/.qoder/skills/impeccable/scripts/live-browser.js
@@ -57,6 +57,7 @@
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 PICK_CURSOR_CLASS = PREFIX + '-pick-cursor';
const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;
const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
prefix: PREFIX,
@@ -98,7 +99,7 @@
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'] },
{ key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] },
- { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-configure-input-wrap', PREFIX + '-input', PREFIX + '-configure-voice'] },
+ { 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'] },
@@ -131,6 +132,8 @@
let currentPreviewFile = null;
let currentPreviewMode = null;
let recoveryWaitingForAnchor = false;
+ let pickedAnchorSnapshot = null;
+ let pendingVariantAnchorRetryObserver = null;
let pendingAcceptedSession = null;
let variantObserver = null;
let variantSelectionInFlight = false;
@@ -283,31 +286,53 @@
uiAppend(tooltipEl);
}
+ function shouldShowHighlightTagTooltip() {
+ // Configure/edit carry the tag in the bar selection pill — 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 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);
+ 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' });
- tooltipEl.style.transition = 'none';
- Object.assign(tooltipEl.style, { top: tipY, left: tipX, 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(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });
}
}
@@ -1010,8 +1035,8 @@
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: '6px',
- maxWidth: '520px', minWidth: '320px',
+ padding: '5px',
+ maxWidth: '560px', minWidth: '340px',
});
uiAppend(barEl);
defangOutsideHandlers(barEl);
@@ -1019,14 +1044,29 @@
function positionBar() {
if (!barEl) return;
- const anchor = resolveBarAnchor();
- if (!anchor) return;
- const r = anchor.getBoundingClientRect();
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.
@@ -1054,8 +1094,14 @@
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
+ 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(() => {
@@ -1075,6 +1121,7 @@
setTimeout(() => { if (barEl && hideSeq === barHideSeq) barEl.style.display = 'none'; }, 250);
hideActionPicker();
closeTunePopover();
+ hideConfigureBarTooltip();
if (state === 'EDITING') restoreInlineEditDrafts();
disableInlineEdit();
}
@@ -1083,36 +1130,472 @@
if (!barEl || barEl.style.display === 'none') return;
if (mode === 'cycling' && !ensureCyclingRenderable('update-bar')) return;
barEl.innerHTML = '';
- // Reset bar styling to the kinpaku picker palette
- barEl.style.background = BP.surface;
- barEl.style.border = '1px solid ' + BP.border;
- barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
- else if (mode === 'saving') barEl.appendChild(buildSavingRow());
- else if (mode === 'confirmed') {
- barEl.appendChild(buildConfirmedRow());
- barEl.style.background = 'oklch(95% 0.05 145)';
- barEl.style.border = '1px solid oklch(75% 0.12 145 / 0.4)';
+ 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
+ // 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 =
+ '';
+
+ 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 wrap = uiGetById(PREFIX + '-configure-input-wrap');
- const input = uiGetById(PREFIX + '-input');
- if (!wrap || !input) return;
+ 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;
- wrap.dataset.inputFocused = focused ? 'true' : 'false';
- wrap.dataset.voiceListening = (voiceListening && voiceCtx?.mode === 'configure') ? 'true' : 'false';
- wrap.style.borderColor = (voiceListening && voiceCtx?.mode === 'configure')
+ 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.hairline);
+ : (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.02em',
+ }));
+ 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)
@@ -1434,14 +1917,38 @@
let pageInteractionCursorActive = false;
- /** Page-level cursor while insert mode is choosing a before/after edge. */
+ function ensurePickCursorStyle() {
+ if (document.getElementById(PREFIX + '-pick-cursor-style')) return;
+ const style = document.createElement('style');
+ style.id = PREFIX + '-pick-cursor-style';
+ style.textContent =
+ 'html.' + PICK_CURSOR_CLASS + ' * { cursor: crosshair !important; }\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"],\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"] * { cursor: revert !important; }';
+ // Styles the host page, not the chrome - inside the adapter's shadow UI
+ // root (uiAppendStyle's target) these selectors would match nothing.
+ document.head.appendChild(style);
+ }
+
+ /** Page-level cursor while pick or insert mode is targeting page elements. */
function syncPageInteractionCursor() {
- let next = '';
+ const pickCursor = state === 'PICKING' && pickActive && !insertActive;
+ let axisCursor = '';
if (state === 'PICKING' && insertActive) {
- next = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
+ axisCursor = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
}
- if (next) {
- document.documentElement.style.cursor = next;
+
+ if (pickCursor) {
+ ensurePickCursorStyle();
+ document.documentElement.classList.add(PICK_CURSOR_CLASS);
+ document.documentElement.style.cursor = '';
+ pageInteractionCursorActive = true;
+ return;
+ }
+
+ document.documentElement.classList.remove(PICK_CURSOR_CLASS);
+ if (axisCursor) {
+ document.documentElement.style.cursor = axisCursor;
pageInteractionCursorActive = true;
} else if (pageInteractionCursorActive) {
document.documentElement.style.cursor = '';
@@ -1449,6 +1956,16 @@
}
}
+ /**
+ * 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;
+ syncPageInteractionCursor();
+ }
+
/** Element used to position the floating bar / shader during a session. */
function resolveBarAnchor() {
if (svelteComponentSession?.sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
@@ -1767,69 +2284,35 @@
}
}
+ /** 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: 'center', gap: '6px',
+ display: 'flex', alignItems: 'stretch', width: '100%', height: CONFIGURE_BAR_H,
});
- // Action pill - dark graphite chip (matches kinpaku-kit .live-demo-ctx-pill)
- const pill = el('button', {
- display: 'inline-flex', alignItems: 'center', gap: '4px',
- padding: '5px 10px', borderRadius: '6px',
- background: BP.chatSurface, color: BP.text,
- fontFamily: FONT, fontSize: '12px', fontWeight: '500',
- border: '1px solid ' + BP.hairline, cursor: 'pointer',
- transition: 'background 0.12s ease, border-color 0.12s ease, transform 0.1s ease',
- whiteSpace: 'nowrap', flexShrink: '0',
- });
- pill.textContent = actionLabel() + ' \u25BE';
- pill.disabled = controlsLocked;
- pill.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- pill.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) pill.title = 'Apply is still running';
- pill.addEventListener('mouseenter', () => {
- if (controlsLocked) return;
- pill.style.background = BP.accentSoft;
- pill.style.borderColor = BP.accent;
- });
- pill.addEventListener('mouseleave', () => {
- if (controlsLocked) return;
- pill.style.background = BP.chatSurface;
- pill.style.borderColor = BP.hairline;
- });
- pill.addEventListener('mousedown', () => { if (!controlsLocked) pill.style.transform = 'scale(0.97)'; });
- pill.addEventListener('mouseup', () => pill.style.transform = 'scale(1)');
- pill.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleActionPicker();
- });
- row.appendChild(pill);
-
- // Prompt field - same chat-surface chrome as the bottom Steer bar
- const inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-configure-input-wrap';
+ const inputShell = el('div', configureInputShellStyle());
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
- input.placeholder = selectedAction === 'impeccable' ? 'describe what you want…' : 'refine further (optional)…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the change');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1837,33 +2320,31 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, background 0.12s ease',
+ const action = buildConfigureActionControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleActionPicker();
+ },
});
- voiceBtn.id = PREFIX + '-configure-voice';
- 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';
- if (!uiGetById(PREFIX + '-configure-input-style')) {
- 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::placeholder { color: ' + BP.textDim + '; opacity: 1; }' +
- '#' + PREFIX + '-configure-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 { animation: none; opacity: 1; } }' +
- '#' + PREFIX + '-configure-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
- uiAppendStyle(s);
- }
+ 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());
@@ -1873,81 +2354,33 @@
e.stopPropagation();
e.preventDefault();
input.blur();
- disableInlineEdit();
- hideBar();
- renderEditBadge('hidden');
- state = 'PICKING';
- syncPageChatFocus('configure-input-escape');
+ exitConfigureToPicking('configure-input-escape');
return;
}
- // Let arrow keys pass through to the element picker when the input is empty
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !input.value) return;
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ const voiceBtn = buildConfigureVoiceButton({
+ id: PREFIX + '-configure-voice',
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleConfigureVoice();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
+ const go = buildConfigureSubmitButton({
+ controlsLocked,
+ ariaLabel: 'Generate variants',
+ onClick: (e) => { e.stopPropagation(); handleGo(); },
+ });
+
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([action, count], voiceBtn, go));
syncConfigureInputChrome();
- // Variant count toggle
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, border-color 0.12s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.title = 'Variants: click to change';
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) count.title = 'Apply is still running';
- count.addEventListener('mouseenter', () => { if (!controlsLocked) { count.style.color = BP.text; count.style.borderColor = BP.text; } });
- count.addEventListener('mouseleave', () => { if (!controlsLocked) { count.style.color = BP.textDim; count.style.borderColor = BP.hairline; } });
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- // Go button
- const go = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- cursor: 'pointer',
- transition: 'filter 0.12s ease, transform 0.1s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- go.textContent = 'Go \u2192';
- go.disabled = controlsLocked;
- go.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- go.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) go.title = 'Apply is still running';
- go.addEventListener('mouseenter', () => { if (!controlsLocked) go.style.filter = 'brightness(1.1)'; });
- go.addEventListener('mouseleave', () => go.style.filter = 'none');
- go.addEventListener('mousedown', () => { if (!controlsLocked) go.style.transform = 'scale(0.97)'; });
- go.addEventListener('mouseup', () => go.style.transform = 'scale(1)');
- go.addEventListener('click', (e) => { e.stopPropagation(); handleGo(); });
- row.appendChild(go);
-
- // Auto-focus input after a beat
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
@@ -1956,34 +2389,20 @@
function buildInsertConfigureRow() {
const controlsLocked = pendingApplyInFlight === true;
const row = el('div', {
- display: 'flex', alignItems: 'center', gap: '6px',
+ 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 inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-insert-input-wrap';
- inputWrap.addEventListener('pointerdown', (e) => e.stopPropagation());
- inputWrap.addEventListener('mousedown', (e) => e.stopPropagation());
- inputWrap.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 = 'describe what to insert…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the new element');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1991,20 +2410,22 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
+ const count = buildConfigureCountControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ count.textContent = '\u00D7' + cycleSelectedCount();
+ if (count.matches(':hover')) {
+ showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));
+ }
+ },
});
- voiceBtn.id = PREFIX + '-insert-voice';
- 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';
+
+ inputShell.appendChild(buildSelectionPill({ el: selectedElement, controlsLocked }));
+ inputShell.appendChild(input);
+
+ ensureConfigureInputStyle();
input.addEventListener('input', () => syncInsertCreateButton());
input.addEventListener('pointerdown', (e) => e.stopPropagation());
@@ -2026,48 +2447,31 @@
}
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ 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();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
-
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer', flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- const create = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- flexShrink: '0', whiteSpace: 'nowrap',
+ 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.textContent = 'Create \u2192';
- create.disabled = controlsLocked;
create.addEventListener('mouseenter', () => {
if (controlsLocked) return;
if (isInsertCreateEnabled(create)) {
@@ -2077,15 +2481,10 @@
showInsertCreateTooltip(create, insertCreateDisabledReason(insertCreateGateState(input)));
});
create.addEventListener('mouseleave', hideInsertCreateTooltip);
- create.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- if (!isInsertCreateEnabled(create)) return;
- handleInsertCreate();
- });
- row.appendChild(create);
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([count], voiceBtn, create));
syncInsertCreateButton(create, input);
+ syncConfigureInputChrome();
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
}
@@ -2375,7 +2774,7 @@
position: 'fixed', zIndex: Z.picker,
display: 'none', opacity: '0',
transform: 'scale(0.96) translateY(4px)',
- transformOrigin: 'bottom left',
+ transformOrigin: 'bottom right',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1px solid ' + P.border,
@@ -2451,14 +2850,18 @@
chip.style.background = isActive ? P.accentSoft : 'transparent';
chip.style.color = isActive ? P.accent : P.text;
});
- // Position above the bar
+ // 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: barRect.left + 'px',
- display: 'block',
+ top: top + 'px',
+ left: left + 'px',
});
requestAnimationFrame(() => {
pickerEl.style.opacity = '1';
@@ -2941,7 +3344,7 @@
function enterEditingMode() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
- state = 'EDITING';
+ setLiveState('EDITING');
hideBar();
hideAnnotOverlay();
renderEditBadge('editing');
@@ -2974,7 +3377,7 @@
function cancelEditing() {
restoreInlineEditDrafts();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -2988,12 +3391,33 @@
hideAnnotOverlay();
clearAnnotations();
renderEditBadge('hidden');
- state = 'PICKING';
+ 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 ),
// climb to the nearest ancestor with one. The CLI uses tag+class together,
// so tag must come from the same node as the locator.
@@ -3215,7 +3639,7 @@
updatePendingCounter(stashResult.pendingCount || 0);
maybeShowFirstSaveToast();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -3895,6 +4319,12 @@
// Edit content badge - floating button at element top-right to enter EDITING mode
//
+ const EDIT_COPY_LABEL = 'Edit copy';
+ const EDIT_COPY_ICON =
+ '';
+
function usesShadowChromeRoot() {
const root = liveUiRoot();
return root && root !== document.body && root.host && root.host.id === PREFIX + '-root';
@@ -4045,7 +4475,7 @@
editBadgeProxyRoot.appendChild(proxy);
editBadgeProxyByTarget.set(target, proxy);
}
- proxy.title = target.title || target.textContent || 'Edit copy';
+ proxy.title = target.title || target.getAttribute('aria-label') || target.textContent || EDIT_COPY_LABEL;
styleEditBadgeProxy(proxy, target);
}
}
@@ -4085,13 +4515,16 @@
}
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, r.right - bw) + '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;
@@ -4129,16 +4562,36 @@
const disabled = mode === 'idle-disabled';
editBadgeEl.innerHTML = '';
const btn = document.createElement('button');
- btn.textContent = 'Edit copy';
- Object.assign(btn.style, calloutStyle(disabled ? MUTED : ACCENT, disabled ? HAIRLINE : ACCENT));
+ 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;
- btn.title = 'Edit copy is disabled while the current copy edit is applying';
+ 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', () => { btn.style.background = ACCENT; btn.style.color = PRIMARY_TEXT; });
- btn.addEventListener('mouseleave', () => { btn.style.background = SURFACE; btn.style.color = ACCENT; });
+ btn.addEventListener('mouseenter', () => showConfigureBarTooltip(btn, EDIT_COPY_LABEL));
+ btn.addEventListener('mouseleave', hideConfigureBarTooltip);
btn.onclick = enterEditingMode;
}
editBadgeEl.appendChild(btn);
@@ -4154,9 +4607,8 @@
cancel.onclick = cancelEditing;
const save = document.createElement('button');
save.textContent = 'Save';
- Object.assign(save.style, calloutStyle(ACCENT));
- save.addEventListener('mouseenter', () => { save.style.background = ACCENT; save.style.color = PRIMARY_TEXT; });
- save.addEventListener('mouseleave', () => { save.style.background = SURFACE; save.style.color = ACCENT; });
+ Object.assign(save.style, calloutStyle(PRIMARY_TEXT, ACCENT));
+ save.style.background = ACCENT;
save.onclick = applyEditing;
editBadgeEl.append(cancel, save);
}
@@ -4392,31 +4844,142 @@
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 = origContent.className;
- let liveEl = null;
+ const cls = normalizeElementClassName(origContent);
+ const candidates = [...document.getElementsByTagName(tag)];
+
if (origContent.id) {
- liveEl = document.getElementById(origContent.id);
- } else if (cls) {
- const candidates = [...document.getElementsByTagName(tag)];
- for (const c of candidates) {
- if (c.className === cls && !own(c)) { liveEl = c; break; }
- }
- if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
- if (expectedClasses.length > 0) {
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
- }
+ 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;
}
}
}
- return liveEl;
+
+ 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) {
@@ -4428,7 +4991,45 @@
const anchor = findInsertAnchorInDom();
if (anchor?.parentElement) return anchor;
}
- return findLiveElementForOriginalMarkup(manifest?.originalMarkup || manifest?.anchorMarkup || '');
+ 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);
+ }
}
function loadSvelteRuntime(runtimeModule) {
@@ -4595,7 +5196,7 @@
previewFile: manifestPath,
previewMode: 'svelte-component',
});
- if (state !== 'CYCLING') state = 'GENERATING';
+ if (state !== 'CYCLING') setLiveState('GENERATING');
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper && svelteComponentSession?.sessionId === sessionId) {
@@ -4605,7 +5206,7 @@
expectedVariants = arrivedVariants;
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants ? visibleVariant : 1;
await mountSvelteComponentVariant(visibleVariant || 1);
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
saveSession();
return;
@@ -4621,15 +5222,8 @@
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants
? visibleVariant
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
- showBar('generating');
- startScrollTracking();
- saveSession();
- queueCheckpoint('svelte_component_anchor_missing');
+ enterRecoveryWaitingForAnchor({ checkpointReason: 'svelte_component_anchor_missing', trackScroll: true });
waitForSvelteComponentTargetAndRetry({ manifestPath, sessionId, manifest });
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return;
}
@@ -4692,7 +5286,7 @@
}
selectedElement = mountTarget.firstElementChild || mountTarget;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -4741,6 +5335,7 @@
hideShaderOverlay();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
if (pendingSvelteComponentRetryObserver) { pendingSvelteComponentRetryObserver.disconnect(); pendingSvelteComponentRetryObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearSession();
clearHandled();
@@ -4750,7 +5345,7 @@
arrivedVariants = 0;
visibleVariant = 0;
selectedElement = null;
- state = 'PICKING';
+ setLiveState('PICKING');
hideBar();
if (message) showToast(message, 5000);
}
@@ -4799,21 +5394,26 @@
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
- const liveEl = findLiveElementForOriginalMarkup(origContent.outerHTML);
+ const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
- selectedElement = document.body;
- recoveryWaitingForAnchor = true;
- state = 'GENERATING';
- showBar('generating');
- saveSession();
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
+ 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.
@@ -4834,7 +5434,7 @@
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -5240,7 +5840,7 @@
if (expected > 0) expectedVariants = expected;
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
if (wrapper.dataset.impeccableMode === 'insert') finalizeInsertSession();
@@ -5326,7 +5926,8 @@
syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll();
restoreFromActiveSessions(msg.activeSessions, 'sse_connected');
- if (state === 'IDLE' && (pickActive || insertActive)) state = 'PICKING';
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('sse-connected');
break;
case 'agent_polling':
@@ -5352,7 +5953,7 @@
// Variants already arrived via HMR → normal transition.
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
if (state === 'GENERATING') {
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
disableInlineEdit();
refreshParamsPanel();
@@ -5402,7 +6003,7 @@
case 'error':
if (pendingAcceptedSession?.id && msg.id === pendingAcceptedSession.id) {
pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
updateBarContent('cycling');
showToast('Could not complete accept cleanup. Try Accept again.', 5000);
break;
@@ -5412,7 +6013,7 @@
showToast('Error: ' + msg.message, 5000);
hideBar();
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
break;
}
};
@@ -5450,7 +6051,7 @@
// transient disconnect as an explicit discard.
selectedElement = null;
selectedAction = 'impeccable';
- state = recoveryState;
+ setLiveState(recoveryState);
if (currentSessionId) saveSession();
}
@@ -5592,15 +6193,7 @@
&& !selectedElement.contains(e.target)
) {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- hideBar();
- stopScrollTracking();
- hideAnnotOverlay();
- clearAnnotations();
- renderEditBadge('hidden');
- state = 'PICKING';
- hoveredElement = null;
- hideHighlight();
- syncPageChatFocus('configure-outside-click');
+ exitConfigureToPicking('configure-outside-click', { clearHover: true });
return;
}
if (state === 'PICKING' && insertActive) {
@@ -5617,13 +6210,12 @@
hideInsertLine();
configureKind = 'insert';
selectedElement = placeholder;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
hideHighlight();
clearAnnotations();
showAnnotOverlay(placeholder);
showBar('configure');
startScrollTracking();
- syncPageInteractionCursor();
return;
}
if (state !== 'PICKING' || !pickActive) return;
@@ -5636,7 +6228,7 @@
e.preventDefault();
e.stopPropagation();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5721,6 +6313,16 @@
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;
@@ -5729,6 +6331,7 @@
deepActive
&& own(deepActive)
&& /^(INPUT|TEXTAREA|SELECT)$/.test(deepActive.tagName || '')
+ && !shouldPassthroughElementNav(deepActive, e)
) {
return;
}
@@ -5767,14 +6370,15 @@
if (state === 'EDITING') { cancelEditing(); return; }
if (state === 'CONFIGURING') {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- disableInlineEdit(); hideBar(); stopScrollTracking(); hideAnnotOverlay(); clearAnnotations(); renderEditBadge('hidden'); state = 'PICKING'; syncPageChatFocus('escape-from-configure'); 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(); state = 'IDLE'; }
+ else { hideHighlight(); setLiveState('IDLE'); }
return;
}
}
@@ -5798,7 +6402,7 @@
} else if (e.key === 'Enter') {
e.preventDefault();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5859,6 +6463,7 @@
// screenshot is uploaded (or capture fails - we still emit, just without
// screenshotPath).
const elForCapture = selectedElement;
+ pickedAnchorSnapshot = buildPickedAnchorSnapshot(elForCapture);
const captureRect = elForCapture.getBoundingClientRect();
const snapshot = {
comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })),
@@ -5879,7 +6484,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ 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
@@ -5904,7 +6509,7 @@
clearInsertPicking();
configureKind = 'replace';
selectedElement = null;
- state = insertActive ? 'PICKING' : 'IDLE';
+ setLiveState(insertActive ? 'PICKING' : 'IDLE');
hideHighlight();
syncPageChatFocus('insert-configure-cancel');
}
@@ -5954,7 +6559,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ setLiveState('GENERATING');
showBar('generating');
startScrollTracking();
saveSession();
@@ -6712,7 +7317,7 @@ void main() {
|| acceptWrapper?.dataset?.impeccablePreview === 'svelte-component';
const acceptedSnapshot = snapshotAcceptedVariantDom(acceptedSessionId, acceptedVariant);
- state = 'SAVING';
+ setLiveState('SAVING');
updateBarContent('saving');
pendingAcceptedSession = {
id: acceptedSessionId,
@@ -6727,7 +7332,7 @@ void main() {
.then(() => {})
.catch(() => {
if (pendingAcceptedSession?.id === acceptedSessionId) pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
showToast('Could not confirm accept with the live server. Session kept for recovery; try Accept again.', 5000);
});
@@ -6746,7 +7351,7 @@ void main() {
if (pending.isSvelteComponent) {
commitAcceptedSvelteComponentToDom(pending.id);
}
- state = 'CONFIRMED';
+ setLiveState('CONFIRMED');
updateBarContent('confirmed');
scheduleAcceptCleanup(pending);
return true;
@@ -6870,7 +7475,7 @@ void main() {
selectedAction = 'impeccable';
pendingAcceptedSession = null;
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
function commitAcceptedVariantToDom(sessionId, variantId) {
@@ -6922,6 +7527,7 @@ void main() {
currentPreviewFile = null;
currentPreviewMode = null;
recoveryWaitingForAnchor = false;
+ pickedAnchorSnapshot = null;
}
function rememberSessionFileMeta(meta = {}) {
@@ -6946,6 +7552,7 @@ void main() {
if (!saved) return;
rememberSessionFileMeta(saved);
if (saved.insertPlaceholder) insertPlaceholderSnapshot = saved.insertPlaceholder;
+ if (saved.pickedAnchor) pickedAnchorSnapshot = saved.pickedAnchor;
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
if (saved.previewMode) currentPreviewMode = saved.previewMode;
@@ -7012,9 +7619,10 @@ void main() {
|| clampVariantIndex(serverSession?.visibleVariant, arrivedVariants || expectedVariants)
|| (arrivedVariants > 0 ? 1 : 0);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
+ const restoredAnchor = findLiveElementFromAnchorSnapshot(pickedAnchorSnapshot);
+ selectedElement = restoredAnchor || document.body;
+ setLiveState('GENERATING');
+ recoveryWaitingForAnchor = !restoredAnchor;
showBar('generating');
startScrollTracking();
if (variantObserver) variantObserver.disconnect();
@@ -7030,7 +7638,6 @@ void main() {
return true;
}
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return true;
}
@@ -7059,6 +7666,7 @@ void main() {
pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues },
insertPlaceholder: insertPlaceholderSnapshot || undefined,
+ pickedAnchor: pickedAnchorSnapshot || undefined,
});
}
@@ -7118,6 +7726,7 @@ void main() {
hideHighlight();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearScrollY();
finalizeInsertSession();
@@ -7127,15 +7736,21 @@ void main() {
currentSessionId = null;
selectedAction = 'impeccable';
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
//
// Toast
//
+ function dismissToast() {
+ if (!toastEl) return;
+ toastEl.remove();
+ toastEl = null;
+ }
+
function showToast(message, duration) {
- if (toastEl) toastEl.remove();
+ 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
@@ -7224,7 +7839,7 @@ void main() {
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
selectedElement = resolveSvelteComponentAnchor()
|| wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
hideShaderOverlay();
showBar('cycling');
startScrollTracking();
@@ -7260,7 +7875,7 @@ void main() {
const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
const origEl = pickVariantContent(wrapper, 'original');
- state = resumedState;
+ setLiveState(resumedState);
if (isInsert && resumedState === 'GENERATING' && arrivedVariants === 0) {
selectedElement = ensureInsertPlaceholder() || findInsertAnchorInDom() || wrapper;
} else {
@@ -7419,6 +8034,8 @@ void main() {
let voiceCtx = null;
const PAGE_CHAT_COLLAPSED_W = '88px';
const PAGE_CHAT_PROCESSING_W = '76px';
+ 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(56% 0.032 82 / 0.78)';
@@ -7509,13 +8126,16 @@ void main() {
function syncPageChatChrome() {
if (!pageChatEl) return;
const P = pageChatPalette();
+ const inputFocused = pageChatInput && activeElementDeep() === pageChatInput;
pageChatEl.style.background = P.chatSurface;
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
+ 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 : P.textDim;
+ 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';
@@ -7686,24 +8306,44 @@ void main() {
function syncPageChatFocusRing() {
if (!pageChatEl || !pageChatInput) return;
const focused = activeElementDeep() === pageChatInput;
+ const typingReady = focused && !steerLocked;
pageChatEl.dataset.inputFocused = focused ? 'true' : 'false';
- const P = pageChatPalette();
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
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.color = steerLocked
- ? P.patinaPale
- : ((!pageChatExpanded && focused) ? P.patinaPale : P.textDim);
- }
- if (!pageChatExpanded) {
- pageChatInput.style.width = '0';
- pageChatInput.style.padding = '0';
- pageChatInput.style.opacity = '0';
- pageChatInput.style.pointerEvents = focused ? 'auto' : 'none';
- if (pageChatHint) pageChatHint.style.visibility = '';
+ 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) {
@@ -7723,6 +8363,7 @@ void main() {
try { window.focus(); } catch { /* embed may block */ }
try { pageChatInput.focus({ preventScroll: true }); } catch { pageChatInput.focus(); }
syncPageChatFocusRing();
+ syncPageChatChrome();
steerFocusLog('focusSteerChat result', {
reason,
before: steerFocusTargetLabel(before),
@@ -7769,6 +8410,7 @@ void main() {
pageChatEl.dataset.expanded = 'true';
pageChatEl.style.width = PAGE_CHAT_EXPANDED_W;
pageChatEl.style.cursor = steerLocked ? 'default' : 'text';
+ pageChatInput.placeholder = PAGE_CHAT_PLACEHOLDER_EXPANDED;
if (pageChatHint) {
pageChatHint.style.display = 'none';
pageChatHint.style.opacity = '0';
@@ -7780,6 +8422,20 @@ void main() {
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(); }
@@ -7938,7 +8594,9 @@ void main() {
if (pageChatEl) pageChatEl.dataset.voiceListening = listening ? 'true' : 'false';
syncPageChatChrome();
} else if (voiceCtx?.mode === 'configure') {
- const voiceBtn = uiGetById(PREFIX + '-configure-voice');
+ // 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';
@@ -8180,12 +8838,12 @@ void main() {
height: '28px', margin: '0 4px 0 ' + (GLOBAL_BAR_SECTION_GAP - GLOBAL_BAR_INNER_GAP) + 'px',
borderRadius: '7px',
background: P.chatSurface,
- border: '1px solid ' + P.hairline,
+ border: '1px solid transparent',
overflow: 'hidden',
cursor: 'pointer',
flexShrink: '0',
width: PAGE_CHAT_COLLAPSED_W,
- transition: 'border-color 0.15s ease',
+ transition: 'width 0.18s ease, border-color 0.15s ease',
});
pageChatEl.id = PREFIX + '-page-chat';
pageChatEl.dataset.expanded = 'false';
@@ -8211,13 +8869,14 @@ void main() {
pageChatInput = document.createElement('input');
pageChatInput.id = PREFIX + '-page-chat-input';
pageChatInput.type = 'text';
- pageChatInput.placeholder = 'Steer the page…';
+ 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',
});
@@ -8252,18 +8911,23 @@ void main() {
'#' + 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(63% 0.024 82); 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.024 82); }' +
'#' + PREFIX + '-page-chat-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
uiAppendStyle(s);
}
- pageChatEl.addEventListener('pointerdown', keepSteerPointerInside);
+ pageChatEl.addEventListener('pointerdown', (e) => {
+ keepSteerPointerInside(e);
+ if (steerLocked || pageChatVoiceBtn.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)) return;
- expandPageChat({ focus: false });
- focusPageChatInput('page-chat-click');
+ armPageChatForTyping({ expand: true, focus: true });
});
pageChatVoiceBtn.addEventListener('pointerdown', keepSteerPointerInside);
@@ -8286,7 +8950,9 @@ void main() {
});
pageChatInput.addEventListener('focus', () => {
+ steerInputWasFocused = true;
syncPageChatFocusRing();
+ syncPageChatChrome();
});
pageChatInput.addEventListener('blur', () => {
@@ -8953,14 +9619,15 @@ void main() {
cancelInsertConfigure();
return;
}
+ teardownConfigureChrome();
hideHighlight();
- hideBar();
hideActionPicker();
selectedElement = null;
+ hoveredElement = null;
configureKind = 'replace';
- if (state === 'PICKING' || state === 'CONFIGURING') state = 'IDLE';
+ if (state === 'PICKING' || state === 'CONFIGURING') setLiveState('IDLE');
} else {
- if (state === 'IDLE') state = 'PICKING';
+ if (state === 'IDLE') setLiveState('PICKING');
}
syncPageChatFocus('toggle-pick');
}
@@ -8976,10 +9643,10 @@ void main() {
selectedElement = null;
configureKind = 'replace';
if (state === 'CONFIGURING') cancelInsertConfigure();
- else if (state === 'IDLE' || state === 'PICKING') state = 'PICKING';
+ else if (state === 'IDLE' || state === 'PICKING') setLiveState('PICKING');
} else {
clearInsertPicking();
- if (state === 'PICKING' && !pickActive) state = 'IDLE';
+ if (state === 'PICKING' && !pickActive) setLiveState('IDLE');
}
saveInteractionPrefs();
updateGlobalBarState();
@@ -9060,6 +9727,7 @@ void main() {
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; }
@@ -9073,7 +9741,8 @@ void main() {
window.removeEventListener('message', onDetectMessage);
// Remove detection overlays
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
- state = 'IDLE';
+ setLiveState('IDLE');
+ document.getElementById(PREFIX + '-pick-cursor-style')?.remove();
window.__IMPECCABLE_LIVE_INIT__ = false;
console.log('[impeccable] Live mode exited.');
}
@@ -10250,6 +10919,8 @@ void main() {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('init-complete');
}
diff --git a/.qoder/skills/impeccable/scripts/live/ui-core.mjs b/.qoder/skills/impeccable/scripts/live/ui-core.mjs
index 4b7adde8b..bbb2c5c35 100644
--- a/.qoder/skills/impeccable/scripts/live/ui-core.mjs
+++ b/.qoder/skills/impeccable/scripts/live/ui-core.mjs
@@ -42,9 +42,10 @@ export const LIVE_UI_SURFACES = Object.freeze([
'impeccable-live-highlight',
'impeccable-live-tooltip',
'impeccable-live-bar',
- 'impeccable-live-configure-input-wrap',
+ 'impeccable-live-selection-pill',
'impeccable-live-input',
'impeccable-live-configure-voice',
+ 'impeccable-live-configure-bar-tooltip',
],
states: ['rest', 'hover', 'focus-visible', 'pressed', 'disabled'],
},
diff --git a/.rovodev/skills/impeccable/scripts/live-browser.js b/.rovodev/skills/impeccable/scripts/live-browser.js
index 9092338eb..539cd07d9 100644
--- a/.rovodev/skills/impeccable/scripts/live-browser.js
+++ b/.rovodev/skills/impeccable/scripts/live-browser.js
@@ -57,6 +57,7 @@
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 PICK_CURSOR_CLASS = PREFIX + '-pick-cursor';
const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;
const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
prefix: PREFIX,
@@ -98,7 +99,7 @@
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'] },
{ key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] },
- { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-configure-input-wrap', PREFIX + '-input', PREFIX + '-configure-voice'] },
+ { 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'] },
@@ -131,6 +132,8 @@
let currentPreviewFile = null;
let currentPreviewMode = null;
let recoveryWaitingForAnchor = false;
+ let pickedAnchorSnapshot = null;
+ let pendingVariantAnchorRetryObserver = null;
let pendingAcceptedSession = null;
let variantObserver = null;
let variantSelectionInFlight = false;
@@ -283,31 +286,53 @@
uiAppend(tooltipEl);
}
+ function shouldShowHighlightTagTooltip() {
+ // Configure/edit carry the tag in the bar selection pill — 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 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);
+ 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' });
- tooltipEl.style.transition = 'none';
- Object.assign(tooltipEl.style, { top: tipY, left: tipX, 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(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });
}
}
@@ -1010,8 +1035,8 @@
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: '6px',
- maxWidth: '520px', minWidth: '320px',
+ padding: '5px',
+ maxWidth: '560px', minWidth: '340px',
});
uiAppend(barEl);
defangOutsideHandlers(barEl);
@@ -1019,14 +1044,29 @@
function positionBar() {
if (!barEl) return;
- const anchor = resolveBarAnchor();
- if (!anchor) return;
- const r = anchor.getBoundingClientRect();
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.
@@ -1054,8 +1094,14 @@
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
+ 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(() => {
@@ -1075,6 +1121,7 @@
setTimeout(() => { if (barEl && hideSeq === barHideSeq) barEl.style.display = 'none'; }, 250);
hideActionPicker();
closeTunePopover();
+ hideConfigureBarTooltip();
if (state === 'EDITING') restoreInlineEditDrafts();
disableInlineEdit();
}
@@ -1083,36 +1130,472 @@
if (!barEl || barEl.style.display === 'none') return;
if (mode === 'cycling' && !ensureCyclingRenderable('update-bar')) return;
barEl.innerHTML = '';
- // Reset bar styling to the kinpaku picker palette
- barEl.style.background = BP.surface;
- barEl.style.border = '1px solid ' + BP.border;
- barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
- else if (mode === 'saving') barEl.appendChild(buildSavingRow());
- else if (mode === 'confirmed') {
- barEl.appendChild(buildConfirmedRow());
- barEl.style.background = 'oklch(95% 0.05 145)';
- barEl.style.border = '1px solid oklch(75% 0.12 145 / 0.4)';
+ 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
+ // 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 =
+ '';
+
+ 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 wrap = uiGetById(PREFIX + '-configure-input-wrap');
- const input = uiGetById(PREFIX + '-input');
- if (!wrap || !input) return;
+ 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;
- wrap.dataset.inputFocused = focused ? 'true' : 'false';
- wrap.dataset.voiceListening = (voiceListening && voiceCtx?.mode === 'configure') ? 'true' : 'false';
- wrap.style.borderColor = (voiceListening && voiceCtx?.mode === 'configure')
+ 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.hairline);
+ : (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.02em',
+ }));
+ 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)
@@ -1434,14 +1917,38 @@
let pageInteractionCursorActive = false;
- /** Page-level cursor while insert mode is choosing a before/after edge. */
+ function ensurePickCursorStyle() {
+ if (document.getElementById(PREFIX + '-pick-cursor-style')) return;
+ const style = document.createElement('style');
+ style.id = PREFIX + '-pick-cursor-style';
+ style.textContent =
+ 'html.' + PICK_CURSOR_CLASS + ' * { cursor: crosshair !important; }\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"],\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"] * { cursor: revert !important; }';
+ // Styles the host page, not the chrome - inside the adapter's shadow UI
+ // root (uiAppendStyle's target) these selectors would match nothing.
+ document.head.appendChild(style);
+ }
+
+ /** Page-level cursor while pick or insert mode is targeting page elements. */
function syncPageInteractionCursor() {
- let next = '';
+ const pickCursor = state === 'PICKING' && pickActive && !insertActive;
+ let axisCursor = '';
if (state === 'PICKING' && insertActive) {
- next = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
+ axisCursor = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
}
- if (next) {
- document.documentElement.style.cursor = next;
+
+ if (pickCursor) {
+ ensurePickCursorStyle();
+ document.documentElement.classList.add(PICK_CURSOR_CLASS);
+ document.documentElement.style.cursor = '';
+ pageInteractionCursorActive = true;
+ return;
+ }
+
+ document.documentElement.classList.remove(PICK_CURSOR_CLASS);
+ if (axisCursor) {
+ document.documentElement.style.cursor = axisCursor;
pageInteractionCursorActive = true;
} else if (pageInteractionCursorActive) {
document.documentElement.style.cursor = '';
@@ -1449,6 +1956,16 @@
}
}
+ /**
+ * 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;
+ syncPageInteractionCursor();
+ }
+
/** Element used to position the floating bar / shader during a session. */
function resolveBarAnchor() {
if (svelteComponentSession?.sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
@@ -1767,69 +2284,35 @@
}
}
+ /** 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: 'center', gap: '6px',
+ display: 'flex', alignItems: 'stretch', width: '100%', height: CONFIGURE_BAR_H,
});
- // Action pill - dark graphite chip (matches kinpaku-kit .live-demo-ctx-pill)
- const pill = el('button', {
- display: 'inline-flex', alignItems: 'center', gap: '4px',
- padding: '5px 10px', borderRadius: '6px',
- background: BP.chatSurface, color: BP.text,
- fontFamily: FONT, fontSize: '12px', fontWeight: '500',
- border: '1px solid ' + BP.hairline, cursor: 'pointer',
- transition: 'background 0.12s ease, border-color 0.12s ease, transform 0.1s ease',
- whiteSpace: 'nowrap', flexShrink: '0',
- });
- pill.textContent = actionLabel() + ' \u25BE';
- pill.disabled = controlsLocked;
- pill.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- pill.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) pill.title = 'Apply is still running';
- pill.addEventListener('mouseenter', () => {
- if (controlsLocked) return;
- pill.style.background = BP.accentSoft;
- pill.style.borderColor = BP.accent;
- });
- pill.addEventListener('mouseleave', () => {
- if (controlsLocked) return;
- pill.style.background = BP.chatSurface;
- pill.style.borderColor = BP.hairline;
- });
- pill.addEventListener('mousedown', () => { if (!controlsLocked) pill.style.transform = 'scale(0.97)'; });
- pill.addEventListener('mouseup', () => pill.style.transform = 'scale(1)');
- pill.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleActionPicker();
- });
- row.appendChild(pill);
-
- // Prompt field - same chat-surface chrome as the bottom Steer bar
- const inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-configure-input-wrap';
+ const inputShell = el('div', configureInputShellStyle());
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
- input.placeholder = selectedAction === 'impeccable' ? 'describe what you want…' : 'refine further (optional)…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the change');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1837,33 +2320,31 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, background 0.12s ease',
+ const action = buildConfigureActionControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleActionPicker();
+ },
});
- voiceBtn.id = PREFIX + '-configure-voice';
- 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';
- if (!uiGetById(PREFIX + '-configure-input-style')) {
- 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::placeholder { color: ' + BP.textDim + '; opacity: 1; }' +
- '#' + PREFIX + '-configure-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 { animation: none; opacity: 1; } }' +
- '#' + PREFIX + '-configure-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
- uiAppendStyle(s);
- }
+ 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());
@@ -1873,81 +2354,33 @@
e.stopPropagation();
e.preventDefault();
input.blur();
- disableInlineEdit();
- hideBar();
- renderEditBadge('hidden');
- state = 'PICKING';
- syncPageChatFocus('configure-input-escape');
+ exitConfigureToPicking('configure-input-escape');
return;
}
- // Let arrow keys pass through to the element picker when the input is empty
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !input.value) return;
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ const voiceBtn = buildConfigureVoiceButton({
+ id: PREFIX + '-configure-voice',
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleConfigureVoice();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
+ const go = buildConfigureSubmitButton({
+ controlsLocked,
+ ariaLabel: 'Generate variants',
+ onClick: (e) => { e.stopPropagation(); handleGo(); },
+ });
+
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([action, count], voiceBtn, go));
syncConfigureInputChrome();
- // Variant count toggle
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, border-color 0.12s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.title = 'Variants: click to change';
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) count.title = 'Apply is still running';
- count.addEventListener('mouseenter', () => { if (!controlsLocked) { count.style.color = BP.text; count.style.borderColor = BP.text; } });
- count.addEventListener('mouseleave', () => { if (!controlsLocked) { count.style.color = BP.textDim; count.style.borderColor = BP.hairline; } });
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- // Go button
- const go = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- cursor: 'pointer',
- transition: 'filter 0.12s ease, transform 0.1s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- go.textContent = 'Go \u2192';
- go.disabled = controlsLocked;
- go.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- go.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) go.title = 'Apply is still running';
- go.addEventListener('mouseenter', () => { if (!controlsLocked) go.style.filter = 'brightness(1.1)'; });
- go.addEventListener('mouseleave', () => go.style.filter = 'none');
- go.addEventListener('mousedown', () => { if (!controlsLocked) go.style.transform = 'scale(0.97)'; });
- go.addEventListener('mouseup', () => go.style.transform = 'scale(1)');
- go.addEventListener('click', (e) => { e.stopPropagation(); handleGo(); });
- row.appendChild(go);
-
- // Auto-focus input after a beat
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
@@ -1956,34 +2389,20 @@
function buildInsertConfigureRow() {
const controlsLocked = pendingApplyInFlight === true;
const row = el('div', {
- display: 'flex', alignItems: 'center', gap: '6px',
+ 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 inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-insert-input-wrap';
- inputWrap.addEventListener('pointerdown', (e) => e.stopPropagation());
- inputWrap.addEventListener('mousedown', (e) => e.stopPropagation());
- inputWrap.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 = 'describe what to insert…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the new element');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1991,20 +2410,22 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
+ const count = buildConfigureCountControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ count.textContent = '\u00D7' + cycleSelectedCount();
+ if (count.matches(':hover')) {
+ showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));
+ }
+ },
});
- voiceBtn.id = PREFIX + '-insert-voice';
- 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';
+
+ inputShell.appendChild(buildSelectionPill({ el: selectedElement, controlsLocked }));
+ inputShell.appendChild(input);
+
+ ensureConfigureInputStyle();
input.addEventListener('input', () => syncInsertCreateButton());
input.addEventListener('pointerdown', (e) => e.stopPropagation());
@@ -2026,48 +2447,31 @@
}
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ 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();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
-
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer', flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- const create = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- flexShrink: '0', whiteSpace: 'nowrap',
+ 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.textContent = 'Create \u2192';
- create.disabled = controlsLocked;
create.addEventListener('mouseenter', () => {
if (controlsLocked) return;
if (isInsertCreateEnabled(create)) {
@@ -2077,15 +2481,10 @@
showInsertCreateTooltip(create, insertCreateDisabledReason(insertCreateGateState(input)));
});
create.addEventListener('mouseleave', hideInsertCreateTooltip);
- create.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- if (!isInsertCreateEnabled(create)) return;
- handleInsertCreate();
- });
- row.appendChild(create);
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([count], voiceBtn, create));
syncInsertCreateButton(create, input);
+ syncConfigureInputChrome();
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
}
@@ -2375,7 +2774,7 @@
position: 'fixed', zIndex: Z.picker,
display: 'none', opacity: '0',
transform: 'scale(0.96) translateY(4px)',
- transformOrigin: 'bottom left',
+ transformOrigin: 'bottom right',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1px solid ' + P.border,
@@ -2451,14 +2850,18 @@
chip.style.background = isActive ? P.accentSoft : 'transparent';
chip.style.color = isActive ? P.accent : P.text;
});
- // Position above the bar
+ // 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: barRect.left + 'px',
- display: 'block',
+ top: top + 'px',
+ left: left + 'px',
});
requestAnimationFrame(() => {
pickerEl.style.opacity = '1';
@@ -2941,7 +3344,7 @@
function enterEditingMode() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
- state = 'EDITING';
+ setLiveState('EDITING');
hideBar();
hideAnnotOverlay();
renderEditBadge('editing');
@@ -2974,7 +3377,7 @@
function cancelEditing() {
restoreInlineEditDrafts();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -2988,12 +3391,33 @@
hideAnnotOverlay();
clearAnnotations();
renderEditBadge('hidden');
- state = 'PICKING';
+ 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 ),
// climb to the nearest ancestor with one. The CLI uses tag+class together,
// so tag must come from the same node as the locator.
@@ -3215,7 +3639,7 @@
updatePendingCounter(stashResult.pendingCount || 0);
maybeShowFirstSaveToast();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -3895,6 +4319,12 @@
// Edit content badge - floating button at element top-right to enter EDITING mode
//
+ const EDIT_COPY_LABEL = 'Edit copy';
+ const EDIT_COPY_ICON =
+ '';
+
function usesShadowChromeRoot() {
const root = liveUiRoot();
return root && root !== document.body && root.host && root.host.id === PREFIX + '-root';
@@ -4045,7 +4475,7 @@
editBadgeProxyRoot.appendChild(proxy);
editBadgeProxyByTarget.set(target, proxy);
}
- proxy.title = target.title || target.textContent || 'Edit copy';
+ proxy.title = target.title || target.getAttribute('aria-label') || target.textContent || EDIT_COPY_LABEL;
styleEditBadgeProxy(proxy, target);
}
}
@@ -4085,13 +4515,16 @@
}
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, r.right - bw) + '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;
@@ -4129,16 +4562,36 @@
const disabled = mode === 'idle-disabled';
editBadgeEl.innerHTML = '';
const btn = document.createElement('button');
- btn.textContent = 'Edit copy';
- Object.assign(btn.style, calloutStyle(disabled ? MUTED : ACCENT, disabled ? HAIRLINE : ACCENT));
+ 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;
- btn.title = 'Edit copy is disabled while the current copy edit is applying';
+ 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', () => { btn.style.background = ACCENT; btn.style.color = PRIMARY_TEXT; });
- btn.addEventListener('mouseleave', () => { btn.style.background = SURFACE; btn.style.color = ACCENT; });
+ btn.addEventListener('mouseenter', () => showConfigureBarTooltip(btn, EDIT_COPY_LABEL));
+ btn.addEventListener('mouseleave', hideConfigureBarTooltip);
btn.onclick = enterEditingMode;
}
editBadgeEl.appendChild(btn);
@@ -4154,9 +4607,8 @@
cancel.onclick = cancelEditing;
const save = document.createElement('button');
save.textContent = 'Save';
- Object.assign(save.style, calloutStyle(ACCENT));
- save.addEventListener('mouseenter', () => { save.style.background = ACCENT; save.style.color = PRIMARY_TEXT; });
- save.addEventListener('mouseleave', () => { save.style.background = SURFACE; save.style.color = ACCENT; });
+ Object.assign(save.style, calloutStyle(PRIMARY_TEXT, ACCENT));
+ save.style.background = ACCENT;
save.onclick = applyEditing;
editBadgeEl.append(cancel, save);
}
@@ -4392,31 +4844,142 @@
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 = origContent.className;
- let liveEl = null;
+ const cls = normalizeElementClassName(origContent);
+ const candidates = [...document.getElementsByTagName(tag)];
+
if (origContent.id) {
- liveEl = document.getElementById(origContent.id);
- } else if (cls) {
- const candidates = [...document.getElementsByTagName(tag)];
- for (const c of candidates) {
- if (c.className === cls && !own(c)) { liveEl = c; break; }
- }
- if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
- if (expectedClasses.length > 0) {
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
- }
+ 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;
}
}
}
- return liveEl;
+
+ 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) {
@@ -4428,7 +4991,45 @@
const anchor = findInsertAnchorInDom();
if (anchor?.parentElement) return anchor;
}
- return findLiveElementForOriginalMarkup(manifest?.originalMarkup || manifest?.anchorMarkup || '');
+ 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);
+ }
}
function loadSvelteRuntime(runtimeModule) {
@@ -4595,7 +5196,7 @@
previewFile: manifestPath,
previewMode: 'svelte-component',
});
- if (state !== 'CYCLING') state = 'GENERATING';
+ if (state !== 'CYCLING') setLiveState('GENERATING');
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper && svelteComponentSession?.sessionId === sessionId) {
@@ -4605,7 +5206,7 @@
expectedVariants = arrivedVariants;
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants ? visibleVariant : 1;
await mountSvelteComponentVariant(visibleVariant || 1);
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
saveSession();
return;
@@ -4621,15 +5222,8 @@
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants
? visibleVariant
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
- showBar('generating');
- startScrollTracking();
- saveSession();
- queueCheckpoint('svelte_component_anchor_missing');
+ enterRecoveryWaitingForAnchor({ checkpointReason: 'svelte_component_anchor_missing', trackScroll: true });
waitForSvelteComponentTargetAndRetry({ manifestPath, sessionId, manifest });
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return;
}
@@ -4692,7 +5286,7 @@
}
selectedElement = mountTarget.firstElementChild || mountTarget;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -4741,6 +5335,7 @@
hideShaderOverlay();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
if (pendingSvelteComponentRetryObserver) { pendingSvelteComponentRetryObserver.disconnect(); pendingSvelteComponentRetryObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearSession();
clearHandled();
@@ -4750,7 +5345,7 @@
arrivedVariants = 0;
visibleVariant = 0;
selectedElement = null;
- state = 'PICKING';
+ setLiveState('PICKING');
hideBar();
if (message) showToast(message, 5000);
}
@@ -4799,21 +5394,26 @@
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
- const liveEl = findLiveElementForOriginalMarkup(origContent.outerHTML);
+ const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
- selectedElement = document.body;
- recoveryWaitingForAnchor = true;
- state = 'GENERATING';
- showBar('generating');
- saveSession();
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
+ 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.
@@ -4834,7 +5434,7 @@
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -5240,7 +5840,7 @@
if (expected > 0) expectedVariants = expected;
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
if (wrapper.dataset.impeccableMode === 'insert') finalizeInsertSession();
@@ -5326,7 +5926,8 @@
syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll();
restoreFromActiveSessions(msg.activeSessions, 'sse_connected');
- if (state === 'IDLE' && (pickActive || insertActive)) state = 'PICKING';
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('sse-connected');
break;
case 'agent_polling':
@@ -5352,7 +5953,7 @@
// Variants already arrived via HMR → normal transition.
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
if (state === 'GENERATING') {
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
disableInlineEdit();
refreshParamsPanel();
@@ -5402,7 +6003,7 @@
case 'error':
if (pendingAcceptedSession?.id && msg.id === pendingAcceptedSession.id) {
pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
updateBarContent('cycling');
showToast('Could not complete accept cleanup. Try Accept again.', 5000);
break;
@@ -5412,7 +6013,7 @@
showToast('Error: ' + msg.message, 5000);
hideBar();
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
break;
}
};
@@ -5450,7 +6051,7 @@
// transient disconnect as an explicit discard.
selectedElement = null;
selectedAction = 'impeccable';
- state = recoveryState;
+ setLiveState(recoveryState);
if (currentSessionId) saveSession();
}
@@ -5592,15 +6193,7 @@
&& !selectedElement.contains(e.target)
) {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- hideBar();
- stopScrollTracking();
- hideAnnotOverlay();
- clearAnnotations();
- renderEditBadge('hidden');
- state = 'PICKING';
- hoveredElement = null;
- hideHighlight();
- syncPageChatFocus('configure-outside-click');
+ exitConfigureToPicking('configure-outside-click', { clearHover: true });
return;
}
if (state === 'PICKING' && insertActive) {
@@ -5617,13 +6210,12 @@
hideInsertLine();
configureKind = 'insert';
selectedElement = placeholder;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
hideHighlight();
clearAnnotations();
showAnnotOverlay(placeholder);
showBar('configure');
startScrollTracking();
- syncPageInteractionCursor();
return;
}
if (state !== 'PICKING' || !pickActive) return;
@@ -5636,7 +6228,7 @@
e.preventDefault();
e.stopPropagation();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5721,6 +6313,16 @@
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;
@@ -5729,6 +6331,7 @@
deepActive
&& own(deepActive)
&& /^(INPUT|TEXTAREA|SELECT)$/.test(deepActive.tagName || '')
+ && !shouldPassthroughElementNav(deepActive, e)
) {
return;
}
@@ -5767,14 +6370,15 @@
if (state === 'EDITING') { cancelEditing(); return; }
if (state === 'CONFIGURING') {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- disableInlineEdit(); hideBar(); stopScrollTracking(); hideAnnotOverlay(); clearAnnotations(); renderEditBadge('hidden'); state = 'PICKING'; syncPageChatFocus('escape-from-configure'); 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(); state = 'IDLE'; }
+ else { hideHighlight(); setLiveState('IDLE'); }
return;
}
}
@@ -5798,7 +6402,7 @@
} else if (e.key === 'Enter') {
e.preventDefault();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5859,6 +6463,7 @@
// screenshot is uploaded (or capture fails - we still emit, just without
// screenshotPath).
const elForCapture = selectedElement;
+ pickedAnchorSnapshot = buildPickedAnchorSnapshot(elForCapture);
const captureRect = elForCapture.getBoundingClientRect();
const snapshot = {
comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })),
@@ -5879,7 +6484,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ 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
@@ -5904,7 +6509,7 @@
clearInsertPicking();
configureKind = 'replace';
selectedElement = null;
- state = insertActive ? 'PICKING' : 'IDLE';
+ setLiveState(insertActive ? 'PICKING' : 'IDLE');
hideHighlight();
syncPageChatFocus('insert-configure-cancel');
}
@@ -5954,7 +6559,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ setLiveState('GENERATING');
showBar('generating');
startScrollTracking();
saveSession();
@@ -6712,7 +7317,7 @@ void main() {
|| acceptWrapper?.dataset?.impeccablePreview === 'svelte-component';
const acceptedSnapshot = snapshotAcceptedVariantDom(acceptedSessionId, acceptedVariant);
- state = 'SAVING';
+ setLiveState('SAVING');
updateBarContent('saving');
pendingAcceptedSession = {
id: acceptedSessionId,
@@ -6727,7 +7332,7 @@ void main() {
.then(() => {})
.catch(() => {
if (pendingAcceptedSession?.id === acceptedSessionId) pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
showToast('Could not confirm accept with the live server. Session kept for recovery; try Accept again.', 5000);
});
@@ -6746,7 +7351,7 @@ void main() {
if (pending.isSvelteComponent) {
commitAcceptedSvelteComponentToDom(pending.id);
}
- state = 'CONFIRMED';
+ setLiveState('CONFIRMED');
updateBarContent('confirmed');
scheduleAcceptCleanup(pending);
return true;
@@ -6870,7 +7475,7 @@ void main() {
selectedAction = 'impeccable';
pendingAcceptedSession = null;
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
function commitAcceptedVariantToDom(sessionId, variantId) {
@@ -6922,6 +7527,7 @@ void main() {
currentPreviewFile = null;
currentPreviewMode = null;
recoveryWaitingForAnchor = false;
+ pickedAnchorSnapshot = null;
}
function rememberSessionFileMeta(meta = {}) {
@@ -6946,6 +7552,7 @@ void main() {
if (!saved) return;
rememberSessionFileMeta(saved);
if (saved.insertPlaceholder) insertPlaceholderSnapshot = saved.insertPlaceholder;
+ if (saved.pickedAnchor) pickedAnchorSnapshot = saved.pickedAnchor;
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
if (saved.previewMode) currentPreviewMode = saved.previewMode;
@@ -7012,9 +7619,10 @@ void main() {
|| clampVariantIndex(serverSession?.visibleVariant, arrivedVariants || expectedVariants)
|| (arrivedVariants > 0 ? 1 : 0);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
+ const restoredAnchor = findLiveElementFromAnchorSnapshot(pickedAnchorSnapshot);
+ selectedElement = restoredAnchor || document.body;
+ setLiveState('GENERATING');
+ recoveryWaitingForAnchor = !restoredAnchor;
showBar('generating');
startScrollTracking();
if (variantObserver) variantObserver.disconnect();
@@ -7030,7 +7638,6 @@ void main() {
return true;
}
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return true;
}
@@ -7059,6 +7666,7 @@ void main() {
pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues },
insertPlaceholder: insertPlaceholderSnapshot || undefined,
+ pickedAnchor: pickedAnchorSnapshot || undefined,
});
}
@@ -7118,6 +7726,7 @@ void main() {
hideHighlight();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearScrollY();
finalizeInsertSession();
@@ -7127,15 +7736,21 @@ void main() {
currentSessionId = null;
selectedAction = 'impeccable';
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
//
// Toast
//
+ function dismissToast() {
+ if (!toastEl) return;
+ toastEl.remove();
+ toastEl = null;
+ }
+
function showToast(message, duration) {
- if (toastEl) toastEl.remove();
+ 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
@@ -7224,7 +7839,7 @@ void main() {
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
selectedElement = resolveSvelteComponentAnchor()
|| wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
hideShaderOverlay();
showBar('cycling');
startScrollTracking();
@@ -7260,7 +7875,7 @@ void main() {
const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
const origEl = pickVariantContent(wrapper, 'original');
- state = resumedState;
+ setLiveState(resumedState);
if (isInsert && resumedState === 'GENERATING' && arrivedVariants === 0) {
selectedElement = ensureInsertPlaceholder() || findInsertAnchorInDom() || wrapper;
} else {
@@ -7419,6 +8034,8 @@ void main() {
let voiceCtx = null;
const PAGE_CHAT_COLLAPSED_W = '88px';
const PAGE_CHAT_PROCESSING_W = '76px';
+ 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(56% 0.032 82 / 0.78)';
@@ -7509,13 +8126,16 @@ void main() {
function syncPageChatChrome() {
if (!pageChatEl) return;
const P = pageChatPalette();
+ const inputFocused = pageChatInput && activeElementDeep() === pageChatInput;
pageChatEl.style.background = P.chatSurface;
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
+ 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 : P.textDim;
+ 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';
@@ -7686,24 +8306,44 @@ void main() {
function syncPageChatFocusRing() {
if (!pageChatEl || !pageChatInput) return;
const focused = activeElementDeep() === pageChatInput;
+ const typingReady = focused && !steerLocked;
pageChatEl.dataset.inputFocused = focused ? 'true' : 'false';
- const P = pageChatPalette();
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
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.color = steerLocked
- ? P.patinaPale
- : ((!pageChatExpanded && focused) ? P.patinaPale : P.textDim);
- }
- if (!pageChatExpanded) {
- pageChatInput.style.width = '0';
- pageChatInput.style.padding = '0';
- pageChatInput.style.opacity = '0';
- pageChatInput.style.pointerEvents = focused ? 'auto' : 'none';
- if (pageChatHint) pageChatHint.style.visibility = '';
+ 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) {
@@ -7723,6 +8363,7 @@ void main() {
try { window.focus(); } catch { /* embed may block */ }
try { pageChatInput.focus({ preventScroll: true }); } catch { pageChatInput.focus(); }
syncPageChatFocusRing();
+ syncPageChatChrome();
steerFocusLog('focusSteerChat result', {
reason,
before: steerFocusTargetLabel(before),
@@ -7769,6 +8410,7 @@ void main() {
pageChatEl.dataset.expanded = 'true';
pageChatEl.style.width = PAGE_CHAT_EXPANDED_W;
pageChatEl.style.cursor = steerLocked ? 'default' : 'text';
+ pageChatInput.placeholder = PAGE_CHAT_PLACEHOLDER_EXPANDED;
if (pageChatHint) {
pageChatHint.style.display = 'none';
pageChatHint.style.opacity = '0';
@@ -7780,6 +8422,20 @@ void main() {
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(); }
@@ -7938,7 +8594,9 @@ void main() {
if (pageChatEl) pageChatEl.dataset.voiceListening = listening ? 'true' : 'false';
syncPageChatChrome();
} else if (voiceCtx?.mode === 'configure') {
- const voiceBtn = uiGetById(PREFIX + '-configure-voice');
+ // 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';
@@ -8180,12 +8838,12 @@ void main() {
height: '28px', margin: '0 4px 0 ' + (GLOBAL_BAR_SECTION_GAP - GLOBAL_BAR_INNER_GAP) + 'px',
borderRadius: '7px',
background: P.chatSurface,
- border: '1px solid ' + P.hairline,
+ border: '1px solid transparent',
overflow: 'hidden',
cursor: 'pointer',
flexShrink: '0',
width: PAGE_CHAT_COLLAPSED_W,
- transition: 'border-color 0.15s ease',
+ transition: 'width 0.18s ease, border-color 0.15s ease',
});
pageChatEl.id = PREFIX + '-page-chat';
pageChatEl.dataset.expanded = 'false';
@@ -8211,13 +8869,14 @@ void main() {
pageChatInput = document.createElement('input');
pageChatInput.id = PREFIX + '-page-chat-input';
pageChatInput.type = 'text';
- pageChatInput.placeholder = 'Steer the page…';
+ 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',
});
@@ -8252,18 +8911,23 @@ void main() {
'#' + 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(63% 0.024 82); 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.024 82); }' +
'#' + PREFIX + '-page-chat-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
uiAppendStyle(s);
}
- pageChatEl.addEventListener('pointerdown', keepSteerPointerInside);
+ pageChatEl.addEventListener('pointerdown', (e) => {
+ keepSteerPointerInside(e);
+ if (steerLocked || pageChatVoiceBtn.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)) return;
- expandPageChat({ focus: false });
- focusPageChatInput('page-chat-click');
+ armPageChatForTyping({ expand: true, focus: true });
});
pageChatVoiceBtn.addEventListener('pointerdown', keepSteerPointerInside);
@@ -8286,7 +8950,9 @@ void main() {
});
pageChatInput.addEventListener('focus', () => {
+ steerInputWasFocused = true;
syncPageChatFocusRing();
+ syncPageChatChrome();
});
pageChatInput.addEventListener('blur', () => {
@@ -8953,14 +9619,15 @@ void main() {
cancelInsertConfigure();
return;
}
+ teardownConfigureChrome();
hideHighlight();
- hideBar();
hideActionPicker();
selectedElement = null;
+ hoveredElement = null;
configureKind = 'replace';
- if (state === 'PICKING' || state === 'CONFIGURING') state = 'IDLE';
+ if (state === 'PICKING' || state === 'CONFIGURING') setLiveState('IDLE');
} else {
- if (state === 'IDLE') state = 'PICKING';
+ if (state === 'IDLE') setLiveState('PICKING');
}
syncPageChatFocus('toggle-pick');
}
@@ -8976,10 +9643,10 @@ void main() {
selectedElement = null;
configureKind = 'replace';
if (state === 'CONFIGURING') cancelInsertConfigure();
- else if (state === 'IDLE' || state === 'PICKING') state = 'PICKING';
+ else if (state === 'IDLE' || state === 'PICKING') setLiveState('PICKING');
} else {
clearInsertPicking();
- if (state === 'PICKING' && !pickActive) state = 'IDLE';
+ if (state === 'PICKING' && !pickActive) setLiveState('IDLE');
}
saveInteractionPrefs();
updateGlobalBarState();
@@ -9060,6 +9727,7 @@ void main() {
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; }
@@ -9073,7 +9741,8 @@ void main() {
window.removeEventListener('message', onDetectMessage);
// Remove detection overlays
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
- state = 'IDLE';
+ setLiveState('IDLE');
+ document.getElementById(PREFIX + '-pick-cursor-style')?.remove();
window.__IMPECCABLE_LIVE_INIT__ = false;
console.log('[impeccable] Live mode exited.');
}
@@ -10250,6 +10919,8 @@ void main() {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('init-complete');
}
diff --git a/.rovodev/skills/impeccable/scripts/live/ui-core.mjs b/.rovodev/skills/impeccable/scripts/live/ui-core.mjs
index 4b7adde8b..bbb2c5c35 100644
--- a/.rovodev/skills/impeccable/scripts/live/ui-core.mjs
+++ b/.rovodev/skills/impeccable/scripts/live/ui-core.mjs
@@ -42,9 +42,10 @@ export const LIVE_UI_SURFACES = Object.freeze([
'impeccable-live-highlight',
'impeccable-live-tooltip',
'impeccable-live-bar',
- 'impeccable-live-configure-input-wrap',
+ 'impeccable-live-selection-pill',
'impeccable-live-input',
'impeccable-live-configure-voice',
+ 'impeccable-live-configure-bar-tooltip',
],
states: ['rest', 'hover', 'focus-visible', 'pressed', 'disabled'],
},
diff --git a/.trae-cn/skills/impeccable/scripts/live-browser.js b/.trae-cn/skills/impeccable/scripts/live-browser.js
index 9092338eb..539cd07d9 100644
--- a/.trae-cn/skills/impeccable/scripts/live-browser.js
+++ b/.trae-cn/skills/impeccable/scripts/live-browser.js
@@ -57,6 +57,7 @@
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 PICK_CURSOR_CLASS = PREFIX + '-pick-cursor';
const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;
const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
prefix: PREFIX,
@@ -98,7 +99,7 @@
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'] },
{ key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] },
- { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-configure-input-wrap', PREFIX + '-input', PREFIX + '-configure-voice'] },
+ { 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'] },
@@ -131,6 +132,8 @@
let currentPreviewFile = null;
let currentPreviewMode = null;
let recoveryWaitingForAnchor = false;
+ let pickedAnchorSnapshot = null;
+ let pendingVariantAnchorRetryObserver = null;
let pendingAcceptedSession = null;
let variantObserver = null;
let variantSelectionInFlight = false;
@@ -283,31 +286,53 @@
uiAppend(tooltipEl);
}
+ function shouldShowHighlightTagTooltip() {
+ // Configure/edit carry the tag in the bar selection pill — 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 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);
+ 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' });
- tooltipEl.style.transition = 'none';
- Object.assign(tooltipEl.style, { top: tipY, left: tipX, 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(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });
}
}
@@ -1010,8 +1035,8 @@
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: '6px',
- maxWidth: '520px', minWidth: '320px',
+ padding: '5px',
+ maxWidth: '560px', minWidth: '340px',
});
uiAppend(barEl);
defangOutsideHandlers(barEl);
@@ -1019,14 +1044,29 @@
function positionBar() {
if (!barEl) return;
- const anchor = resolveBarAnchor();
- if (!anchor) return;
- const r = anchor.getBoundingClientRect();
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.
@@ -1054,8 +1094,14 @@
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
+ 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(() => {
@@ -1075,6 +1121,7 @@
setTimeout(() => { if (barEl && hideSeq === barHideSeq) barEl.style.display = 'none'; }, 250);
hideActionPicker();
closeTunePopover();
+ hideConfigureBarTooltip();
if (state === 'EDITING') restoreInlineEditDrafts();
disableInlineEdit();
}
@@ -1083,36 +1130,472 @@
if (!barEl || barEl.style.display === 'none') return;
if (mode === 'cycling' && !ensureCyclingRenderable('update-bar')) return;
barEl.innerHTML = '';
- // Reset bar styling to the kinpaku picker palette
- barEl.style.background = BP.surface;
- barEl.style.border = '1px solid ' + BP.border;
- barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
- else if (mode === 'saving') barEl.appendChild(buildSavingRow());
- else if (mode === 'confirmed') {
- barEl.appendChild(buildConfirmedRow());
- barEl.style.background = 'oklch(95% 0.05 145)';
- barEl.style.border = '1px solid oklch(75% 0.12 145 / 0.4)';
+ 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
+ // 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 =
+ '';
+
+ 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 wrap = uiGetById(PREFIX + '-configure-input-wrap');
- const input = uiGetById(PREFIX + '-input');
- if (!wrap || !input) return;
+ 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;
- wrap.dataset.inputFocused = focused ? 'true' : 'false';
- wrap.dataset.voiceListening = (voiceListening && voiceCtx?.mode === 'configure') ? 'true' : 'false';
- wrap.style.borderColor = (voiceListening && voiceCtx?.mode === 'configure')
+ 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.hairline);
+ : (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.02em',
+ }));
+ 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)
@@ -1434,14 +1917,38 @@
let pageInteractionCursorActive = false;
- /** Page-level cursor while insert mode is choosing a before/after edge. */
+ function ensurePickCursorStyle() {
+ if (document.getElementById(PREFIX + '-pick-cursor-style')) return;
+ const style = document.createElement('style');
+ style.id = PREFIX + '-pick-cursor-style';
+ style.textContent =
+ 'html.' + PICK_CURSOR_CLASS + ' * { cursor: crosshair !important; }\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"],\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"] * { cursor: revert !important; }';
+ // Styles the host page, not the chrome - inside the adapter's shadow UI
+ // root (uiAppendStyle's target) these selectors would match nothing.
+ document.head.appendChild(style);
+ }
+
+ /** Page-level cursor while pick or insert mode is targeting page elements. */
function syncPageInteractionCursor() {
- let next = '';
+ const pickCursor = state === 'PICKING' && pickActive && !insertActive;
+ let axisCursor = '';
if (state === 'PICKING' && insertActive) {
- next = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
+ axisCursor = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
}
- if (next) {
- document.documentElement.style.cursor = next;
+
+ if (pickCursor) {
+ ensurePickCursorStyle();
+ document.documentElement.classList.add(PICK_CURSOR_CLASS);
+ document.documentElement.style.cursor = '';
+ pageInteractionCursorActive = true;
+ return;
+ }
+
+ document.documentElement.classList.remove(PICK_CURSOR_CLASS);
+ if (axisCursor) {
+ document.documentElement.style.cursor = axisCursor;
pageInteractionCursorActive = true;
} else if (pageInteractionCursorActive) {
document.documentElement.style.cursor = '';
@@ -1449,6 +1956,16 @@
}
}
+ /**
+ * 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;
+ syncPageInteractionCursor();
+ }
+
/** Element used to position the floating bar / shader during a session. */
function resolveBarAnchor() {
if (svelteComponentSession?.sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
@@ -1767,69 +2284,35 @@
}
}
+ /** 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: 'center', gap: '6px',
+ display: 'flex', alignItems: 'stretch', width: '100%', height: CONFIGURE_BAR_H,
});
- // Action pill - dark graphite chip (matches kinpaku-kit .live-demo-ctx-pill)
- const pill = el('button', {
- display: 'inline-flex', alignItems: 'center', gap: '4px',
- padding: '5px 10px', borderRadius: '6px',
- background: BP.chatSurface, color: BP.text,
- fontFamily: FONT, fontSize: '12px', fontWeight: '500',
- border: '1px solid ' + BP.hairline, cursor: 'pointer',
- transition: 'background 0.12s ease, border-color 0.12s ease, transform 0.1s ease',
- whiteSpace: 'nowrap', flexShrink: '0',
- });
- pill.textContent = actionLabel() + ' \u25BE';
- pill.disabled = controlsLocked;
- pill.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- pill.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) pill.title = 'Apply is still running';
- pill.addEventListener('mouseenter', () => {
- if (controlsLocked) return;
- pill.style.background = BP.accentSoft;
- pill.style.borderColor = BP.accent;
- });
- pill.addEventListener('mouseleave', () => {
- if (controlsLocked) return;
- pill.style.background = BP.chatSurface;
- pill.style.borderColor = BP.hairline;
- });
- pill.addEventListener('mousedown', () => { if (!controlsLocked) pill.style.transform = 'scale(0.97)'; });
- pill.addEventListener('mouseup', () => pill.style.transform = 'scale(1)');
- pill.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleActionPicker();
- });
- row.appendChild(pill);
-
- // Prompt field - same chat-surface chrome as the bottom Steer bar
- const inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-configure-input-wrap';
+ const inputShell = el('div', configureInputShellStyle());
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
- input.placeholder = selectedAction === 'impeccable' ? 'describe what you want…' : 'refine further (optional)…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the change');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1837,33 +2320,31 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, background 0.12s ease',
+ const action = buildConfigureActionControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleActionPicker();
+ },
});
- voiceBtn.id = PREFIX + '-configure-voice';
- 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';
- if (!uiGetById(PREFIX + '-configure-input-style')) {
- 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::placeholder { color: ' + BP.textDim + '; opacity: 1; }' +
- '#' + PREFIX + '-configure-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 { animation: none; opacity: 1; } }' +
- '#' + PREFIX + '-configure-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
- uiAppendStyle(s);
- }
+ 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());
@@ -1873,81 +2354,33 @@
e.stopPropagation();
e.preventDefault();
input.blur();
- disableInlineEdit();
- hideBar();
- renderEditBadge('hidden');
- state = 'PICKING';
- syncPageChatFocus('configure-input-escape');
+ exitConfigureToPicking('configure-input-escape');
return;
}
- // Let arrow keys pass through to the element picker when the input is empty
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !input.value) return;
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ const voiceBtn = buildConfigureVoiceButton({
+ id: PREFIX + '-configure-voice',
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleConfigureVoice();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
+ const go = buildConfigureSubmitButton({
+ controlsLocked,
+ ariaLabel: 'Generate variants',
+ onClick: (e) => { e.stopPropagation(); handleGo(); },
+ });
+
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([action, count], voiceBtn, go));
syncConfigureInputChrome();
- // Variant count toggle
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, border-color 0.12s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.title = 'Variants: click to change';
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) count.title = 'Apply is still running';
- count.addEventListener('mouseenter', () => { if (!controlsLocked) { count.style.color = BP.text; count.style.borderColor = BP.text; } });
- count.addEventListener('mouseleave', () => { if (!controlsLocked) { count.style.color = BP.textDim; count.style.borderColor = BP.hairline; } });
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- // Go button
- const go = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- cursor: 'pointer',
- transition: 'filter 0.12s ease, transform 0.1s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- go.textContent = 'Go \u2192';
- go.disabled = controlsLocked;
- go.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- go.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) go.title = 'Apply is still running';
- go.addEventListener('mouseenter', () => { if (!controlsLocked) go.style.filter = 'brightness(1.1)'; });
- go.addEventListener('mouseleave', () => go.style.filter = 'none');
- go.addEventListener('mousedown', () => { if (!controlsLocked) go.style.transform = 'scale(0.97)'; });
- go.addEventListener('mouseup', () => go.style.transform = 'scale(1)');
- go.addEventListener('click', (e) => { e.stopPropagation(); handleGo(); });
- row.appendChild(go);
-
- // Auto-focus input after a beat
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
@@ -1956,34 +2389,20 @@
function buildInsertConfigureRow() {
const controlsLocked = pendingApplyInFlight === true;
const row = el('div', {
- display: 'flex', alignItems: 'center', gap: '6px',
+ 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 inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-insert-input-wrap';
- inputWrap.addEventListener('pointerdown', (e) => e.stopPropagation());
- inputWrap.addEventListener('mousedown', (e) => e.stopPropagation());
- inputWrap.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 = 'describe what to insert…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the new element');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1991,20 +2410,22 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
+ const count = buildConfigureCountControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ count.textContent = '\u00D7' + cycleSelectedCount();
+ if (count.matches(':hover')) {
+ showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));
+ }
+ },
});
- voiceBtn.id = PREFIX + '-insert-voice';
- 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';
+
+ inputShell.appendChild(buildSelectionPill({ el: selectedElement, controlsLocked }));
+ inputShell.appendChild(input);
+
+ ensureConfigureInputStyle();
input.addEventListener('input', () => syncInsertCreateButton());
input.addEventListener('pointerdown', (e) => e.stopPropagation());
@@ -2026,48 +2447,31 @@
}
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ 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();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
-
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer', flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- const create = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- flexShrink: '0', whiteSpace: 'nowrap',
+ 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.textContent = 'Create \u2192';
- create.disabled = controlsLocked;
create.addEventListener('mouseenter', () => {
if (controlsLocked) return;
if (isInsertCreateEnabled(create)) {
@@ -2077,15 +2481,10 @@
showInsertCreateTooltip(create, insertCreateDisabledReason(insertCreateGateState(input)));
});
create.addEventListener('mouseleave', hideInsertCreateTooltip);
- create.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- if (!isInsertCreateEnabled(create)) return;
- handleInsertCreate();
- });
- row.appendChild(create);
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([count], voiceBtn, create));
syncInsertCreateButton(create, input);
+ syncConfigureInputChrome();
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
}
@@ -2375,7 +2774,7 @@
position: 'fixed', zIndex: Z.picker,
display: 'none', opacity: '0',
transform: 'scale(0.96) translateY(4px)',
- transformOrigin: 'bottom left',
+ transformOrigin: 'bottom right',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1px solid ' + P.border,
@@ -2451,14 +2850,18 @@
chip.style.background = isActive ? P.accentSoft : 'transparent';
chip.style.color = isActive ? P.accent : P.text;
});
- // Position above the bar
+ // 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: barRect.left + 'px',
- display: 'block',
+ top: top + 'px',
+ left: left + 'px',
});
requestAnimationFrame(() => {
pickerEl.style.opacity = '1';
@@ -2941,7 +3344,7 @@
function enterEditingMode() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
- state = 'EDITING';
+ setLiveState('EDITING');
hideBar();
hideAnnotOverlay();
renderEditBadge('editing');
@@ -2974,7 +3377,7 @@
function cancelEditing() {
restoreInlineEditDrafts();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -2988,12 +3391,33 @@
hideAnnotOverlay();
clearAnnotations();
renderEditBadge('hidden');
- state = 'PICKING';
+ 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 ),
// climb to the nearest ancestor with one. The CLI uses tag+class together,
// so tag must come from the same node as the locator.
@@ -3215,7 +3639,7 @@
updatePendingCounter(stashResult.pendingCount || 0);
maybeShowFirstSaveToast();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -3895,6 +4319,12 @@
// Edit content badge - floating button at element top-right to enter EDITING mode
//
+ const EDIT_COPY_LABEL = 'Edit copy';
+ const EDIT_COPY_ICON =
+ '';
+
function usesShadowChromeRoot() {
const root = liveUiRoot();
return root && root !== document.body && root.host && root.host.id === PREFIX + '-root';
@@ -4045,7 +4475,7 @@
editBadgeProxyRoot.appendChild(proxy);
editBadgeProxyByTarget.set(target, proxy);
}
- proxy.title = target.title || target.textContent || 'Edit copy';
+ proxy.title = target.title || target.getAttribute('aria-label') || target.textContent || EDIT_COPY_LABEL;
styleEditBadgeProxy(proxy, target);
}
}
@@ -4085,13 +4515,16 @@
}
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, r.right - bw) + '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;
@@ -4129,16 +4562,36 @@
const disabled = mode === 'idle-disabled';
editBadgeEl.innerHTML = '';
const btn = document.createElement('button');
- btn.textContent = 'Edit copy';
- Object.assign(btn.style, calloutStyle(disabled ? MUTED : ACCENT, disabled ? HAIRLINE : ACCENT));
+ 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;
- btn.title = 'Edit copy is disabled while the current copy edit is applying';
+ 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', () => { btn.style.background = ACCENT; btn.style.color = PRIMARY_TEXT; });
- btn.addEventListener('mouseleave', () => { btn.style.background = SURFACE; btn.style.color = ACCENT; });
+ btn.addEventListener('mouseenter', () => showConfigureBarTooltip(btn, EDIT_COPY_LABEL));
+ btn.addEventListener('mouseleave', hideConfigureBarTooltip);
btn.onclick = enterEditingMode;
}
editBadgeEl.appendChild(btn);
@@ -4154,9 +4607,8 @@
cancel.onclick = cancelEditing;
const save = document.createElement('button');
save.textContent = 'Save';
- Object.assign(save.style, calloutStyle(ACCENT));
- save.addEventListener('mouseenter', () => { save.style.background = ACCENT; save.style.color = PRIMARY_TEXT; });
- save.addEventListener('mouseleave', () => { save.style.background = SURFACE; save.style.color = ACCENT; });
+ Object.assign(save.style, calloutStyle(PRIMARY_TEXT, ACCENT));
+ save.style.background = ACCENT;
save.onclick = applyEditing;
editBadgeEl.append(cancel, save);
}
@@ -4392,31 +4844,142 @@
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 = origContent.className;
- let liveEl = null;
+ const cls = normalizeElementClassName(origContent);
+ const candidates = [...document.getElementsByTagName(tag)];
+
if (origContent.id) {
- liveEl = document.getElementById(origContent.id);
- } else if (cls) {
- const candidates = [...document.getElementsByTagName(tag)];
- for (const c of candidates) {
- if (c.className === cls && !own(c)) { liveEl = c; break; }
- }
- if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
- if (expectedClasses.length > 0) {
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
- }
+ 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;
}
}
}
- return liveEl;
+
+ 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) {
@@ -4428,7 +4991,45 @@
const anchor = findInsertAnchorInDom();
if (anchor?.parentElement) return anchor;
}
- return findLiveElementForOriginalMarkup(manifest?.originalMarkup || manifest?.anchorMarkup || '');
+ 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);
+ }
}
function loadSvelteRuntime(runtimeModule) {
@@ -4595,7 +5196,7 @@
previewFile: manifestPath,
previewMode: 'svelte-component',
});
- if (state !== 'CYCLING') state = 'GENERATING';
+ if (state !== 'CYCLING') setLiveState('GENERATING');
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper && svelteComponentSession?.sessionId === sessionId) {
@@ -4605,7 +5206,7 @@
expectedVariants = arrivedVariants;
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants ? visibleVariant : 1;
await mountSvelteComponentVariant(visibleVariant || 1);
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
saveSession();
return;
@@ -4621,15 +5222,8 @@
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants
? visibleVariant
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
- showBar('generating');
- startScrollTracking();
- saveSession();
- queueCheckpoint('svelte_component_anchor_missing');
+ enterRecoveryWaitingForAnchor({ checkpointReason: 'svelte_component_anchor_missing', trackScroll: true });
waitForSvelteComponentTargetAndRetry({ manifestPath, sessionId, manifest });
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return;
}
@@ -4692,7 +5286,7 @@
}
selectedElement = mountTarget.firstElementChild || mountTarget;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -4741,6 +5335,7 @@
hideShaderOverlay();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
if (pendingSvelteComponentRetryObserver) { pendingSvelteComponentRetryObserver.disconnect(); pendingSvelteComponentRetryObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearSession();
clearHandled();
@@ -4750,7 +5345,7 @@
arrivedVariants = 0;
visibleVariant = 0;
selectedElement = null;
- state = 'PICKING';
+ setLiveState('PICKING');
hideBar();
if (message) showToast(message, 5000);
}
@@ -4799,21 +5394,26 @@
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
- const liveEl = findLiveElementForOriginalMarkup(origContent.outerHTML);
+ const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
- selectedElement = document.body;
- recoveryWaitingForAnchor = true;
- state = 'GENERATING';
- showBar('generating');
- saveSession();
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
+ 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.
@@ -4834,7 +5434,7 @@
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -5240,7 +5840,7 @@
if (expected > 0) expectedVariants = expected;
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
if (wrapper.dataset.impeccableMode === 'insert') finalizeInsertSession();
@@ -5326,7 +5926,8 @@
syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll();
restoreFromActiveSessions(msg.activeSessions, 'sse_connected');
- if (state === 'IDLE' && (pickActive || insertActive)) state = 'PICKING';
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('sse-connected');
break;
case 'agent_polling':
@@ -5352,7 +5953,7 @@
// Variants already arrived via HMR → normal transition.
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
if (state === 'GENERATING') {
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
disableInlineEdit();
refreshParamsPanel();
@@ -5402,7 +6003,7 @@
case 'error':
if (pendingAcceptedSession?.id && msg.id === pendingAcceptedSession.id) {
pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
updateBarContent('cycling');
showToast('Could not complete accept cleanup. Try Accept again.', 5000);
break;
@@ -5412,7 +6013,7 @@
showToast('Error: ' + msg.message, 5000);
hideBar();
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
break;
}
};
@@ -5450,7 +6051,7 @@
// transient disconnect as an explicit discard.
selectedElement = null;
selectedAction = 'impeccable';
- state = recoveryState;
+ setLiveState(recoveryState);
if (currentSessionId) saveSession();
}
@@ -5592,15 +6193,7 @@
&& !selectedElement.contains(e.target)
) {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- hideBar();
- stopScrollTracking();
- hideAnnotOverlay();
- clearAnnotations();
- renderEditBadge('hidden');
- state = 'PICKING';
- hoveredElement = null;
- hideHighlight();
- syncPageChatFocus('configure-outside-click');
+ exitConfigureToPicking('configure-outside-click', { clearHover: true });
return;
}
if (state === 'PICKING' && insertActive) {
@@ -5617,13 +6210,12 @@
hideInsertLine();
configureKind = 'insert';
selectedElement = placeholder;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
hideHighlight();
clearAnnotations();
showAnnotOverlay(placeholder);
showBar('configure');
startScrollTracking();
- syncPageInteractionCursor();
return;
}
if (state !== 'PICKING' || !pickActive) return;
@@ -5636,7 +6228,7 @@
e.preventDefault();
e.stopPropagation();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5721,6 +6313,16 @@
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;
@@ -5729,6 +6331,7 @@
deepActive
&& own(deepActive)
&& /^(INPUT|TEXTAREA|SELECT)$/.test(deepActive.tagName || '')
+ && !shouldPassthroughElementNav(deepActive, e)
) {
return;
}
@@ -5767,14 +6370,15 @@
if (state === 'EDITING') { cancelEditing(); return; }
if (state === 'CONFIGURING') {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- disableInlineEdit(); hideBar(); stopScrollTracking(); hideAnnotOverlay(); clearAnnotations(); renderEditBadge('hidden'); state = 'PICKING'; syncPageChatFocus('escape-from-configure'); 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(); state = 'IDLE'; }
+ else { hideHighlight(); setLiveState('IDLE'); }
return;
}
}
@@ -5798,7 +6402,7 @@
} else if (e.key === 'Enter') {
e.preventDefault();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5859,6 +6463,7 @@
// screenshot is uploaded (or capture fails - we still emit, just without
// screenshotPath).
const elForCapture = selectedElement;
+ pickedAnchorSnapshot = buildPickedAnchorSnapshot(elForCapture);
const captureRect = elForCapture.getBoundingClientRect();
const snapshot = {
comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })),
@@ -5879,7 +6484,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ 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
@@ -5904,7 +6509,7 @@
clearInsertPicking();
configureKind = 'replace';
selectedElement = null;
- state = insertActive ? 'PICKING' : 'IDLE';
+ setLiveState(insertActive ? 'PICKING' : 'IDLE');
hideHighlight();
syncPageChatFocus('insert-configure-cancel');
}
@@ -5954,7 +6559,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ setLiveState('GENERATING');
showBar('generating');
startScrollTracking();
saveSession();
@@ -6712,7 +7317,7 @@ void main() {
|| acceptWrapper?.dataset?.impeccablePreview === 'svelte-component';
const acceptedSnapshot = snapshotAcceptedVariantDom(acceptedSessionId, acceptedVariant);
- state = 'SAVING';
+ setLiveState('SAVING');
updateBarContent('saving');
pendingAcceptedSession = {
id: acceptedSessionId,
@@ -6727,7 +7332,7 @@ void main() {
.then(() => {})
.catch(() => {
if (pendingAcceptedSession?.id === acceptedSessionId) pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
showToast('Could not confirm accept with the live server. Session kept for recovery; try Accept again.', 5000);
});
@@ -6746,7 +7351,7 @@ void main() {
if (pending.isSvelteComponent) {
commitAcceptedSvelteComponentToDom(pending.id);
}
- state = 'CONFIRMED';
+ setLiveState('CONFIRMED');
updateBarContent('confirmed');
scheduleAcceptCleanup(pending);
return true;
@@ -6870,7 +7475,7 @@ void main() {
selectedAction = 'impeccable';
pendingAcceptedSession = null;
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
function commitAcceptedVariantToDom(sessionId, variantId) {
@@ -6922,6 +7527,7 @@ void main() {
currentPreviewFile = null;
currentPreviewMode = null;
recoveryWaitingForAnchor = false;
+ pickedAnchorSnapshot = null;
}
function rememberSessionFileMeta(meta = {}) {
@@ -6946,6 +7552,7 @@ void main() {
if (!saved) return;
rememberSessionFileMeta(saved);
if (saved.insertPlaceholder) insertPlaceholderSnapshot = saved.insertPlaceholder;
+ if (saved.pickedAnchor) pickedAnchorSnapshot = saved.pickedAnchor;
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
if (saved.previewMode) currentPreviewMode = saved.previewMode;
@@ -7012,9 +7619,10 @@ void main() {
|| clampVariantIndex(serverSession?.visibleVariant, arrivedVariants || expectedVariants)
|| (arrivedVariants > 0 ? 1 : 0);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
+ const restoredAnchor = findLiveElementFromAnchorSnapshot(pickedAnchorSnapshot);
+ selectedElement = restoredAnchor || document.body;
+ setLiveState('GENERATING');
+ recoveryWaitingForAnchor = !restoredAnchor;
showBar('generating');
startScrollTracking();
if (variantObserver) variantObserver.disconnect();
@@ -7030,7 +7638,6 @@ void main() {
return true;
}
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return true;
}
@@ -7059,6 +7666,7 @@ void main() {
pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues },
insertPlaceholder: insertPlaceholderSnapshot || undefined,
+ pickedAnchor: pickedAnchorSnapshot || undefined,
});
}
@@ -7118,6 +7726,7 @@ void main() {
hideHighlight();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearScrollY();
finalizeInsertSession();
@@ -7127,15 +7736,21 @@ void main() {
currentSessionId = null;
selectedAction = 'impeccable';
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
//
// Toast
//
+ function dismissToast() {
+ if (!toastEl) return;
+ toastEl.remove();
+ toastEl = null;
+ }
+
function showToast(message, duration) {
- if (toastEl) toastEl.remove();
+ 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
@@ -7224,7 +7839,7 @@ void main() {
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
selectedElement = resolveSvelteComponentAnchor()
|| wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
hideShaderOverlay();
showBar('cycling');
startScrollTracking();
@@ -7260,7 +7875,7 @@ void main() {
const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
const origEl = pickVariantContent(wrapper, 'original');
- state = resumedState;
+ setLiveState(resumedState);
if (isInsert && resumedState === 'GENERATING' && arrivedVariants === 0) {
selectedElement = ensureInsertPlaceholder() || findInsertAnchorInDom() || wrapper;
} else {
@@ -7419,6 +8034,8 @@ void main() {
let voiceCtx = null;
const PAGE_CHAT_COLLAPSED_W = '88px';
const PAGE_CHAT_PROCESSING_W = '76px';
+ 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(56% 0.032 82 / 0.78)';
@@ -7509,13 +8126,16 @@ void main() {
function syncPageChatChrome() {
if (!pageChatEl) return;
const P = pageChatPalette();
+ const inputFocused = pageChatInput && activeElementDeep() === pageChatInput;
pageChatEl.style.background = P.chatSurface;
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
+ 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 : P.textDim;
+ 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';
@@ -7686,24 +8306,44 @@ void main() {
function syncPageChatFocusRing() {
if (!pageChatEl || !pageChatInput) return;
const focused = activeElementDeep() === pageChatInput;
+ const typingReady = focused && !steerLocked;
pageChatEl.dataset.inputFocused = focused ? 'true' : 'false';
- const P = pageChatPalette();
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
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.color = steerLocked
- ? P.patinaPale
- : ((!pageChatExpanded && focused) ? P.patinaPale : P.textDim);
- }
- if (!pageChatExpanded) {
- pageChatInput.style.width = '0';
- pageChatInput.style.padding = '0';
- pageChatInput.style.opacity = '0';
- pageChatInput.style.pointerEvents = focused ? 'auto' : 'none';
- if (pageChatHint) pageChatHint.style.visibility = '';
+ 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) {
@@ -7723,6 +8363,7 @@ void main() {
try { window.focus(); } catch { /* embed may block */ }
try { pageChatInput.focus({ preventScroll: true }); } catch { pageChatInput.focus(); }
syncPageChatFocusRing();
+ syncPageChatChrome();
steerFocusLog('focusSteerChat result', {
reason,
before: steerFocusTargetLabel(before),
@@ -7769,6 +8410,7 @@ void main() {
pageChatEl.dataset.expanded = 'true';
pageChatEl.style.width = PAGE_CHAT_EXPANDED_W;
pageChatEl.style.cursor = steerLocked ? 'default' : 'text';
+ pageChatInput.placeholder = PAGE_CHAT_PLACEHOLDER_EXPANDED;
if (pageChatHint) {
pageChatHint.style.display = 'none';
pageChatHint.style.opacity = '0';
@@ -7780,6 +8422,20 @@ void main() {
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(); }
@@ -7938,7 +8594,9 @@ void main() {
if (pageChatEl) pageChatEl.dataset.voiceListening = listening ? 'true' : 'false';
syncPageChatChrome();
} else if (voiceCtx?.mode === 'configure') {
- const voiceBtn = uiGetById(PREFIX + '-configure-voice');
+ // 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';
@@ -8180,12 +8838,12 @@ void main() {
height: '28px', margin: '0 4px 0 ' + (GLOBAL_BAR_SECTION_GAP - GLOBAL_BAR_INNER_GAP) + 'px',
borderRadius: '7px',
background: P.chatSurface,
- border: '1px solid ' + P.hairline,
+ border: '1px solid transparent',
overflow: 'hidden',
cursor: 'pointer',
flexShrink: '0',
width: PAGE_CHAT_COLLAPSED_W,
- transition: 'border-color 0.15s ease',
+ transition: 'width 0.18s ease, border-color 0.15s ease',
});
pageChatEl.id = PREFIX + '-page-chat';
pageChatEl.dataset.expanded = 'false';
@@ -8211,13 +8869,14 @@ void main() {
pageChatInput = document.createElement('input');
pageChatInput.id = PREFIX + '-page-chat-input';
pageChatInput.type = 'text';
- pageChatInput.placeholder = 'Steer the page…';
+ 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',
});
@@ -8252,18 +8911,23 @@ void main() {
'#' + 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(63% 0.024 82); 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.024 82); }' +
'#' + PREFIX + '-page-chat-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
uiAppendStyle(s);
}
- pageChatEl.addEventListener('pointerdown', keepSteerPointerInside);
+ pageChatEl.addEventListener('pointerdown', (e) => {
+ keepSteerPointerInside(e);
+ if (steerLocked || pageChatVoiceBtn.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)) return;
- expandPageChat({ focus: false });
- focusPageChatInput('page-chat-click');
+ armPageChatForTyping({ expand: true, focus: true });
});
pageChatVoiceBtn.addEventListener('pointerdown', keepSteerPointerInside);
@@ -8286,7 +8950,9 @@ void main() {
});
pageChatInput.addEventListener('focus', () => {
+ steerInputWasFocused = true;
syncPageChatFocusRing();
+ syncPageChatChrome();
});
pageChatInput.addEventListener('blur', () => {
@@ -8953,14 +9619,15 @@ void main() {
cancelInsertConfigure();
return;
}
+ teardownConfigureChrome();
hideHighlight();
- hideBar();
hideActionPicker();
selectedElement = null;
+ hoveredElement = null;
configureKind = 'replace';
- if (state === 'PICKING' || state === 'CONFIGURING') state = 'IDLE';
+ if (state === 'PICKING' || state === 'CONFIGURING') setLiveState('IDLE');
} else {
- if (state === 'IDLE') state = 'PICKING';
+ if (state === 'IDLE') setLiveState('PICKING');
}
syncPageChatFocus('toggle-pick');
}
@@ -8976,10 +9643,10 @@ void main() {
selectedElement = null;
configureKind = 'replace';
if (state === 'CONFIGURING') cancelInsertConfigure();
- else if (state === 'IDLE' || state === 'PICKING') state = 'PICKING';
+ else if (state === 'IDLE' || state === 'PICKING') setLiveState('PICKING');
} else {
clearInsertPicking();
- if (state === 'PICKING' && !pickActive) state = 'IDLE';
+ if (state === 'PICKING' && !pickActive) setLiveState('IDLE');
}
saveInteractionPrefs();
updateGlobalBarState();
@@ -9060,6 +9727,7 @@ void main() {
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; }
@@ -9073,7 +9741,8 @@ void main() {
window.removeEventListener('message', onDetectMessage);
// Remove detection overlays
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
- state = 'IDLE';
+ setLiveState('IDLE');
+ document.getElementById(PREFIX + '-pick-cursor-style')?.remove();
window.__IMPECCABLE_LIVE_INIT__ = false;
console.log('[impeccable] Live mode exited.');
}
@@ -10250,6 +10919,8 @@ void main() {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('init-complete');
}
diff --git a/.trae-cn/skills/impeccable/scripts/live/ui-core.mjs b/.trae-cn/skills/impeccable/scripts/live/ui-core.mjs
index 4b7adde8b..bbb2c5c35 100644
--- a/.trae-cn/skills/impeccable/scripts/live/ui-core.mjs
+++ b/.trae-cn/skills/impeccable/scripts/live/ui-core.mjs
@@ -42,9 +42,10 @@ export const LIVE_UI_SURFACES = Object.freeze([
'impeccable-live-highlight',
'impeccable-live-tooltip',
'impeccable-live-bar',
- 'impeccable-live-configure-input-wrap',
+ 'impeccable-live-selection-pill',
'impeccable-live-input',
'impeccable-live-configure-voice',
+ 'impeccable-live-configure-bar-tooltip',
],
states: ['rest', 'hover', 'focus-visible', 'pressed', 'disabled'],
},
diff --git a/.trae/skills/impeccable/scripts/live-browser.js b/.trae/skills/impeccable/scripts/live-browser.js
index 9092338eb..539cd07d9 100644
--- a/.trae/skills/impeccable/scripts/live-browser.js
+++ b/.trae/skills/impeccable/scripts/live-browser.js
@@ -57,6 +57,7 @@
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 PICK_CURSOR_CLASS = PREFIX + '-pick-cursor';
const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;
const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
prefix: PREFIX,
@@ -98,7 +99,7 @@
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'] },
{ key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] },
- { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-configure-input-wrap', PREFIX + '-input', PREFIX + '-configure-voice'] },
+ { 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'] },
@@ -131,6 +132,8 @@
let currentPreviewFile = null;
let currentPreviewMode = null;
let recoveryWaitingForAnchor = false;
+ let pickedAnchorSnapshot = null;
+ let pendingVariantAnchorRetryObserver = null;
let pendingAcceptedSession = null;
let variantObserver = null;
let variantSelectionInFlight = false;
@@ -283,31 +286,53 @@
uiAppend(tooltipEl);
}
+ function shouldShowHighlightTagTooltip() {
+ // Configure/edit carry the tag in the bar selection pill — 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 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);
+ 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' });
- tooltipEl.style.transition = 'none';
- Object.assign(tooltipEl.style, { top: tipY, left: tipX, 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(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });
}
}
@@ -1010,8 +1035,8 @@
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: '6px',
- maxWidth: '520px', minWidth: '320px',
+ padding: '5px',
+ maxWidth: '560px', minWidth: '340px',
});
uiAppend(barEl);
defangOutsideHandlers(barEl);
@@ -1019,14 +1044,29 @@
function positionBar() {
if (!barEl) return;
- const anchor = resolveBarAnchor();
- if (!anchor) return;
- const r = anchor.getBoundingClientRect();
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.
@@ -1054,8 +1094,14 @@
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
+ 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(() => {
@@ -1075,6 +1121,7 @@
setTimeout(() => { if (barEl && hideSeq === barHideSeq) barEl.style.display = 'none'; }, 250);
hideActionPicker();
closeTunePopover();
+ hideConfigureBarTooltip();
if (state === 'EDITING') restoreInlineEditDrafts();
disableInlineEdit();
}
@@ -1083,36 +1130,472 @@
if (!barEl || barEl.style.display === 'none') return;
if (mode === 'cycling' && !ensureCyclingRenderable('update-bar')) return;
barEl.innerHTML = '';
- // Reset bar styling to the kinpaku picker palette
- barEl.style.background = BP.surface;
- barEl.style.border = '1px solid ' + BP.border;
- barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
- else if (mode === 'saving') barEl.appendChild(buildSavingRow());
- else if (mode === 'confirmed') {
- barEl.appendChild(buildConfirmedRow());
- barEl.style.background = 'oklch(95% 0.05 145)';
- barEl.style.border = '1px solid oklch(75% 0.12 145 / 0.4)';
+ 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
+ // 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 =
+ '';
+
+ 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 wrap = uiGetById(PREFIX + '-configure-input-wrap');
- const input = uiGetById(PREFIX + '-input');
- if (!wrap || !input) return;
+ 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;
- wrap.dataset.inputFocused = focused ? 'true' : 'false';
- wrap.dataset.voiceListening = (voiceListening && voiceCtx?.mode === 'configure') ? 'true' : 'false';
- wrap.style.borderColor = (voiceListening && voiceCtx?.mode === 'configure')
+ 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.hairline);
+ : (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.02em',
+ }));
+ 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)
@@ -1434,14 +1917,38 @@
let pageInteractionCursorActive = false;
- /** Page-level cursor while insert mode is choosing a before/after edge. */
+ function ensurePickCursorStyle() {
+ if (document.getElementById(PREFIX + '-pick-cursor-style')) return;
+ const style = document.createElement('style');
+ style.id = PREFIX + '-pick-cursor-style';
+ style.textContent =
+ 'html.' + PICK_CURSOR_CLASS + ' * { cursor: crosshair !important; }\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"],\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"] * { cursor: revert !important; }';
+ // Styles the host page, not the chrome - inside the adapter's shadow UI
+ // root (uiAppendStyle's target) these selectors would match nothing.
+ document.head.appendChild(style);
+ }
+
+ /** Page-level cursor while pick or insert mode is targeting page elements. */
function syncPageInteractionCursor() {
- let next = '';
+ const pickCursor = state === 'PICKING' && pickActive && !insertActive;
+ let axisCursor = '';
if (state === 'PICKING' && insertActive) {
- next = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
+ axisCursor = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
}
- if (next) {
- document.documentElement.style.cursor = next;
+
+ if (pickCursor) {
+ ensurePickCursorStyle();
+ document.documentElement.classList.add(PICK_CURSOR_CLASS);
+ document.documentElement.style.cursor = '';
+ pageInteractionCursorActive = true;
+ return;
+ }
+
+ document.documentElement.classList.remove(PICK_CURSOR_CLASS);
+ if (axisCursor) {
+ document.documentElement.style.cursor = axisCursor;
pageInteractionCursorActive = true;
} else if (pageInteractionCursorActive) {
document.documentElement.style.cursor = '';
@@ -1449,6 +1956,16 @@
}
}
+ /**
+ * 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;
+ syncPageInteractionCursor();
+ }
+
/** Element used to position the floating bar / shader during a session. */
function resolveBarAnchor() {
if (svelteComponentSession?.sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
@@ -1767,69 +2284,35 @@
}
}
+ /** 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: 'center', gap: '6px',
+ display: 'flex', alignItems: 'stretch', width: '100%', height: CONFIGURE_BAR_H,
});
- // Action pill - dark graphite chip (matches kinpaku-kit .live-demo-ctx-pill)
- const pill = el('button', {
- display: 'inline-flex', alignItems: 'center', gap: '4px',
- padding: '5px 10px', borderRadius: '6px',
- background: BP.chatSurface, color: BP.text,
- fontFamily: FONT, fontSize: '12px', fontWeight: '500',
- border: '1px solid ' + BP.hairline, cursor: 'pointer',
- transition: 'background 0.12s ease, border-color 0.12s ease, transform 0.1s ease',
- whiteSpace: 'nowrap', flexShrink: '0',
- });
- pill.textContent = actionLabel() + ' \u25BE';
- pill.disabled = controlsLocked;
- pill.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- pill.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) pill.title = 'Apply is still running';
- pill.addEventListener('mouseenter', () => {
- if (controlsLocked) return;
- pill.style.background = BP.accentSoft;
- pill.style.borderColor = BP.accent;
- });
- pill.addEventListener('mouseleave', () => {
- if (controlsLocked) return;
- pill.style.background = BP.chatSurface;
- pill.style.borderColor = BP.hairline;
- });
- pill.addEventListener('mousedown', () => { if (!controlsLocked) pill.style.transform = 'scale(0.97)'; });
- pill.addEventListener('mouseup', () => pill.style.transform = 'scale(1)');
- pill.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleActionPicker();
- });
- row.appendChild(pill);
-
- // Prompt field - same chat-surface chrome as the bottom Steer bar
- const inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-configure-input-wrap';
+ const inputShell = el('div', configureInputShellStyle());
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
- input.placeholder = selectedAction === 'impeccable' ? 'describe what you want…' : 'refine further (optional)…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the change');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1837,33 +2320,31 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, background 0.12s ease',
+ const action = buildConfigureActionControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleActionPicker();
+ },
});
- voiceBtn.id = PREFIX + '-configure-voice';
- 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';
- if (!uiGetById(PREFIX + '-configure-input-style')) {
- 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::placeholder { color: ' + BP.textDim + '; opacity: 1; }' +
- '#' + PREFIX + '-configure-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 { animation: none; opacity: 1; } }' +
- '#' + PREFIX + '-configure-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
- uiAppendStyle(s);
- }
+ 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());
@@ -1873,81 +2354,33 @@
e.stopPropagation();
e.preventDefault();
input.blur();
- disableInlineEdit();
- hideBar();
- renderEditBadge('hidden');
- state = 'PICKING';
- syncPageChatFocus('configure-input-escape');
+ exitConfigureToPicking('configure-input-escape');
return;
}
- // Let arrow keys pass through to the element picker when the input is empty
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !input.value) return;
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ const voiceBtn = buildConfigureVoiceButton({
+ id: PREFIX + '-configure-voice',
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleConfigureVoice();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
+ const go = buildConfigureSubmitButton({
+ controlsLocked,
+ ariaLabel: 'Generate variants',
+ onClick: (e) => { e.stopPropagation(); handleGo(); },
+ });
+
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([action, count], voiceBtn, go));
syncConfigureInputChrome();
- // Variant count toggle
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, border-color 0.12s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.title = 'Variants: click to change';
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) count.title = 'Apply is still running';
- count.addEventListener('mouseenter', () => { if (!controlsLocked) { count.style.color = BP.text; count.style.borderColor = BP.text; } });
- count.addEventListener('mouseleave', () => { if (!controlsLocked) { count.style.color = BP.textDim; count.style.borderColor = BP.hairline; } });
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- // Go button
- const go = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- cursor: 'pointer',
- transition: 'filter 0.12s ease, transform 0.1s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- go.textContent = 'Go \u2192';
- go.disabled = controlsLocked;
- go.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- go.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) go.title = 'Apply is still running';
- go.addEventListener('mouseenter', () => { if (!controlsLocked) go.style.filter = 'brightness(1.1)'; });
- go.addEventListener('mouseleave', () => go.style.filter = 'none');
- go.addEventListener('mousedown', () => { if (!controlsLocked) go.style.transform = 'scale(0.97)'; });
- go.addEventListener('mouseup', () => go.style.transform = 'scale(1)');
- go.addEventListener('click', (e) => { e.stopPropagation(); handleGo(); });
- row.appendChild(go);
-
- // Auto-focus input after a beat
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
@@ -1956,34 +2389,20 @@
function buildInsertConfigureRow() {
const controlsLocked = pendingApplyInFlight === true;
const row = el('div', {
- display: 'flex', alignItems: 'center', gap: '6px',
+ 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 inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-insert-input-wrap';
- inputWrap.addEventListener('pointerdown', (e) => e.stopPropagation());
- inputWrap.addEventListener('mousedown', (e) => e.stopPropagation());
- inputWrap.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 = 'describe what to insert…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the new element');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1991,20 +2410,22 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
+ const count = buildConfigureCountControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ count.textContent = '\u00D7' + cycleSelectedCount();
+ if (count.matches(':hover')) {
+ showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));
+ }
+ },
});
- voiceBtn.id = PREFIX + '-insert-voice';
- 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';
+
+ inputShell.appendChild(buildSelectionPill({ el: selectedElement, controlsLocked }));
+ inputShell.appendChild(input);
+
+ ensureConfigureInputStyle();
input.addEventListener('input', () => syncInsertCreateButton());
input.addEventListener('pointerdown', (e) => e.stopPropagation());
@@ -2026,48 +2447,31 @@
}
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ 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();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
-
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer', flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- const create = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- flexShrink: '0', whiteSpace: 'nowrap',
+ 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.textContent = 'Create \u2192';
- create.disabled = controlsLocked;
create.addEventListener('mouseenter', () => {
if (controlsLocked) return;
if (isInsertCreateEnabled(create)) {
@@ -2077,15 +2481,10 @@
showInsertCreateTooltip(create, insertCreateDisabledReason(insertCreateGateState(input)));
});
create.addEventListener('mouseleave', hideInsertCreateTooltip);
- create.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- if (!isInsertCreateEnabled(create)) return;
- handleInsertCreate();
- });
- row.appendChild(create);
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([count], voiceBtn, create));
syncInsertCreateButton(create, input);
+ syncConfigureInputChrome();
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
}
@@ -2375,7 +2774,7 @@
position: 'fixed', zIndex: Z.picker,
display: 'none', opacity: '0',
transform: 'scale(0.96) translateY(4px)',
- transformOrigin: 'bottom left',
+ transformOrigin: 'bottom right',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1px solid ' + P.border,
@@ -2451,14 +2850,18 @@
chip.style.background = isActive ? P.accentSoft : 'transparent';
chip.style.color = isActive ? P.accent : P.text;
});
- // Position above the bar
+ // 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: barRect.left + 'px',
- display: 'block',
+ top: top + 'px',
+ left: left + 'px',
});
requestAnimationFrame(() => {
pickerEl.style.opacity = '1';
@@ -2941,7 +3344,7 @@
function enterEditingMode() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
- state = 'EDITING';
+ setLiveState('EDITING');
hideBar();
hideAnnotOverlay();
renderEditBadge('editing');
@@ -2974,7 +3377,7 @@
function cancelEditing() {
restoreInlineEditDrafts();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -2988,12 +3391,33 @@
hideAnnotOverlay();
clearAnnotations();
renderEditBadge('hidden');
- state = 'PICKING';
+ 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 ),
// climb to the nearest ancestor with one. The CLI uses tag+class together,
// so tag must come from the same node as the locator.
@@ -3215,7 +3639,7 @@
updatePendingCounter(stashResult.pendingCount || 0);
maybeShowFirstSaveToast();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -3895,6 +4319,12 @@
// Edit content badge - floating button at element top-right to enter EDITING mode
//
+ const EDIT_COPY_LABEL = 'Edit copy';
+ const EDIT_COPY_ICON =
+ '';
+
function usesShadowChromeRoot() {
const root = liveUiRoot();
return root && root !== document.body && root.host && root.host.id === PREFIX + '-root';
@@ -4045,7 +4475,7 @@
editBadgeProxyRoot.appendChild(proxy);
editBadgeProxyByTarget.set(target, proxy);
}
- proxy.title = target.title || target.textContent || 'Edit copy';
+ proxy.title = target.title || target.getAttribute('aria-label') || target.textContent || EDIT_COPY_LABEL;
styleEditBadgeProxy(proxy, target);
}
}
@@ -4085,13 +4515,16 @@
}
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, r.right - bw) + '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;
@@ -4129,16 +4562,36 @@
const disabled = mode === 'idle-disabled';
editBadgeEl.innerHTML = '';
const btn = document.createElement('button');
- btn.textContent = 'Edit copy';
- Object.assign(btn.style, calloutStyle(disabled ? MUTED : ACCENT, disabled ? HAIRLINE : ACCENT));
+ 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;
- btn.title = 'Edit copy is disabled while the current copy edit is applying';
+ 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', () => { btn.style.background = ACCENT; btn.style.color = PRIMARY_TEXT; });
- btn.addEventListener('mouseleave', () => { btn.style.background = SURFACE; btn.style.color = ACCENT; });
+ btn.addEventListener('mouseenter', () => showConfigureBarTooltip(btn, EDIT_COPY_LABEL));
+ btn.addEventListener('mouseleave', hideConfigureBarTooltip);
btn.onclick = enterEditingMode;
}
editBadgeEl.appendChild(btn);
@@ -4154,9 +4607,8 @@
cancel.onclick = cancelEditing;
const save = document.createElement('button');
save.textContent = 'Save';
- Object.assign(save.style, calloutStyle(ACCENT));
- save.addEventListener('mouseenter', () => { save.style.background = ACCENT; save.style.color = PRIMARY_TEXT; });
- save.addEventListener('mouseleave', () => { save.style.background = SURFACE; save.style.color = ACCENT; });
+ Object.assign(save.style, calloutStyle(PRIMARY_TEXT, ACCENT));
+ save.style.background = ACCENT;
save.onclick = applyEditing;
editBadgeEl.append(cancel, save);
}
@@ -4392,31 +4844,142 @@
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 = origContent.className;
- let liveEl = null;
+ const cls = normalizeElementClassName(origContent);
+ const candidates = [...document.getElementsByTagName(tag)];
+
if (origContent.id) {
- liveEl = document.getElementById(origContent.id);
- } else if (cls) {
- const candidates = [...document.getElementsByTagName(tag)];
- for (const c of candidates) {
- if (c.className === cls && !own(c)) { liveEl = c; break; }
- }
- if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
- if (expectedClasses.length > 0) {
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
- }
+ 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;
}
}
}
- return liveEl;
+
+ 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) {
@@ -4428,7 +4991,45 @@
const anchor = findInsertAnchorInDom();
if (anchor?.parentElement) return anchor;
}
- return findLiveElementForOriginalMarkup(manifest?.originalMarkup || manifest?.anchorMarkup || '');
+ 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);
+ }
}
function loadSvelteRuntime(runtimeModule) {
@@ -4595,7 +5196,7 @@
previewFile: manifestPath,
previewMode: 'svelte-component',
});
- if (state !== 'CYCLING') state = 'GENERATING';
+ if (state !== 'CYCLING') setLiveState('GENERATING');
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper && svelteComponentSession?.sessionId === sessionId) {
@@ -4605,7 +5206,7 @@
expectedVariants = arrivedVariants;
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants ? visibleVariant : 1;
await mountSvelteComponentVariant(visibleVariant || 1);
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
saveSession();
return;
@@ -4621,15 +5222,8 @@
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants
? visibleVariant
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
- showBar('generating');
- startScrollTracking();
- saveSession();
- queueCheckpoint('svelte_component_anchor_missing');
+ enterRecoveryWaitingForAnchor({ checkpointReason: 'svelte_component_anchor_missing', trackScroll: true });
waitForSvelteComponentTargetAndRetry({ manifestPath, sessionId, manifest });
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return;
}
@@ -4692,7 +5286,7 @@
}
selectedElement = mountTarget.firstElementChild || mountTarget;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -4741,6 +5335,7 @@
hideShaderOverlay();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
if (pendingSvelteComponentRetryObserver) { pendingSvelteComponentRetryObserver.disconnect(); pendingSvelteComponentRetryObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearSession();
clearHandled();
@@ -4750,7 +5345,7 @@
arrivedVariants = 0;
visibleVariant = 0;
selectedElement = null;
- state = 'PICKING';
+ setLiveState('PICKING');
hideBar();
if (message) showToast(message, 5000);
}
@@ -4799,21 +5394,26 @@
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
- const liveEl = findLiveElementForOriginalMarkup(origContent.outerHTML);
+ const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
- selectedElement = document.body;
- recoveryWaitingForAnchor = true;
- state = 'GENERATING';
- showBar('generating');
- saveSession();
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
+ 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.
@@ -4834,7 +5434,7 @@
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -5240,7 +5840,7 @@
if (expected > 0) expectedVariants = expected;
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
if (wrapper.dataset.impeccableMode === 'insert') finalizeInsertSession();
@@ -5326,7 +5926,8 @@
syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll();
restoreFromActiveSessions(msg.activeSessions, 'sse_connected');
- if (state === 'IDLE' && (pickActive || insertActive)) state = 'PICKING';
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('sse-connected');
break;
case 'agent_polling':
@@ -5352,7 +5953,7 @@
// Variants already arrived via HMR → normal transition.
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
if (state === 'GENERATING') {
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
disableInlineEdit();
refreshParamsPanel();
@@ -5402,7 +6003,7 @@
case 'error':
if (pendingAcceptedSession?.id && msg.id === pendingAcceptedSession.id) {
pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
updateBarContent('cycling');
showToast('Could not complete accept cleanup. Try Accept again.', 5000);
break;
@@ -5412,7 +6013,7 @@
showToast('Error: ' + msg.message, 5000);
hideBar();
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
break;
}
};
@@ -5450,7 +6051,7 @@
// transient disconnect as an explicit discard.
selectedElement = null;
selectedAction = 'impeccable';
- state = recoveryState;
+ setLiveState(recoveryState);
if (currentSessionId) saveSession();
}
@@ -5592,15 +6193,7 @@
&& !selectedElement.contains(e.target)
) {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- hideBar();
- stopScrollTracking();
- hideAnnotOverlay();
- clearAnnotations();
- renderEditBadge('hidden');
- state = 'PICKING';
- hoveredElement = null;
- hideHighlight();
- syncPageChatFocus('configure-outside-click');
+ exitConfigureToPicking('configure-outside-click', { clearHover: true });
return;
}
if (state === 'PICKING' && insertActive) {
@@ -5617,13 +6210,12 @@
hideInsertLine();
configureKind = 'insert';
selectedElement = placeholder;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
hideHighlight();
clearAnnotations();
showAnnotOverlay(placeholder);
showBar('configure');
startScrollTracking();
- syncPageInteractionCursor();
return;
}
if (state !== 'PICKING' || !pickActive) return;
@@ -5636,7 +6228,7 @@
e.preventDefault();
e.stopPropagation();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5721,6 +6313,16 @@
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;
@@ -5729,6 +6331,7 @@
deepActive
&& own(deepActive)
&& /^(INPUT|TEXTAREA|SELECT)$/.test(deepActive.tagName || '')
+ && !shouldPassthroughElementNav(deepActive, e)
) {
return;
}
@@ -5767,14 +6370,15 @@
if (state === 'EDITING') { cancelEditing(); return; }
if (state === 'CONFIGURING') {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- disableInlineEdit(); hideBar(); stopScrollTracking(); hideAnnotOverlay(); clearAnnotations(); renderEditBadge('hidden'); state = 'PICKING'; syncPageChatFocus('escape-from-configure'); 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(); state = 'IDLE'; }
+ else { hideHighlight(); setLiveState('IDLE'); }
return;
}
}
@@ -5798,7 +6402,7 @@
} else if (e.key === 'Enter') {
e.preventDefault();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5859,6 +6463,7 @@
// screenshot is uploaded (or capture fails - we still emit, just without
// screenshotPath).
const elForCapture = selectedElement;
+ pickedAnchorSnapshot = buildPickedAnchorSnapshot(elForCapture);
const captureRect = elForCapture.getBoundingClientRect();
const snapshot = {
comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })),
@@ -5879,7 +6484,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ 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
@@ -5904,7 +6509,7 @@
clearInsertPicking();
configureKind = 'replace';
selectedElement = null;
- state = insertActive ? 'PICKING' : 'IDLE';
+ setLiveState(insertActive ? 'PICKING' : 'IDLE');
hideHighlight();
syncPageChatFocus('insert-configure-cancel');
}
@@ -5954,7 +6559,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ setLiveState('GENERATING');
showBar('generating');
startScrollTracking();
saveSession();
@@ -6712,7 +7317,7 @@ void main() {
|| acceptWrapper?.dataset?.impeccablePreview === 'svelte-component';
const acceptedSnapshot = snapshotAcceptedVariantDom(acceptedSessionId, acceptedVariant);
- state = 'SAVING';
+ setLiveState('SAVING');
updateBarContent('saving');
pendingAcceptedSession = {
id: acceptedSessionId,
@@ -6727,7 +7332,7 @@ void main() {
.then(() => {})
.catch(() => {
if (pendingAcceptedSession?.id === acceptedSessionId) pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
showToast('Could not confirm accept with the live server. Session kept for recovery; try Accept again.', 5000);
});
@@ -6746,7 +7351,7 @@ void main() {
if (pending.isSvelteComponent) {
commitAcceptedSvelteComponentToDom(pending.id);
}
- state = 'CONFIRMED';
+ setLiveState('CONFIRMED');
updateBarContent('confirmed');
scheduleAcceptCleanup(pending);
return true;
@@ -6870,7 +7475,7 @@ void main() {
selectedAction = 'impeccable';
pendingAcceptedSession = null;
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
function commitAcceptedVariantToDom(sessionId, variantId) {
@@ -6922,6 +7527,7 @@ void main() {
currentPreviewFile = null;
currentPreviewMode = null;
recoveryWaitingForAnchor = false;
+ pickedAnchorSnapshot = null;
}
function rememberSessionFileMeta(meta = {}) {
@@ -6946,6 +7552,7 @@ void main() {
if (!saved) return;
rememberSessionFileMeta(saved);
if (saved.insertPlaceholder) insertPlaceholderSnapshot = saved.insertPlaceholder;
+ if (saved.pickedAnchor) pickedAnchorSnapshot = saved.pickedAnchor;
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
if (saved.previewMode) currentPreviewMode = saved.previewMode;
@@ -7012,9 +7619,10 @@ void main() {
|| clampVariantIndex(serverSession?.visibleVariant, arrivedVariants || expectedVariants)
|| (arrivedVariants > 0 ? 1 : 0);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
+ const restoredAnchor = findLiveElementFromAnchorSnapshot(pickedAnchorSnapshot);
+ selectedElement = restoredAnchor || document.body;
+ setLiveState('GENERATING');
+ recoveryWaitingForAnchor = !restoredAnchor;
showBar('generating');
startScrollTracking();
if (variantObserver) variantObserver.disconnect();
@@ -7030,7 +7638,6 @@ void main() {
return true;
}
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return true;
}
@@ -7059,6 +7666,7 @@ void main() {
pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues },
insertPlaceholder: insertPlaceholderSnapshot || undefined,
+ pickedAnchor: pickedAnchorSnapshot || undefined,
});
}
@@ -7118,6 +7726,7 @@ void main() {
hideHighlight();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearScrollY();
finalizeInsertSession();
@@ -7127,15 +7736,21 @@ void main() {
currentSessionId = null;
selectedAction = 'impeccable';
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
//
// Toast
//
+ function dismissToast() {
+ if (!toastEl) return;
+ toastEl.remove();
+ toastEl = null;
+ }
+
function showToast(message, duration) {
- if (toastEl) toastEl.remove();
+ 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
@@ -7224,7 +7839,7 @@ void main() {
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
selectedElement = resolveSvelteComponentAnchor()
|| wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
hideShaderOverlay();
showBar('cycling');
startScrollTracking();
@@ -7260,7 +7875,7 @@ void main() {
const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
const origEl = pickVariantContent(wrapper, 'original');
- state = resumedState;
+ setLiveState(resumedState);
if (isInsert && resumedState === 'GENERATING' && arrivedVariants === 0) {
selectedElement = ensureInsertPlaceholder() || findInsertAnchorInDom() || wrapper;
} else {
@@ -7419,6 +8034,8 @@ void main() {
let voiceCtx = null;
const PAGE_CHAT_COLLAPSED_W = '88px';
const PAGE_CHAT_PROCESSING_W = '76px';
+ 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(56% 0.032 82 / 0.78)';
@@ -7509,13 +8126,16 @@ void main() {
function syncPageChatChrome() {
if (!pageChatEl) return;
const P = pageChatPalette();
+ const inputFocused = pageChatInput && activeElementDeep() === pageChatInput;
pageChatEl.style.background = P.chatSurface;
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
+ 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 : P.textDim;
+ 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';
@@ -7686,24 +8306,44 @@ void main() {
function syncPageChatFocusRing() {
if (!pageChatEl || !pageChatInput) return;
const focused = activeElementDeep() === pageChatInput;
+ const typingReady = focused && !steerLocked;
pageChatEl.dataset.inputFocused = focused ? 'true' : 'false';
- const P = pageChatPalette();
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
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.color = steerLocked
- ? P.patinaPale
- : ((!pageChatExpanded && focused) ? P.patinaPale : P.textDim);
- }
- if (!pageChatExpanded) {
- pageChatInput.style.width = '0';
- pageChatInput.style.padding = '0';
- pageChatInput.style.opacity = '0';
- pageChatInput.style.pointerEvents = focused ? 'auto' : 'none';
- if (pageChatHint) pageChatHint.style.visibility = '';
+ 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) {
@@ -7723,6 +8363,7 @@ void main() {
try { window.focus(); } catch { /* embed may block */ }
try { pageChatInput.focus({ preventScroll: true }); } catch { pageChatInput.focus(); }
syncPageChatFocusRing();
+ syncPageChatChrome();
steerFocusLog('focusSteerChat result', {
reason,
before: steerFocusTargetLabel(before),
@@ -7769,6 +8410,7 @@ void main() {
pageChatEl.dataset.expanded = 'true';
pageChatEl.style.width = PAGE_CHAT_EXPANDED_W;
pageChatEl.style.cursor = steerLocked ? 'default' : 'text';
+ pageChatInput.placeholder = PAGE_CHAT_PLACEHOLDER_EXPANDED;
if (pageChatHint) {
pageChatHint.style.display = 'none';
pageChatHint.style.opacity = '0';
@@ -7780,6 +8422,20 @@ void main() {
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(); }
@@ -7938,7 +8594,9 @@ void main() {
if (pageChatEl) pageChatEl.dataset.voiceListening = listening ? 'true' : 'false';
syncPageChatChrome();
} else if (voiceCtx?.mode === 'configure') {
- const voiceBtn = uiGetById(PREFIX + '-configure-voice');
+ // 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';
@@ -8180,12 +8838,12 @@ void main() {
height: '28px', margin: '0 4px 0 ' + (GLOBAL_BAR_SECTION_GAP - GLOBAL_BAR_INNER_GAP) + 'px',
borderRadius: '7px',
background: P.chatSurface,
- border: '1px solid ' + P.hairline,
+ border: '1px solid transparent',
overflow: 'hidden',
cursor: 'pointer',
flexShrink: '0',
width: PAGE_CHAT_COLLAPSED_W,
- transition: 'border-color 0.15s ease',
+ transition: 'width 0.18s ease, border-color 0.15s ease',
});
pageChatEl.id = PREFIX + '-page-chat';
pageChatEl.dataset.expanded = 'false';
@@ -8211,13 +8869,14 @@ void main() {
pageChatInput = document.createElement('input');
pageChatInput.id = PREFIX + '-page-chat-input';
pageChatInput.type = 'text';
- pageChatInput.placeholder = 'Steer the page…';
+ 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',
});
@@ -8252,18 +8911,23 @@ void main() {
'#' + 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(63% 0.024 82); 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.024 82); }' +
'#' + PREFIX + '-page-chat-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
uiAppendStyle(s);
}
- pageChatEl.addEventListener('pointerdown', keepSteerPointerInside);
+ pageChatEl.addEventListener('pointerdown', (e) => {
+ keepSteerPointerInside(e);
+ if (steerLocked || pageChatVoiceBtn.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)) return;
- expandPageChat({ focus: false });
- focusPageChatInput('page-chat-click');
+ armPageChatForTyping({ expand: true, focus: true });
});
pageChatVoiceBtn.addEventListener('pointerdown', keepSteerPointerInside);
@@ -8286,7 +8950,9 @@ void main() {
});
pageChatInput.addEventListener('focus', () => {
+ steerInputWasFocused = true;
syncPageChatFocusRing();
+ syncPageChatChrome();
});
pageChatInput.addEventListener('blur', () => {
@@ -8953,14 +9619,15 @@ void main() {
cancelInsertConfigure();
return;
}
+ teardownConfigureChrome();
hideHighlight();
- hideBar();
hideActionPicker();
selectedElement = null;
+ hoveredElement = null;
configureKind = 'replace';
- if (state === 'PICKING' || state === 'CONFIGURING') state = 'IDLE';
+ if (state === 'PICKING' || state === 'CONFIGURING') setLiveState('IDLE');
} else {
- if (state === 'IDLE') state = 'PICKING';
+ if (state === 'IDLE') setLiveState('PICKING');
}
syncPageChatFocus('toggle-pick');
}
@@ -8976,10 +9643,10 @@ void main() {
selectedElement = null;
configureKind = 'replace';
if (state === 'CONFIGURING') cancelInsertConfigure();
- else if (state === 'IDLE' || state === 'PICKING') state = 'PICKING';
+ else if (state === 'IDLE' || state === 'PICKING') setLiveState('PICKING');
} else {
clearInsertPicking();
- if (state === 'PICKING' && !pickActive) state = 'IDLE';
+ if (state === 'PICKING' && !pickActive) setLiveState('IDLE');
}
saveInteractionPrefs();
updateGlobalBarState();
@@ -9060,6 +9727,7 @@ void main() {
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; }
@@ -9073,7 +9741,8 @@ void main() {
window.removeEventListener('message', onDetectMessage);
// Remove detection overlays
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
- state = 'IDLE';
+ setLiveState('IDLE');
+ document.getElementById(PREFIX + '-pick-cursor-style')?.remove();
window.__IMPECCABLE_LIVE_INIT__ = false;
console.log('[impeccable] Live mode exited.');
}
@@ -10250,6 +10919,8 @@ void main() {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('init-complete');
}
diff --git a/.trae/skills/impeccable/scripts/live/ui-core.mjs b/.trae/skills/impeccable/scripts/live/ui-core.mjs
index 4b7adde8b..bbb2c5c35 100644
--- a/.trae/skills/impeccable/scripts/live/ui-core.mjs
+++ b/.trae/skills/impeccable/scripts/live/ui-core.mjs
@@ -42,9 +42,10 @@ export const LIVE_UI_SURFACES = Object.freeze([
'impeccable-live-highlight',
'impeccable-live-tooltip',
'impeccable-live-bar',
- 'impeccable-live-configure-input-wrap',
+ 'impeccable-live-selection-pill',
'impeccable-live-input',
'impeccable-live-configure-voice',
+ 'impeccable-live-configure-bar-tooltip',
],
states: ['rest', 'hover', 'focus-visible', 'pressed', 'disabled'],
},
diff --git a/plugin/skills/impeccable/scripts/live-browser.js b/plugin/skills/impeccable/scripts/live-browser.js
index 9092338eb..539cd07d9 100644
--- a/plugin/skills/impeccable/scripts/live-browser.js
+++ b/plugin/skills/impeccable/scripts/live-browser.js
@@ -57,6 +57,7 @@
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 PICK_CURSOR_CLASS = PREFIX + '-pick-cursor';
const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;
const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
prefix: PREFIX,
@@ -98,7 +99,7 @@
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'] },
{ key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] },
- { key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-configure-input-wrap', PREFIX + '-input', PREFIX + '-configure-voice'] },
+ { 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'] },
@@ -131,6 +132,8 @@
let currentPreviewFile = null;
let currentPreviewMode = null;
let recoveryWaitingForAnchor = false;
+ let pickedAnchorSnapshot = null;
+ let pendingVariantAnchorRetryObserver = null;
let pendingAcceptedSession = null;
let variantObserver = null;
let variantSelectionInFlight = false;
@@ -283,31 +286,53 @@
uiAppend(tooltipEl);
}
+ function shouldShowHighlightTagTooltip() {
+ // Configure/edit carry the tag in the bar selection pill — 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 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);
+ 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' });
- tooltipEl.style.transition = 'none';
- Object.assign(tooltipEl.style, { top: tipY, left: tipX, 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(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });
}
}
@@ -1010,8 +1035,8 @@
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: '6px',
- maxWidth: '520px', minWidth: '320px',
+ padding: '5px',
+ maxWidth: '560px', minWidth: '340px',
});
uiAppend(barEl);
defangOutsideHandlers(barEl);
@@ -1019,14 +1044,29 @@
function positionBar() {
if (!barEl) return;
- const anchor = resolveBarAnchor();
- if (!anchor) return;
- const r = anchor.getBoundingClientRect();
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.
@@ -1054,8 +1094,14 @@
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
+ 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(() => {
@@ -1075,6 +1121,7 @@
setTimeout(() => { if (barEl && hideSeq === barHideSeq) barEl.style.display = 'none'; }, 250);
hideActionPicker();
closeTunePopover();
+ hideConfigureBarTooltip();
if (state === 'EDITING') restoreInlineEditDrafts();
disableInlineEdit();
}
@@ -1083,36 +1130,472 @@
if (!barEl || barEl.style.display === 'none') return;
if (mode === 'cycling' && !ensureCyclingRenderable('update-bar')) return;
barEl.innerHTML = '';
- // Reset bar styling to the kinpaku picker palette
- barEl.style.background = BP.surface;
- barEl.style.border = '1px solid ' + BP.border;
- barEl.style.boxShadow = BP.shadow;
if (mode === 'configure') {
barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());
if (configureKind === 'insert') syncInsertCreateButton();
- } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow());
- else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());
- else if (mode === 'saving') barEl.appendChild(buildSavingRow());
- else if (mode === 'confirmed') {
- barEl.appendChild(buildConfirmedRow());
- barEl.style.background = 'oklch(95% 0.05 145)';
- barEl.style.border = '1px solid oklch(75% 0.12 145 / 0.4)';
+ 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
+ // 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 =
+ '';
+
+ 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 wrap = uiGetById(PREFIX + '-configure-input-wrap');
- const input = uiGetById(PREFIX + '-input');
- if (!wrap || !input) return;
+ 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;
- wrap.dataset.inputFocused = focused ? 'true' : 'false';
- wrap.dataset.voiceListening = (voiceListening && voiceCtx?.mode === 'configure') ? 'true' : 'false';
- wrap.style.borderColor = (voiceListening && voiceCtx?.mode === 'configure')
+ 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.hairline);
+ : (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.02em',
+ }));
+ 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)
@@ -1434,14 +1917,38 @@
let pageInteractionCursorActive = false;
- /** Page-level cursor while insert mode is choosing a before/after edge. */
+ function ensurePickCursorStyle() {
+ if (document.getElementById(PREFIX + '-pick-cursor-style')) return;
+ const style = document.createElement('style');
+ style.id = PREFIX + '-pick-cursor-style';
+ style.textContent =
+ 'html.' + PICK_CURSOR_CLASS + ' * { cursor: crosshair !important; }\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"],\n'
+ + 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"] * { cursor: revert !important; }';
+ // Styles the host page, not the chrome - inside the adapter's shadow UI
+ // root (uiAppendStyle's target) these selectors would match nothing.
+ document.head.appendChild(style);
+ }
+
+ /** Page-level cursor while pick or insert mode is targeting page elements. */
function syncPageInteractionCursor() {
- let next = '';
+ const pickCursor = state === 'PICKING' && pickActive && !insertActive;
+ let axisCursor = '';
if (state === 'PICKING' && insertActive) {
- next = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
+ axisCursor = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';
}
- if (next) {
- document.documentElement.style.cursor = next;
+
+ if (pickCursor) {
+ ensurePickCursorStyle();
+ document.documentElement.classList.add(PICK_CURSOR_CLASS);
+ document.documentElement.style.cursor = '';
+ pageInteractionCursorActive = true;
+ return;
+ }
+
+ document.documentElement.classList.remove(PICK_CURSOR_CLASS);
+ if (axisCursor) {
+ document.documentElement.style.cursor = axisCursor;
pageInteractionCursorActive = true;
} else if (pageInteractionCursorActive) {
document.documentElement.style.cursor = '';
@@ -1449,6 +1956,16 @@
}
}
+ /**
+ * 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;
+ syncPageInteractionCursor();
+ }
+
/** Element used to position the floating bar / shader during a session. */
function resolveBarAnchor() {
if (svelteComponentSession?.sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
@@ -1767,69 +2284,35 @@
}
}
+ /** 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: 'center', gap: '6px',
+ display: 'flex', alignItems: 'stretch', width: '100%', height: CONFIGURE_BAR_H,
});
- // Action pill - dark graphite chip (matches kinpaku-kit .live-demo-ctx-pill)
- const pill = el('button', {
- display: 'inline-flex', alignItems: 'center', gap: '4px',
- padding: '5px 10px', borderRadius: '6px',
- background: BP.chatSurface, color: BP.text,
- fontFamily: FONT, fontSize: '12px', fontWeight: '500',
- border: '1px solid ' + BP.hairline, cursor: 'pointer',
- transition: 'background 0.12s ease, border-color 0.12s ease, transform 0.1s ease',
- whiteSpace: 'nowrap', flexShrink: '0',
- });
- pill.textContent = actionLabel() + ' \u25BE';
- pill.disabled = controlsLocked;
- pill.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- pill.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) pill.title = 'Apply is still running';
- pill.addEventListener('mouseenter', () => {
- if (controlsLocked) return;
- pill.style.background = BP.accentSoft;
- pill.style.borderColor = BP.accent;
- });
- pill.addEventListener('mouseleave', () => {
- if (controlsLocked) return;
- pill.style.background = BP.chatSurface;
- pill.style.borderColor = BP.hairline;
- });
- pill.addEventListener('mousedown', () => { if (!controlsLocked) pill.style.transform = 'scale(0.97)'; });
- pill.addEventListener('mouseup', () => pill.style.transform = 'scale(1)');
- pill.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleActionPicker();
- });
- row.appendChild(pill);
-
- // Prompt field - same chat-surface chrome as the bottom Steer bar
- const inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-configure-input-wrap';
+ const inputShell = el('div', configureInputShellStyle());
const input = document.createElement('input');
input.id = PREFIX + '-input';
input.type = 'text';
- input.placeholder = selectedAction === 'impeccable' ? 'describe what you want…' : 'refine further (optional)…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the change');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1837,33 +2320,31 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, background 0.12s ease',
+ const action = buildConfigureActionControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleActionPicker();
+ },
});
- voiceBtn.id = PREFIX + '-configure-voice';
- 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';
- if (!uiGetById(PREFIX + '-configure-input-style')) {
- 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::placeholder { color: ' + BP.textDim + '; opacity: 1; }' +
- '#' + PREFIX + '-configure-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 { animation: none; opacity: 1; } }' +
- '#' + PREFIX + '-configure-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
- uiAppendStyle(s);
- }
+ 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());
@@ -1873,81 +2354,33 @@
e.stopPropagation();
e.preventDefault();
input.blur();
- disableInlineEdit();
- hideBar();
- renderEditBadge('hidden');
- state = 'PICKING';
- syncPageChatFocus('configure-input-escape');
+ exitConfigureToPicking('configure-input-escape');
return;
}
- // Let arrow keys pass through to the element picker when the input is empty
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !input.value) return;
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ const voiceBtn = buildConfigureVoiceButton({
+ id: PREFIX + '-configure-voice',
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ toggleConfigureVoice();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
+ const go = buildConfigureSubmitButton({
+ controlsLocked,
+ ariaLabel: 'Generate variants',
+ onClick: (e) => { e.stopPropagation(); handleGo(); },
+ });
+
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([action, count], voiceBtn, go));
syncConfigureInputChrome();
- // Variant count toggle
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer',
- transition: 'color 0.12s ease, border-color 0.12s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.title = 'Variants: click to change';
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) count.title = 'Apply is still running';
- count.addEventListener('mouseenter', () => { if (!controlsLocked) { count.style.color = BP.text; count.style.borderColor = BP.text; } });
- count.addEventListener('mouseleave', () => { if (!controlsLocked) { count.style.color = BP.textDim; count.style.borderColor = BP.hairline; } });
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- // Go button
- const go = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- cursor: 'pointer',
- transition: 'filter 0.12s ease, transform 0.1s ease',
- flexShrink: '0', whiteSpace: 'nowrap',
- });
- go.textContent = 'Go \u2192';
- go.disabled = controlsLocked;
- go.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- go.style.opacity = controlsLocked ? '0.58' : '1';
- if (controlsLocked) go.title = 'Apply is still running';
- go.addEventListener('mouseenter', () => { if (!controlsLocked) go.style.filter = 'brightness(1.1)'; });
- go.addEventListener('mouseleave', () => go.style.filter = 'none');
- go.addEventListener('mousedown', () => { if (!controlsLocked) go.style.transform = 'scale(0.97)'; });
- go.addEventListener('mouseup', () => go.style.transform = 'scale(1)');
- go.addEventListener('click', (e) => { e.stopPropagation(); handleGo(); });
- row.appendChild(go);
-
- // Auto-focus input after a beat
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
@@ -1956,34 +2389,20 @@
function buildInsertConfigureRow() {
const controlsLocked = pendingApplyInFlight === true;
const row = el('div', {
- display: 'flex', alignItems: 'center', gap: '6px',
+ 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 inputWrap = el('div', {
- display: 'inline-flex', alignItems: 'center',
- flex: '1', minWidth: '0', height: '28px',
- borderRadius: '7px',
- background: BP.chatSurface,
- border: '1px solid ' + BP.hairline,
- overflow: 'hidden',
- transition: 'border-color 0.15s ease',
- });
- inputWrap.id = PREFIX + '-insert-input-wrap';
- inputWrap.addEventListener('pointerdown', (e) => e.stopPropagation());
- inputWrap.addEventListener('mousedown', (e) => e.stopPropagation());
- inputWrap.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 = 'describe what to insert…';
+ input.placeholder = '';
input.setAttribute('aria-label', 'Describe the new element');
- Object.assign(input.style, {
- flex: '1', minWidth: '0', width: '100%',
- padding: '0 6px', border: 'none', background: 'transparent',
- fontFamily: FONT, fontSize: '11.5px', color: BP.text,
- outline: 'none',
- });
+ Object.assign(input.style, configureInputFieldStyle());
input.disabled = controlsLocked;
if (controlsLocked) {
input.placeholder = 'apply is running...';
@@ -1991,20 +2410,22 @@
input.style.opacity = '0.58';
}
- const voiceBtn = 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: BP.textDim, cursor: 'pointer',
+ const count = buildConfigureCountControl({
+ controlsLocked,
+ onClick: (e) => {
+ e.stopPropagation();
+ if (controlsLocked) { showManualApplyBusyToast(); return; }
+ count.textContent = '\u00D7' + cycleSelectedCount();
+ if (count.matches(':hover')) {
+ showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));
+ }
+ },
});
- voiceBtn.id = PREFIX + '-insert-voice';
- 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';
+
+ inputShell.appendChild(buildSelectionPill({ el: selectedElement, controlsLocked }));
+ inputShell.appendChild(input);
+
+ ensureConfigureInputStyle();
input.addEventListener('input', () => syncInsertCreateButton());
input.addEventListener('pointerdown', (e) => e.stopPropagation());
@@ -2026,48 +2447,31 @@
}
e.stopPropagation();
});
- voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());
- voiceBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- toggleConfigureVoice();
+ 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();
+ },
});
- inputWrap.appendChild(input);
- inputWrap.appendChild(voiceBtn);
- row.appendChild(inputWrap);
-
- const count = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 6px',
- borderRadius: '5px',
- border: '1px solid ' + BP.hairline, background: 'transparent',
- fontFamily: MONO, fontSize: '11px', fontWeight: '600',
- color: BP.textDim, cursor: 'pointer', flexShrink: '0', whiteSpace: 'nowrap',
- });
- count.textContent = '\u00D7' + selectedCount;
- count.disabled = controlsLocked;
- count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';
- count.style.opacity = controlsLocked ? '0.58' : '1';
- count.addEventListener('click', (e) => {
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1;
- count.textContent = '\u00D7' + selectedCount;
- });
- row.appendChild(count);
-
- const create = el('button', {
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
- boxSizing: 'border-box', height: '28px', padding: '0 12px',
- borderRadius: '6px',
- border: 'none', background: BP.accent, color: C.ink,
- fontFamily: FONT, fontSize: '12px', fontWeight: '600',
- flexShrink: '0', whiteSpace: 'nowrap',
+ 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.textContent = 'Create \u2192';
- create.disabled = controlsLocked;
create.addEventListener('mouseenter', () => {
if (controlsLocked) return;
if (isInsertCreateEnabled(create)) {
@@ -2077,15 +2481,10 @@
showInsertCreateTooltip(create, insertCreateDisabledReason(insertCreateGateState(input)));
});
create.addEventListener('mouseleave', hideInsertCreateTooltip);
- create.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- if (controlsLocked) { showManualApplyBusyToast(); return; }
- if (!isInsertCreateEnabled(create)) return;
- handleInsertCreate();
- });
- row.appendChild(create);
+ row.appendChild(inputShell);
+ row.appendChild(buildConfigureTrailingCluster([count], voiceBtn, create));
syncInsertCreateButton(create, input);
+ syncConfigureInputChrome();
if (!controlsLocked) setTimeout(() => input.focus(), 60);
return row;
}
@@ -2375,7 +2774,7 @@
position: 'fixed', zIndex: Z.picker,
display: 'none', opacity: '0',
transform: 'scale(0.96) translateY(4px)',
- transformOrigin: 'bottom left',
+ transformOrigin: 'bottom right',
transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,
background: P.surface,
border: '1px solid ' + P.border,
@@ -2451,14 +2850,18 @@
chip.style.background = isActive ? P.accentSoft : 'transparent';
chip.style.color = isActive ? P.accent : P.text;
});
- // Position above the bar
+ // 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: barRect.left + 'px',
- display: 'block',
+ top: top + 'px',
+ left: left + 'px',
});
requestAnimationFrame(() => {
pickerEl.style.opacity = '1';
@@ -2941,7 +3344,7 @@
function enterEditingMode() {
if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }
- state = 'EDITING';
+ setLiveState('EDITING');
hideBar();
hideAnnotOverlay();
renderEditBadge('editing');
@@ -2974,7 +3377,7 @@
function cancelEditing() {
restoreInlineEditDrafts();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -2988,12 +3391,33 @@
hideAnnotOverlay();
clearAnnotations();
renderEditBadge('hidden');
- state = 'PICKING';
+ 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 ),
// climb to the nearest ancestor with one. The CLI uses tag+class together,
// so tag must come from the same node as the locator.
@@ -3215,7 +3639,7 @@
updatePendingCounter(stashResult.pendingCount || 0);
maybeShowFirstSaveToast();
disableInlineEdit();
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showBar('configure');
showAnnotOverlay(selectedElement);
renderEditBadge('idle');
@@ -3895,6 +4319,12 @@
// Edit content badge - floating button at element top-right to enter EDITING mode
//
+ const EDIT_COPY_LABEL = 'Edit copy';
+ const EDIT_COPY_ICON =
+ '';
+
function usesShadowChromeRoot() {
const root = liveUiRoot();
return root && root !== document.body && root.host && root.host.id === PREFIX + '-root';
@@ -4045,7 +4475,7 @@
editBadgeProxyRoot.appendChild(proxy);
editBadgeProxyByTarget.set(target, proxy);
}
- proxy.title = target.title || target.textContent || 'Edit copy';
+ proxy.title = target.title || target.getAttribute('aria-label') || target.textContent || EDIT_COPY_LABEL;
styleEditBadgeProxy(proxy, target);
}
}
@@ -4085,13 +4515,16 @@
}
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, r.right - bw) + '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;
@@ -4129,16 +4562,36 @@
const disabled = mode === 'idle-disabled';
editBadgeEl.innerHTML = '';
const btn = document.createElement('button');
- btn.textContent = 'Edit copy';
- Object.assign(btn.style, calloutStyle(disabled ? MUTED : ACCENT, disabled ? HAIRLINE : ACCENT));
+ 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;
- btn.title = 'Edit copy is disabled while the current copy edit is applying';
+ 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', () => { btn.style.background = ACCENT; btn.style.color = PRIMARY_TEXT; });
- btn.addEventListener('mouseleave', () => { btn.style.background = SURFACE; btn.style.color = ACCENT; });
+ btn.addEventListener('mouseenter', () => showConfigureBarTooltip(btn, EDIT_COPY_LABEL));
+ btn.addEventListener('mouseleave', hideConfigureBarTooltip);
btn.onclick = enterEditingMode;
}
editBadgeEl.appendChild(btn);
@@ -4154,9 +4607,8 @@
cancel.onclick = cancelEditing;
const save = document.createElement('button');
save.textContent = 'Save';
- Object.assign(save.style, calloutStyle(ACCENT));
- save.addEventListener('mouseenter', () => { save.style.background = ACCENT; save.style.color = PRIMARY_TEXT; });
- save.addEventListener('mouseleave', () => { save.style.background = SURFACE; save.style.color = ACCENT; });
+ Object.assign(save.style, calloutStyle(PRIMARY_TEXT, ACCENT));
+ save.style.background = ACCENT;
save.onclick = applyEditing;
editBadgeEl.append(cancel, save);
}
@@ -4392,31 +4844,142 @@
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 = origContent.className;
- let liveEl = null;
+ const cls = normalizeElementClassName(origContent);
+ const candidates = [...document.getElementsByTagName(tag)];
+
if (origContent.id) {
- liveEl = document.getElementById(origContent.id);
- } else if (cls) {
- const candidates = [...document.getElementsByTagName(tag)];
- for (const c of candidates) {
- if (c.className === cls && !own(c)) { liveEl = c; break; }
- }
- if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
- if (expectedClasses.length > 0) {
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
- }
+ 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;
}
}
}
- return liveEl;
+
+ 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) {
@@ -4428,7 +4991,45 @@
const anchor = findInsertAnchorInDom();
if (anchor?.parentElement) return anchor;
}
- return findLiveElementForOriginalMarkup(manifest?.originalMarkup || manifest?.anchorMarkup || '');
+ 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);
+ }
}
function loadSvelteRuntime(runtimeModule) {
@@ -4595,7 +5196,7 @@
previewFile: manifestPath,
previewMode: 'svelte-component',
});
- if (state !== 'CYCLING') state = 'GENERATING';
+ if (state !== 'CYCLING') setLiveState('GENERATING');
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper && svelteComponentSession?.sessionId === sessionId) {
@@ -4605,7 +5206,7 @@
expectedVariants = arrivedVariants;
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants ? visibleVariant : 1;
await mountSvelteComponentVariant(visibleVariant || 1);
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
saveSession();
return;
@@ -4621,15 +5222,8 @@
visibleVariant = visibleVariant > 0 && visibleVariant <= arrivedVariants
? visibleVariant
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
- showBar('generating');
- startScrollTracking();
- saveSession();
- queueCheckpoint('svelte_component_anchor_missing');
+ enterRecoveryWaitingForAnchor({ checkpointReason: 'svelte_component_anchor_missing', trackScroll: true });
waitForSvelteComponentTargetAndRetry({ manifestPath, sessionId, manifest });
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return;
}
@@ -4692,7 +5286,7 @@
}
selectedElement = mountTarget.firstElementChild || mountTarget;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -4741,6 +5335,7 @@
hideShaderOverlay();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
if (pendingSvelteComponentRetryObserver) { pendingSvelteComponentRetryObserver.disconnect(); pendingSvelteComponentRetryObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearSession();
clearHandled();
@@ -4750,7 +5345,7 @@
arrivedVariants = 0;
visibleVariant = 0;
selectedElement = null;
- state = 'PICKING';
+ setLiveState('PICKING');
hideBar();
if (message) showToast(message, 5000);
}
@@ -4799,21 +5394,26 @@
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
- const liveEl = findLiveElementForOriginalMarkup(origContent.outerHTML);
+ const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
- selectedElement = document.body;
- recoveryWaitingForAnchor = true;
- state = 'GENERATING';
- showBar('generating');
- saveSession();
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
+ 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.
@@ -4834,7 +5434,7 @@
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
showOrUpdateCyclingBar();
@@ -5240,7 +5840,7 @@
if (expected > 0) expectedVariants = expected;
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
- state = 'CYCLING';
+ setLiveState('CYCLING');
recoveryWaitingForAnchor = false;
hideShaderOverlay();
if (wrapper.dataset.impeccableMode === 'insert') finalizeInsertSession();
@@ -5326,7 +5926,8 @@
syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll();
restoreFromActiveSessions(msg.activeSessions, 'sse_connected');
- if (state === 'IDLE' && (pickActive || insertActive)) state = 'PICKING';
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('sse-connected');
break;
case 'agent_polling':
@@ -5352,7 +5953,7 @@
// Variants already arrived via HMR → normal transition.
if (arrivedVariants >= expectedVariants && expectedVariants > 0) {
if (state === 'GENERATING') {
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
disableInlineEdit();
refreshParamsPanel();
@@ -5402,7 +6003,7 @@
case 'error':
if (pendingAcceptedSession?.id && msg.id === pendingAcceptedSession.id) {
pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
updateBarContent('cycling');
showToast('Could not complete accept cleanup. Try Accept again.', 5000);
break;
@@ -5412,7 +6013,7 @@
showToast('Error: ' + msg.message, 5000);
hideBar();
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
break;
}
};
@@ -5450,7 +6051,7 @@
// transient disconnect as an explicit discard.
selectedElement = null;
selectedAction = 'impeccable';
- state = recoveryState;
+ setLiveState(recoveryState);
if (currentSessionId) saveSession();
}
@@ -5592,15 +6193,7 @@
&& !selectedElement.contains(e.target)
) {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- hideBar();
- stopScrollTracking();
- hideAnnotOverlay();
- clearAnnotations();
- renderEditBadge('hidden');
- state = 'PICKING';
- hoveredElement = null;
- hideHighlight();
- syncPageChatFocus('configure-outside-click');
+ exitConfigureToPicking('configure-outside-click', { clearHover: true });
return;
}
if (state === 'PICKING' && insertActive) {
@@ -5617,13 +6210,12 @@
hideInsertLine();
configureKind = 'insert';
selectedElement = placeholder;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
hideHighlight();
clearAnnotations();
showAnnotOverlay(placeholder);
showBar('configure');
startScrollTracking();
- syncPageInteractionCursor();
return;
}
if (state !== 'PICKING' || !pickActive) return;
@@ -5636,7 +6228,7 @@
e.preventDefault();
e.stopPropagation();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5721,6 +6313,16 @@
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;
@@ -5729,6 +6331,7 @@
deepActive
&& own(deepActive)
&& /^(INPUT|TEXTAREA|SELECT)$/.test(deepActive.tagName || '')
+ && !shouldPassthroughElementNav(deepActive, e)
) {
return;
}
@@ -5767,14 +6370,15 @@
if (state === 'EDITING') { cancelEditing(); return; }
if (state === 'CONFIGURING') {
if (configureKind === 'insert') { cancelInsertConfigure(); return; }
- disableInlineEdit(); hideBar(); stopScrollTracking(); hideAnnotOverlay(); clearAnnotations(); renderEditBadge('hidden'); state = 'PICKING'; syncPageChatFocus('escape-from-configure'); 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(); state = 'IDLE'; }
+ else { hideHighlight(); setLiveState('IDLE'); }
return;
}
}
@@ -5798,7 +6402,7 @@
} else if (e.key === 'Enter') {
e.preventDefault();
selectedElement = hoveredElement;
- state = 'CONFIGURING';
+ setLiveState('CONFIGURING');
showHighlight(selectedElement);
clearAnnotations();
showAnnotOverlay(selectedElement);
@@ -5859,6 +6463,7 @@
// screenshot is uploaded (or capture fails - we still emit, just without
// screenshotPath).
const elForCapture = selectedElement;
+ pickedAnchorSnapshot = buildPickedAnchorSnapshot(elForCapture);
const captureRect = elForCapture.getBoundingClientRect();
const snapshot = {
comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })),
@@ -5879,7 +6484,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ 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
@@ -5904,7 +6509,7 @@
clearInsertPicking();
configureKind = 'replace';
selectedElement = null;
- state = insertActive ? 'PICKING' : 'IDLE';
+ setLiveState(insertActive ? 'PICKING' : 'IDLE');
hideHighlight();
syncPageChatFocus('insert-configure-cancel');
}
@@ -5954,7 +6559,7 @@
hideAnnotOverlay();
clearAnnotations();
- state = 'GENERATING';
+ setLiveState('GENERATING');
showBar('generating');
startScrollTracking();
saveSession();
@@ -6712,7 +7317,7 @@ void main() {
|| acceptWrapper?.dataset?.impeccablePreview === 'svelte-component';
const acceptedSnapshot = snapshotAcceptedVariantDom(acceptedSessionId, acceptedVariant);
- state = 'SAVING';
+ setLiveState('SAVING');
updateBarContent('saving');
pendingAcceptedSession = {
id: acceptedSessionId,
@@ -6727,7 +7332,7 @@ void main() {
.then(() => {})
.catch(() => {
if (pendingAcceptedSession?.id === acceptedSessionId) pendingAcceptedSession = null;
- state = 'CYCLING';
+ setLiveState('CYCLING');
showOrUpdateCyclingBar();
showToast('Could not confirm accept with the live server. Session kept for recovery; try Accept again.', 5000);
});
@@ -6746,7 +7351,7 @@ void main() {
if (pending.isSvelteComponent) {
commitAcceptedSvelteComponentToDom(pending.id);
}
- state = 'CONFIRMED';
+ setLiveState('CONFIRMED');
updateBarContent('confirmed');
scheduleAcceptCleanup(pending);
return true;
@@ -6870,7 +7475,7 @@ void main() {
selectedAction = 'impeccable';
pendingAcceptedSession = null;
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
function commitAcceptedVariantToDom(sessionId, variantId) {
@@ -6922,6 +7527,7 @@ void main() {
currentPreviewFile = null;
currentPreviewMode = null;
recoveryWaitingForAnchor = false;
+ pickedAnchorSnapshot = null;
}
function rememberSessionFileMeta(meta = {}) {
@@ -6946,6 +7552,7 @@ void main() {
if (!saved) return;
rememberSessionFileMeta(saved);
if (saved.insertPlaceholder) insertPlaceholderSnapshot = saved.insertPlaceholder;
+ if (saved.pickedAnchor) pickedAnchorSnapshot = saved.pickedAnchor;
if (saved.action) selectedAction = saved.action;
if (saved.count) selectedCount = saved.count;
if (saved.previewMode) currentPreviewMode = saved.previewMode;
@@ -7012,9 +7619,10 @@ void main() {
|| clampVariantIndex(serverSession?.visibleVariant, arrivedVariants || expectedVariants)
|| (arrivedVariants > 0 ? 1 : 0);
- selectedElement = document.body;
- state = 'GENERATING';
- recoveryWaitingForAnchor = true;
+ const restoredAnchor = findLiveElementFromAnchorSnapshot(pickedAnchorSnapshot);
+ selectedElement = restoredAnchor || document.body;
+ setLiveState('GENERATING');
+ recoveryWaitingForAnchor = !restoredAnchor;
showBar('generating');
startScrollTracking();
if (variantObserver) variantObserver.disconnect();
@@ -7030,7 +7638,6 @@ void main() {
return true;
}
- showToast('Variants ready. Reveal the selected element to resume.', 15000);
return true;
}
@@ -7059,6 +7666,7 @@ void main() {
pageUrl: location.pathname,
paramValues: { ...paramsCurrentValues },
insertPlaceholder: insertPlaceholderSnapshot || undefined,
+ pickedAnchor: pickedAnchorSnapshot || undefined,
});
}
@@ -7118,6 +7726,7 @@ void main() {
hideHighlight();
stopScrollTracking();
if (variantObserver) { variantObserver.disconnect(); variantObserver = null; }
+ if (pendingVariantAnchorRetryObserver) { pendingVariantAnchorRetryObserver.disconnect(); pendingVariantAnchorRetryObserver = null; }
stopScrollLock();
clearScrollY();
finalizeInsertSession();
@@ -7127,15 +7736,21 @@ void main() {
currentSessionId = null;
selectedAction = 'impeccable';
renderEditBadge('hidden');
- state = 'PICKING';
+ setLiveState('PICKING');
}
//
// Toast
//
+ function dismissToast() {
+ if (!toastEl) return;
+ toastEl.remove();
+ toastEl = null;
+ }
+
function showToast(message, duration) {
- if (toastEl) toastEl.remove();
+ 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
@@ -7224,7 +7839,7 @@ void main() {
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
selectedElement = resolveSvelteComponentAnchor()
|| wrapper.parentElement;
- state = 'CYCLING';
+ setLiveState('CYCLING');
hideShaderOverlay();
showBar('cycling');
startScrollTracking();
@@ -7260,7 +7875,7 @@ void main() {
const isInsert = wrapper.dataset.impeccableMode === 'insert';
const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null;
const origEl = pickVariantContent(wrapper, 'original');
- state = resumedState;
+ setLiveState(resumedState);
if (isInsert && resumedState === 'GENERATING' && arrivedVariants === 0) {
selectedElement = ensureInsertPlaceholder() || findInsertAnchorInDom() || wrapper;
} else {
@@ -7419,6 +8034,8 @@ void main() {
let voiceCtx = null;
const PAGE_CHAT_COLLAPSED_W = '88px';
const PAGE_CHAT_PROCESSING_W = '76px';
+ 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(56% 0.032 82 / 0.78)';
@@ -7509,13 +8126,16 @@ void main() {
function syncPageChatChrome() {
if (!pageChatEl) return;
const P = pageChatPalette();
+ const inputFocused = pageChatInput && activeElementDeep() === pageChatInput;
pageChatEl.style.background = P.chatSurface;
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
+ 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 : P.textDim;
+ 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';
@@ -7686,24 +8306,44 @@ void main() {
function syncPageChatFocusRing() {
if (!pageChatEl || !pageChatInput) return;
const focused = activeElementDeep() === pageChatInput;
+ const typingReady = focused && !steerLocked;
pageChatEl.dataset.inputFocused = focused ? 'true' : 'false';
- const P = pageChatPalette();
- pageChatEl.style.borderColor = steerLocked
- ? P.patinaSoft
- : (pageChatExpanded ? P.accentSoft : P.hairline);
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.color = steerLocked
- ? P.patinaPale
- : ((!pageChatExpanded && focused) ? P.patinaPale : P.textDim);
- }
- if (!pageChatExpanded) {
- pageChatInput.style.width = '0';
- pageChatInput.style.padding = '0';
- pageChatInput.style.opacity = '0';
- pageChatInput.style.pointerEvents = focused ? 'auto' : 'none';
- if (pageChatHint) pageChatHint.style.visibility = '';
+ 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) {
@@ -7723,6 +8363,7 @@ void main() {
try { window.focus(); } catch { /* embed may block */ }
try { pageChatInput.focus({ preventScroll: true }); } catch { pageChatInput.focus(); }
syncPageChatFocusRing();
+ syncPageChatChrome();
steerFocusLog('focusSteerChat result', {
reason,
before: steerFocusTargetLabel(before),
@@ -7769,6 +8410,7 @@ void main() {
pageChatEl.dataset.expanded = 'true';
pageChatEl.style.width = PAGE_CHAT_EXPANDED_W;
pageChatEl.style.cursor = steerLocked ? 'default' : 'text';
+ pageChatInput.placeholder = PAGE_CHAT_PLACEHOLDER_EXPANDED;
if (pageChatHint) {
pageChatHint.style.display = 'none';
pageChatHint.style.opacity = '0';
@@ -7780,6 +8422,20 @@ void main() {
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(); }
@@ -7938,7 +8594,9 @@ void main() {
if (pageChatEl) pageChatEl.dataset.voiceListening = listening ? 'true' : 'false';
syncPageChatChrome();
} else if (voiceCtx?.mode === 'configure') {
- const voiceBtn = uiGetById(PREFIX + '-configure-voice');
+ // 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';
@@ -8180,12 +8838,12 @@ void main() {
height: '28px', margin: '0 4px 0 ' + (GLOBAL_BAR_SECTION_GAP - GLOBAL_BAR_INNER_GAP) + 'px',
borderRadius: '7px',
background: P.chatSurface,
- border: '1px solid ' + P.hairline,
+ border: '1px solid transparent',
overflow: 'hidden',
cursor: 'pointer',
flexShrink: '0',
width: PAGE_CHAT_COLLAPSED_W,
- transition: 'border-color 0.15s ease',
+ transition: 'width 0.18s ease, border-color 0.15s ease',
});
pageChatEl.id = PREFIX + '-page-chat';
pageChatEl.dataset.expanded = 'false';
@@ -8211,13 +8869,14 @@ void main() {
pageChatInput = document.createElement('input');
pageChatInput.id = PREFIX + '-page-chat-input';
pageChatInput.type = 'text';
- pageChatInput.placeholder = 'Steer the page…';
+ 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',
});
@@ -8252,18 +8911,23 @@ void main() {
'#' + 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(63% 0.024 82); 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.024 82); }' +
'#' + PREFIX + '-page-chat-voice:hover { background: oklch(78% 0.12 82 / 0.12); }';
uiAppendStyle(s);
}
- pageChatEl.addEventListener('pointerdown', keepSteerPointerInside);
+ pageChatEl.addEventListener('pointerdown', (e) => {
+ keepSteerPointerInside(e);
+ if (steerLocked || pageChatVoiceBtn.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)) return;
- expandPageChat({ focus: false });
- focusPageChatInput('page-chat-click');
+ armPageChatForTyping({ expand: true, focus: true });
});
pageChatVoiceBtn.addEventListener('pointerdown', keepSteerPointerInside);
@@ -8286,7 +8950,9 @@ void main() {
});
pageChatInput.addEventListener('focus', () => {
+ steerInputWasFocused = true;
syncPageChatFocusRing();
+ syncPageChatChrome();
});
pageChatInput.addEventListener('blur', () => {
@@ -8953,14 +9619,15 @@ void main() {
cancelInsertConfigure();
return;
}
+ teardownConfigureChrome();
hideHighlight();
- hideBar();
hideActionPicker();
selectedElement = null;
+ hoveredElement = null;
configureKind = 'replace';
- if (state === 'PICKING' || state === 'CONFIGURING') state = 'IDLE';
+ if (state === 'PICKING' || state === 'CONFIGURING') setLiveState('IDLE');
} else {
- if (state === 'IDLE') state = 'PICKING';
+ if (state === 'IDLE') setLiveState('PICKING');
}
syncPageChatFocus('toggle-pick');
}
@@ -8976,10 +9643,10 @@ void main() {
selectedElement = null;
configureKind = 'replace';
if (state === 'CONFIGURING') cancelInsertConfigure();
- else if (state === 'IDLE' || state === 'PICKING') state = 'PICKING';
+ else if (state === 'IDLE' || state === 'PICKING') setLiveState('PICKING');
} else {
clearInsertPicking();
- if (state === 'PICKING' && !pickActive) state = 'IDLE';
+ if (state === 'PICKING' && !pickActive) setLiveState('IDLE');
}
saveInteractionPrefs();
updateGlobalBarState();
@@ -9060,6 +9727,7 @@ void main() {
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; }
@@ -9073,7 +9741,8 @@ void main() {
window.removeEventListener('message', onDetectMessage);
// Remove detection overlays
window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
- state = 'IDLE';
+ setLiveState('IDLE');
+ document.getElementById(PREFIX + '-pick-cursor-style')?.remove();
window.__IMPECCABLE_LIVE_INIT__ = false;
console.log('[impeccable] Live mode exited.');
}
@@ -10250,6 +10919,8 @@ void main() {
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
}
+ if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
+ syncPageInteractionCursor();
syncPageChatFocus('init-complete');
}
diff --git a/plugin/skills/impeccable/scripts/live/ui-core.mjs b/plugin/skills/impeccable/scripts/live/ui-core.mjs
index 4b7adde8b..bbb2c5c35 100644
--- a/plugin/skills/impeccable/scripts/live/ui-core.mjs
+++ b/plugin/skills/impeccable/scripts/live/ui-core.mjs
@@ -42,9 +42,10 @@ export const LIVE_UI_SURFACES = Object.freeze([
'impeccable-live-highlight',
'impeccable-live-tooltip',
'impeccable-live-bar',
- 'impeccable-live-configure-input-wrap',
+ 'impeccable-live-selection-pill',
'impeccable-live-input',
'impeccable-live-configure-voice',
+ 'impeccable-live-configure-bar-tooltip',
],
states: ['rest', 'hover', 'focus-visible', 'pressed', 'disabled'],
},