Sync generated provider output

This commit is contained in:
github-actions[bot]
2026-09-01 22:02:32 +00:00
parent 38e102f0b2
commit 6f6af815af
16 changed files with 2080 additions and 1968 deletions
+131 -124
View File
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
+131 -124
View File
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
+131 -124
View File
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
+131 -124
View File
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
+131 -124
View File
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
+131 -124
View File
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
+131 -124
View File
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
+131 -124
View File
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
+131 -124
View File
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
+131 -124
View File
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
+131 -124
View File
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
+131 -124
View File
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
+131 -124
View File
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
+131 -124
View File
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;
+131 -124
View File
@@ -6212,98 +6212,18 @@
showToast('The previous live session no longer matches the source file, so it was discarded. Pick an element to start fresh.', 6000);
}
/**
* No-HMR fallback: fetch the raw source file from the live server,
* parse it, extract the variant wrapper, and inject it into the live DOM.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
const url = 'http://localhost:' + PORT + '/source?token=' + TOKEN + '&path=' + encodeURIComponent(filePath);
fetch(url)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(html => {
const parser = new DOMParser();
let srcWrapper = null;
// Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction.
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(normalizeSourceFallbackBlock(block, filePath), 'text/html');
srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (!srcWrapper) {
console.warn('[impeccable] Variant wrapper not found in source file.');
// A resumed cycling session whose wrapper is gone from source is an
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
function isJsxSourceFile(filePath) {
return /\.[cm]?[jt]sx$/i.test(String(filePath || ''));
}
const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0;
const wrapper = srcWrapper.cloneNode(true);
// Wrapper already in DOM (wrap HMR landed, variant insert did not).
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
} else {
const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child');
if (!origContent) return;
const liveEl = resolveLiveInjectionAnchor(origContent.outerHTML);
if (!liveEl) {
console.warn('[impeccable] Could not find original element in live DOM.');
enterRecoveryWaitingForAnchor({
filePath,
sessionId,
srcWrapper,
checkpointReason: 'variant_anchor_missing',
trackScroll: false,
});
return;
}
liveEl.parentElement.replaceChild(wrapper, liveEl);
}
function completeSourceInjection(wrapper, sessionId, opts) {
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 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);
@@ -6329,7 +6249,7 @@
setTimeout(() => {
if (state !== 'GENERATING' || currentSessionId !== sessionId) return;
if (arrivedVariants > 0) return;
injectVariantsFromSource(filePath, sessionId, { ...opts, attempt: attempt + 1 });
injectVariantsFromSource(opts.filePath, sessionId, { ...opts, attempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
return;
}
@@ -6344,7 +6264,6 @@
: (savedVisibleVariant > 0 && savedVisibleVariant <= arrivedVariants ? savedVisibleVariant : 1);
showVariantInDOM(sessionId, visibleVariant);
// Update selectedElement to the visible variant's content
selectedElement = pickVariantContent(wrapper, visibleVariant) || wrapper.parentElement;
setLiveState('CYCLING');
@@ -6357,6 +6276,132 @@
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.
* This works even when the dev server caches HTML (Bun, static servers).
*
* opts.generationCompleted marks callers that KNOW the agent finished (a
* `done` arrived or the server reported a completed generation). For them an
* empty read is a stale source view and no further event is coming, so the
* read retries a few times and then surfaces recovery. Callers without the
* flag may be mid-generation and wait indefinitely for the real completion.
*/
function injectVariantsFromSource(filePath, sessionId, opts = {}) {
if (isSvelteComponentManifestPath(filePath)) {
injectSvelteComponentsFromManifest(filePath, sessionId);
return;
}
rememberSessionFileMeta({ file: filePath });
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();
const startMark = '<!-- impeccable-variants-start ' + sessionId + ' -->';
const endMark = '<!-- impeccable-variants-end ' + sessionId + ' -->';
const startIdx = html.indexOf(startMark);
const endIdx = html.indexOf(endMark);
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
const doc = parser.parseFromString(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
// ORPHAN: the file was edited or regenerated out from under it, so
// no reload, HMR push, or server restart can ever complete it, and
// the frozen picker it leaves behind used to need a manual
// live-complete --discarded. Retry a few reads first (an agent
// rewrite or HMR patch may be mid-flight), then self-discard and
// hand the surface back to the picker.
if (opts.orphanDiscard && sessionId === currentSessionId) {
const attempt = opts._orphanAttempt || 0;
if (attempt < COMPLETED_SOURCE_FALLBACK_RETRIES) {
setTimeout(() => {
if (sessionId !== currentSessionId) return;
if (state !== 'GENERATING' && state !== 'CYCLING') return;
injectVariantsFromSource(filePath, sessionId, { ...opts, _orphanAttempt: attempt + 1 });
}, COMPLETED_SOURCE_FALLBACK_RETRY_MS);
} else {
discardOrphanedSession('variant wrapper missing from source');
}
}
return;
}
const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
if (existingWrapper) {
const wrapper = srcWrapper.cloneNode(true);
existingWrapper.parentElement.replaceChild(wrapper, existingWrapper);
completeSourceInjection(wrapper, sessionId, { ...opts, filePath });
return;
}
const wrapper = srcWrapper.cloneNode(true);
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);
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(
/<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
(_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
)
.replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
const literalClasses = value.replace(/\$\{[^}]*\}/g, ' ').replace(/\s+/g, ' ').trim();
return literalClasses ? 'class="' + escapeHtml(literalClasses) + '"' : '';
})
.replace(/\bclassName\s*=/g, 'class=')
.replace(/\sstyle=\{\{([\s\S]*?)\}\}/g, (_match, body) => {
const css = jsxStyleObjectToCss(body);
return css ? ' style="' + escapeHtml(css) + '"' : '';
});
}
function jsxStyleObjectToCss(body) {
const declarations = [];
const re = /(["'][^"']+["']|[A-Za-z_$][\w$-]*)\s*:\s*(?:"([^"]*)"|'([^']*)'|(-?\d+(?:\.\d+)?))/g;
let match;
while ((match = re.exec(String(body || '')))) {
const prop = jsxStylePropToCss(match[1]);
const value = match[2] ?? match[3] ?? match[4] ?? '';
if (!prop || value === '') continue;
declarations.push(prop + ': ' + value);
}
return declarations.join('; ');
}
function jsxStylePropToCss(prop) {
let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
if (!out) return '';
if (out.startsWith('--')) return out;
return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^-ms-/, '-ms-');
}
function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
const map = new Map();
if (!sourceOriginal || !liveOriginal) return map;