diff --git a/.agents/skills/impeccable/scripts/live-browser.js b/.agents/skills/impeccable/scripts/live-browser.js
index 7bf6ef31c..302b9ec23 100644
--- a/.agents/skills/impeccable/scripts/live-browser.js
+++ b/.agents/skills/impeccable/scripts/live-browser.js
@@ -4503,15 +4503,17 @@
if (origContent.id) {
liveEl = document.getElementById(origContent.id);
} else if (cls) {
- const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
+ const candidates = [...document.getElementsByTagName(tag)];
for (const c of candidates) {
if (c.className === cls && !own(c)) { liveEl = c; break; }
}
if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter(Boolean);
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
+ if (expectedClasses.length > 0) {
+ for (const c of candidates) {
+ if (own(c)) continue;
+ if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ }
}
}
}
@@ -4880,7 +4882,7 @@
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
- const doc = parser.parseFromString(block, 'text/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.');
@@ -4949,6 +4951,44 @@
});
}
+ function normalizeSourceFallbackBlock(block, filePath) {
+ if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
+ return String(block)
+ .replace(
+ /',
+ )
+ .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;
@@ -5422,7 +5462,11 @@
}
// Source fallback when HMR did not land variants in this tab.
if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) {
- injectVariantsFromSource(msg.file, msg.id);
+ setTimeout(() => {
+ if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
+ if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
+ injectVariantsFromSource(msg.file, msg.id);
+ }, 750);
break;
}
// Variants are in source but not in the DOM yet. Common when the
@@ -5514,8 +5558,11 @@
function sendEvent(msg, opts) {
msg.token = TOKEN;
function handleFailure(err) {
- console.error('[impeccable] Failed to send event:', err);
- if (opts && opts.throwOnError) throw err;
+ if (opts && opts.throwOnError) {
+ console.error('[impeccable] Failed to send event:', err);
+ throw err;
+ }
+ console.debug('[impeccable] Dropped optional live event:', err);
return null;
}
return fetch('http://localhost:' + PORT + '/events', {
@@ -6843,12 +6890,16 @@ void main() {
function ensureAcceptedDomClean(pending) {
const sessionId = pending?.id;
const variantId = pending?.variant;
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
+ const wrapper = findAcceptedRuntimeWrapper(sessionId);
const accepted = wrapper?.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
if (!wrapper) {
restoreAcceptedDomFromSnapshot(pending);
return;
}
+ if (acceptedDomAlreadyClean(pending)) {
+ wrapper.remove();
+ return;
+ }
if (!accepted) {
wrapper.remove();
restoreAcceptedDomFromSnapshot(pending);
@@ -6862,6 +6913,12 @@ void main() {
wrapper.remove();
}
+ function findAcceptedRuntimeWrapper(sessionId) {
+ if (!sessionId) return null;
+ return document.querySelector('[data-impeccable-variants="' + sessionId + '"]')
+ || document.querySelector('[data-impeccable-carbonize="' + sessionId + '"]');
+ }
+
function restoreAcceptedDomFromSnapshot(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (!pending?.acceptedHtml) {
diff --git a/.claude/skills/impeccable/scripts/live-browser.js b/.claude/skills/impeccable/scripts/live-browser.js
index 7bf6ef31c..302b9ec23 100644
--- a/.claude/skills/impeccable/scripts/live-browser.js
+++ b/.claude/skills/impeccable/scripts/live-browser.js
@@ -4503,15 +4503,17 @@
if (origContent.id) {
liveEl = document.getElementById(origContent.id);
} else if (cls) {
- const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
+ const candidates = [...document.getElementsByTagName(tag)];
for (const c of candidates) {
if (c.className === cls && !own(c)) { liveEl = c; break; }
}
if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter(Boolean);
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
+ if (expectedClasses.length > 0) {
+ for (const c of candidates) {
+ if (own(c)) continue;
+ if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ }
}
}
}
@@ -4880,7 +4882,7 @@
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
- const doc = parser.parseFromString(block, 'text/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.');
@@ -4949,6 +4951,44 @@
});
}
+ function normalizeSourceFallbackBlock(block, filePath) {
+ if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
+ return String(block)
+ .replace(
+ /',
+ )
+ .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;
@@ -5422,7 +5462,11 @@
}
// Source fallback when HMR did not land variants in this tab.
if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) {
- injectVariantsFromSource(msg.file, msg.id);
+ setTimeout(() => {
+ if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
+ if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
+ injectVariantsFromSource(msg.file, msg.id);
+ }, 750);
break;
}
// Variants are in source but not in the DOM yet. Common when the
@@ -5514,8 +5558,11 @@
function sendEvent(msg, opts) {
msg.token = TOKEN;
function handleFailure(err) {
- console.error('[impeccable] Failed to send event:', err);
- if (opts && opts.throwOnError) throw err;
+ if (opts && opts.throwOnError) {
+ console.error('[impeccable] Failed to send event:', err);
+ throw err;
+ }
+ console.debug('[impeccable] Dropped optional live event:', err);
return null;
}
return fetch('http://localhost:' + PORT + '/events', {
@@ -6843,12 +6890,16 @@ void main() {
function ensureAcceptedDomClean(pending) {
const sessionId = pending?.id;
const variantId = pending?.variant;
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
+ const wrapper = findAcceptedRuntimeWrapper(sessionId);
const accepted = wrapper?.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
if (!wrapper) {
restoreAcceptedDomFromSnapshot(pending);
return;
}
+ if (acceptedDomAlreadyClean(pending)) {
+ wrapper.remove();
+ return;
+ }
if (!accepted) {
wrapper.remove();
restoreAcceptedDomFromSnapshot(pending);
@@ -6862,6 +6913,12 @@ void main() {
wrapper.remove();
}
+ function findAcceptedRuntimeWrapper(sessionId) {
+ if (!sessionId) return null;
+ return document.querySelector('[data-impeccable-variants="' + sessionId + '"]')
+ || document.querySelector('[data-impeccable-carbonize="' + sessionId + '"]');
+ }
+
function restoreAcceptedDomFromSnapshot(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (!pending?.acceptedHtml) {
diff --git a/.cursor/skills/impeccable/scripts/live-browser.js b/.cursor/skills/impeccable/scripts/live-browser.js
index 7bf6ef31c..302b9ec23 100644
--- a/.cursor/skills/impeccable/scripts/live-browser.js
+++ b/.cursor/skills/impeccable/scripts/live-browser.js
@@ -4503,15 +4503,17 @@
if (origContent.id) {
liveEl = document.getElementById(origContent.id);
} else if (cls) {
- const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
+ const candidates = [...document.getElementsByTagName(tag)];
for (const c of candidates) {
if (c.className === cls && !own(c)) { liveEl = c; break; }
}
if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter(Boolean);
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
+ if (expectedClasses.length > 0) {
+ for (const c of candidates) {
+ if (own(c)) continue;
+ if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ }
}
}
}
@@ -4880,7 +4882,7 @@
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
- const doc = parser.parseFromString(block, 'text/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.');
@@ -4949,6 +4951,44 @@
});
}
+ function normalizeSourceFallbackBlock(block, filePath) {
+ if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
+ return String(block)
+ .replace(
+ /',
+ )
+ .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;
@@ -5422,7 +5462,11 @@
}
// Source fallback when HMR did not land variants in this tab.
if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) {
- injectVariantsFromSource(msg.file, msg.id);
+ setTimeout(() => {
+ if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
+ if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
+ injectVariantsFromSource(msg.file, msg.id);
+ }, 750);
break;
}
// Variants are in source but not in the DOM yet. Common when the
@@ -5514,8 +5558,11 @@
function sendEvent(msg, opts) {
msg.token = TOKEN;
function handleFailure(err) {
- console.error('[impeccable] Failed to send event:', err);
- if (opts && opts.throwOnError) throw err;
+ if (opts && opts.throwOnError) {
+ console.error('[impeccable] Failed to send event:', err);
+ throw err;
+ }
+ console.debug('[impeccable] Dropped optional live event:', err);
return null;
}
return fetch('http://localhost:' + PORT + '/events', {
@@ -6843,12 +6890,16 @@ void main() {
function ensureAcceptedDomClean(pending) {
const sessionId = pending?.id;
const variantId = pending?.variant;
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
+ const wrapper = findAcceptedRuntimeWrapper(sessionId);
const accepted = wrapper?.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
if (!wrapper) {
restoreAcceptedDomFromSnapshot(pending);
return;
}
+ if (acceptedDomAlreadyClean(pending)) {
+ wrapper.remove();
+ return;
+ }
if (!accepted) {
wrapper.remove();
restoreAcceptedDomFromSnapshot(pending);
@@ -6862,6 +6913,12 @@ void main() {
wrapper.remove();
}
+ function findAcceptedRuntimeWrapper(sessionId) {
+ if (!sessionId) return null;
+ return document.querySelector('[data-impeccable-variants="' + sessionId + '"]')
+ || document.querySelector('[data-impeccable-carbonize="' + sessionId + '"]');
+ }
+
function restoreAcceptedDomFromSnapshot(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (!pending?.acceptedHtml) {
diff --git a/.gemini/skills/impeccable/scripts/live-browser.js b/.gemini/skills/impeccable/scripts/live-browser.js
index 7bf6ef31c..302b9ec23 100644
--- a/.gemini/skills/impeccable/scripts/live-browser.js
+++ b/.gemini/skills/impeccable/scripts/live-browser.js
@@ -4503,15 +4503,17 @@
if (origContent.id) {
liveEl = document.getElementById(origContent.id);
} else if (cls) {
- const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
+ const candidates = [...document.getElementsByTagName(tag)];
for (const c of candidates) {
if (c.className === cls && !own(c)) { liveEl = c; break; }
}
if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter(Boolean);
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
+ if (expectedClasses.length > 0) {
+ for (const c of candidates) {
+ if (own(c)) continue;
+ if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ }
}
}
}
@@ -4880,7 +4882,7 @@
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
- const doc = parser.parseFromString(block, 'text/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.');
@@ -4949,6 +4951,44 @@
});
}
+ function normalizeSourceFallbackBlock(block, filePath) {
+ if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
+ return String(block)
+ .replace(
+ /',
+ )
+ .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;
@@ -5422,7 +5462,11 @@
}
// Source fallback when HMR did not land variants in this tab.
if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) {
- injectVariantsFromSource(msg.file, msg.id);
+ setTimeout(() => {
+ if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
+ if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
+ injectVariantsFromSource(msg.file, msg.id);
+ }, 750);
break;
}
// Variants are in source but not in the DOM yet. Common when the
@@ -5514,8 +5558,11 @@
function sendEvent(msg, opts) {
msg.token = TOKEN;
function handleFailure(err) {
- console.error('[impeccable] Failed to send event:', err);
- if (opts && opts.throwOnError) throw err;
+ if (opts && opts.throwOnError) {
+ console.error('[impeccable] Failed to send event:', err);
+ throw err;
+ }
+ console.debug('[impeccable] Dropped optional live event:', err);
return null;
}
return fetch('http://localhost:' + PORT + '/events', {
@@ -6843,12 +6890,16 @@ void main() {
function ensureAcceptedDomClean(pending) {
const sessionId = pending?.id;
const variantId = pending?.variant;
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
+ const wrapper = findAcceptedRuntimeWrapper(sessionId);
const accepted = wrapper?.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
if (!wrapper) {
restoreAcceptedDomFromSnapshot(pending);
return;
}
+ if (acceptedDomAlreadyClean(pending)) {
+ wrapper.remove();
+ return;
+ }
if (!accepted) {
wrapper.remove();
restoreAcceptedDomFromSnapshot(pending);
@@ -6862,6 +6913,12 @@ void main() {
wrapper.remove();
}
+ function findAcceptedRuntimeWrapper(sessionId) {
+ if (!sessionId) return null;
+ return document.querySelector('[data-impeccable-variants="' + sessionId + '"]')
+ || document.querySelector('[data-impeccable-carbonize="' + sessionId + '"]');
+ }
+
function restoreAcceptedDomFromSnapshot(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (!pending?.acceptedHtml) {
diff --git a/.github/skills/impeccable/scripts/live-browser.js b/.github/skills/impeccable/scripts/live-browser.js
index 7bf6ef31c..302b9ec23 100644
--- a/.github/skills/impeccable/scripts/live-browser.js
+++ b/.github/skills/impeccable/scripts/live-browser.js
@@ -4503,15 +4503,17 @@
if (origContent.id) {
liveEl = document.getElementById(origContent.id);
} else if (cls) {
- const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
+ const candidates = [...document.getElementsByTagName(tag)];
for (const c of candidates) {
if (c.className === cls && !own(c)) { liveEl = c; break; }
}
if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter(Boolean);
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
+ if (expectedClasses.length > 0) {
+ for (const c of candidates) {
+ if (own(c)) continue;
+ if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ }
}
}
}
@@ -4880,7 +4882,7 @@
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
- const doc = parser.parseFromString(block, 'text/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.');
@@ -4949,6 +4951,44 @@
});
}
+ function normalizeSourceFallbackBlock(block, filePath) {
+ if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
+ return String(block)
+ .replace(
+ /',
+ )
+ .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;
@@ -5422,7 +5462,11 @@
}
// Source fallback when HMR did not land variants in this tab.
if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) {
- injectVariantsFromSource(msg.file, msg.id);
+ setTimeout(() => {
+ if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
+ if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
+ injectVariantsFromSource(msg.file, msg.id);
+ }, 750);
break;
}
// Variants are in source but not in the DOM yet. Common when the
@@ -5514,8 +5558,11 @@
function sendEvent(msg, opts) {
msg.token = TOKEN;
function handleFailure(err) {
- console.error('[impeccable] Failed to send event:', err);
- if (opts && opts.throwOnError) throw err;
+ if (opts && opts.throwOnError) {
+ console.error('[impeccable] Failed to send event:', err);
+ throw err;
+ }
+ console.debug('[impeccable] Dropped optional live event:', err);
return null;
}
return fetch('http://localhost:' + PORT + '/events', {
@@ -6843,12 +6890,16 @@ void main() {
function ensureAcceptedDomClean(pending) {
const sessionId = pending?.id;
const variantId = pending?.variant;
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
+ const wrapper = findAcceptedRuntimeWrapper(sessionId);
const accepted = wrapper?.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
if (!wrapper) {
restoreAcceptedDomFromSnapshot(pending);
return;
}
+ if (acceptedDomAlreadyClean(pending)) {
+ wrapper.remove();
+ return;
+ }
if (!accepted) {
wrapper.remove();
restoreAcceptedDomFromSnapshot(pending);
@@ -6862,6 +6913,12 @@ void main() {
wrapper.remove();
}
+ function findAcceptedRuntimeWrapper(sessionId) {
+ if (!sessionId) return null;
+ return document.querySelector('[data-impeccable-variants="' + sessionId + '"]')
+ || document.querySelector('[data-impeccable-carbonize="' + sessionId + '"]');
+ }
+
function restoreAcceptedDomFromSnapshot(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (!pending?.acceptedHtml) {
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 423938ee9..60f449b9c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,15 +5,49 @@ on:
branches: [main]
pull_request:
branches: [main]
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
jobs:
- test:
+ changes:
runs-on: ubuntu-latest
-
+ outputs:
+ core: ${{ steps.plan.outputs.core }}
+ detector: ${{ steps.plan.outputs.detector }}
+ live: ${{ steps.plan.outputs.live }}
+ framework: ${{ steps.plan.outputs.framework }}
+ cli_remote_e2e: ${{ steps.plan.outputs.cli_remote_e2e }}
+ live_e2e: ${{ steps.plan.outputs.live_e2e }}
+ live_e2e_accept_cleanup: ${{ steps.plan.outputs.live_e2e_accept_cleanup }}
+ skill_behavior: ${{ steps.plan.outputs.skill_behavior }}
+ live_svelte_adapter_deepseek: ${{ steps.plan.outputs.live_svelte_adapter_deepseek }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
-
+ with:
+ fetch-depth: 0
+
+ - name: Setup Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+
+ - name: Detect test plan
+ id: plan
+ env:
+ GITHUB_EVENT_BEFORE: ${{ github.event.before }}
+ run: node scripts/ci-test-plan.mjs
+
+ test:
+ runs-on: ubuntu-latest
+ needs: changes
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
- name: Setup Node
uses: actions/setup-node@v6
with:
@@ -27,18 +61,358 @@ jobs:
- name: Install dependencies
run: bun install
+ - name: Run core tests
+ run: bun run test:core
+
- name: Install Puppeteer browser
+ if: needs.changes.outputs.detector == 'true'
run: bunx puppeteer browsers install chrome
- - name: Run tests
- run: bun run test
-
+ - name: Run detector tests
+ if: needs.changes.outputs.detector == 'true'
+ run: bun run test:detector
+
+ - name: Run live unit tests
+ if: needs.changes.outputs.live == 'true'
+ run: bun run test:live
+
+ - name: Run framework fixture tests
+ if: needs.changes.outputs.framework == 'true'
+ run: bun run test:framework
+
+ - name: Rebuild browser detector
+ if: needs.changes.outputs.detector == 'true'
+ run: bun run build:browser
+
+ - name: Rebuild extension detector
+ if: needs.changes.outputs.detector == 'true'
+ run: bun run build:extension
+
- name: Build
run: bun run build
-
+
+ - name: Verify generated tracked outputs
+ run: git diff --exit-code -- .agents .claude .cursor .gemini .github/skills plugin cli/engine/detect-antipatterns-browser.js
+
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: impeccable-dist
path: dist/
retention-days: 7
+
+ cli-remote-e2e:
+ runs-on: ubuntu-latest
+ needs: changes
+ if: needs.changes.outputs.cli_remote_e2e == 'true'
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Setup Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Install dependencies
+ run: bun install
+
+ - name: Run remote CLI E2E smoke
+ run: bun run test:cli-remote-e2e
+
+ live-e2e-smoke:
+ name: live-e2e smoke (${{ matrix.group }})
+ runs-on: ubuntu-latest
+ needs: changes
+ if: needs.changes.outputs.live_e2e == 'true' && github.event_name != 'workflow_dispatch'
+ timeout-minutes: 15
+ strategy:
+ fail-fast: true
+ matrix:
+ include:
+ - group: platform
+ fixtures: astro-vite7,nextjs-app-router,vite8-sveltekit
+ - group: react
+ fixtures: vite8-react-css-modules,vite8-react-insert,vite8-react-plain
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Setup Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Cache fixture npm downloads
+ uses: actions/cache@v4
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-fixture-npm-${{ hashFiles('tests/framework-fixtures/**/files/package.json') }}
+ restore-keys: |
+ ${{ runner.os }}-fixture-npm-
+
+ - name: Cache Playwright Chromium
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/ms-playwright
+ key: ${{ runner.os }}-playwright-chromium-${{ hashFiles('package.json', 'bun.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-playwright-chromium-
+
+ - name: Install dependencies
+ run: bun install
+
+ - name: Install Playwright Chromium
+ run: npx playwright install chromium
+
+ - name: Run live E2E tests
+ run: bun run test:live-e2e
+ env:
+ IMPECCABLE_E2E_ONLY: ${{ matrix.fixtures }}
+ IMPECCABLE_E2E_SCENARIOS: core
+ IMPECCABLE_E2E_TEST_TIMEOUT_MS: 180000
+ IMPECCABLE_E2E_INSTALL_TIMEOUT_MS: 120000
+ IMPECCABLE_E2E_DEV_READY_TIMEOUT_MS: 60000
+ IMPECCABLE_E2E_ARTIFACT_DIR: test-results/live-e2e/${{ matrix.group }}
+
+ - name: Upload live E2E failure artifacts
+ if: failure()
+ uses: actions/upload-artifact@v7
+ with:
+ name: live-e2e-smoke-${{ matrix.group }}-artifacts
+ path: test-results/live-e2e
+ if-no-files-found: ignore
+
+ live-e2e-full:
+ name: live-e2e full (${{ matrix.group }})
+ runs-on: ubuntu-latest
+ needs: changes
+ if: needs.changes.outputs.live_e2e == 'true' && github.event_name == 'workflow_dispatch'
+ timeout-minutes: 25
+ strategy:
+ fail-fast: true
+ matrix:
+ include:
+ - group: platform
+ fixtures: astro-vite7,nextjs-app-router,vite8-sveltekit
+ - group: react-a
+ fixtures: vite8-https,vite8-react-base-path,vite8-react-csp-meta,vite8-react-css-modules,vite8-react-emotion
+ - group: react-b
+ fixtures: vite8-react-insert,vite8-react-mapped-list,vite8-react-modal,vite8-react-plain
+ - group: stateful
+ fixtures: vite8-react-radix-dialog,vite8-react-router-spa,vite8-react-styled-components,vite8-react-tabs
+ - group: styling
+ fixtures: vite8-react-tailwindv3,vite8-react-tailwindv4,vite8-react-ts,vite8-react-tsx-repeated-aside,vite8-react-unocss,vite8-react-vanilla-extract
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Setup Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Cache fixture npm downloads
+ uses: actions/cache@v4
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-fixture-npm-${{ hashFiles('tests/framework-fixtures/**/files/package.json') }}
+ restore-keys: |
+ ${{ runner.os }}-fixture-npm-
+
+ - name: Cache Playwright Chromium
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/ms-playwright
+ key: ${{ runner.os }}-playwright-chromium-${{ hashFiles('package.json', 'bun.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-playwright-chromium-
+
+ - name: Install dependencies
+ run: bun install
+
+ - name: Install Playwright Chromium
+ run: npx playwright install chromium
+
+ - name: Run live E2E tests
+ run: bun run test:live-e2e
+ env:
+ IMPECCABLE_E2E_ONLY: ${{ matrix.fixtures }}
+ IMPECCABLE_E2E_TEST_TIMEOUT_MS: 300000
+ IMPECCABLE_E2E_INSTALL_TIMEOUT_MS: 180000
+ IMPECCABLE_E2E_DEV_READY_TIMEOUT_MS: 120000
+ IMPECCABLE_E2E_ARTIFACT_DIR: test-results/live-e2e/${{ matrix.group }}
+
+ - name: Upload live E2E failure artifacts
+ if: failure()
+ uses: actions/upload-artifact@v7
+ with:
+ name: live-e2e-full-${{ matrix.group }}-artifacts
+ path: test-results/live-e2e
+ if-no-files-found: ignore
+
+ live-e2e-accept-cleanup:
+ runs-on: ubuntu-latest
+ needs: changes
+ if: needs.changes.outputs.live_e2e_accept_cleanup == 'true'
+ timeout-minutes: 15
+ env:
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
+ steps:
+ - name: Skip without provider key
+ if: ${{ env.ANTHROPIC_API_KEY == '' && env.DEEPSEEK_API_KEY == '' }}
+ run: echo "Skipping provider-backed accept-cleanup regression because no provider API key is configured."
+
+ - name: Checkout repository
+ if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
+ uses: actions/checkout@v6
+
+ - name: Setup Node
+ if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+
+ - name: Setup Bun
+ if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Cache fixture npm downloads
+ if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
+ uses: actions/cache@v4
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-fixture-npm-${{ hashFiles('tests/framework-fixtures/**/files/package.json') }}
+ restore-keys: |
+ ${{ runner.os }}-fixture-npm-
+
+ - name: Cache Playwright Chromium
+ if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/ms-playwright
+ key: ${{ runner.os }}-playwright-chromium-${{ hashFiles('package.json', 'bun.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-playwright-chromium-
+
+ - name: Install dependencies
+ if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
+ run: bun install
+
+ - name: Install Playwright Chromium
+ if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
+ run: npx playwright install chromium
+
+ - name: Run accept cleanup regression
+ if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
+ run: |
+ if [ -n "$DEEPSEEK_API_KEY" ]; then
+ export IMPECCABLE_E2E_LLM_PROVIDER=deepseek
+ else
+ export IMPECCABLE_E2E_LLM_PROVIDER=anthropic
+ fi
+ bun run test:live-e2e-accept-cleanup
+
+ live-svelte-adapter-deepseek:
+ runs-on: ubuntu-latest
+ needs: changes
+ if: needs.changes.outputs.live_svelte_adapter_deepseek == 'true'
+ timeout-minutes: 25
+ env:
+ DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
+ steps:
+ - name: Skip without DeepSeek key
+ if: ${{ env.DEEPSEEK_API_KEY == '' }}
+ run: echo "Skipping Svelte adapter DeepSeek sweep because DEEPSEEK_API_KEY is not configured."
+
+ - name: Checkout repository
+ if: ${{ env.DEEPSEEK_API_KEY != '' }}
+ uses: actions/checkout@v6
+
+ - name: Setup Node
+ if: ${{ env.DEEPSEEK_API_KEY != '' }}
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+
+ - name: Setup Bun
+ if: ${{ env.DEEPSEEK_API_KEY != '' }}
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Cache fixture npm downloads
+ if: ${{ env.DEEPSEEK_API_KEY != '' }}
+ uses: actions/cache@v4
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-fixture-npm-${{ hashFiles('tests/framework-fixtures/**/files/package.json') }}
+ restore-keys: |
+ ${{ runner.os }}-fixture-npm-
+
+ - name: Cache Playwright Chromium
+ if: ${{ env.DEEPSEEK_API_KEY != '' }}
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/ms-playwright
+ key: ${{ runner.os }}-playwright-chromium-${{ hashFiles('package.json', 'bun.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-playwright-chromium-
+
+ - name: Install dependencies
+ if: ${{ env.DEEPSEEK_API_KEY != '' }}
+ run: bun install
+
+ - name: Install Playwright Chromium
+ if: ${{ env.DEEPSEEK_API_KEY != '' }}
+ run: npx playwright install chromium
+
+ - name: Run Svelte adapter DeepSeek sweep
+ if: ${{ env.DEEPSEEK_API_KEY != '' }}
+ run: bun run test:live-svelte-adapter-deepseek
+
+ skill-behavior:
+ runs-on: ubuntu-latest
+ needs: changes
+ if: needs.changes.outputs.skill_behavior == 'true' && github.event_name != 'pull_request'
+ env:
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+ GOOGLE_CLOUD_API_KEY: ${{ secrets.GOOGLE_CLOUD_API_KEY }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Setup Node
+ uses: actions/setup-node@v6
+ with:
+ node-version: 24
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Install dependencies
+ run: bun install
+
+ - name: Run skill behavior tests
+ run: bun run test:skill-behavior
diff --git a/.kiro/skills/impeccable/scripts/live-browser.js b/.kiro/skills/impeccable/scripts/live-browser.js
index 7bf6ef31c..302b9ec23 100644
--- a/.kiro/skills/impeccable/scripts/live-browser.js
+++ b/.kiro/skills/impeccable/scripts/live-browser.js
@@ -4503,15 +4503,17 @@
if (origContent.id) {
liveEl = document.getElementById(origContent.id);
} else if (cls) {
- const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
+ const candidates = [...document.getElementsByTagName(tag)];
for (const c of candidates) {
if (c.className === cls && !own(c)) { liveEl = c; break; }
}
if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter(Boolean);
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
+ if (expectedClasses.length > 0) {
+ for (const c of candidates) {
+ if (own(c)) continue;
+ if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ }
}
}
}
@@ -4880,7 +4882,7 @@
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
- const doc = parser.parseFromString(block, 'text/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.');
@@ -4949,6 +4951,44 @@
});
}
+ function normalizeSourceFallbackBlock(block, filePath) {
+ if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
+ return String(block)
+ .replace(
+ /',
+ )
+ .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;
@@ -5422,7 +5462,11 @@
}
// Source fallback when HMR did not land variants in this tab.
if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) {
- injectVariantsFromSource(msg.file, msg.id);
+ setTimeout(() => {
+ if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
+ if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
+ injectVariantsFromSource(msg.file, msg.id);
+ }, 750);
break;
}
// Variants are in source but not in the DOM yet. Common when the
@@ -5514,8 +5558,11 @@
function sendEvent(msg, opts) {
msg.token = TOKEN;
function handleFailure(err) {
- console.error('[impeccable] Failed to send event:', err);
- if (opts && opts.throwOnError) throw err;
+ if (opts && opts.throwOnError) {
+ console.error('[impeccable] Failed to send event:', err);
+ throw err;
+ }
+ console.debug('[impeccable] Dropped optional live event:', err);
return null;
}
return fetch('http://localhost:' + PORT + '/events', {
@@ -6843,12 +6890,16 @@ void main() {
function ensureAcceptedDomClean(pending) {
const sessionId = pending?.id;
const variantId = pending?.variant;
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
+ const wrapper = findAcceptedRuntimeWrapper(sessionId);
const accepted = wrapper?.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
if (!wrapper) {
restoreAcceptedDomFromSnapshot(pending);
return;
}
+ if (acceptedDomAlreadyClean(pending)) {
+ wrapper.remove();
+ return;
+ }
if (!accepted) {
wrapper.remove();
restoreAcceptedDomFromSnapshot(pending);
@@ -6862,6 +6913,12 @@ void main() {
wrapper.remove();
}
+ function findAcceptedRuntimeWrapper(sessionId) {
+ if (!sessionId) return null;
+ return document.querySelector('[data-impeccable-variants="' + sessionId + '"]')
+ || document.querySelector('[data-impeccable-carbonize="' + sessionId + '"]');
+ }
+
function restoreAcceptedDomFromSnapshot(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (!pending?.acceptedHtml) {
diff --git a/.opencode/skills/impeccable/scripts/live-browser.js b/.opencode/skills/impeccable/scripts/live-browser.js
index 7bf6ef31c..302b9ec23 100644
--- a/.opencode/skills/impeccable/scripts/live-browser.js
+++ b/.opencode/skills/impeccable/scripts/live-browser.js
@@ -4503,15 +4503,17 @@
if (origContent.id) {
liveEl = document.getElementById(origContent.id);
} else if (cls) {
- const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
+ const candidates = [...document.getElementsByTagName(tag)];
for (const c of candidates) {
if (c.className === cls && !own(c)) { liveEl = c; break; }
}
if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter(Boolean);
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
+ if (expectedClasses.length > 0) {
+ for (const c of candidates) {
+ if (own(c)) continue;
+ if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ }
}
}
}
@@ -4880,7 +4882,7 @@
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
- const doc = parser.parseFromString(block, 'text/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.');
@@ -4949,6 +4951,44 @@
});
}
+ function normalizeSourceFallbackBlock(block, filePath) {
+ if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
+ return String(block)
+ .replace(
+ /',
+ )
+ .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;
@@ -5422,7 +5462,11 @@
}
// Source fallback when HMR did not land variants in this tab.
if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) {
- injectVariantsFromSource(msg.file, msg.id);
+ setTimeout(() => {
+ if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
+ if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
+ injectVariantsFromSource(msg.file, msg.id);
+ }, 750);
break;
}
// Variants are in source but not in the DOM yet. Common when the
@@ -5514,8 +5558,11 @@
function sendEvent(msg, opts) {
msg.token = TOKEN;
function handleFailure(err) {
- console.error('[impeccable] Failed to send event:', err);
- if (opts && opts.throwOnError) throw err;
+ if (opts && opts.throwOnError) {
+ console.error('[impeccable] Failed to send event:', err);
+ throw err;
+ }
+ console.debug('[impeccable] Dropped optional live event:', err);
return null;
}
return fetch('http://localhost:' + PORT + '/events', {
@@ -6843,12 +6890,16 @@ void main() {
function ensureAcceptedDomClean(pending) {
const sessionId = pending?.id;
const variantId = pending?.variant;
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
+ const wrapper = findAcceptedRuntimeWrapper(sessionId);
const accepted = wrapper?.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
if (!wrapper) {
restoreAcceptedDomFromSnapshot(pending);
return;
}
+ if (acceptedDomAlreadyClean(pending)) {
+ wrapper.remove();
+ return;
+ }
if (!accepted) {
wrapper.remove();
restoreAcceptedDomFromSnapshot(pending);
@@ -6862,6 +6913,12 @@ void main() {
wrapper.remove();
}
+ function findAcceptedRuntimeWrapper(sessionId) {
+ if (!sessionId) return null;
+ return document.querySelector('[data-impeccable-variants="' + sessionId + '"]')
+ || document.querySelector('[data-impeccable-carbonize="' + sessionId + '"]');
+ }
+
function restoreAcceptedDomFromSnapshot(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (!pending?.acceptedHtml) {
diff --git a/.pi/skills/impeccable/scripts/live-browser.js b/.pi/skills/impeccable/scripts/live-browser.js
index 7bf6ef31c..302b9ec23 100644
--- a/.pi/skills/impeccable/scripts/live-browser.js
+++ b/.pi/skills/impeccable/scripts/live-browser.js
@@ -4503,15 +4503,17 @@
if (origContent.id) {
liveEl = document.getElementById(origContent.id);
} else if (cls) {
- const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
+ const candidates = [...document.getElementsByTagName(tag)];
for (const c of candidates) {
if (c.className === cls && !own(c)) { liveEl = c; break; }
}
if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter(Boolean);
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
+ if (expectedClasses.length > 0) {
+ for (const c of candidates) {
+ if (own(c)) continue;
+ if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ }
}
}
}
@@ -4880,7 +4882,7 @@
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
- const doc = parser.parseFromString(block, 'text/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.');
@@ -4949,6 +4951,44 @@
});
}
+ function normalizeSourceFallbackBlock(block, filePath) {
+ if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
+ return String(block)
+ .replace(
+ /',
+ )
+ .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;
@@ -5422,7 +5462,11 @@
}
// Source fallback when HMR did not land variants in this tab.
if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) {
- injectVariantsFromSource(msg.file, msg.id);
+ setTimeout(() => {
+ if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
+ if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
+ injectVariantsFromSource(msg.file, msg.id);
+ }, 750);
break;
}
// Variants are in source but not in the DOM yet. Common when the
@@ -5514,8 +5558,11 @@
function sendEvent(msg, opts) {
msg.token = TOKEN;
function handleFailure(err) {
- console.error('[impeccable] Failed to send event:', err);
- if (opts && opts.throwOnError) throw err;
+ if (opts && opts.throwOnError) {
+ console.error('[impeccable] Failed to send event:', err);
+ throw err;
+ }
+ console.debug('[impeccable] Dropped optional live event:', err);
return null;
}
return fetch('http://localhost:' + PORT + '/events', {
@@ -6843,12 +6890,16 @@ void main() {
function ensureAcceptedDomClean(pending) {
const sessionId = pending?.id;
const variantId = pending?.variant;
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
+ const wrapper = findAcceptedRuntimeWrapper(sessionId);
const accepted = wrapper?.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
if (!wrapper) {
restoreAcceptedDomFromSnapshot(pending);
return;
}
+ if (acceptedDomAlreadyClean(pending)) {
+ wrapper.remove();
+ return;
+ }
if (!accepted) {
wrapper.remove();
restoreAcceptedDomFromSnapshot(pending);
@@ -6862,6 +6913,12 @@ void main() {
wrapper.remove();
}
+ function findAcceptedRuntimeWrapper(sessionId) {
+ if (!sessionId) return null;
+ return document.querySelector('[data-impeccable-variants="' + sessionId + '"]')
+ || document.querySelector('[data-impeccable-carbonize="' + sessionId + '"]');
+ }
+
function restoreAcceptedDomFromSnapshot(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (!pending?.acceptedHtml) {
diff --git a/.qoder/skills/impeccable/scripts/live-browser.js b/.qoder/skills/impeccable/scripts/live-browser.js
index 7bf6ef31c..302b9ec23 100644
--- a/.qoder/skills/impeccable/scripts/live-browser.js
+++ b/.qoder/skills/impeccable/scripts/live-browser.js
@@ -4503,15 +4503,17 @@
if (origContent.id) {
liveEl = document.getElementById(origContent.id);
} else if (cls) {
- const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
+ const candidates = [...document.getElementsByTagName(tag)];
for (const c of candidates) {
if (c.className === cls && !own(c)) { liveEl = c; break; }
}
if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter(Boolean);
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
+ if (expectedClasses.length > 0) {
+ for (const c of candidates) {
+ if (own(c)) continue;
+ if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ }
}
}
}
@@ -4880,7 +4882,7 @@
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
- const doc = parser.parseFromString(block, 'text/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.');
@@ -4949,6 +4951,44 @@
});
}
+ function normalizeSourceFallbackBlock(block, filePath) {
+ if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
+ return String(block)
+ .replace(
+ /',
+ )
+ .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;
@@ -5422,7 +5462,11 @@
}
// Source fallback when HMR did not land variants in this tab.
if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) {
- injectVariantsFromSource(msg.file, msg.id);
+ setTimeout(() => {
+ if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
+ if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
+ injectVariantsFromSource(msg.file, msg.id);
+ }, 750);
break;
}
// Variants are in source but not in the DOM yet. Common when the
@@ -5514,8 +5558,11 @@
function sendEvent(msg, opts) {
msg.token = TOKEN;
function handleFailure(err) {
- console.error('[impeccable] Failed to send event:', err);
- if (opts && opts.throwOnError) throw err;
+ if (opts && opts.throwOnError) {
+ console.error('[impeccable] Failed to send event:', err);
+ throw err;
+ }
+ console.debug('[impeccable] Dropped optional live event:', err);
return null;
}
return fetch('http://localhost:' + PORT + '/events', {
@@ -6843,12 +6890,16 @@ void main() {
function ensureAcceptedDomClean(pending) {
const sessionId = pending?.id;
const variantId = pending?.variant;
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
+ const wrapper = findAcceptedRuntimeWrapper(sessionId);
const accepted = wrapper?.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
if (!wrapper) {
restoreAcceptedDomFromSnapshot(pending);
return;
}
+ if (acceptedDomAlreadyClean(pending)) {
+ wrapper.remove();
+ return;
+ }
if (!accepted) {
wrapper.remove();
restoreAcceptedDomFromSnapshot(pending);
@@ -6862,6 +6913,12 @@ void main() {
wrapper.remove();
}
+ function findAcceptedRuntimeWrapper(sessionId) {
+ if (!sessionId) return null;
+ return document.querySelector('[data-impeccable-variants="' + sessionId + '"]')
+ || document.querySelector('[data-impeccable-carbonize="' + sessionId + '"]');
+ }
+
function restoreAcceptedDomFromSnapshot(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (!pending?.acceptedHtml) {
diff --git a/.rovodev/skills/impeccable/scripts/live-browser.js b/.rovodev/skills/impeccable/scripts/live-browser.js
index 7bf6ef31c..302b9ec23 100644
--- a/.rovodev/skills/impeccable/scripts/live-browser.js
+++ b/.rovodev/skills/impeccable/scripts/live-browser.js
@@ -4503,15 +4503,17 @@
if (origContent.id) {
liveEl = document.getElementById(origContent.id);
} else if (cls) {
- const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
+ const candidates = [...document.getElementsByTagName(tag)];
for (const c of candidates) {
if (c.className === cls && !own(c)) { liveEl = c; break; }
}
if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter(Boolean);
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
+ if (expectedClasses.length > 0) {
+ for (const c of candidates) {
+ if (own(c)) continue;
+ if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ }
}
}
}
@@ -4880,7 +4882,7 @@
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
- const doc = parser.parseFromString(block, 'text/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.');
@@ -4949,6 +4951,44 @@
});
}
+ function normalizeSourceFallbackBlock(block, filePath) {
+ if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
+ return String(block)
+ .replace(
+ /',
+ )
+ .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;
@@ -5422,7 +5462,11 @@
}
// Source fallback when HMR did not land variants in this tab.
if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) {
- injectVariantsFromSource(msg.file, msg.id);
+ setTimeout(() => {
+ if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
+ if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
+ injectVariantsFromSource(msg.file, msg.id);
+ }, 750);
break;
}
// Variants are in source but not in the DOM yet. Common when the
@@ -5514,8 +5558,11 @@
function sendEvent(msg, opts) {
msg.token = TOKEN;
function handleFailure(err) {
- console.error('[impeccable] Failed to send event:', err);
- if (opts && opts.throwOnError) throw err;
+ if (opts && opts.throwOnError) {
+ console.error('[impeccable] Failed to send event:', err);
+ throw err;
+ }
+ console.debug('[impeccable] Dropped optional live event:', err);
return null;
}
return fetch('http://localhost:' + PORT + '/events', {
@@ -6843,12 +6890,16 @@ void main() {
function ensureAcceptedDomClean(pending) {
const sessionId = pending?.id;
const variantId = pending?.variant;
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
+ const wrapper = findAcceptedRuntimeWrapper(sessionId);
const accepted = wrapper?.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
if (!wrapper) {
restoreAcceptedDomFromSnapshot(pending);
return;
}
+ if (acceptedDomAlreadyClean(pending)) {
+ wrapper.remove();
+ return;
+ }
if (!accepted) {
wrapper.remove();
restoreAcceptedDomFromSnapshot(pending);
@@ -6862,6 +6913,12 @@ void main() {
wrapper.remove();
}
+ function findAcceptedRuntimeWrapper(sessionId) {
+ if (!sessionId) return null;
+ return document.querySelector('[data-impeccable-variants="' + sessionId + '"]')
+ || document.querySelector('[data-impeccable-carbonize="' + sessionId + '"]');
+ }
+
function restoreAcceptedDomFromSnapshot(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (!pending?.acceptedHtml) {
diff --git a/.trae-cn/skills/impeccable/scripts/live-browser.js b/.trae-cn/skills/impeccable/scripts/live-browser.js
index 7bf6ef31c..302b9ec23 100644
--- a/.trae-cn/skills/impeccable/scripts/live-browser.js
+++ b/.trae-cn/skills/impeccable/scripts/live-browser.js
@@ -4503,15 +4503,17 @@
if (origContent.id) {
liveEl = document.getElementById(origContent.id);
} else if (cls) {
- const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
+ const candidates = [...document.getElementsByTagName(tag)];
for (const c of candidates) {
if (c.className === cls && !own(c)) { liveEl = c; break; }
}
if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter(Boolean);
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
+ if (expectedClasses.length > 0) {
+ for (const c of candidates) {
+ if (own(c)) continue;
+ if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ }
}
}
}
@@ -4880,7 +4882,7 @@
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
- const doc = parser.parseFromString(block, 'text/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.');
@@ -4949,6 +4951,44 @@
});
}
+ function normalizeSourceFallbackBlock(block, filePath) {
+ if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
+ return String(block)
+ .replace(
+ /',
+ )
+ .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;
@@ -5422,7 +5462,11 @@
}
// Source fallback when HMR did not land variants in this tab.
if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) {
- injectVariantsFromSource(msg.file, msg.id);
+ setTimeout(() => {
+ if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
+ if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
+ injectVariantsFromSource(msg.file, msg.id);
+ }, 750);
break;
}
// Variants are in source but not in the DOM yet. Common when the
@@ -5514,8 +5558,11 @@
function sendEvent(msg, opts) {
msg.token = TOKEN;
function handleFailure(err) {
- console.error('[impeccable] Failed to send event:', err);
- if (opts && opts.throwOnError) throw err;
+ if (opts && opts.throwOnError) {
+ console.error('[impeccable] Failed to send event:', err);
+ throw err;
+ }
+ console.debug('[impeccable] Dropped optional live event:', err);
return null;
}
return fetch('http://localhost:' + PORT + '/events', {
@@ -6843,12 +6890,16 @@ void main() {
function ensureAcceptedDomClean(pending) {
const sessionId = pending?.id;
const variantId = pending?.variant;
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
+ const wrapper = findAcceptedRuntimeWrapper(sessionId);
const accepted = wrapper?.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
if (!wrapper) {
restoreAcceptedDomFromSnapshot(pending);
return;
}
+ if (acceptedDomAlreadyClean(pending)) {
+ wrapper.remove();
+ return;
+ }
if (!accepted) {
wrapper.remove();
restoreAcceptedDomFromSnapshot(pending);
@@ -6862,6 +6913,12 @@ void main() {
wrapper.remove();
}
+ function findAcceptedRuntimeWrapper(sessionId) {
+ if (!sessionId) return null;
+ return document.querySelector('[data-impeccable-variants="' + sessionId + '"]')
+ || document.querySelector('[data-impeccable-carbonize="' + sessionId + '"]');
+ }
+
function restoreAcceptedDomFromSnapshot(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (!pending?.acceptedHtml) {
diff --git a/.trae/skills/impeccable/scripts/live-browser.js b/.trae/skills/impeccable/scripts/live-browser.js
index 7bf6ef31c..302b9ec23 100644
--- a/.trae/skills/impeccable/scripts/live-browser.js
+++ b/.trae/skills/impeccable/scripts/live-browser.js
@@ -4503,15 +4503,17 @@
if (origContent.id) {
liveEl = document.getElementById(origContent.id);
} else if (cls) {
- const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
+ const candidates = [...document.getElementsByTagName(tag)];
for (const c of candidates) {
if (c.className === cls && !own(c)) { liveEl = c; break; }
}
if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter(Boolean);
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
+ if (expectedClasses.length > 0) {
+ for (const c of candidates) {
+ if (own(c)) continue;
+ if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ }
}
}
}
@@ -4880,7 +4882,7 @@
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
- const doc = parser.parseFromString(block, 'text/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.');
@@ -4949,6 +4951,44 @@
});
}
+ function normalizeSourceFallbackBlock(block, filePath) {
+ if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
+ return String(block)
+ .replace(
+ /',
+ )
+ .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;
@@ -5422,7 +5462,11 @@
}
// Source fallback when HMR did not land variants in this tab.
if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) {
- injectVariantsFromSource(msg.file, msg.id);
+ setTimeout(() => {
+ if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
+ if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
+ injectVariantsFromSource(msg.file, msg.id);
+ }, 750);
break;
}
// Variants are in source but not in the DOM yet. Common when the
@@ -5514,8 +5558,11 @@
function sendEvent(msg, opts) {
msg.token = TOKEN;
function handleFailure(err) {
- console.error('[impeccable] Failed to send event:', err);
- if (opts && opts.throwOnError) throw err;
+ if (opts && opts.throwOnError) {
+ console.error('[impeccable] Failed to send event:', err);
+ throw err;
+ }
+ console.debug('[impeccable] Dropped optional live event:', err);
return null;
}
return fetch('http://localhost:' + PORT + '/events', {
@@ -6843,12 +6890,16 @@ void main() {
function ensureAcceptedDomClean(pending) {
const sessionId = pending?.id;
const variantId = pending?.variant;
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
+ const wrapper = findAcceptedRuntimeWrapper(sessionId);
const accepted = wrapper?.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
if (!wrapper) {
restoreAcceptedDomFromSnapshot(pending);
return;
}
+ if (acceptedDomAlreadyClean(pending)) {
+ wrapper.remove();
+ return;
+ }
if (!accepted) {
wrapper.remove();
restoreAcceptedDomFromSnapshot(pending);
@@ -6862,6 +6913,12 @@ void main() {
wrapper.remove();
}
+ function findAcceptedRuntimeWrapper(sessionId) {
+ if (!sessionId) return null;
+ return document.querySelector('[data-impeccable-variants="' + sessionId + '"]')
+ || document.querySelector('[data-impeccable-carbonize="' + sessionId + '"]');
+ }
+
function restoreAcceptedDomFromSnapshot(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (!pending?.acceptedHtml) {
diff --git a/cli/bin/commands/skills.mjs b/cli/bin/commands/skills.mjs
index 8b727940f..c3c87e5eb 100644
--- a/cli/bin/commands/skills.mjs
+++ b/cli/bin/commands/skills.mjs
@@ -9,7 +9,7 @@
*/
import { execSync } from 'node:child_process';
-import { existsSync, readFileSync, readdirSync, statSync, lstatSync, unlinkSync, mkdirSync, writeFileSync, rmSync, renameSync, createWriteStream, realpathSync, symlinkSync, readlinkSync } from 'node:fs';
+import { existsSync, readFileSync, readdirSync, statSync, lstatSync, unlinkSync, mkdirSync, writeFileSync, rmSync, renameSync, createWriteStream, realpathSync, symlinkSync, readlinkSync, cpSync } from 'node:fs';
import { join, resolve, dirname, relative, isAbsolute } from 'node:path';
import { createInterface } from 'node:readline';
import { fileURLToPath } from 'node:url';
@@ -134,6 +134,9 @@ function hashSkillsDir(skillsDir) {
* Caller is responsible for cleanup.
*/
async function downloadAndExtractBundle() {
+ const localBundle = process.env.IMPECCABLE_BUNDLE_PATH;
+ if (localBundle) return copyOrExtractLocalBundle(localBundle);
+
const tmpZip = join(tmpdir(), `impeccable-update-${Date.now()}.zip`);
const tmpDir = join(tmpdir(), `impeccable-update-${Date.now()}`);
await downloadFile(`${API_BASE}/api/download/bundle/universal`, tmpZip);
@@ -143,6 +146,24 @@ async function downloadAndExtractBundle() {
return tmpDir;
}
+async function copyOrExtractLocalBundle(sourceValue) {
+ const source = resolve(sourceValue);
+ if (!existsSync(source)) {
+ throw new Error(`Local bundle not found: ${source}`);
+ }
+
+ const tmpDir = join(tmpdir(), `impeccable-local-bundle-${process.pid}-${Date.now()}`);
+ mkdirSync(tmpDir, { recursive: true });
+
+ if (statSync(source).isDirectory()) {
+ cpSync(source, tmpDir, { recursive: true });
+ return tmpDir;
+ }
+
+ await extract(source, { dir: tmpDir });
+ return tmpDir;
+}
+
/**
* Normalize a SKILL.md's content for comparison by stripping
* provider-specific paths. Different install methods (npx skills add
diff --git a/package.json b/package.json
index cdec77868..d3cb4ab8f 100644
--- a/package.json
+++ b/package.json
@@ -50,12 +50,18 @@
"dev": "bun run scripts/gen-dev-api.mjs && npx astro dev",
"preview": "bun run build && npx astro preview",
"deploy": "bun run build && wrangler pages deploy build/",
- "test": "bun test tests/build.test.js tests/detect-antipatterns.test.js tests/windows-path-fix.test.js tests/lib/detector-bundle.test.js tests/lib/provider-blocks.test.js tests/lib/transformers/provider-blocks.test.js tests/lib/utils.test.js tests/lib/transformers/factory.test.js tests/lib/transformers/providers.test.js tests/skills-cli.test.js && node --test tests/critique-storage.test.mjs && node --test tests/detect-antipatterns-fixtures.test.mjs && node --test tests/detect-antipatterns-browser.test.mjs && node --test tests/cleanup-deprecated.test.mjs && node --test tests/impeccable-paths.test.mjs && node --test tests/live-wrap.test.mjs && node --test tests/live-wrap-buffer-aware.test.mjs && node --test tests/live-insert.test.mjs && node --test tests/live-insert-ui.test.mjs && node --test tests/live-event-validation.test.mjs && node --test tests/live-reference.test.mjs && node --test tests/live-e2e-agent-output.test.mjs && node --test tests/live-e2e-llm-agent.test.mjs && node --test tests/live-e2e-cli-options.test.mjs && node --test tests/live-accept.test.mjs && node --test tests/live-accept-scrub.test.mjs && node --test tests/live-commit-manual-edits.test.mjs && node --test tests/live-discard-manual-edits.test.mjs && node --test tests/live-manual-edits-buffer.test.mjs && node --test tests/live-inject.test.mjs && node --test tests/live-poll.test.mjs && node --test tests/live-poll-stream.test.mjs && node --test tests/live-server.test.mjs && node --test tests/live-copy-edit-agent.test.mjs && node --test tests/live-browser-regression.test.mjs && node --test tests/live-session-store.test.mjs && node --test tests/live-browser-session.test.mjs && node --test tests/live-browser-source.test.mjs && node --test tests/live-completion.test.mjs && node --test tests/live-recovery-commands.test.mjs && node --test tests/framework-fixtures.test.mjs",
- "test:cli-e2e": "IMPECCABLE_CLI_E2E=1 bun test tests/skills-cli.test.js",
- "test:live-e2e": "node --test --test-timeout=600000 tests/live-e2e.test.mjs",
- "test:live-e2e-agent": "node --test tests/live-e2e/agent-insert.test.mjs",
- "test:skill-behavior": "node --test --test-timeout=300000 tests/skill-behavior/scenarios.test.mjs",
- "test:live-svelte-adapter-deepseek": "node --test --test-timeout=1200000 tests/live-svelte-adapter-deepseek.test.mjs",
+ "test": "node scripts/run-tests.mjs default",
+ "test:core": "node scripts/run-tests.mjs core",
+ "test:detector": "node scripts/run-tests.mjs detector",
+ "test:framework": "node scripts/run-tests.mjs framework",
+ "test:live": "node scripts/run-tests.mjs live",
+ "test:cli-e2e": "node scripts/run-tests.mjs cli-e2e",
+ "test:cli-remote-e2e": "node scripts/run-tests.mjs cli-remote-e2e",
+ "test:live-e2e": "node scripts/run-tests.mjs live-e2e",
+ "test:live-e2e-accept-cleanup": "node scripts/run-tests.mjs live-e2e-accept-cleanup",
+ "test:live-e2e-agent": "node scripts/run-tests.mjs live-e2e-agent",
+ "test:skill-behavior": "node scripts/run-tests.mjs skill-behavior",
+ "test:live-svelte-adapter-deepseek": "node scripts/run-tests.mjs live-svelte-adapter-deepseek",
"bench:detector": "node scripts/benchmark-detector.mjs",
"bench:detector:browser": "node scripts/benchmark-detector.mjs --browser",
"audit": "bun audit --audit-level=moderate",
diff --git a/plugin/skills/impeccable/scripts/live-browser.js b/plugin/skills/impeccable/scripts/live-browser.js
index 7bf6ef31c..302b9ec23 100644
--- a/plugin/skills/impeccable/scripts/live-browser.js
+++ b/plugin/skills/impeccable/scripts/live-browser.js
@@ -4503,15 +4503,17 @@
if (origContent.id) {
liveEl = document.getElementById(origContent.id);
} else if (cls) {
- const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
+ const candidates = [...document.getElementsByTagName(tag)];
for (const c of candidates) {
if (c.className === cls && !own(c)) { liveEl = c; break; }
}
if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter(Boolean);
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
+ if (expectedClasses.length > 0) {
+ for (const c of candidates) {
+ if (own(c)) continue;
+ if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ }
}
}
}
@@ -4880,7 +4882,7 @@
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
- const doc = parser.parseFromString(block, 'text/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.');
@@ -4949,6 +4951,44 @@
});
}
+ function normalizeSourceFallbackBlock(block, filePath) {
+ if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
+ return String(block)
+ .replace(
+ /',
+ )
+ .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;
@@ -5422,7 +5462,11 @@
}
// Source fallback when HMR did not land variants in this tab.
if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) {
- injectVariantsFromSource(msg.file, msg.id);
+ setTimeout(() => {
+ if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
+ if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
+ injectVariantsFromSource(msg.file, msg.id);
+ }, 750);
break;
}
// Variants are in source but not in the DOM yet. Common when the
@@ -5514,8 +5558,11 @@
function sendEvent(msg, opts) {
msg.token = TOKEN;
function handleFailure(err) {
- console.error('[impeccable] Failed to send event:', err);
- if (opts && opts.throwOnError) throw err;
+ if (opts && opts.throwOnError) {
+ console.error('[impeccable] Failed to send event:', err);
+ throw err;
+ }
+ console.debug('[impeccable] Dropped optional live event:', err);
return null;
}
return fetch('http://localhost:' + PORT + '/events', {
@@ -6843,12 +6890,16 @@ void main() {
function ensureAcceptedDomClean(pending) {
const sessionId = pending?.id;
const variantId = pending?.variant;
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
+ const wrapper = findAcceptedRuntimeWrapper(sessionId);
const accepted = wrapper?.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
if (!wrapper) {
restoreAcceptedDomFromSnapshot(pending);
return;
}
+ if (acceptedDomAlreadyClean(pending)) {
+ wrapper.remove();
+ return;
+ }
if (!accepted) {
wrapper.remove();
restoreAcceptedDomFromSnapshot(pending);
@@ -6862,6 +6913,12 @@ void main() {
wrapper.remove();
}
+ function findAcceptedRuntimeWrapper(sessionId) {
+ if (!sessionId) return null;
+ return document.querySelector('[data-impeccable-variants="' + sessionId + '"]')
+ || document.querySelector('[data-impeccable-carbonize="' + sessionId + '"]');
+ }
+
function restoreAcceptedDomFromSnapshot(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (!pending?.acceptedHtml) {
diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs
new file mode 100644
index 000000000..8888fe144
--- /dev/null
+++ b/scripts/ci-test-plan.mjs
@@ -0,0 +1,91 @@
+#!/usr/bin/env node
+import fs from 'node:fs';
+import { execFileSync } from 'node:child_process';
+import { DEFAULT_SUITES, matchesSuiteTriggers } from './test-suites.mjs';
+
+const eventName = process.env.GITHUB_EVENT_NAME || '';
+const localNoChanges = !eventName && !process.env.CI_CHANGED_FILES;
+const changedFiles = localNoChanges ? [] : getChangedFiles();
+const forceDeterministic = localNoChanges || eventName === 'push' || eventName === 'workflow_dispatch';
+const forceOptIn = eventName === 'workflow_dispatch';
+
+const plan = {
+ core: true,
+ detector: forceDeterministic || matchesSuiteTriggers('detector', changedFiles),
+ live: forceDeterministic || matchesSuiteTriggers('live', changedFiles),
+ framework: forceDeterministic || matchesSuiteTriggers('framework', changedFiles),
+ cli_remote_e2e: forceOptIn,
+ live_e2e: forceOptIn || matchesSuiteTriggers('live-e2e', changedFiles),
+ live_e2e_accept_cleanup: forceOptIn || matchesSuiteTriggers('live-e2e-accept-cleanup', changedFiles),
+ skill_behavior: forceOptIn || matchesSuiteTriggers('skill-behavior', changedFiles),
+ live_svelte_adapter_deepseek: forceOptIn || matchesSuiteTriggers('live-svelte-adapter-deepseek', changedFiles),
+};
+
+writeGithubOutputs(plan);
+printSummary(plan, changedFiles);
+
+function getChangedFiles() {
+ if (process.env.CI_CHANGED_FILES) {
+ return process.env.CI_CHANGED_FILES
+ .split(/\r?\n/)
+ .map((file) => file.trim())
+ .filter(Boolean);
+ }
+
+ const event = process.env.GITHUB_EVENT_NAME || '';
+ const sha = process.env.GITHUB_SHA || 'HEAD';
+
+ if (event === 'pull_request' && process.env.GITHUB_BASE_REF) {
+ const base = `origin/${process.env.GITHUB_BASE_REF}`;
+ return gitDiffNames(`${base}...${sha}`) || gitDiffNames(`${base}...HEAD`) || allChanged();
+ }
+
+ const before = process.env.GITHUB_EVENT_BEFORE;
+ if (before && !/^0+$/.test(before)) {
+ return gitDiffNames(`${before}..${sha}`) || allChanged();
+ }
+
+ return allChanged();
+}
+
+function allChanged() {
+ return git(['ls-files']).split(/\r?\n/).filter(Boolean);
+}
+
+function gitDiffNames(range) {
+ try {
+ return git(['diff', '--name-only', range]).split(/\r?\n/).filter(Boolean);
+ } catch {
+ return null;
+ }
+}
+
+function git(args) {
+ return execFileSync('git', args, { encoding: 'utf-8' });
+}
+
+function writeGithubOutputs(outputs) {
+ const outputPath = process.env.GITHUB_OUTPUT;
+ if (!outputPath) return;
+ const lines = [];
+ for (const [key, value] of Object.entries(outputs)) {
+ lines.push(`${key}=${value ? 'true' : 'false'}`);
+ }
+ fs.appendFileSync(outputPath, lines.join('\n') + '\n');
+}
+
+function printSummary(outputs, files) {
+ const deterministic = DEFAULT_SUITES.map((name) => `${name}=${outputs[name]}`).join(' ');
+ console.log(`Event: ${eventName || 'local'}`);
+ console.log(`Changed files: ${files.length}`);
+ console.log(`Deterministic suites: ${deterministic}`);
+ console.log(
+ [
+ `cli_remote_e2e=${outputs.cli_remote_e2e}`,
+ `live_e2e=${outputs.live_e2e}`,
+ `live_e2e_accept_cleanup=${outputs.live_e2e_accept_cleanup}`,
+ `skill_behavior=${outputs.skill_behavior}`,
+ `deepseek=${outputs.live_svelte_adapter_deepseek}`,
+ ].join(' '),
+ );
+}
diff --git a/scripts/run-tests.mjs b/scripts/run-tests.mjs
new file mode 100644
index 000000000..0841d3da8
--- /dev/null
+++ b/scripts/run-tests.mjs
@@ -0,0 +1,95 @@
+#!/usr/bin/env node
+import { spawnSync } from 'node:child_process';
+import { DEFAULT_SUITES, OPT_IN_SUITES, SUITES, expandSuites } from './test-suites.mjs';
+
+const args = process.argv.slice(2);
+
+if (args.includes('--help') || args.includes('-h')) {
+ printHelp();
+ process.exit(0);
+}
+
+if (args.includes('--list')) {
+ printSuites();
+ process.exit(0);
+}
+
+const requestedSuites = args.filter((arg) => !arg.startsWith('-'));
+let suites;
+try {
+ suites = expandSuites(requestedSuites);
+} catch (err) {
+ console.error(err.message);
+ process.exit(1);
+}
+
+for (const suiteName of suites) {
+ const suite = SUITES[suiteName];
+ console.log(`\n## test:${suiteName}`);
+ console.log(suite.description);
+ for (const command of suite.commands) {
+ runCommand(command);
+ }
+}
+
+function runCommand(command) {
+ const env = { ...process.env, ...(command.env || {}) };
+ if (command.runner === 'bun') {
+ runProcess('bun', ['test', ...command.files], { env });
+ return;
+ }
+
+ if (command.runner === 'node') {
+ for (const file of command.files) {
+ const args = ['--test'];
+ if (command.timeoutMs) args.push(`--test-timeout=${command.timeoutMs}`);
+ if (command.forceExit) args.push('--test-force-exit');
+ args.push(file);
+ runProcess(process.execPath, args, { env });
+ }
+ return;
+ }
+
+ throw new Error(`Unsupported test runner "${command.runner}"`);
+}
+
+function runProcess(cmd, args, { env }) {
+ console.log(`$ ${formatCommand(cmd, args)}`);
+ const result = spawnSync(cmd, args, {
+ stdio: 'inherit',
+ env,
+ });
+ if (result.error) {
+ console.error(result.error.message);
+ process.exit(1);
+ }
+ if (result.status !== 0) process.exit(result.status || 1);
+}
+
+function formatCommand(cmd, args) {
+ const bin = cmd === process.execPath ? 'node' : cmd;
+ return [bin, ...args].join(' ');
+}
+
+function printHelp() {
+ console.log(`Usage: node scripts/run-tests.mjs [suite...]
+
+Aliases:
+ default ${DEFAULT_SUITES.join(', ')}
+ all-local ${DEFAULT_SUITES.join(', ')}
+ all ${[...DEFAULT_SUITES, ...OPT_IN_SUITES].join(', ')}
+
+Run with --list to see suite contents.`);
+}
+
+function printSuites() {
+ for (const [name, suite] of Object.entries(SUITES)) {
+ const marker = suite.optIn ? ' (opt-in)' : '';
+ console.log(`\n${name}${marker}`);
+ console.log(` ${suite.description}`);
+ for (const command of suite.commands) {
+ console.log(` ${command.runner}:`);
+ for (const file of command.files) console.log(` ${file}`);
+ }
+ }
+}
diff --git a/scripts/test-suites.mjs b/scripts/test-suites.mjs
new file mode 100644
index 000000000..134093030
--- /dev/null
+++ b/scripts/test-suites.mjs
@@ -0,0 +1,308 @@
+import fs from 'node:fs';
+import path from 'node:path';
+
+export const DEFAULT_SUITES = ['core', 'detector', 'live', 'framework'];
+export const OPT_IN_SUITES = [
+ 'cli-remote-e2e',
+ 'live-e2e',
+ 'live-e2e-accept-cleanup',
+ 'skill-behavior',
+ 'live-svelte-adapter-deepseek',
+];
+
+const COMMON_INFRA_PATTERNS = [
+ /^package\.json$/,
+ /^bun\.lock$/,
+ /^scripts\/run-tests\.mjs$/,
+ /^scripts\/test-suites\.mjs$/,
+ /^scripts\/ci-test-plan\.mjs$/,
+ /^\.github\/workflows\/ci\.yml$/,
+];
+
+export const SUITES = {
+ core: {
+ description: 'Build, provider transforms, CLI helpers, context, and storage unit tests.',
+ triggers: [
+ ...COMMON_INFRA_PATTERNS,
+ /^scripts\/(?!benchmark-detector|build-browser-detector|build-extension)/,
+ /^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|context|context-signals|critique-storage|design-parser|impeccable-paths|is-generated))/,
+ /^cli\/bin\//,
+ /^tests\/(build|cleanup-deprecated|context|context-signals|critique-storage|design-parser|impeccable-paths|skills-cli|test-suites|windows-path-fix)\.test\.(js|mjs)$/,
+ /^tests\/lib\//,
+ ],
+ commands: [
+ {
+ runner: 'bun',
+ files: [
+ 'tests/build.test.js',
+ 'tests/windows-path-fix.test.js',
+ 'tests/lib/provider-blocks.test.js',
+ 'tests/lib/transformers/provider-blocks.test.js',
+ 'tests/lib/utils.test.js',
+ 'tests/lib/transformers/factory.test.js',
+ 'tests/lib/transformers/providers.test.js',
+ 'tests/skills-cli.test.js',
+ ],
+ },
+ {
+ runner: 'node',
+ files: [
+ 'tests/ci-test-plan.test.mjs',
+ 'tests/cleanup-deprecated.test.mjs',
+ 'tests/context.test.mjs',
+ 'tests/context-signals.test.mjs',
+ 'tests/critique-storage.test.mjs',
+ 'tests/design-parser.test.mjs',
+ 'tests/impeccable-paths.test.mjs',
+ 'tests/test-suites.test.mjs',
+ ],
+ },
+ ],
+ },
+ detector: {
+ description: 'Anti-pattern detector tests across text, jsdom fixtures, and Puppeteer browser paths.',
+ needsPuppeteer: true,
+ triggers: [
+ ...COMMON_INFRA_PATTERNS,
+ /^cli\/engine\//,
+ /^extension\/(background|content|detector|devtools|popup|manifest\.json)/,
+ /^scripts\/(benchmark-detector|build-browser-detector|build-extension)\.js$/,
+ /^site\/(pages\/detector|public\/antipattern|data\/anti-patterns-catalog\.js)/,
+ /^tests\/(detect-antipatterns|fixtures\/antipatterns)/,
+ ],
+ commands: [
+ {
+ runner: 'bun',
+ files: [
+ 'tests/detect-antipatterns.test.js',
+ 'tests/lib/detector-bundle.test.js',
+ ],
+ },
+ {
+ runner: 'node',
+ files: [
+ 'tests/detect-antipatterns-fixtures.test.mjs',
+ 'tests/detect-antipatterns-browser.test.mjs',
+ ],
+ },
+ ],
+ },
+ live: {
+ description: 'Fast live-mode unit and local-server integration tests, excluding full browser fixture sweeps.',
+ triggers: [
+ ...COMMON_INFRA_PATTERNS,
+ /^skill\/(reference\/live\.md|scripts\/(detect-csp|is-generated|live|live-|modern-screenshot|pin|palette))/,
+ /^tests\/live-/,
+ /^tests\/live-e2e\/(agent|agents\/llm-agent|cli-options|preactions|session|steer|ui)\.mjs$/,
+ /^tests\/live-e2e\/agent-insert\.test\.mjs$/,
+ ],
+ commands: [
+ {
+ runner: 'node',
+ files: [
+ 'tests/live-accept.test.mjs',
+ 'tests/live-accept-scrub.test.mjs',
+ 'tests/live-browser-regression.test.mjs',
+ 'tests/live-browser-session.test.mjs',
+ 'tests/live-browser-source.test.mjs',
+ 'tests/live-commit-manual-edits.test.mjs',
+ 'tests/live-completion.test.mjs',
+ 'tests/live-copy-edit-agent.test.mjs',
+ 'tests/live-discard-manual-edits.test.mjs',
+ 'tests/live-e2e-agent-output.test.mjs',
+ 'tests/live-e2e-cli-options.test.mjs',
+ 'tests/live-e2e-llm-agent.test.mjs',
+ 'tests/live-e2e-steer-agent.test.mjs',
+ 'tests/live-e2e/agent-insert.test.mjs',
+ 'tests/live-event-validation.test.mjs',
+ 'tests/live-inject.test.mjs',
+ 'tests/live-insert.test.mjs',
+ 'tests/live-insert-ui.test.mjs',
+ 'tests/live-manual-edits-buffer.test.mjs',
+ 'tests/live-poll.test.mjs',
+ 'tests/live-poll-stream.test.mjs',
+ 'tests/live-recovery-commands.test.mjs',
+ 'tests/live-reference.test.mjs',
+ 'tests/live-server.test.mjs',
+ 'tests/live-session-store.test.mjs',
+ 'tests/live-wrap.test.mjs',
+ 'tests/live-wrap-buffer-aware.test.mjs',
+ ],
+ },
+ ],
+ },
+ framework: {
+ description: 'Framework fixture coverage for live injection, CSP, generated-file detection, and wrapping.',
+ triggers: [
+ ...COMMON_INFRA_PATTERNS,
+ /^tests\/framework-fixtures/,
+ /^tests\/framework-fixtures\.test\.mjs$/,
+ /^skill\/scripts\/(detect-csp|is-generated|live-inject|live-sveltekit-adapter|live-wrap)\.mjs$/,
+ ],
+ commands: [
+ {
+ runner: 'node',
+ files: ['tests/framework-fixtures.test.mjs'],
+ },
+ ],
+ },
+ 'cli-e2e': {
+ description: 'Deterministic CLI install/update tests against a local universal bundle.',
+ commands: [
+ {
+ runner: 'bun',
+ files: ['tests/skills-cli.test.js'],
+ },
+ ],
+ },
+ 'cli-remote-e2e': {
+ description: 'Remote CLI install/update smoke tests against impeccable.style.',
+ optIn: true,
+ triggers: [
+ ...COMMON_INFRA_PATTERNS,
+ /^cli\/bin\/commands\/skills\.mjs$/,
+ /^tests\/skills-cli\.test\.js$/,
+ ],
+ commands: [
+ {
+ runner: 'bun',
+ env: { IMPECCABLE_CLI_REMOTE_E2E: '1' },
+ files: ['tests/skills-cli.test.js'],
+ },
+ ],
+ },
+ 'live-e2e': {
+ description: 'Full Playwright live-mode click-to-accept sweep across runtime framework fixtures.',
+ optIn: true,
+ needsPlaywright: true,
+ triggers: [
+ ...COMMON_INFRA_PATTERNS,
+ /^skill\/scripts\/live/,
+ /^tests\/framework-fixtures/,
+ /^tests\/live-e2e(\.test\.mjs|\/)/,
+ ],
+ commands: [
+ {
+ runner: 'node',
+ timeoutMs: 600000,
+ forceExit: true,
+ files: ['tests/live-e2e.test.mjs'],
+ },
+ ],
+ },
+ 'live-e2e-accept-cleanup': {
+ description: 'Provider-backed post-accept cleanup regression.',
+ optIn: true,
+ needsPlaywright: true,
+ triggers: [
+ ...COMMON_INFRA_PATTERNS,
+ /^skill\/scripts\/(live-accept|live-browser|live-server|live-sveltekit-adapter|live-wrap)\.mjs$/,
+ /^tests\/live-e2e-accept-cleanup-regression\.test\.mjs$/,
+ /^tests\/live-e2e\//,
+ ],
+ commands: [
+ {
+ runner: 'node',
+ timeoutMs: 600000,
+ files: ['tests/live-e2e-accept-cleanup-regression.test.mjs'],
+ },
+ ],
+ },
+ 'live-e2e-agent': {
+ description: 'Focused insert-mode fake-agent helper tests.',
+ commands: [
+ {
+ runner: 'node',
+ files: ['tests/live-e2e/agent-insert.test.mjs'],
+ },
+ ],
+ },
+ 'skill-behavior': {
+ description: 'LLM-backed skill setup behavior scenarios.',
+ optIn: true,
+ triggers: [
+ ...COMMON_INFRA_PATTERNS,
+ /^skill\/SKILL\.src\.md$/,
+ /^skill\/reference\/(init|document|brand|product|shape|craft|audit|polish|live)\.md$/,
+ /^skill\/scripts\/(context|context-signals|detect|detect-csp)\.mjs$/,
+ /^tests\/skill-behavior\//,
+ ],
+ commands: [
+ {
+ runner: 'node',
+ timeoutMs: 300000,
+ files: ['tests/skill-behavior/scenarios.test.mjs'],
+ },
+ ],
+ },
+ 'live-svelte-adapter-deepseek': {
+ description: 'DeepSeek-backed Svelte adapter browser sweep.',
+ optIn: true,
+ needsPlaywright: true,
+ triggers: [
+ ...COMMON_INFRA_PATTERNS,
+ /^skill\/scripts\/(live-sveltekit-adapter|live-svelte-component|live-server|live-wrap)\.mjs$/,
+ /^tests\/framework-fixtures\/vite8-sveltekit-stateful\//,
+ /^tests\/live-svelte-adapter-deepseek\.test\.mjs$/,
+ ],
+ commands: [
+ {
+ runner: 'node',
+ timeoutMs: 1200000,
+ files: ['tests/live-svelte-adapter-deepseek.test.mjs'],
+ },
+ ],
+ },
+};
+
+export function expandSuites(requested) {
+ const names = requested.length === 0 ? ['default'] : requested;
+ const expanded = [];
+ for (const name of names) {
+ if (name === 'default' || name === 'all-local') {
+ expanded.push(...DEFAULT_SUITES);
+ } else if (name === 'all') {
+ expanded.push(...DEFAULT_SUITES, ...OPT_IN_SUITES);
+ } else if (SUITES[name]) {
+ expanded.push(name);
+ } else {
+ throw new Error(`Unknown test suite "${name}". Run: node scripts/run-tests.mjs --list`);
+ }
+ }
+ return [...new Set(expanded)];
+}
+
+export function suiteFiles(suiteNames) {
+ const files = [];
+ for (const name of suiteNames) {
+ const suite = SUITES[name];
+ if (!suite) throw new Error(`Unknown test suite "${name}"`);
+ for (const command of suite.commands) {
+ files.push(...command.files);
+ }
+ }
+ return files;
+}
+
+export function findTestFiles(root = process.cwd()) {
+ const out = [];
+ const stack = [path.join(root, 'tests')];
+ while (stack.length) {
+ const dir = stack.pop();
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const abs = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ stack.push(abs);
+ } else if (/\.test\.(js|mjs)$/.test(entry.name)) {
+ out.push(path.relative(root, abs).split(path.sep).join('/'));
+ }
+ }
+ }
+ return out.sort();
+}
+
+export function matchesSuiteTriggers(suiteName, changedFiles) {
+ const suite = SUITES[suiteName];
+ if (!suite) throw new Error(`Unknown test suite "${suiteName}"`);
+ return changedFiles.some((file) => suite.triggers?.some((pattern) => pattern.test(file)));
+}
diff --git a/skill/scripts/live-browser.js b/skill/scripts/live-browser.js
index 7bf6ef31c..302b9ec23 100644
--- a/skill/scripts/live-browser.js
+++ b/skill/scripts/live-browser.js
@@ -4503,15 +4503,17 @@
if (origContent.id) {
liveEl = document.getElementById(origContent.id);
} else if (cls) {
- const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]);
+ const candidates = [...document.getElementsByTagName(tag)];
for (const c of candidates) {
if (c.className === cls && !own(c)) { liveEl = c; break; }
}
if (!liveEl) {
- const expectedClasses = String(cls).split(/\s+/).filter(Boolean);
- for (const c of candidates) {
- if (own(c)) continue;
- if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ const expectedClasses = String(cls).split(/\s+/).filter((name) => /^[A-Za-z_-][\w-]*$/.test(name));
+ if (expectedClasses.length > 0) {
+ for (const c of candidates) {
+ if (own(c)) continue;
+ if (expectedClasses.every((name) => c.classList.contains(name))) { liveEl = c; break; }
+ }
}
}
}
@@ -4880,7 +4882,7 @@
const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx
? html.slice(startIdx + startMark.length, endIdx).trim()
: html;
- const doc = parser.parseFromString(block, 'text/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.');
@@ -4949,6 +4951,44 @@
});
}
+ function normalizeSourceFallbackBlock(block, filePath) {
+ if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
+ return String(block)
+ .replace(
+ /',
+ )
+ .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;
@@ -5422,7 +5462,11 @@
}
// Source fallback when HMR did not land variants in this tab.
if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) {
- injectVariantsFromSource(msg.file, msg.id);
+ setTimeout(() => {
+ if (arrivedVariants >= expectedVariants && expectedVariants > 0) return;
+ if (state !== 'GENERATING' || msg.id !== currentSessionId) return;
+ injectVariantsFromSource(msg.file, msg.id);
+ }, 750);
break;
}
// Variants are in source but not in the DOM yet. Common when the
@@ -5514,8 +5558,11 @@
function sendEvent(msg, opts) {
msg.token = TOKEN;
function handleFailure(err) {
- console.error('[impeccable] Failed to send event:', err);
- if (opts && opts.throwOnError) throw err;
+ if (opts && opts.throwOnError) {
+ console.error('[impeccable] Failed to send event:', err);
+ throw err;
+ }
+ console.debug('[impeccable] Dropped optional live event:', err);
return null;
}
return fetch('http://localhost:' + PORT + '/events', {
@@ -6843,12 +6890,16 @@ void main() {
function ensureAcceptedDomClean(pending) {
const sessionId = pending?.id;
const variantId = pending?.variant;
- const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]');
+ const wrapper = findAcceptedRuntimeWrapper(sessionId);
const accepted = wrapper?.querySelector?.('[data-impeccable-variant="' + variantId + '"]');
if (!wrapper) {
restoreAcceptedDomFromSnapshot(pending);
return;
}
+ if (acceptedDomAlreadyClean(pending)) {
+ wrapper.remove();
+ return;
+ }
if (!accepted) {
wrapper.remove();
restoreAcceptedDomFromSnapshot(pending);
@@ -6862,6 +6913,12 @@ void main() {
wrapper.remove();
}
+ function findAcceptedRuntimeWrapper(sessionId) {
+ if (!sessionId) return null;
+ return document.querySelector('[data-impeccable-variants="' + sessionId + '"]')
+ || document.querySelector('[data-impeccable-carbonize="' + sessionId + '"]');
+ }
+
function restoreAcceptedDomFromSnapshot(pending) {
if (acceptedDomAlreadyClean(pending)) return;
if (!pending?.acceptedHtml) {
diff --git a/tests/ci-test-plan.test.mjs b/tests/ci-test-plan.test.mjs
new file mode 100644
index 000000000..8c2d0bbac
--- /dev/null
+++ b/tests/ci-test-plan.test.mjs
@@ -0,0 +1,124 @@
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
+import { spawnSync } from 'node:child_process';
+import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
+import { join } from 'node:path';
+import { tmpdir } from 'node:os';
+
+const SCRIPT = 'scripts/ci-test-plan.mjs';
+
+describe('ci-test-plan', () => {
+ it('keeps docs-only pull requests on the core suite', () => {
+ const outputs = runPlan({
+ GITHUB_EVENT_NAME: 'pull_request',
+ CI_CHANGED_FILES: 'README.md',
+ });
+
+ assert.equal(outputs.core, 'true');
+ assert.equal(outputs.detector, 'false');
+ assert.equal(outputs.live, 'false');
+ assert.equal(outputs.framework, 'false');
+ assert.equal(outputs.live_e2e, 'false');
+ assert.equal(outputs.live_e2e_accept_cleanup, 'false');
+ assert.equal(outputs.live_svelte_adapter_deepseek, 'false');
+ });
+
+ it('routes detector changes to detector tests only', () => {
+ const outputs = runPlan({
+ GITHUB_EVENT_NAME: 'pull_request',
+ CI_CHANGED_FILES: 'cli/engine/detect-antipatterns.mjs',
+ });
+
+ assert.equal(outputs.detector, 'true');
+ assert.equal(outputs.live, 'false');
+ assert.equal(outputs.framework, 'false');
+ });
+
+ it('routes live server changes to live unit and full live E2E lanes', () => {
+ const outputs = runPlan({
+ GITHUB_EVENT_NAME: 'pull_request',
+ CI_CHANGED_FILES: 'skill/scripts/live-server.mjs',
+ });
+
+ assert.equal(outputs.live, 'true');
+ assert.equal(outputs.live_e2e, 'true');
+ assert.equal(outputs.live_e2e_accept_cleanup, 'true');
+ assert.equal(outputs.live_svelte_adapter_deepseek, 'true');
+ assert.equal(outputs.detector, 'false');
+ });
+
+ it('routes skill setup changes to the skill behavior lane', () => {
+ const outputs = runPlan({
+ GITHUB_EVENT_NAME: 'pull_request',
+ CI_CHANGED_FILES: 'skill/SKILL.src.md',
+ });
+
+ assert.equal(outputs.skill_behavior, 'true');
+ assert.equal(outputs.detector, 'false');
+ assert.equal(outputs.live, 'false');
+ });
+
+ it('forces deterministic suites on push without forcing opt-in E2E suites', () => {
+ const outputs = runPlan({
+ GITHUB_EVENT_NAME: 'push',
+ CI_CHANGED_FILES: 'README.md',
+ });
+
+ assert.equal(outputs.core, 'true');
+ assert.equal(outputs.detector, 'true');
+ assert.equal(outputs.live, 'true');
+ assert.equal(outputs.framework, 'true');
+ assert.equal(outputs.cli_remote_e2e, 'false');
+ assert.equal(outputs.live_e2e, 'false');
+ assert.equal(outputs.live_e2e_accept_cleanup, 'false');
+ assert.equal(outputs.live_svelte_adapter_deepseek, 'false');
+ });
+
+ it('enables remote smoke suites on manual dispatch', () => {
+ const outputs = runPlan({
+ GITHUB_EVENT_NAME: 'workflow_dispatch',
+ CI_CHANGED_FILES: 'README.md',
+ });
+
+ assert.equal(outputs.cli_remote_e2e, 'true');
+ assert.equal(outputs.live_e2e, 'true');
+ assert.equal(outputs.live_e2e_accept_cleanup, 'true');
+ assert.equal(outputs.skill_behavior, 'true');
+ assert.equal(outputs.live_svelte_adapter_deepseek, 'true');
+ });
+
+ it('exposes planned opt-in suite outputs to workflow jobs', () => {
+ const workflow = readFileSync('.github/workflows/ci.yml', 'utf-8');
+
+ assert.match(workflow, /live_e2e_accept_cleanup:\s*\$\{\{\s*steps\.plan\.outputs\.live_e2e_accept_cleanup\s*\}\}/);
+ assert.match(workflow, /live_svelte_adapter_deepseek:\s*\$\{\{\s*steps\.plan\.outputs\.live_svelte_adapter_deepseek\s*\}\}/);
+ assert.match(workflow, /live-e2e-accept-cleanup:/);
+ assert.match(workflow, /live-svelte-adapter-deepseek:/);
+ });
+});
+
+function runPlan(env) {
+ const tmp = mkdtempSync(join(tmpdir(), 'impeccable-ci-plan-'));
+ const outputPath = join(tmp, 'github-output');
+ try {
+ const result = spawnSync(process.execPath, [SCRIPT], {
+ cwd: process.cwd(),
+ encoding: 'utf-8',
+ env: {
+ ...process.env,
+ GITHUB_OUTPUT: outputPath,
+ ...env,
+ },
+ });
+ assert.equal(result.status, 0, result.stderr || result.stdout);
+ return Object.fromEntries(
+ readFileSync(outputPath, 'utf-8')
+ .trim()
+ .split(/\r?\n/)
+ .filter(Boolean)
+ .map((line) => line.split('=')),
+ );
+ } finally {
+ rmSync(tmp, { recursive: true, force: true });
+ }
+}
diff --git a/tests/framework-fixtures/README.md b/tests/framework-fixtures/README.md
index df0aa27a8..2b638001e 100644
--- a/tests/framework-fixtures/README.md
+++ b/tests/framework-fixtures/README.md
@@ -84,6 +84,12 @@ The `runtime` block is optional. Fixtures without it only run the static unit ch
6. Runs a **Steer smoke** step (unless `runtime.steer === false`): submit a message in the global Steer bar, wait for the fake agent to reply `steer_done`, assert the bar unlocks and a `data-impeccable-steer` marker lands in source + DOM. Then continues with pick → Go → cycle → accept.
7. Tears everything down (Playwright close, dev server SIGTERM, live-server stop, tmp rm).
+Useful runtime E2E filters:
+
+- `IMPECCABLE_E2E_ONLY=[,]` scopes the run to selected fixture names.
+- `IMPECCABLE_E2E_SCENARIOS=core` runs only the main click → Go → cycle → accept path; omit it or use `all` to include manual edit, annotation, and exit probes.
+- `IMPECCABLE_E2E_TEST_TIMEOUT_MS`, `IMPECCABLE_E2E_INSTALL_TIMEOUT_MS`, and `IMPECCABLE_E2E_DEV_READY_TIMEOUT_MS` tighten CI smoke timeouts without changing fixture metadata.
+
Optional `runtime.steer` fields:
```json
diff --git a/tests/live-browser-source.test.mjs b/tests/live-browser-source.test.mjs
index ad1c47ceb..4fa41dcd3 100644
--- a/tests/live-browser-source.test.mjs
+++ b/tests/live-browser-source.test.mjs
@@ -267,7 +267,7 @@ describe('live-browser source contracts', () => {
it('keeps sendEvent fire-and-forget by default while accept/discard opt into rejection', () => {
assert.match(
SOURCE,
- /function sendEvent\(msg, opts\)[\s\S]*if \(opts && opts\.throwOnError\) throw err;[\s\S]*return null;/,
+ /function sendEvent\(msg, opts\)[\s\S]*if \(opts && opts\.throwOnError\) \{[\s\S]*console\.error\('\[impeccable\] Failed to send event:', err\);[\s\S]*throw err;[\s\S]*\}[\s\S]*console\.debug\('\[impeccable\] Dropped optional live event:', err\);[\s\S]*return null;/,
'event=live_browser.send_event_contract actor=browser operation=send_event_failure risk=fire_and_forget_callers_get_unhandled_rejections expected=default swallow with opt-in throw actual=missing',
);
assert.match(SOURCE, /if \(res\.ok\) return res;[\s\S]*const body = await res\.json\(\)\.catch\(\(\) => \(\{\}\)\);[\s\S]*handleFailure\(new Error\(body\.error \|\| \('HTTP ' \+ res\.status \+ ' ' \+ res\.statusText\)\)\)/);
@@ -321,9 +321,14 @@ describe('live-browser source contracts', () => {
);
assert.match(
SOURCE,
- /function ensureAcceptedDomClean\(pending\)[\s\S]*?parent\.insertBefore\(accepted\.firstChild, wrapper\);[\s\S]*?wrapper\.remove\(\);/,
+ /function ensureAcceptedDomClean\(pending\)[\s\S]*?findAcceptedRuntimeWrapper\(sessionId\)[\s\S]*?acceptedDomAlreadyClean\(pending\)[\s\S]*?wrapper\.remove\(\);[\s\S]*?parent\.insertBefore\(accepted\.firstChild, wrapper\);[\s\S]*?wrapper\.remove\(\);/,
'post-cleanup fallback should unwrap the accepted variant instead of preserving live runtime wrappers',
);
+ assert.match(
+ SOURCE,
+ /function findAcceptedRuntimeWrapper\(sessionId\)[\s\S]*?data-impeccable-variants[\s\S]*?data-impeccable-carbonize/,
+ 'post-cleanup fallback should also remove stale carbonize wrappers left by React HMR after accept',
+ );
assert.match(
SOURCE,
/if \(!accepted\) \{[\s\S]{0,120}?wrapper\.remove\(\);[\s\S]{0,120}?restoreAcceptedDomFromSnapshot\(pending\);[\s\S]{0,80}?return;/,
@@ -340,4 +345,37 @@ describe('live-browser source contracts', () => {
'missing accepted DOM after clean source should recover by reloading the clean page',
);
});
+
+ it('normalizes generated JSX source before source-fallback DOM parsing', () => {
+ assert.match(
+ SOURCE,
+ /parser\.parseFromString\(normalizeSourceFallbackBlock\(block, filePath\), 'text\/html'\)/,
+ 'source fallback should normalize JSX wrapper syntax before DOMParser sees it',
+ );
+ assert.match(
+ SOURCE,
+ /function normalizeSourceFallbackBlock\(block, filePath\)[\s\S]*?