diff --git a/site/styles/kinpaku-kit.css b/site/styles/kinpaku-kit.css
index e3558a221..acc01f55b 100644
--- a/site/styles/kinpaku-kit.css
+++ b/site/styles/kinpaku-kit.css
@@ -1198,18 +1198,17 @@
margin: 0 4px 0 6px;
border-radius: 5px;
background: var(--ks-graphite);
- border: 1px solid oklch(92% 0 0 / 0.1);
+ border: 1px solid transparent;
overflow: hidden;
cursor: pointer;
flex-shrink: 0;
- transition: border-color 0.15s ease;
+ transition: width 0.18s ease, border-color 0.15s ease;
}
.home-kinpaku .live-demo-gbar-chat.is-expanded,
.live-mode-kinpaku .live-demo-gbar-chat.is-expanded {
width: min(220px, 42vw);
cursor: text;
- border-color: oklch(78% 0.12 82 / 0.55);
}
.home-kinpaku .live-demo-gbar-chat-icon,
@@ -1251,9 +1250,23 @@
outline: none;
opacity: 0;
pointer-events: none;
+ caret-color: var(--ks-kinpaku);
transition: opacity 0.15s ease;
}
+.home-kinpaku .live-demo-gbar-chat[data-input-focused="true"]:not([data-expanded="true"]) .live-demo-gbar-chat-hint,
+.live-mode-kinpaku .live-demo-gbar-chat[data-input-focused="true"]:not([data-expanded="true"]) .live-demo-gbar-chat-hint {
+ display: none;
+}
+
+.home-kinpaku .live-demo-gbar-chat[data-input-focused="true"]:not([data-expanded="true"]) .live-demo-gbar-chat-input,
+.live-mode-kinpaku .live-demo-gbar-chat[data-input-focused="true"]:not([data-expanded="true"]) .live-demo-gbar-chat-input {
+ width: auto;
+ padding: 0 4px;
+ opacity: 1;
+ pointer-events: auto;
+}
+
.home-kinpaku .live-demo-gbar-chat.is-expanded .live-demo-gbar-chat-hint,
.live-mode-kinpaku .live-demo-gbar-chat.is-expanded .live-demo-gbar-chat-hint {
display: none;
@@ -1316,47 +1329,159 @@
/* Context bar sub-elements (the inline action panel that appears
next to a picked element during "configure" / "cycling" / "accepted"
states). */
-.home-kinpaku .live-demo-ctx-pill,
-.live-mode-kinpaku .live-demo-ctx-pill {
+.home-kinpaku .live-demo-ctx[data-phase="configuring"],
+.live-mode-kinpaku .live-demo-ctx[data-phase="configuring"] {
+ padding: 0;
background: var(--ks-graphite-2);
- border: 1px solid oklch(92% 0 0 / 0.12);
- color: var(--ks-champagne);
+ overflow: hidden;
}
-/* Even heights across the configure row: the pill, input, count, and Go button
- share one 30px baseline instead of stepping up and down. */
-.home-kinpaku .live-demo-ctx-row--configure > *,
-.live-mode-kinpaku .live-demo-ctx-row--configure > * {
- height: 30px;
- box-sizing: border-box;
+.home-kinpaku .live-demo-ctx-command,
+.live-mode-kinpaku .live-demo-ctx-command {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ min-width: 0;
+ height: 36px;
+ padding: 0 6px 0 7px;
+ background: transparent;
+ border: 0;
+ overflow: hidden;
+}
+
+.home-kinpaku .live-demo-ctx-trailing,
+.live-mode-kinpaku .live-demo-ctx-trailing {
+ display: inline-flex;
+ align-items: stretch;
+ flex-shrink: 0;
+ border-left: 1px solid oklch(92% 0 0 / 0.12);
+}
+
+.home-kinpaku .live-demo-ctx-modifiers,
+.live-mode-kinpaku .live-demo-ctx-modifiers {
display: inline-flex;
align-items: center;
+ gap: 8px;
+ padding: 0 10px;
+ flex-shrink: 0;
}
-.home-kinpaku .live-demo-ctx-pill-caret,
-.live-mode-kinpaku .live-demo-ctx-pill-caret {
+.home-kinpaku .live-demo-ctx-selection,
+.live-mode-kinpaku .live-demo-ctx-selection {
+ box-sizing: border-box;
+ min-width: 28px;
+ height: auto;
+ padding: 1px 4px;
+ border: 1px solid oklch(70% 0.12 188);
+ border-radius: 7px;
+ flex-shrink: 0;
+ background: transparent;
+ color: oklch(70% 0.12 188);
+ font-family: var(--ks-mono);
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 18px;
+ transition: background 0.15s ease, color 0.15s ease;
+}
+
+.home-kinpaku .live-demo-ctx-action,
+.home-kinpaku .live-demo-ctx-count,
+.live-mode-kinpaku .live-demo-ctx-action,
+.live-mode-kinpaku .live-demo-ctx-count {
+ box-sizing: border-box;
+ height: 18px;
+ padding: 0;
+ border: 0;
+ border-radius: 0;
+ background: transparent;
color: var(--ks-text-muted);
-}
-
-.home-kinpaku .live-demo-ctx-input,
-.live-mode-kinpaku .live-demo-ctx-input {
- background: var(--ks-graphite);
- border: 1px solid oklch(92% 0 0 / 0.12);
- color: var(--ks-champagne);
-}
-
-.home-kinpaku .live-demo-ctx-caret,
-.live-mode-kinpaku .live-demo-ctx-caret {
- background: var(--ks-kinpaku);
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 18px;
+ transition: color 0.12s ease;
}
.home-kinpaku .live-demo-ctx-count,
.live-mode-kinpaku .live-demo-ctx-count {
- background: var(--ks-graphite-2);
- border: 1px solid oklch(92% 0 0 / 0.12);
+ font-family: var(--ks-mono);
+ font-weight: 600;
+}
+
+.home-kinpaku .live-demo-ctx-row--configure,
+.live-mode-kinpaku .live-demo-ctx-row--configure {
+ width: 100%;
+ min-height: 36px;
+}
+
+.home-kinpaku .live-demo-ctx-actions,
+.live-mode-kinpaku .live-demo-ctx-actions {
+ display: inline-flex;
+ align-items: stretch;
+ border-left: 1px solid oklch(92% 0 0 / 0.12);
+}
+
+.home-kinpaku .live-demo-ctx-voice,
+.live-mode-kinpaku .live-demo-ctx-voice {
+ width: 36px;
+ height: 36px;
+ padding: 0;
+ border: 0;
+ border-right: 1px solid oklch(92% 0 0 / 0.12);
+ border-radius: 0;
+ background: transparent;
color: var(--ks-text-muted);
}
+.home-kinpaku .live-demo-ctx-actions .live-demo-ctx-go,
+.live-mode-kinpaku .live-demo-ctx-actions .live-demo-ctx-go {
+ width: 36px;
+ height: 36px;
+ padding: 0;
+ border-radius: 0;
+ border-left: 0;
+}
+
+.home-kinpaku .live-demo-ctx-action-caret,
+.live-mode-kinpaku .live-demo-ctx-action-caret {
+ font-size: 10px;
+ color: inherit;
+}
+
+.home-kinpaku .live-demo-ctx-selection:hover,
+.live-mode-kinpaku .live-demo-ctx-selection:hover {
+ background: oklch(27% 0 0);
+}
+
+.home-kinpaku .live-demo-ctx-action:hover,
+.home-kinpaku .live-demo-ctx-count:hover,
+.live-mode-kinpaku .live-demo-ctx-action:hover,
+.live-mode-kinpaku .live-demo-ctx-count:hover {
+ color: var(--ks-text);
+}
+
+.home-kinpaku .live-demo-ctx-input,
+.live-mode-kinpaku .live-demo-ctx-input {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ flex: 1;
+ min-width: 0;
+ height: 18px;
+ padding: 0;
+ background: transparent;
+ border: 0;
+ border-radius: 0;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 18px;
+ color: oklch(94% 0.02 82);
+}
+
+.home-kinpaku .live-demo-ctx-caret,
+.live-mode-kinpaku .live-demo-ctx-caret {
+ background: oklch(94% 0.02 82);
+}
+
.home-kinpaku .live-demo-ctx-go,
.live-mode-kinpaku .live-demo-ctx-go,
.home-kinpaku .live-demo-ctx-accept,
diff --git a/site/styles/light-mode.css b/site/styles/light-mode.css
index 08a81538a..bdc81401f 100644
--- a/site/styles/light-mode.css
+++ b/site/styles/light-mode.css
@@ -1364,15 +1364,54 @@ html.light .live-mode-kinpaku .live-demo-gbar-chat-input {
was an 11%-darker olive-cast block that read as heavy and unrefined on the
white bar. A barely-there warm off-white recess with a crisp hairline reads
as polished product chrome — the border does the separating, not a gray slab. */
-html.light .home-kinpaku .live-demo-ctx-pill,
-html.light .home-kinpaku .live-demo-ctx-input,
-html.light .home-kinpaku .live-demo-ctx-count,
-html.light .live-mode-kinpaku .live-demo-ctx-pill,
-html.light .live-mode-kinpaku .live-demo-ctx-input,
-html.light .live-mode-kinpaku .live-demo-ctx-count {
- background: oklch(97.5% 0.004 95);
- border-color: oklch(25% 0.02 95 / 0.14);
- color: var(--ks-text);
+/* The configure-phase bar mirrors the real overlay chrome, which keeps a dark
+ surface in both themes. The pale light-mode bar from the rule above would
+ swallow the near-white prompt text below, so the configuring phase opts back
+ into the overlay's dark surface. */
+html.light .home-kinpaku .live-demo-ctx[data-phase="configuring"],
+html.light .live-mode-kinpaku .live-demo-ctx[data-phase="configuring"] {
+ background: oklch(15% 0.008 95);
+ border-color: oklch(92% 0 0 / 0.14);
+}
+
+html.light .home-kinpaku .why-live-frame > .why-live-ctx.live-demo-ctx[data-phase="configuring"] {
+ background: oklch(15% 0.008 95);
+ border-color: oklch(92% 0 0 / 0.14);
+}
+
+html.light .home-kinpaku .live-demo-ctx[data-phase="configuring"] .live-demo-ctx-input,
+html.light .live-mode-kinpaku .live-demo-ctx[data-phase="configuring"] .live-demo-ctx-input {
+ background: transparent;
+ border-color: transparent;
+ color: oklch(94% 0.02 82);
+}
+
+html.light .home-kinpaku .live-demo-ctx[data-phase="configuring"] .live-demo-ctx-caret,
+html.light .live-mode-kinpaku .live-demo-ctx[data-phase="configuring"] .live-demo-ctx-caret {
+ background: oklch(94% 0.02 82);
+}
+
+html.light .home-kinpaku .live-demo-ctx[data-phase="configuring"] .live-demo-ctx-selection,
+html.light .live-mode-kinpaku .live-demo-ctx[data-phase="configuring"] .live-demo-ctx-selection {
+ background: transparent;
+ border: 1px solid oklch(70% 0.12 188);
+ color: oklch(70% 0.12 188);
+}
+
+html.light .home-kinpaku .live-demo-ctx[data-phase="configuring"] .live-demo-ctx-action,
+html.light .home-kinpaku .live-demo-ctx[data-phase="configuring"] .live-demo-ctx-count,
+html.light .live-mode-kinpaku .live-demo-ctx[data-phase="configuring"] .live-demo-ctx-action,
+html.light .live-mode-kinpaku .live-demo-ctx[data-phase="configuring"] .live-demo-ctx-count {
+ background: transparent;
+ border: 0;
+ /* Light muted tone from the base sheet: these sit on the dark configure
+ surface, where the light-theme text-muted token would vanish. */
+ color: oklch(63% 0.024 82);
+}
+
+html.light .home-kinpaku .live-demo-ctx[data-phase="configuring"] .live-demo-ctx-voice,
+html.light .live-mode-kinpaku .live-demo-ctx[data-phase="configuring"] .live-demo-ctx-voice {
+ color: oklch(63% 0.024 82);
}
html.light .home-kinpaku .live-demo-ctx-go,
diff --git a/site/styles/live-mode.css b/site/styles/live-mode.css
index 0c5308da4..9734f7b10 100644
--- a/site/styles/live-mode.css
+++ b/site/styles/live-mode.css
@@ -417,64 +417,163 @@
display: flex;
}
-/* Configure row: [delight ▾] [input] [×3] [Go →] */
-.live-demo-ctx-pill {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 5px 10px;
- background: var(--color-ink);
- color: var(--color-paper);
- border: 0;
- border-radius: 6px;
- font-family: var(--font-body);
- font-size: 12px;
- font-weight: 500;
- cursor: pointer;
- white-space: nowrap;
+.live-demo-ctx[data-phase="configuring"] {
+ padding: 0;
+ background: oklch(15% 0.008 95);
+ overflow: hidden;
}
-.live-demo-ctx-pill-caret { font-size: 9px; opacity: 0.7; margin-left: 2px; }
-.live-demo-ctx-input {
- display: inline-flex;
+
+/* Configure row: [h1 pill | input …] | [adapt ▾ ×3 | voice →] */
+.live-demo-ctx-row--configure {
+ width: 100%;
+ min-height: 36px;
+}
+.live-demo-ctx-command {
+ display: flex;
align-items: center;
flex: 1;
min-width: 180px;
- padding: 5px 8px;
+ height: 36px;
+ padding: 0 6px 0 7px;
+ background: transparent;
+ border: 0;
+ overflow: hidden;
+}
+.live-demo-ctx-trailing {
+ display: inline-flex;
+ align-items: stretch;
+ flex-shrink: 0;
+ border-left: 1px solid oklch(92% 0 0 / 0.12);
+}
+.live-demo-ctx-modifiers {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 0 10px;
+ flex-shrink: 0;
+}
+.live-demo-ctx-selection {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 2px;
+ box-sizing: border-box;
+ min-width: 28px;
+ height: auto;
+ padding: 1px 4px;
+ background: transparent;
+ color: oklch(70% 0.12 188);
+ border: 1px solid oklch(70% 0.12 188);
+ border-radius: 7px;
+ flex-shrink: 0;
+ font-family: var(--font-mono);
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 18px;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: background 0.15s ease, color 0.15s ease;
+}
+.live-demo-ctx-selection:hover {
+ background: oklch(27% 0 0);
+}
+.live-demo-ctx-action,
+.live-demo-ctx-count {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 2px;
+ box-sizing: border-box;
+ height: 18px;
+ padding: 0;
+ background: transparent;
+ color: oklch(63% 0.024 82);
+ border: 0;
+ border-radius: 0;
font-family: var(--font-body);
font-size: 12px;
- color: var(--color-ink);
+ font-weight: 500;
+ line-height: 18px;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: color 0.12s ease;
+}
+.live-demo-ctx-count {
+ font-family: var(--font-mono);
+ font-weight: 600;
+}
+.live-demo-ctx-action:hover,
+.live-demo-ctx-count:hover {
+ color: oklch(84% 0.035 82);
+}
+.live-demo-ctx-action-caret {
+ font-size: 10px;
+ margin-left: 2px;
+ color: inherit;
+}
+.live-demo-ctx-input {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ flex: 1;
+ min-width: 0;
+ height: 18px;
+ padding: 0;
+ font-family: var(--font-body);
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 18px;
+ color: oklch(94% 0.02 82);
+ background: transparent;
+ border: 0;
+}
+.live-demo-ctx-actions {
+ display: inline-flex;
+ align-items: stretch;
+ flex-shrink: 0;
+ border-left: 1px solid oklch(92% 0 0 / 0.12);
+}
+.live-demo-ctx-voice {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ padding: 0;
+ background: transparent;
+ color: oklch(63% 0.024 82);
+ border: 0;
+ border-right: 1px solid oklch(22% 0 0);
+ border-radius: 0;
+ cursor: pointer;
}
.live-demo-ctx-caret {
display: inline-block;
width: 1px;
height: 13px;
- background: var(--color-ink);
+ background: oklch(94% 0.02 82);
margin-left: 2px;
animation: liveDemoCaret 1s steps(1) infinite;
}
@keyframes liveDemoCaret {
50% { opacity: 0; }
}
-.live-demo-ctx-count {
- padding: 4px 6px;
- background: transparent;
- border: 1px solid var(--color-mist);
- border-radius: 5px;
- font-family: var(--font-mono);
- font-size: 11px;
- font-weight: 600;
- color: var(--color-ash);
- cursor: pointer;
-}
.live-demo-ctx-go {
- padding: 5px 12px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ box-sizing: border-box;
+ padding: 0;
background: var(--color-accent);
color: var(--color-paper);
border: 0;
- border-radius: 6px;
+ border-left: 1px solid oklch(22% 0 0);
+ border-radius: 0;
font-family: var(--font-body);
- font-size: 12px;
- font-weight: 600;
+ font-size: 0;
+ line-height: 0;
cursor: pointer;
white-space: nowrap;
}
diff --git a/skill/scripts/live-browser.js b/skill/scripts/live-browser.js
index 9092338eb..539cd07d9 100644
--- a/skill/scripts/live-browser.js
+++ b/skill/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/skill/scripts/live/ui-core.mjs b/skill/scripts/live/ui-core.mjs
index 4b7adde8b..bbb2c5c35 100644
--- a/skill/scripts/live/ui-core.mjs
+++ b/skill/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/tests/live-browser-regression.test.mjs b/tests/live-browser-regression.test.mjs
index 3858d266f..27e814c06 100644
--- a/tests/live-browser-regression.test.mjs
+++ b/tests/live-browser-regression.test.mjs
@@ -128,7 +128,7 @@ describe('live-browser.js regression guards', () => {
it('exits inline editing directly on outside click', () => {
assert.match(
SOURCE,
- /function cancelEditingToPicking\(\) \{[\s\S]{0,600}?state = 'PICKING';/,
+ /function cancelEditingToPicking\(\) \{[\s\S]{0,600}?setLiveState\('PICKING'\);/,
'outside-click editing cancel should avoid rebuilding configure UI before hiding it',
);
assert.match(
@@ -201,7 +201,7 @@ describe('live-browser.js regression guards', () => {
);
assert.match(
SOURCE,
- /function handleServerLost\(\)[\s\S]{0,300}?const recoveryState = currentSessionId \? state : 'IDLE';[\s\S]{0,1200}?state = recoveryState;[\s\S]{0,120}?if \(currentSessionId\) saveSession\(\);/,
+ /function handleServerLost\(\)[\s\S]{0,300}?const recoveryState = currentSessionId \? state : 'IDLE';[\s\S]{0,1200}?setLiveState\(recoveryState\);[\s\S]{0,120}?if \(currentSessionId\) saveSession\(\);/,
'server-lost cleanup should keep the current session phase in local recovery state instead of rewriting it to GENERATING',
);
});
@@ -305,6 +305,61 @@ describe('live-browser.js regression guards', () => {
/steer-blur-recover/,
'steer blur should recover focus for type-to-steer when not selecting page text',
);
+ assert.match(
+ SOURCE,
+ /function syncPageChatFocusRing\(\)[\s\S]{0,900}?typingReady[\s\S]{0,500}?pageChatInput\.style\.opacity = '1'/,
+ 'collapsed steer focus must reveal the real input and caret instead of a patina truncated label',
+ );
+ assert.match(
+ SOURCE,
+ /armPageChatForTyping\([\s\S]{0,120}?expand: true/,
+ 'steer pill pointerdown must expand before click so focus never flashes a dead collapsed state',
+ );
+ assert.match(
+ SOURCE,
+ /function syncPageChatChrome\(\)[\s\S]{0,400}?pageChatEl\.style\.borderColor = 'transparent'/,
+ 'steer pill must stay borderless; surface contrast carries the affordance',
+ );
+ assert.match(
+ SOURCE,
+ /floating bar surface IS the input; modifier pills sit left of the field/,
+ 'element configure bar must use the bar surface as the input, not a nested field',
+ );
+ });
+
+ it('empty configure and steer inputs passthrough arrow nav to handleKeyDown', () => {
+ assert.match(
+ SOURCE,
+ /function shouldPassthroughElementNav\(deepActive, e\)[\s\S]{0,500}?PREFIX \+ '-input' && state === 'CONFIGURING'/,
+ 'configure prompt must passthrough empty arrow keys for sibling navigation',
+ );
+ assert.match(
+ SOURCE,
+ /function shouldPassthroughElementNav\(deepActive, e\)[\s\S]{0,700}?PREFIX \+ '-page-chat-input' && state === 'PICKING'/,
+ 'steer prompt must passthrough empty arrow keys while picking',
+ );
+ assert.match(
+ SOURCE,
+ /&& !shouldPassthroughElementNav\(deepActive, e\)/,
+ 'global input guard must honor empty-input arrow passthrough',
+ );
+ });
+
+ it('configure input Escape tears down annotation overlay before returning to picking', () => {
+ // The configure prompt auto-focuses. While focused, the global keydown
+ // handler bails on own() inputs, so this local Escape path must hide the
+ // annotation overlay. Leaving it active strands a crosshair layer that
+ // flashes comment pins on every click after pick mode is turned off.
+ assert.match(
+ SOURCE,
+ /input\.addEventListener\('keydown', \(e\) => \{[\s\S]{0,400}?if \(e\.key === 'Escape'\) \{[\s\S]{0,200}?exitConfigureToPicking\('configure-input-escape'\)/,
+ 'configure input Escape must call exitConfigureToPicking so annot overlay is removed',
+ );
+ assert.match(
+ SOURCE,
+ /function togglePick\(\)[\s\S]{0,500}?teardownConfigureChrome\(\);/,
+ 'togglePick off must tear down configure chrome so a stuck annot overlay cannot survive IDLE',
+ );
});
it('pick mode preference persists in localStorage', () => {
@@ -325,7 +380,7 @@ describe('live-browser.js regression guards', () => {
);
assert.match(
SOURCE,
- /if \(state === 'IDLE' && \(pickActive \|\| insertActive\)\) state = 'PICKING';/,
+ /if \(state === 'IDLE' && \(pickActive \|\| insertActive\)\) setLiveState\('PICKING'\);/,
'SSE connected must arm insert mode when saved preference has insert on',
);
});
@@ -386,9 +441,19 @@ describe('live-browser.js regression guards', () => {
assert.match(SOURCE, /border: '2px dotted ' \+ BP\.accent/, 'placeholder border matches insert line (dotted)');
assert.match(
SOURCE,
- /function syncPageInteractionCursor\(\)[\s\S]{0,280}?cursorForInsertAxis/,
+ /function syncPageInteractionCursor\(\)[\s\S]{0,420}?cursorForInsertAxis/,
'insert picking cursor follows row/column axis',
);
+ assert.match(
+ SOURCE,
+ /function ensurePickCursorStyle\(\)[\s\S]{0,420}?cursor: crosshair !important/,
+ 'pick mode injects a crosshair cursor that wins over page pointer styles',
+ );
+ assert.match(
+ SOURCE,
+ /document\.documentElement\.classList\.add\(PICK_CURSOR_CLASS\)/,
+ 'pick mode toggles a document-level class for the crosshair cursor',
+ );
assert.match(SOURCE, /function hitSiblingInsertGap\(/, 'insert mode detects gaps between siblings');
assert.match(SOURCE, /function resolveInsertHover\(/, 'insert hover resolves axis-aware boundaries');
assert.match(SOURCE, /data-impeccable-placeholder-resize/, 'placeholder edge handles on annotation overlay');
@@ -406,12 +471,12 @@ describe('live-browser.js regression guards', () => {
);
assert.match(
SOURCE,
- /buildInsertConfigureRow[\s\S]*?const count = el\('button', \{[\s\S]{0,320}?height: '28px'/,
- 'insert count toggle must match input height',
+ /buildInsertConfigureRow[\s\S]*?buildConfigureCountControl\(/,
+ 'insert count toggle uses the same inline bar control as configure mode',
);
assert.match(
SOURCE,
- /buildInsertConfigureRow[\s\S]*?const create = el\('button', \{[\s\S]{0,320}?height: '28px'/,
+ /buildInsertConfigureRow[\s\S]*?buildConfigureSubmitButton\(/,
'insert Create button must match input height',
);
assert.match(SOURCE, /function resolveBarAnchor\(\)/, 'bar positions from a connected anchor');
@@ -463,6 +528,113 @@ describe('live-browser.js regression guards', () => {
);
});
+ it('configure row groups selection and input on the left, trailing controls before submit', () => {
+ assert.match(
+ SOURCE,
+ /function configureModifierPillStyle\(extra = \{\}\)[\s\S]{0,480}?background: 'transparent'[\s\S]{0,120}?color: P\.textDim/,
+ 'configure modifier chips use muted global-bar idle chrome, not active gold',
+ );
+ assert.match(
+ SOURCE,
+ /const CONFIGURE_ROW_TRACK_H = '18px'/,
+ 'configure row shares one text track height across pills and input',
+ );
+ assert.match(
+ SOURCE,
+ /function configureInputFieldStyle\(extra = \{\}\)[\s\S]{0,320}?height: CONFIGURE_ROW_TRACK_H/,
+ 'configure input uses the same track height as modifier pills',
+ );
+ assert.match(
+ SOURCE,
+ /const CONFIGURE_PILL_PAD_Y = '3px'/,
+ 'modifier pills share 3px vertical padding',
+ );
+ assert.match(
+ SOURCE,
+ /function configureInputShellStyle\(\)[\s\S]{0,200}?alignItems: 'center'[\s\S]{0,120}?padding: '0 6px 0 ' \+ CONFIGURE_BAR_INSET/,
+ 'configure shell vertically centers the row; inset matches centered pill margin',
+ );
+ assert.match(
+ SOURCE,
+ /inputShell\.appendChild\(buildSelectionPill[\s\S]{0,120}?inputShell\.appendChild\(input\)/,
+ 'selection pill and prompt input share the left side of the bar',
+ );
+ assert.match(
+ SOURCE,
+ /buildConfigureTrailingCluster\(\[action, count\], voiceBtn, go\)/,
+ 'freeform and variant count sit just left of voice and submit',
+ );
+ });
+
+ it('configure bar keeps selection pill, inline controls, outline, and instant tooltips', () => {
+ assert.match(SOURCE, /function buildSelectionPill\(/, 'selected element tag lives in the configure bar');
+ assert.match(SOURCE, /CONFIGURE_SELECTION_PILL_PAD = '1px 4px'/, 'selection pill uses 1px 4px padding');
+ assert.match(
+ SOURCE,
+ /function configureSelectionPillStyle\(extra = \{\}\)[\s\S]{0,400}?color: P\.patina/,
+ 'selection pill label uses patina text color',
+ );
+ assert.doesNotMatch(
+ SOURCE,
+ /function configureSelectionPillStyle\(extra = \{\}\)[\s\S]{0,400}?minHeight:/,
+ 'selection pill must not impose a min-height',
+ );
+ assert.match(
+ SOURCE,
+ /gridArea: '1 \/ 1'/,
+ 'selection pill swaps tag and clear faces without resizing the bar',
+ );
+ assert.match(
+ SOURCE,
+ /function shouldShowHighlightTagTooltip\(\)[\s\S]{0,160}?state !== 'CONFIGURING'/,
+ 'configure mode keeps the outline but drops the floating tag tooltip',
+ );
+ assert.match(SOURCE, /function buildConfigureActionControl\(/, 'action dropdown is an inline bar control, not a pill');
+ assert.match(SOURCE, /function buildConfigureCountControl\(/, 'variant count is an inline bar control, not a pill');
+ assert.match(
+ SOURCE,
+ /const hiTarget = resolveBarAnchor\(\);[\s\S]{0,200}?showHighlight\(hiTarget\);/,
+ 'scroll tracking keeps the element outline visible while configuring',
+ );
+ assert.match(SOURCE, /function showConfigureBarTooltip\(/, 'configure controls use instant custom tooltips');
+ assert.doesNotMatch(
+ SOURCE,
+ /count\.title = controlsLocked \? 'Apply is still running' : 'Variants: click to change'/,
+ 'variant count must not rely on native title tooltips',
+ );
+ });
+
+ it('variant count pill cycles through x1', () => {
+ assert.match(
+ SOURCE,
+ /function cycleSelectedCount\(\)[\s\S]{0,200}?selectedCount = VARIANT_COUNT_MIN/,
+ 'variant count must wrap back to x1 after the max',
+ );
+ assert.doesNotMatch(
+ SOURCE,
+ /selectedCount >= 4 \? 2/,
+ 'variant count must not skip x1 by wrapping 4 back to 2',
+ );
+ });
+
+ it('variant injection resolves the picked anchor before entering recovery', () => {
+ assert.match(
+ SOURCE,
+ /function resolveLiveInjectionAnchor\(originalMarkup\)/,
+ 'variant source fallback must try the live picked element before giving up on anchor resolution',
+ );
+ assert.match(
+ SOURCE,
+ /pickedAnchor: pickedAnchorSnapshot/,
+ 'picked anchor snapshot must persist in session storage for resume and reinjection',
+ );
+ assert.doesNotMatch(
+ SOURCE,
+ /showToast\('Variants ready\. Reveal the selected element to resume\.'/,
+ 'recovery chrome already shows this message in the generating bar; a duplicate toast stacks two bars',
+ );
+ });
+
it('editing focus timeout does not read a stale inline edit row', () => {
assert.doesNotMatch(
SOURCE,
diff --git a/tests/live-browser-source.test.mjs b/tests/live-browser-source.test.mjs
index b9e8ec085..dc62d88bc 100644
--- a/tests/live-browser-source.test.mjs
+++ b/tests/live-browser-source.test.mjs
@@ -93,9 +93,14 @@ describe('live-browser source contracts', () => {
);
assert.match(
SOURCE,
- /function buildConfigureRow\(\)[\s\S]{0,80}?const controlsLocked = pendingApplyInFlight === true;[\s\S]*?go\.disabled = controlsLocked;/,
+ /function buildConfigureRow\(\)[\s\S]{0,80}?const controlsLocked = pendingApplyInFlight === true;[\s\S]*?const go = buildConfigureSubmitButton\(\{\s*controlsLocked,/,
'Configure controls should render disabled while manual copy edits are applying',
);
+ assert.match(
+ SOURCE,
+ /function buildConfigureSubmitButton\(\{ controlsLocked[\s\S]{0,700}?btn\.disabled = controlsLocked;/,
+ 'the configure submit button must disable itself while manual copy edits are applying',
+ );
assert.match(
SOURCE,
/function handleMouseMove\(e\) \{[\s\S]{0,80}?if \(pendingApplyInFlight\) return;/,
@@ -293,7 +298,7 @@ describe('live-browser source contracts', () => {
);
assert.match(
SOURCE,
- /case 'error':\s*if \(pendingAcceptedSession\?\.id && msg\.id === pendingAcceptedSession\.id\) \{[\s\S]{0,80}?pendingAcceptedSession = null;[\s\S]{0,80}?state = 'CYCLING';[\s\S]{0,80}?updateBarContent\('cycling'\);[\s\S]{0,160}?break;/,
+ /case 'error':\s*if \(pendingAcceptedSession\?\.id && msg\.id === pendingAcceptedSession\.id\) \{[\s\S]{0,80}?pendingAcceptedSession = null;[\s\S]{0,80}?setLiveState\('CYCLING'\);[\s\S]{0,80}?updateBarContent\('cycling'\);[\s\S]{0,160}?break;/,
'an SSE error for a queued accept should invalidate pending accept state and keep variants retryable',
);
assert.equal(
diff --git a/tests/live-e2e/ui.mjs b/tests/live-e2e/ui.mjs
index dc533840f..84d332091 100644
--- a/tests/live-e2e/ui.mjs
+++ b/tests/live-e2e/ui.mjs
@@ -5,7 +5,8 @@
* Selector strategy: live-browser.js uses deterministic ids (`impeccable-live-*`)
* for the global bar, per-element bar, action picker, and params panel. Buttons
* inside the per-element bar are matched by visible text or unicode glyph
- * (`Go →`, `← / →`, `✓ Accept`, `✕`). All selectors below come from
+ * (`← / →`, `✓ Accept`, `✕`), or by aria-label for icon-only buttons (the
+ * configure submit button). All selectors below come from
* skill/scripts/live-browser.js — keep this file in sync if
* the bar's text content changes.
*/
@@ -18,6 +19,9 @@ const GLOBAL_BAR_ID = '#impeccable-live-global-bar';
const PICKER_ID = '#impeccable-live-picker';
const EDIT_BADGE_ID = '#impeccable-live-edit-badge';
const PENDING_DOCK_ID = '#impeccable-live-pending-dock';
+// The configure-row submit button is icon-only; its accessible name is the
+// only stable handle (see buildConfigureSubmitButton in live-browser.js).
+const GO_BUTTON_ARIA_LABEL = 'Generate variants';
const STEER_CHAT_ID = '#impeccable-live-page-chat';
const STEER_INPUT_ID = '#impeccable-live-page-chat-input';
const PICK_TOGGLE = '#impeccable-live-pick-toggle';
@@ -423,20 +427,21 @@ export async function pickElement(page, selector, opts = {}) {
await page.waitForSelector(BAR_ID, { state: 'visible', timeout: 1 });
}
}
- // Wait specifically for the Configure-row Go button to be in the bar.
+ // Wait specifically for the Configure-row submit button to be in the bar.
// pickElement returning before that race-conditions with clickGo on
// fixtures whose framework re-renders right after pick (modal open, tab
- // switch). Anchoring the wait on the Go button's text is robust: the bar
- // can be visible-but-empty (state=PICKING) before showBar('configure')
- // populates the row.
+ // switch). Anchoring the wait on the submit button's accessible name is
+ // robust: the bar can be visible-but-empty (state=PICKING) before
+ // showBar('configure') populates the row, and the button itself is
+ // icon-only.
await page.waitForFunction(
- (barSel) => {
+ ({ barSel, goLabel }) => {
const bar = window.__impeccableLiveQuery(barSel);
if (!bar) return false;
const btns = [...bar.querySelectorAll('button')];
- return btns.some((b) => /Go\b/.test(b.textContent || ''));
+ return btns.some((b) => (b.getAttribute('aria-label') || '') === goLabel);
},
- BAR_ID,
+ { barSel: BAR_ID, goLabel: GO_BUTTON_ARIA_LABEL },
{ timeout: 5_000 },
);
}
@@ -532,17 +537,17 @@ export async function setCount(page, count) {
export async function clickGo(page) {
let lastErr;
for (let attempt = 0; attempt < 3; attempt++) {
- await clickBarButton(page, /Go\b/);
+ await clickBarButton(page, { ariaLabel: GO_BUTTON_ARIA_LABEL });
const advanced = await page.waitForFunction(
- (barSel) => {
+ ({ barSel, goLabel }) => {
const bar = window.__impeccableLiveQuery(barSel);
if (!bar) return false;
const text = bar.textContent || '';
if (/Generating\b/.test(text)) return true;
if (/\d+\s*\/\s*\d+/.test(text)) return true;
- return ![...bar.querySelectorAll('button')].some((button) => /Go\b/.test(button.textContent || ''));
+ return ![...bar.querySelectorAll('button')].some((button) => (button.getAttribute('aria-label') || '') === goLabel);
},
- BAR_ID,
+ { barSel: BAR_ID, goLabel: GO_BUTTON_ARIA_LABEL },
{ timeout: 3_000 },
).then(() => true, (err) => {
lastErr = err;
@@ -616,15 +621,21 @@ export async function clickPrev(page) {
await clickBarButton(page, '←');
}
+function barButtonMatch(label) {
+ if (label instanceof RegExp) return { kind: 'regex', source: label.source, flags: label.flags };
+ if (label && typeof label === 'object' && label.ariaLabel) return { kind: 'aria', value: label.ariaLabel };
+ return { kind: 'text', value: String(label) };
+}
+
async function clickBarButton(page, label) {
- const textMatch = label instanceof RegExp
- ? { kind: 'regex', source: label.source, flags: label.flags }
- : { kind: 'text', value: String(label) };
+ const textMatch = barButtonMatch(label);
let lastErr;
for (let attempt = 0; attempt < 3; attempt++) {
try {
await installLiveQueryHelpers(page);
- const button = page.locator(`${BAR_ID} button`, { hasText: label });
+ const button = textMatch.kind === 'aria'
+ ? page.locator(`${BAR_ID} button[aria-label="${textMatch.value}"]`)
+ : page.locator(`${BAR_ID} button`, { hasText: label });
await button.click({ timeout: 5_000 });
return;
} catch (err) {
@@ -651,9 +662,7 @@ async function clickBarButton(page, label) {
async function dispatchBarButton(page, label) {
try {
await installLiveQueryHelpers(page);
- const textMatch = label instanceof RegExp
- ? { kind: 'regex', source: label.source, flags: label.flags }
- : { kind: 'text', value: String(label) };
+ const textMatch = barButtonMatch(label);
return await withTimeout(
page.evaluate(findAndClickBarButton, { barSel: BAR_ID, textMatch }),
5_000,
@@ -671,6 +680,7 @@ function findAndClickBarButton({ barSel, textMatch }) {
.find((candidate) => {
const text = candidate.textContent || '';
if (textMatch.kind === 'regex') return new RegExp(textMatch.source, textMatch.flags).test(text);
+ if (textMatch.kind === 'aria') return (candidate.getAttribute('aria-label') || '') === textMatch.value;
return text.includes(textMatch.value);
});
if (!btn) return false;
@@ -779,7 +789,9 @@ export async function clickSaveEdit(page) {
async function clickEditBadgeButton(page, label) {
const proxyRect = await page.evaluate((text) => {
const proxies = [...document.querySelectorAll('[data-impeccable-edit-badge-proxy="true"]')];
- const proxy = proxies.find((candidate) => (candidate.title || '').includes(text));
+ const proxy = proxies.find((candidate) =>
+ (candidate.title || candidate.getAttribute('aria-label') || '').includes(text)
+ );
if (!proxy) return null;
const rect = proxy.getBoundingClientRect();
if (rect.width < 1 || rect.height < 1) return null;
@@ -797,7 +809,7 @@ async function clickEditBadgeButton(page, label) {
const clicked = await page.evaluate(({ badgeSel, text }) => {
const badge = window.__impeccableLiveQuery(badgeSel);
const btn = [...(badge?.querySelectorAll('button') || [])].find((candidate) =>
- (candidate.textContent || '').includes(text)
+ (candidate.textContent || candidate.getAttribute('aria-label') || candidate.title || '').includes(text)
);
if (!btn) return false;
btn.click();
diff --git a/tests/live-svelte-adapter-deepseek.test.mjs b/tests/live-svelte-adapter-deepseek.test.mjs
index 59441d8a6..786ad1441 100644
--- a/tests/live-svelte-adapter-deepseek.test.mjs
+++ b/tests/live-svelte-adapter-deepseek.test.mjs
@@ -439,6 +439,7 @@ async function assertSelectedElementChrome(page) {
hasPicker: Boolean(picker),
hasBadge: Boolean(badge),
badgeText: badge?.textContent || '',
+ badgeLabel: badge?.querySelector('button')?.getAttribute('aria-label') || badge?.title || '',
hasHighlight: Boolean(highlight),
highlightVisible: highlight?.style.display !== 'none',
hasInput: Boolean(input),
@@ -449,7 +450,7 @@ async function assertSelectedElementChrome(page) {
assert.equal(result.hasBar, true);
assert.equal(result.hasPicker, true);
assert.equal(result.hasBadge, true);
- assert.match(result.badgeText, /Edit copy/);
+ assert.match(result.badgeLabel, /Edit copy/);
assert.equal(result.hasHighlight, true);
assert.equal(result.highlightVisible, true);
assert.equal(result.hasInput, true);