From 6f6af815af7db71602d2fb4bf95f84e677d2414d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:02:32 +0000 Subject: [PATCH] Sync generated provider output --- .../skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- .../skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- .../skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- .../skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- .../skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- .../skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- .../skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- .../skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- .../skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- .pi/skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- .../skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- .../skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- .../skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- .../skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- .../skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- .../skills/impeccable/scripts/live-browser.js | 253 +++++++++--------- 16 files changed, 2080 insertions(+), 1968 deletions(-) diff --git a/.agents/skills/impeccable/scripts/live-browser.js b/.agents/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/.agents/skills/impeccable/scripts/live-browser.js +++ b/.agents/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map; diff --git a/.claude/skills/impeccable/scripts/live-browser.js b/.claude/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/.claude/skills/impeccable/scripts/live-browser.js +++ b/.claude/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map; diff --git a/.cursor/skills/impeccable/scripts/live-browser.js b/.cursor/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/.cursor/skills/impeccable/scripts/live-browser.js +++ b/.cursor/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map; diff --git a/.gemini/skills/impeccable/scripts/live-browser.js b/.gemini/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/.gemini/skills/impeccable/scripts/live-browser.js +++ b/.gemini/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map; diff --git a/.github/skills/impeccable/scripts/live-browser.js b/.github/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/.github/skills/impeccable/scripts/live-browser.js +++ b/.github/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map; diff --git a/.grok/skills/impeccable/scripts/live-browser.js b/.grok/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/.grok/skills/impeccable/scripts/live-browser.js +++ b/.grok/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map; diff --git a/.hermes/skills/impeccable/scripts/live-browser.js b/.hermes/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/.hermes/skills/impeccable/scripts/live-browser.js +++ b/.hermes/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map; diff --git a/.kiro/skills/impeccable/scripts/live-browser.js b/.kiro/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/.kiro/skills/impeccable/scripts/live-browser.js +++ b/.kiro/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map; diff --git a/.opencode/skills/impeccable/scripts/live-browser.js b/.opencode/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/.opencode/skills/impeccable/scripts/live-browser.js +++ b/.opencode/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map; diff --git a/.pi/skills/impeccable/scripts/live-browser.js b/.pi/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/.pi/skills/impeccable/scripts/live-browser.js +++ b/.pi/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map; diff --git a/.qoder/skills/impeccable/scripts/live-browser.js b/.qoder/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/.qoder/skills/impeccable/scripts/live-browser.js +++ b/.qoder/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map; diff --git a/.rovodev/skills/impeccable/scripts/live-browser.js b/.rovodev/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/.rovodev/skills/impeccable/scripts/live-browser.js +++ b/.rovodev/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map; diff --git a/.trae-cn/skills/impeccable/scripts/live-browser.js b/.trae-cn/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/.trae-cn/skills/impeccable/scripts/live-browser.js +++ b/.trae-cn/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map; diff --git a/.trae/skills/impeccable/scripts/live-browser.js b/.trae/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/.trae/skills/impeccable/scripts/live-browser.js +++ b/.trae/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map; diff --git a/.vibe/skills/impeccable/scripts/live-browser.js b/.vibe/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/.vibe/skills/impeccable/scripts/live-browser.js +++ b/.vibe/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map; diff --git a/plugin/skills/impeccable/scripts/live-browser.js b/plugin/skills/impeccable/scripts/live-browser.js index 1f373a894..da026e255 100644 --- a/plugin/skills/impeccable/scripts/live-browser.js +++ b/plugin/skills/impeccable/scripts/live-browser.js @@ -6212,6 +6212,72 @@ showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000); } + function isJsxSourceFile(filePath) { + return /\.[cm]?[jt]sx$/i.test(String(filePath || '')); + } + + function completeSourceInjection(wrapper, sessionId, opts) { + recoveryWaitingForAnchor = false; + if (pendingVariantAnchorRetryObserver) { + pendingVariantAnchorRetryObserver.disconnect(); + pendingVariantAnchorRetryObserver = null; + } + + const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + arrivedVariants = variants.length; + expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); + if (arrivedVariants <= 0) { + if (state === 'GENERATING') { + // Mid-generation the source legitimately holds a scaffold wrapper + // with no variants yet (the server-side preflight wraps before the + // agent writes). Tearing the session down here would destroy an + // in-flight generation; stay in GENERATING — the variant observer + // is armed and the server re-delivers a missed `done`. + if (!opts.generationCompleted) { + console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); + return; + } + // Generation finished, yet the read shows only the scaffold: the + // source view is stale and no further event will fire. Re-read a + // few times before surfacing recovery — a single silent return + // here would strand the tab in GENERATING forever. + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' + + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + if (arrivedVariants > 0) return; + injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + } + recoverEmptyCycling('source-fallback-empty'); + return; + } + const saved = loadSession(); + const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; + visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants + ? previousVisibleVariant + : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); + showVariantInDOM(sessionId, visibleVariant); + + selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + + setLiveState('CYCLING'); + recoveryWaitingForAnchor = false; + hideShaderOverlay(); + showOrUpdateCyclingBar(); + disableInlineEdit(); + refreshParamsPanel(); + positionBar(); + saveSession(); + completeParameterGenerationIfReady(); + console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + } + /** * No-HMR fallback: fetch the raw source file from the live server, * parse it, extract the variant wrapper, and inject it into the live DOM. @@ -6229,14 +6295,53 @@ return; } rememberSessionFileMeta({ file: filePath }); + if (isJsxSourceFile(filePath)) { + const liveWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (liveWrapper && liveWrapper.querySelector('[data-impeccable-variant]:not([data-impeccable-variant="original"])')) { + completeSourceInjection(liveWrapper, sessionId, { ...opts, filePath }); + return; + } + // #454: never fetch/parse JSX. Missing wrap waits for mount (closed + // modal / other route). Insert scaffolds stay for late HMR. A replace + // scaffold with no variants after retries is a failed generation. + if (opts.generationCompleted && sessionId === currentSessionId) { + const attempt = opts.attempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (state !== 'GENERATING' || currentSessionId !== sessionId) return; + injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + return; + } + if (!liveWrapper) { + showToast( + "Variants ready. If the picked element isn't visible, retrace the path that revealed it - they'll appear automatically.", + 15000, + ); + return; + } + if (liveWrapper.dataset.impeccableMode !== 'insert') { + recoverEmptyCycling('source-fallback-empty'); + } + return; + } + if (opts.orphanDiscard && !liveWrapper && sessionId === currentSessionId) { + const attempt = opts._orphanAttempt || 0; + if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { + setTimeout(() => { + if (sessionId !== currentSessionId) return; + if (state !== 'GENERATING' && state !== 'CYCLING') return; + injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 }); + }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); + } + } + return; + } const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath); fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { const parser = new DOMParser(); - let srcWrapper = null; - - // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. const startMark = ''; const endMark = ''; const startIdx = html.indexOf(startMark); @@ -6244,8 +6349,8 @@ const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx ? html.slice(startIdx + startMark.length, endIdx).trim() : html; - const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html'); - srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + const doc = parser.parseFromString(block, 'text/html'); + const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.warn('[impeccable] Variant wrapper not found in source file.'); // A resumed cycling session whose wrapper is gone from source is an @@ -6270,93 +6375,33 @@ return; } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - const wrapper = srcWrapper.cloneNode(true); - - // Wrapper already in DOM (wrap HMR landed, variant insert did not). const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (existingWrapper) { + const wrapper = srcWrapper.cloneNode(true); existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); - } else { - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); - if (!liveEl) { - console.warn('[impeccable] Could not find original element in live DOM.'); - enterRecoveryWaitingForAnchor({ - filePath, - sessionId, - srcWrapper, - checkpointReason: 'variant_anchor_missing', - trackScroll: false, - }); - return; - } - - liveEl.parentElement.replaceChild(wrapper, liveEl); - } - recoveryWaitingForAnchor = false; - if (pendingVariantAnchorRetryObserver) { - pendingVariantAnchorRetryObserver.disconnect(); - pendingVariantAnchorRetryObserver = null; - } - - // Update state: count variants, preserving the user's current variant - // when a late HMR/source reinjection lands after they have cycled. - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - arrivedVariants = variants.length; - expectedVariants = parseInt(wrapper.dataset.impeccableVariantCount || arrivedVariants); - if (arrivedVariants <= 0) { - if (state === 'GENERATING') { - // Mid-generation the source legitimately holds a scaffold wrapper - // with no variants yet (the server-side preflight wraps before the - // agent writes). Tearing the session down here would destroy an - // in-flight generation; stay in GENERATING — the variant observer - // is armed and the server re-delivers a missed `done`. - if (!opts.generationCompleted) { - console.log('[impeccable] Source has scaffold but no variants yet; still generating.'); - return; - } - // Generation finished, yet the read shows only the scaffold: the - // source view is stale and no further event will fire. Re-read a - // few times before surfacing recovery — a single silent return - // here would strand the tab in GENERATING forever. - const attempt = opts.attempt || 0; - if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) { - console.log('[impeccable] Generation is done but source shows no variants yet; retrying read (' - + (attempt + 1) + '/' + COMPLETED_SOURCE_FALLBACK_RETRIES + ').'); - setTimeout(() => { - if (state !== 'GENERATING' || currentSessionId !== sessionId) return; - if (arrivedVariants > 0) return; - injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 }); - }, COMPLETED_SOURCE_FALLBACK_RETRY_MS); - return; - } - } - recoverEmptyCycling('source-fallback-empty'); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); return; } - const saved = loadSession(); - const savedVisibleVariant = saved && saved.id === sessionId ? saved.visible : 0; - visibleVariant = previousVisibleVariant > 0 && previousVisibleVariant <= arrivedVariants - ? previousVisibleVariant - : (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1); - showVariantInDOM(sessionId, visibleVariant); - // Update selectedElement to the visible variant's content - selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement; + const wrapper = srcWrapper.cloneNode(true); + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; - setLiveState('CYCLING'); - recoveryWaitingForAnchor = false; - hideShaderOverlay(); - showOrUpdateCyclingBar(); - disableInlineEdit(); - refreshParamsPanel(); - positionBar(); - saveSession(); - completeParameterGenerationIfReady(); - console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); + const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML); + if (!liveEl) { + console.warn('[impeccable] Could not find original element in live DOM.'); + enterRecoveryWaitingForAnchor({ + filePath, + sessionId, + srcWrapper, + checkpointReason: 'variant_anchor_missing', + trackScroll: false, + }); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + completeSourceInjection(wrapper, sessionId, { ...opts, filePath }); }) .catch(err => { console.error('[impeccable] Failed to fetch source:', err); @@ -6364,44 +6409,6 @@ }); } - function normalizeSourceFallbackBlock(block, filePath) { - if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block; - return String(block) - .replace( - /]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g, - (_match, attrs, css) => '' + css + '', - ) - .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => { - const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim(); - return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : ''; - }) - .replace(/\bclassName\s*=/g, 'class=') - .replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => { - const css = jsxStyleObjectToCss(body); - return css ? ' style="' + escapeHtml(css) + '"' : ''; - }); - } - - function jsxStyleObjectToCss(body) { - const declarations = []; - const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g; - let match; - while ((match = re.exec(String(body || '')))) { - const prop = jsxStylePropToCss(match[1]); - const value = match[2] ?? match[3] ?? match[4] ?? ''; - if (!prop || value === '') continue; - declarations.push(prop + ': ' + value); - } - return declarations.join('; '); - } - - function jsxStylePropToCss(prop) { - let out = String(prop || '').trim().replace(/^["']|["']$/g, ''); - if (!out) return ''; - if (out.startsWith('--')) return out; - return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-'); - } - function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) { const map = new Map(); if (!sourceOriginal || !liveOriginal) return map;