/**
* Agent module for the live-mode E2E test suite.
*
* Two layers:
*
* 1. `runAgentLoop(opts)` — the deterministic wrapper around the live-mode
* poll/wrap/write/accept protocol. This is identical for fake and real
* agents; only the variant-content production step differs.
*
* 2. `createFakeAgent()` — produces canned variants in the EXACT format
* `skill/reference/live.md` describes: a colocated
* ` wraps CSS in a template literal so JSX
* doesn't choke on the {} in CSS
* - non-default visible variants use style={{display: 'none'}}
* - inner element class= becomes className=, style="..." becomes JSX style={{ ... }}
* - data-impeccable-params stays a single-quoted JSON string (JSX-legal)
*/
function renderVariantsBlock({ sessionId, indent, output, commentSyntax, file, styleMode }) {
const isJsx = commentSyntax.open === '{/*';
const isSvelte = !!file && file.endsWith('.svelte');
const isAstroGlobalCss = styleMode === 'astro-global-prefixed';
const styleLines = isJsx
? [
indent + ' ',
]
: [
indent + ' ',
];
const variantBlocks = output.variants.map((v, i) => {
const idx = i + 1;
const paramsAttr = v.params && v.params.length
? " data-impeccable-params='" + attrEscape(JSON.stringify(v.params), { svelte: isSvelte }) + "'"
: '';
let styleAttr = '';
if (i !== 0) styleAttr = isJsx ? " style={{display: 'none'}}" : ' style="display: none"';
const inner = isJsx ? htmlToJsx(v.innerHtml) : v.innerHtml;
return [
indent + ' ' + commentSyntax.open + ' Variant ' + idx + ' ' + commentSyntax.close,
indent + '
',
indent + ' ' + inner,
indent + '
',
].join('\n');
});
return [...styleLines, ...variantBlocks].join('\n');
}
/**
* Read the wrapped file, find the "insert below this line" marker, splice in
* the rendered variants block, write back.
*/
async function spliceVariantsIntoWrapper({ tmp, wrapInfo, sessionId, output }) {
const filePath = path.join(tmp, wrapInfo.file);
const src = await fs.readFile(filePath, 'utf-8');
const lines = src.split('\n');
// Find the "Variants: insert below this line" comment line — definitive
// marker, robust to any indentation off-by-one. Matches in any comment
// style (HTML / JSX / Astro).
const markerIdx = lines.findIndex((l) =>
l.includes('Variants: insert below this line'),
);
if (markerIdx === -1) {
throw new Error('insert marker not found in ' + wrapInfo.file);
}
const indent = (lines[markerIdx].match(/^\s*/) || [''])[0];
// Indent INSIDE the wrapper is one level shallower (the marker is indented
// 2 spaces relative to the wrapper opening). Remove the 2-space comment
// indent to get the wrapper indent.
const wrapperIndent = indent.replace(/ $/, '');
const block = renderVariantsBlock({
sessionId,
indent: wrapperIndent,
output,
commentSyntax: wrapInfo.commentSyntax,
file: wrapInfo.file,
styleMode: wrapInfo.styleMode,
});
const next = [
...lines.slice(0, markerIdx + 1),
block,
...lines.slice(markerIdx + 1),
];
await fs.writeFile(filePath, next.join('\n'), 'utf-8');
}
async function writeSvelteComponentVariants({ tmp, wrapInfo, event, output }) {
const manifestPath = path.join(tmp, wrapInfo.file);
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
const componentDir = path.join(tmp, manifest.componentDir);
const isInsert = manifest.mode === 'insert';
const contract = Array.isArray(manifest.propContract) ? manifest.propContract : [];
const propNames = contract.map((entry) => entry.prop);
const baseMarkup = isInsert ? '' : substituteSvelteExprsWithProps(manifest.originalMarkup || '', contract).trim();
const textValues = isInsert ? [] : extractTextPieces(event.element?.outerHTML || event.element?.textContent || '');
const paramsByVariant = {};
for (let i = 0; i < output.variants.length; i++) {
const variantId = i + 1;
const variant = output.variants[i];
const tag = firstTagName(variant.innerHtml) || firstTagName(baseMarkup) || 'div';
let markup = substituteLiveTextWithProps(variant.innerHtml || '', contract, textValues).trim();
if (!isInsert && contract.length > 0 && !propNames.some((name) => markup.includes(`{${name}}`))) {
markup = mergeTopLevelAttrs(baseMarkup, variant.innerHtml || '') || baseMarkup;
}
if (isInsert && !variantMarkupHasVisibleContent(markup)) {
throw new Error(`Svelte insert variant ${variantId} has no visible content`);
}
if (isInsert && /\bdata-impeccable-[\w-]*\s*=/.test(markup)) {
throw new Error(`Svelte insert variant ${variantId} contains preview-only data-impeccable attributes`);
}
const css = svelteCssForVariant(output.scopedCss || '', variantId, tag);
const component = [
buildSveltePropsScript(contract),
'',
markup || baseMarkup || '',
'',
'',
'',
].join('\n');
await fs.writeFile(path.join(componentDir, `v${variantId}.svelte`), component, 'utf-8');
paramsByVariant[String(variantId)] = Array.isArray(variant.params) ? variant.params : [];
}
await fs.writeFile(path.join(componentDir, 'params.json'), JSON.stringify(paramsByVariant, null, 2) + '\n', 'utf-8');
}
function variantMarkupHasVisibleContent(markup) {
const text = String(markup || '')
.replace(/';
return ``;
}
function substituteSvelteExprsWithProps(markup, contract) {
let out = String(markup || '');
for (const entry of contract) {
out = out.split(`{${entry.expr}}`).join(`{${entry.prop}}`);
}
return out;
}
function substituteLiveTextWithProps(markup, contract, textValues) {
let out = String(markup || '');
for (let i = 0; i < contract.length; i++) {
const value = textValues[i];
if (!value) continue;
out = out.split(htmlEscape(value)).join(`{${contract[i].prop}}`);
out = out.split(value).join(`{${contract[i].prop}}`);
}
return out;
}
function extractTextPieces(html) {
return String(html || '')
.replace(/