mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-13 06:36:26 +03:00
Refresh the Impeccable product experience
Rework the landing page proof, steering demo, feature grid, slop catalog, detector coverage, theming, Live workflow, and responsive behavior.\n\nAI-assisted implementation by OpenAI Codex.
This commit is contained in:
+11
-323
@@ -27,10 +27,6 @@ import { join } from 'node:path';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { completionTypeForAcceptResult } from '../../skill/scripts/live/completion.mjs';
|
||||
import {
|
||||
prepareGenerationArtifact,
|
||||
publishGenerationArtifact,
|
||||
} from '../../skill/scripts/live/generation-publisher.mjs';
|
||||
|
||||
const execFileP = promisify(execFile);
|
||||
|
||||
@@ -1329,25 +1325,15 @@ async function spliceVariantsIntoWrapper({ tmp, wrapInfo, sessionId, output }) {
|
||||
styleMode: wrapInfo.styleMode,
|
||||
});
|
||||
|
||||
const endMarkerIdx = lines.findIndex((line, index) =>
|
||||
index > markerIdx && line.includes('impeccable-variants-end ' + sessionId),
|
||||
);
|
||||
if (endMarkerIdx === -1) {
|
||||
throw new Error('end marker not found in ' + wrapInfo.file);
|
||||
}
|
||||
const tailIdx = wrapInfo.commentSyntax.open === '{/*'
|
||||
? endMarkerIdx
|
||||
: endMarkerIdx - 1;
|
||||
|
||||
const next = [
|
||||
...lines.slice(0, markerIdx + 1),
|
||||
block,
|
||||
...lines.slice(tailIdx),
|
||||
...lines.slice(markerIdx + 1),
|
||||
];
|
||||
await fs.writeFile(filePath, next.join('\n'), 'utf-8');
|
||||
}
|
||||
|
||||
async function writeSvelteComponentVariants({ tmp, wrapInfo, event, output, writeParams = true }) {
|
||||
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);
|
||||
@@ -1387,168 +1373,7 @@ async function writeSvelteComponentVariants({ tmp, wrapInfo, event, output, writ
|
||||
paramsByVariant[String(variantId)] = Array.isArray(variant.params) ? variant.params : [];
|
||||
}
|
||||
|
||||
if (writeParams) {
|
||||
await fs.writeFile(path.join(componentDir, 'params.json'), JSON.stringify(paramsByVariant, null, 2) + '\n', 'utf-8');
|
||||
}
|
||||
manifest.arrivedVariants = output.variants.length;
|
||||
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
||||
}
|
||||
|
||||
async function publishSvelteComponentVariants({ tmp, wrapInfo, event, output, writeParams = true }) {
|
||||
const prepared = prepareGenerationArtifact({
|
||||
id: event.id,
|
||||
sourceFile: wrapInfo.file,
|
||||
cwd: tmp,
|
||||
});
|
||||
if (!prepared.ok) throw new Error(`Svelte publication prepare failed: ${prepared.error}`);
|
||||
|
||||
await writeSvelteComponentVariants({
|
||||
tmp,
|
||||
wrapInfo: { ...wrapInfo, file: prepared.artifactFile },
|
||||
event,
|
||||
output,
|
||||
writeParams,
|
||||
});
|
||||
|
||||
const published = publishGenerationArtifact({
|
||||
id: event.id,
|
||||
epoch: prepared.epoch,
|
||||
sourceFile: wrapInfo.file,
|
||||
artifactFile: prepared.artifactFile,
|
||||
expectedSourceHash: prepared.expectedSourceHash,
|
||||
arrivedVariants: output.variants.length,
|
||||
expectedVariants: event.count,
|
||||
cwd: tmp,
|
||||
});
|
||||
if (!published.ok) throw new Error(`Svelte publication failed: ${published.error}`);
|
||||
return published;
|
||||
}
|
||||
|
||||
async function writeVueComponentVariants({ tmp, wrapInfo, event, output, writeParams = true }) {
|
||||
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 contract = Array.isArray(manifest.propContract) ? manifest.propContract : [];
|
||||
const textValues = 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];
|
||||
let markup = substituteLiveTextWithProps(variant.innerHtml || '', contract, textValues).trim();
|
||||
for (const entry of contract) {
|
||||
markup = markup.replaceAll(`{${entry.prop}}`, `{{ ${entry.prop} }}`);
|
||||
}
|
||||
const css = svelteCssForVariant(output.scopedCss || '', variantId, firstTagName(markup) || 'div');
|
||||
const propsScript = contract.length > 0
|
||||
? ['<script setup>', 'defineProps({', ...contract.map((entry) => ` ${entry.prop}: { default: '' },`), '});', '</script>', '']
|
||||
: [];
|
||||
const component = [
|
||||
...propsScript,
|
||||
'<template>',
|
||||
markup || '<div></div>',
|
||||
'</template>',
|
||||
'',
|
||||
'<style scoped>',
|
||||
css || ':where(*) {}',
|
||||
'</style>',
|
||||
'',
|
||||
].join('\n');
|
||||
await fs.writeFile(path.join(componentDir, `v${variantId}.vue`), component, 'utf-8');
|
||||
paramsByVariant[String(variantId)] = Array.isArray(variant.params) ? variant.params : [];
|
||||
}
|
||||
|
||||
if (writeParams) {
|
||||
await fs.writeFile(path.join(componentDir, 'params.json'), JSON.stringify(paramsByVariant, null, 2) + '\n', 'utf-8');
|
||||
}
|
||||
manifest.arrivedVariants = output.variants.length;
|
||||
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
||||
}
|
||||
|
||||
async function publishVueComponentVariants({ tmp, wrapInfo, event, output, writeParams = true }) {
|
||||
const prepared = prepareGenerationArtifact({ id: event.id, sourceFile: wrapInfo.file, cwd: tmp });
|
||||
if (!prepared.ok) throw new Error(`Vue publication prepare failed: ${prepared.error}`);
|
||||
await writeVueComponentVariants({
|
||||
tmp,
|
||||
wrapInfo: { ...wrapInfo, file: prepared.artifactFile },
|
||||
event,
|
||||
output,
|
||||
writeParams,
|
||||
});
|
||||
const published = publishGenerationArtifact({
|
||||
id: event.id,
|
||||
epoch: prepared.epoch,
|
||||
sourceFile: wrapInfo.file,
|
||||
artifactFile: prepared.artifactFile,
|
||||
expectedSourceHash: prepared.expectedSourceHash,
|
||||
arrivedVariants: output.variants.length,
|
||||
expectedVariants: event.count,
|
||||
cwd: tmp,
|
||||
});
|
||||
if (!published.ok) throw new Error(`Vue publication failed: ${published.error}`);
|
||||
return published;
|
||||
}
|
||||
|
||||
async function publishSourceVariants({ tmp, wrapInfo, event, output }) {
|
||||
const prepared = prepareGenerationArtifact({
|
||||
id: event.id,
|
||||
sourceFile: wrapInfo.file,
|
||||
cwd: tmp,
|
||||
});
|
||||
if (!prepared.ok) throw new Error(`Source publication prepare failed: ${prepared.error}`);
|
||||
|
||||
await spliceVariantsIntoWrapper({
|
||||
tmp,
|
||||
wrapInfo: { ...wrapInfo, file: prepared.artifactFile },
|
||||
sessionId: event.id,
|
||||
output,
|
||||
});
|
||||
|
||||
const published = publishGenerationArtifact({
|
||||
id: event.id,
|
||||
epoch: prepared.epoch,
|
||||
sourceFile: wrapInfo.file,
|
||||
artifactFile: prepared.artifactFile,
|
||||
expectedSourceHash: prepared.expectedSourceHash,
|
||||
arrivedVariants: output.variants.length,
|
||||
expectedVariants: event.count,
|
||||
cwd: tmp,
|
||||
});
|
||||
if (!published.ok) throw new Error(`Source publication failed: ${published.error}`);
|
||||
return published;
|
||||
}
|
||||
|
||||
async function publishVariantProgress({
|
||||
base,
|
||||
token,
|
||||
event,
|
||||
wrapInfo,
|
||||
arrivedVariants,
|
||||
signal,
|
||||
revision = 1,
|
||||
publicationKind = 'variants',
|
||||
}) {
|
||||
const previewMode = wrapInfo.previewMode || 'source';
|
||||
await fetch(`${base}/events`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
token,
|
||||
type: 'checkpoint',
|
||||
id: event.id,
|
||||
revision,
|
||||
revisionDomain: 'publication',
|
||||
phase: 'cycling',
|
||||
reason: 'variants_progress',
|
||||
arrivedVariants,
|
||||
expectedVariants: event.count,
|
||||
sourceFile: wrapInfo.sourceFile || wrapInfo.file,
|
||||
previewFile: wrapInfo.file,
|
||||
previewMode,
|
||||
publicationKind,
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
await fs.writeFile(path.join(componentDir, 'params.json'), JSON.stringify(paramsByVariant, null, 2) + '\n', 'utf-8');
|
||||
}
|
||||
|
||||
function variantMarkupHasVisibleContent(markup) {
|
||||
@@ -1682,11 +1507,6 @@ export async function runAgentLoop({
|
||||
agent,
|
||||
signal,
|
||||
log = () => {},
|
||||
trace = () => {},
|
||||
progressive = false,
|
||||
progressiveDelayMs = 0,
|
||||
progressiveInitialCount = 1,
|
||||
atomicDelayMs = 0,
|
||||
wrapTarget = { classes: 'hero-title', tag: 'h1' },
|
||||
steerSourceFile,
|
||||
steerTarget,
|
||||
@@ -1710,8 +1530,6 @@ export async function runAgentLoop({
|
||||
if (event.type === 'prefetch') continue;
|
||||
if (event.type === 'connected') continue;
|
||||
|
||||
trace('agent.event.received', { id: event.id, type: event.type, clientSentAt: event.clientSentAt ?? null });
|
||||
|
||||
if (event.type === 'steer') {
|
||||
log(`steer id=${event.id} message=${JSON.stringify(event.message)}`);
|
||||
try {
|
||||
@@ -1760,16 +1578,7 @@ export async function runAgentLoop({
|
||||
log(`generate id=${event.id} mode=${isInsert ? 'insert' : 'replace'}${isInsert ? '' : ` action=${event.action}`} count=${event.count}`);
|
||||
try {
|
||||
let wrapInfo;
|
||||
if (event.scaffold) {
|
||||
wrapInfo = event.scaffold;
|
||||
trace('agent.scaffold.reused', {
|
||||
id: event.id,
|
||||
file: wrapInfo.file,
|
||||
previewMode: wrapInfo.previewMode || 'source',
|
||||
durationMs: event.scaffoldDurationMs ?? null,
|
||||
});
|
||||
} else if (isInsert) {
|
||||
trace('agent.scaffold.start', { id: event.id, mode: 'insert' });
|
||||
if (isInsert) {
|
||||
const insertTarget = insertTargetFromEvent(event);
|
||||
wrapInfo = await runInsert({
|
||||
tmp,
|
||||
@@ -1778,9 +1587,7 @@ export async function runAgentLoop({
|
||||
count: event.count,
|
||||
...insertTarget,
|
||||
});
|
||||
trace('agent.scaffold.end', { id: event.id, file: wrapInfo.file, previewMode: wrapInfo.previewMode || 'source' });
|
||||
} else {
|
||||
trace('agent.scaffold.start', { id: event.id, mode: 'replace' });
|
||||
// 1. Wrap the original element in the variant scaffold (deterministic CLI)
|
||||
// wrapTarget can be a static {classes, tag, elementId} (test fixtures
|
||||
// know what they pick) or a function (event) => target (real-use
|
||||
@@ -1799,154 +1606,41 @@ export async function runAgentLoop({
|
||||
...target,
|
||||
text,
|
||||
});
|
||||
trace('agent.scaffold.end', { id: event.id, file: wrapInfo.file, previewMode: wrapInfo.previewMode || 'source' });
|
||||
}
|
||||
log(`scaffolded: ${wrapInfo.file} insertLine=${wrapInfo.insertLine}`);
|
||||
|
||||
// 2. Agent generates variant content (LLM-pluggable seam).
|
||||
// Providers may expose a true split path so variant 1 is written before
|
||||
// the request for the remaining variants completes.
|
||||
trace('agent.generate.start', { id: event.id, count: event.count });
|
||||
const splitProgressive = progressive
|
||||
&& typeof agent.generateFirstVariant === 'function'
|
||||
&& typeof agent.generateRemainingVariants === 'function'
|
||||
&& event.count > 1;
|
||||
let output;
|
||||
let firstOutput;
|
||||
if (splitProgressive) {
|
||||
firstOutput = normalizeVariantOutput(
|
||||
await agent.generateFirstVariant(event, { wrapTarget, wrapInfo }),
|
||||
wrapInfo,
|
||||
);
|
||||
firstOutput = {
|
||||
...firstOutput,
|
||||
variants: firstOutput.variants.slice(0, 1).map((variant) => ({ ...variant, params: [] })),
|
||||
};
|
||||
trace('agent.generate.first_ready', { id: event.id, count: firstOutput.variants.length });
|
||||
trace('agent.first_variant.write.start', { id: event.id, file: wrapInfo.file });
|
||||
if (wrapInfo.previewMode === 'svelte-component') {
|
||||
await publishSvelteComponentVariants({ tmp, wrapInfo, event, output: firstOutput, writeParams: false });
|
||||
} else if (wrapInfo.previewMode === 'vue-component') {
|
||||
await publishVueComponentVariants({ tmp, wrapInfo, event, output: firstOutput, writeParams: false });
|
||||
} else {
|
||||
await publishSourceVariants({ tmp, wrapInfo, event, output: firstOutput });
|
||||
}
|
||||
await publishVariantProgress({
|
||||
base,
|
||||
token,
|
||||
event,
|
||||
wrapInfo,
|
||||
arrivedVariants: firstOutput.variants.length,
|
||||
signal,
|
||||
});
|
||||
trace('agent.first_variant.write.end', { id: event.id, file: wrapInfo.file });
|
||||
output = normalizeVariantOutput(
|
||||
await agent.generateRemainingVariants(event, { wrapTarget, wrapInfo, firstOutput }),
|
||||
wrapInfo,
|
||||
);
|
||||
trace('agent.generate.end', { id: event.id, count: output?.variants?.length || 0 });
|
||||
} else {
|
||||
output = normalizeVariantOutput(
|
||||
await agent.generateVariants(event, { wrapTarget, wrapInfo }),
|
||||
wrapInfo,
|
||||
);
|
||||
if (!progressive && atomicDelayMs > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, atomicDelayMs));
|
||||
}
|
||||
trace('agent.generate.first_ready', { id: event.id, count: output?.variants?.length || 0 });
|
||||
if (!progressive || output.variants.length <= 1) {
|
||||
trace('agent.generate.end', { id: event.id, count: output?.variants?.length || 0 });
|
||||
}
|
||||
|
||||
if (progressive && output.variants.length > 1) {
|
||||
const initialCount = Math.max(1, Math.min(
|
||||
Number(progressiveInitialCount) || 1,
|
||||
output.variants.length - 1,
|
||||
));
|
||||
firstOutput = {
|
||||
...output,
|
||||
variants: output.variants
|
||||
.slice(0, initialCount)
|
||||
.map((variant) => ({ ...variant, params: [] })),
|
||||
};
|
||||
trace('agent.first_variant.write.start', { id: event.id, file: wrapInfo.file });
|
||||
if (wrapInfo.previewMode === 'svelte-component') {
|
||||
await publishSvelteComponentVariants({ tmp, wrapInfo, event, output: firstOutput, writeParams: false });
|
||||
} else if (wrapInfo.previewMode === 'vue-component') {
|
||||
await publishVueComponentVariants({ tmp, wrapInfo, event, output: firstOutput, writeParams: false });
|
||||
} else {
|
||||
await publishSourceVariants({ tmp, wrapInfo, event, output: firstOutput });
|
||||
}
|
||||
await publishVariantProgress({
|
||||
base,
|
||||
token,
|
||||
event,
|
||||
wrapInfo,
|
||||
arrivedVariants: firstOutput.variants.length,
|
||||
signal,
|
||||
});
|
||||
trace('agent.first_variant.write.end', { id: event.id, file: wrapInfo.file });
|
||||
if (progressiveDelayMs > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, progressiveDelayMs));
|
||||
}
|
||||
trace('agent.generate.end', { id: event.id, count: output?.variants?.length || 0 });
|
||||
}
|
||||
}
|
||||
// 2. Agent generates variant content (LLM-pluggable seam)
|
||||
let output = await agent.generateVariants(event, { wrapTarget, wrapInfo });
|
||||
output = normalizeVariantOutput(output, wrapInfo);
|
||||
if (output.variants.length !== event.count) {
|
||||
log(`warning: agent returned ${output.variants.length} variants, expected ${event.count}`);
|
||||
}
|
||||
|
||||
// 3. Write the complete set into the deterministic preview target.
|
||||
trace('agent.write.start', { id: event.id, file: wrapInfo.file });
|
||||
// 3. Write variants into the deterministic preview target.
|
||||
if (wrapInfo.previewMode === 'svelte-component') {
|
||||
await publishSvelteComponentVariants({ tmp, wrapInfo, event, output, writeParams: true });
|
||||
} else if (wrapInfo.previewMode === 'vue-component') {
|
||||
await publishVueComponentVariants({ tmp, wrapInfo, event, output, writeParams: true });
|
||||
} else if (progressive) {
|
||||
await publishSourceVariants({ tmp, wrapInfo, event, output });
|
||||
await writeSvelteComponentVariants({ tmp, wrapInfo, event, output });
|
||||
} else {
|
||||
await spliceVariantsIntoWrapper({ tmp, wrapInfo, sessionId: event.id, output });
|
||||
}
|
||||
trace('agent.write.end', { id: event.id, file: wrapInfo.file });
|
||||
if (progressive) {
|
||||
await publishVariantProgress({
|
||||
base,
|
||||
token,
|
||||
event,
|
||||
wrapInfo,
|
||||
arrivedVariants: output.variants.length,
|
||||
signal,
|
||||
revision: 2,
|
||||
publicationKind: 'params',
|
||||
});
|
||||
}
|
||||
if (process.env.IMPECCABLE_E2E_DEBUG) {
|
||||
const post = await fs.readFile(path.join(tmp, wrapInfo.file), 'utf-8');
|
||||
log(`--- post-splice (variants written) ---\n${post}`);
|
||||
}
|
||||
|
||||
// 4. Tell the server we're done (broadcasts SSE done → browser settles to CYCLING)
|
||||
trace('agent.reply.start', { id: event.id });
|
||||
await fetch(`${base}/poll`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token, type: 'done', sourceEventType: 'generate', id: event.id, file: wrapInfo.file }),
|
||||
body: JSON.stringify({ token, type: 'done', id: event.id, file: wrapInfo.file }),
|
||||
signal,
|
||||
});
|
||||
trace('agent.reply.end', { id: event.id });
|
||||
} catch (err) {
|
||||
if (signal.aborted) return;
|
||||
if (isExpectedGenerationCancellation(err)) {
|
||||
trace('agent.generate.canceled', { id: event.id, reason: 'stale_generation_epoch' });
|
||||
log('generate canceled after Accept/Discard: ' + err.message);
|
||||
continue;
|
||||
}
|
||||
trace('agent.generate.error', { id: event.id, message: err.message });
|
||||
log('generate failed: ' + err.message);
|
||||
await fetch(`${base}/poll`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token, type: 'error', sourceEventType: 'generate', id: event.id, message: err.message }),
|
||||
body: JSON.stringify({ token, type: 'error', id: event.id, message: err.message }),
|
||||
signal,
|
||||
}).catch(() => {});
|
||||
}
|
||||
@@ -2046,7 +1740,6 @@ export async function runAgentLoop({
|
||||
body: JSON.stringify({
|
||||
token,
|
||||
type: completionType,
|
||||
sourceEventType: 'accept',
|
||||
id: event.id,
|
||||
file: acceptResult.file,
|
||||
message: acceptResult.error,
|
||||
@@ -2076,7 +1769,6 @@ export async function runAgentLoop({
|
||||
body: JSON.stringify({
|
||||
token,
|
||||
type: completionType,
|
||||
sourceEventType: 'discard',
|
||||
id: event.id,
|
||||
file: discardResult.file,
|
||||
message: discardResult.error,
|
||||
@@ -2095,10 +1787,6 @@ export async function runAgentLoop({
|
||||
}
|
||||
}
|
||||
|
||||
export function isExpectedGenerationCancellation(error) {
|
||||
return /(?:^|\b)stale_generation_epoch(?:\b|$)/.test(String(error?.message || error || ''));
|
||||
}
|
||||
|
||||
async function runPollReply({ tmp, scriptsDir, id, status, message, data }) {
|
||||
const args = [path.join(scriptsDir, 'live-poll.mjs'), '--reply', id, status];
|
||||
if (data !== undefined) args.push('--data', JSON.stringify(data));
|
||||
|
||||
@@ -192,7 +192,6 @@ const STEER_SYSTEM_INSTRUCTIONS = [
|
||||
* @property {string=} model Override the selected provider's default model.
|
||||
* @property {string=} baseURL Override the provider API base URL.
|
||||
* @property {object=} config Pre-resolved provider config from resolveLlmAgentConfig().
|
||||
* @property {boolean=} includeLiveSpec Attach the full live.md reference. Defaults to true; latency benchmarks disable it to export only the synthetic element contract.
|
||||
* @property {(msg: string) => void=} log Optional logger for debug output.
|
||||
*/
|
||||
|
||||
@@ -241,22 +240,14 @@ export async function createLlmAgent(opts = {}) {
|
||||
const { apiKey, baseURL, model, provider } = config;
|
||||
const log = opts.log || (() => {});
|
||||
|
||||
const liveMd = opts.includeLiveSpec === false ? null : await fs.readFile(LIVE_MD_PATH, 'utf-8');
|
||||
const liveMd = await fs.readFile(LIVE_MD_PATH, 'utf-8');
|
||||
const client = new Anthropic({ apiKey, ...(baseURL ? { baseURL } : {}) });
|
||||
const systemBlocks = (instructions) => [
|
||||
{
|
||||
type: 'text',
|
||||
text: liveMd ? instructions : instructions.replace(/\n\nCONTEXT —[^\n]+$/, ''),
|
||||
},
|
||||
...(liveMd ? [{ type: 'text', text: liveMd, cache_control: { type: 'ephemeral' } }] : []),
|
||||
];
|
||||
|
||||
return {
|
||||
async generateVariants(event, context = {}) {
|
||||
const isInsert = event.mode === 'insert';
|
||||
const baseUserMessage = [
|
||||
`Produce variants for the following ${isInsert ? 'insert request' : 'pick'}. Reply with the JSON object only — no prose.`,
|
||||
progressiveVariantGuidance(event),
|
||||
'',
|
||||
'```json',
|
||||
JSON.stringify(buildVariantRequestPayload(event, context), null, 2),
|
||||
@@ -265,7 +256,6 @@ export async function createLlmAgent(opts = {}) {
|
||||
|
||||
let userMessage = baseUserMessage;
|
||||
for (let attempt = 0; attempt < MANUAL_EDIT_RESPONSE_MAX_ATTEMPTS; attempt += 1) {
|
||||
const lastAttempt = attempt + 1 >= MANUAL_EDIT_RESPONSE_MAX_ATTEMPTS;
|
||||
let response;
|
||||
try {
|
||||
response = await client.messages.create(
|
||||
@@ -273,10 +263,15 @@ export async function createLlmAgent(opts = {}) {
|
||||
model,
|
||||
temperature: 0,
|
||||
max_tokens: 16000,
|
||||
// When present, live.md is the final cacheable stable prefix.
|
||||
// Benchmarks omit it so external payloads contain only the
|
||||
// synthetic element contract and per-run event.
|
||||
system: systemBlocks(VARIANT_SYSTEM_INSTRUCTIONS),
|
||||
system: [
|
||||
{ type: 'text', text: VARIANT_SYSTEM_INSTRUCTIONS },
|
||||
// Cacheable: the entire stable prefix (instructions + spec) is
|
||||
// cached up to this breakpoint. The user message holds all the
|
||||
// per-call volatile content. DeepSeek compatibility support is
|
||||
// provider-reported and best-effort; the usage log below tells us
|
||||
// whether cache reads/writes actually happened.
|
||||
{ type: 'text', text: liveMd, cache_control: { type: 'ephemeral' } },
|
||||
],
|
||||
messages: [{ role: 'user', content: userMessage }],
|
||||
},
|
||||
{
|
||||
@@ -285,7 +280,7 @@ export async function createLlmAgent(opts = {}) {
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
if (lastAttempt) throw err;
|
||||
if (attempt === 1) throw err;
|
||||
log(`variant request failed; retrying: ${err.message}`);
|
||||
userMessage = [
|
||||
baseUserMessage,
|
||||
@@ -305,7 +300,7 @@ export async function createLlmAgent(opts = {}) {
|
||||
`provider=${provider} model=${model} attempt=${attempt + 1} input=${inputTokens} output=${outputTokens} cache_read=${cacheRead} cache_write=${cacheWrite}`,
|
||||
);
|
||||
if (!response || !Array.isArray(response.content)) {
|
||||
if (lastAttempt) throw new Error('LLM agent: provider returned an empty variant response');
|
||||
if (attempt === 1) throw new Error('LLM agent: provider returned an empty variant response');
|
||||
log('variant response validation failed; retrying: provider returned an empty response');
|
||||
userMessage = [
|
||||
baseUserMessage,
|
||||
@@ -325,7 +320,7 @@ export async function createLlmAgent(opts = {}) {
|
||||
try {
|
||||
parsed = parseVariantResponse(text);
|
||||
} catch (err) {
|
||||
if (lastAttempt) throw err;
|
||||
if (attempt === 1) throw err;
|
||||
log(`variant response validation failed; retrying: ${err.message.split('\n')[0]}`);
|
||||
userMessage = [
|
||||
baseUserMessage,
|
||||
@@ -337,13 +332,11 @@ export async function createLlmAgent(opts = {}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const validationError = validateVariantCount(parsed, event)
|
||||
|| validateProgressiveVariantOutput(parsed, event)
|
||||
|| (isInsert
|
||||
? validateInsertVariantOutput(parsed, event)
|
||||
: (validateVariantVisibleCopy(parsed, event.element) || validateVariantMaterialChange(parsed, event.element)));
|
||||
const validationError = isInsert
|
||||
? validateInsertVariantOutput(parsed, event)
|
||||
: (validateVariantVisibleCopy(parsed, event.element) || validateVariantMaterialChange(parsed, event.element));
|
||||
if (!validationError) return parsed;
|
||||
if (lastAttempt) throw new Error(`LLM agent: ${validationError}`);
|
||||
if (attempt === 1) throw new Error(`LLM agent: ${validationError}`);
|
||||
|
||||
log(`variant validation failed; retrying: ${validationError}`);
|
||||
if (isInsert) {
|
||||
@@ -418,7 +411,10 @@ export async function createLlmAgent(opts = {}) {
|
||||
model,
|
||||
temperature: 0,
|
||||
max_tokens: 16000,
|
||||
system: systemBlocks(MANUAL_EDIT_SYSTEM_INSTRUCTIONS),
|
||||
system: [
|
||||
{ type: 'text', text: MANUAL_EDIT_SYSTEM_INSTRUCTIONS },
|
||||
{ type: 'text', text: liveMd, cache_control: { type: 'ephemeral' } },
|
||||
],
|
||||
messages: [{ role: 'user', content: userMessage }],
|
||||
},
|
||||
{
|
||||
@@ -546,7 +542,10 @@ export async function createLlmAgent(opts = {}) {
|
||||
const response = await client.messages.create({
|
||||
model,
|
||||
max_tokens: 4096,
|
||||
system: systemBlocks(STEER_SYSTEM_INSTRUCTIONS),
|
||||
system: [
|
||||
{ type: 'text', text: STEER_SYSTEM_INSTRUCTIONS },
|
||||
{ type: 'text', text: liveMd, cache_control: { type: 'ephemeral' } },
|
||||
],
|
||||
messages: [{ role: 'user', content: userMessage }],
|
||||
});
|
||||
|
||||
@@ -673,7 +672,6 @@ export function buildVariantRequestPayload(event, context = {}) {
|
||||
action: event?.action,
|
||||
freeformPrompt: event?.freeformPrompt,
|
||||
count: event?.count,
|
||||
progressive: event?.progressive,
|
||||
element: isInsert ? null : {
|
||||
outerHTML: event?.element?.outerHTML,
|
||||
tagName: event?.element?.tagName,
|
||||
@@ -693,31 +691,6 @@ export function buildVariantRequestPayload(event, context = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
export function progressiveVariantGuidance(event = {}) {
|
||||
if (event.progressive?.phase === 'first') {
|
||||
return [
|
||||
'PROGRESSIVE FIRST DELIVERY:',
|
||||
`- Return exactly ${event.count} variant now.`,
|
||||
'- Return params: [] for this variant; tunable parameters are generated in the final phase.',
|
||||
'- The innerHtml must be materially different from the picked source, not merely paired with different CSS.',
|
||||
'- For a bare-text element, preserve the full exact copy in one child span inside the unchanged root tag/class.',
|
||||
].join('\n');
|
||||
}
|
||||
if (event.progressive?.phase === 'remaining') {
|
||||
return [
|
||||
'PROGRESSIVE FINAL DELIVERY:',
|
||||
`- Return the complete final set of exactly ${event.count} variants, including variant 1.`,
|
||||
'- progressive.firstVariant is the already-visible variant 1. Keep its innerHtml exactly unchanged and add its deferred params now.',
|
||||
...(event.progressive.omitFirstVariantCss ? [
|
||||
'- Variant 1 CSS is already published and immutable. Do not repeat or modify any scopedCss rule for data-impeccable-variant="1"; return scopedCss rules for variants 2+ only.',
|
||||
] : []),
|
||||
'- Generate the remaining distinct variants and their params in the other array positions.',
|
||||
'- Every remaining variant innerHtml must be materially changed too; for bare text, wrap the full exact copy in one child span with a distinct class instead of relying on CSS alone.',
|
||||
].join('\n');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate a model response into the variant-output schema. Throws
|
||||
* with a `Parsed (first 500 chars): ...` echo on every schema failure so the
|
||||
@@ -877,30 +850,6 @@ export function validateInsertVariantOutput(parsed, event = {}) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateVariantCount(parsed, event = {}) {
|
||||
const expected = Number(event.count);
|
||||
if (!Number.isInteger(expected) || expected < 1) return 'event count must be a positive integer';
|
||||
const actual = Array.isArray(parsed?.variants) ? parsed.variants.length : 0;
|
||||
return actual === expected ? null : `expected exactly ${expected} variants, received ${actual}`;
|
||||
}
|
||||
|
||||
export function validateProgressiveVariantOutput(parsed, event = {}) {
|
||||
if (event.progressive?.phase === 'first') {
|
||||
const hasEarlyParams = (parsed.variants || []).some((variant) => Array.isArray(variant.params) && variant.params.length > 0);
|
||||
return hasEarlyParams ? 'progressive first delivery must defer params with an empty params array' : null;
|
||||
}
|
||||
if (event.progressive?.phase === 'remaining' && event.progressive.firstVariant?.innerHtml) {
|
||||
const expected = String(event.progressive.firstVariant.innerHtml).trim();
|
||||
const actual = String(parsed.variants?.[0]?.innerHtml || '').trim();
|
||||
if (actual !== expected) return 'progressive final delivery must preserve variant 1 innerHtml exactly';
|
||||
if (event.progressive.omitFirstVariantCss && /\[data-impeccable-variant\s*=\s*["']1["'][^\]]*\]/.test(parsed.scopedCss || '')) {
|
||||
return 'progressive final delivery must omit already-published variant 1 CSS';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateVariantMaterialChange(parsed, element) {
|
||||
const originalHtml = normalizeVariantHtml(element?.outerHTML || '');
|
||||
if (!originalHtml) return null;
|
||||
|
||||
+20
-92
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -32,7 +32,8 @@ export { SCRIPTS_DIR, FIXTURES_DIR, REPO_ROOT };
|
||||
// Stage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function stageFixture(name, fixture, { fixtureRoot = join(FIXTURES_DIR, name) } = {}) {
|
||||
export function stageFixture(name, fixture) {
|
||||
const fixtureRoot = join(FIXTURES_DIR, name);
|
||||
const gitignore = readFileSync(join(fixtureRoot, 'gitignore.txt'), 'utf-8');
|
||||
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'impeccable-e2e-'));
|
||||
@@ -55,7 +56,6 @@ export function runInstall(tmp, command, { timeoutMs = readTimeoutEnv('IMPECCABL
|
||||
const installArgs = addNpmInstallDefaults(cmd, args);
|
||||
try {
|
||||
execFileSync(cmd, installArgs, { cwd: tmp, stdio: 'inherit', timeout: timeoutMs });
|
||||
repairMissingRollupOptionalBinary(tmp, { timeoutMs });
|
||||
} catch (err) {
|
||||
if (err.signal === 'SIGTERM' || err.signal === 'SIGKILL' || err.killed) {
|
||||
err.message = `fixture dependency install timed out after ${timeoutMs}ms: ${cmd} ${installArgs.join(' ')}`;
|
||||
@@ -64,26 +64,11 @@ export function runInstall(tmp, command, { timeoutMs = readTimeoutEnv('IMPECCABL
|
||||
}
|
||||
}
|
||||
|
||||
function repairMissingRollupOptionalBinary(tmp, { timeoutMs }) {
|
||||
if (process.platform !== 'darwin' || process.arch !== 'arm64') return;
|
||||
const rollupPackage = join(tmp, 'node_modules', 'rollup', 'package.json');
|
||||
const nativePackage = join(tmp, 'node_modules', '@rollup', 'rollup-darwin-arm64', 'package.json');
|
||||
if (!existsSync(rollupPackage) || existsSync(nativePackage)) return;
|
||||
const version = JSON.parse(readFileSync(rollupPackage, 'utf-8')).version;
|
||||
execFileSync('npm', [
|
||||
'install', '--no-save', '--no-audit', '--no-fund', '--no-progress',
|
||||
`@rollup/rollup-darwin-arm64@${version}`,
|
||||
], { cwd: tmp, stdio: 'inherit', timeout: timeoutMs });
|
||||
}
|
||||
|
||||
function addNpmInstallDefaults(cmd, args) {
|
||||
if (cmd !== 'npm') return args;
|
||||
if (!['install', 'ci'].includes(args[0])) return args;
|
||||
const out = [...args];
|
||||
// npm can omit platform-specific Rollup binaries unless optional
|
||||
// dependencies are requested explicitly (npm/cli#4828). Astro/Vite then
|
||||
// fail before Live starts on fresh staged fixtures.
|
||||
for (const flag of ['--no-progress', '--include=optional']) {
|
||||
for (const flag of ['--prefer-offline', '--no-progress']) {
|
||||
if (!out.some((arg) => arg === flag || arg.startsWith(flag + '='))) out.push(flag);
|
||||
}
|
||||
return out;
|
||||
@@ -215,57 +200,29 @@ export async function stopDevServer(child) {
|
||||
* @param {object} opts
|
||||
* @param {string} opts.name fixture name
|
||||
* @param {object} opts.fixture fixture.json contents
|
||||
* @param {string=} opts.fixtureRoot fixture directory; defaults to the public framework fixture tree
|
||||
* @param {import('playwright').Browser} opts.browser shared browser instance
|
||||
* @param {object} opts.agent VariantAgent (defaults to fake)
|
||||
* @param {object|function=} opts.wrapTarget live-wrap target or event mapper
|
||||
* @param {(context: object) => Promise<object|void>} [opts.startWorker]
|
||||
* Optional production worker factory. Return {stop, done}; when used,
|
||||
* omit `agent` so the deterministic in-process loop is not started.
|
||||
* @param {(context: object) => Promise<void>|void} [opts.prepareTmp]
|
||||
* @param {(msg: string) => void} [opts.log]
|
||||
*/
|
||||
export async function bootFixtureSession({
|
||||
name,
|
||||
fixture,
|
||||
fixtureRoot,
|
||||
browser,
|
||||
agent,
|
||||
wrapTarget,
|
||||
startWorker,
|
||||
prepareTmp,
|
||||
log = () => {},
|
||||
trace = () => {},
|
||||
progressive = false,
|
||||
progressiveDelayMs = 0,
|
||||
progressiveInitialCount = 1,
|
||||
atomicDelayMs = 0,
|
||||
keepTmp = false,
|
||||
}) {
|
||||
export async function bootFixtureSession({ name, fixture, browser, agent, wrapTarget, log = () => {} }) {
|
||||
const runtime = fixture.runtime;
|
||||
if (!runtime) throw new Error(`fixture ${name} has no runtime block`);
|
||||
|
||||
const tmp = stageFixture(name, fixture, { fixtureRoot });
|
||||
const tmp = stageFixture(name, fixture);
|
||||
let live;
|
||||
let dev;
|
||||
let agentAbort;
|
||||
let agentDone;
|
||||
let externalWorker;
|
||||
let ctx;
|
||||
|
||||
const teardown = async () => {
|
||||
try { if (ctx) await ctx.close(); } catch {}
|
||||
try { if (agentAbort) agentAbort.abort(); } catch {}
|
||||
try { if (agentDone) await agentDone.catch(() => {}); } catch {}
|
||||
try { if (externalWorker?.stop) await externalWorker.stop(); } catch {}
|
||||
try { if (externalWorker?.done) await externalWorker.done.catch(() => {}); } catch {}
|
||||
try { if (dev?.child) await stopDevServer(dev.child); } catch {}
|
||||
try { if (live) stopLiveServer(tmp); } catch {}
|
||||
if (!keepTmp) {
|
||||
try { rmSync(tmp, { recursive: true, force: true }); } catch {}
|
||||
} else {
|
||||
log(`kept staged fixture at ${tmp}`);
|
||||
}
|
||||
try { rmSync(tmp, { recursive: true, force: true }); } catch {}
|
||||
};
|
||||
|
||||
const stopLiveForDeferredWork = () => {
|
||||
@@ -276,67 +233,41 @@ export async function bootFixtureSession({
|
||||
|
||||
try {
|
||||
const startedAt = Date.now();
|
||||
if (prepareTmp) await prepareTmp({ tmp, fixture, scriptsDir: SCRIPTS_DIR, trace, log });
|
||||
trace('setup.install.start', { fixture: name });
|
||||
log(`installing deps`);
|
||||
runInstall(tmp, runtime.install);
|
||||
trace('setup.install.end', { fixture: name });
|
||||
log(`deps installed in ${formatDuration(Date.now() - startedAt)}`);
|
||||
|
||||
const liveStartedAt = Date.now();
|
||||
trace('setup.live_server.start', { fixture: name });
|
||||
log(`starting live-server`);
|
||||
live = startLiveServer(tmp);
|
||||
trace('setup.live_server.end', { fixture: name, port: live.port });
|
||||
log(`live-server ready in ${formatDuration(Date.now() - liveStartedAt)}`);
|
||||
|
||||
if (startWorker) {
|
||||
trace('setup.worker.start', { fixture: name });
|
||||
externalWorker = await startWorker({ tmp, fixture, scriptsDir: SCRIPTS_DIR, live, trace, log });
|
||||
trace('setup.worker.end', { fixture: name });
|
||||
}
|
||||
|
||||
const injectStartedAt = Date.now();
|
||||
trace('setup.inject.start', { fixture: name });
|
||||
log(`live-inject --port ${live.port}`);
|
||||
const injectResult = runInject(tmp, live.port);
|
||||
if (!injectResult.ok) throw new Error('live-inject failed: ' + JSON.stringify(injectResult));
|
||||
trace('setup.inject.end', { fixture: name, files: injectResult.files || injectResult.pageFiles || [] });
|
||||
log(`live-inject complete in ${formatDuration(Date.now() - injectStartedAt)}`);
|
||||
|
||||
const devStartedAt = Date.now();
|
||||
trace('setup.dev_server.start', { fixture: name });
|
||||
log(`spawning dev server: ${runtime.devCommand.join(' ')}`);
|
||||
dev = startDevServer(tmp, runtime);
|
||||
const { port: devPort } = await dev.ready;
|
||||
trace('setup.dev_server.end', { fixture: name, port: devPort });
|
||||
log(`dev server ready on ${devPort} in ${formatDuration(Date.now() - devStartedAt)}`);
|
||||
|
||||
// Agent loop runs concurrently — abort on teardown.
|
||||
if (agent) {
|
||||
agentAbort = new AbortController();
|
||||
const loopOptions = {
|
||||
tmp,
|
||||
scriptsDir: SCRIPTS_DIR,
|
||||
port: live.port,
|
||||
token: live.token,
|
||||
agent,
|
||||
wrapTarget,
|
||||
signal: agentAbort.signal,
|
||||
trace,
|
||||
progressive,
|
||||
progressiveDelayMs,
|
||||
progressiveInitialCount,
|
||||
atomicDelayMs,
|
||||
steerSourceFile: runtime.steer?.sourceFile,
|
||||
steerTarget: runtime.steer?.target,
|
||||
};
|
||||
const loops = [runAgentLoop({ ...loopOptions, log: (m) => log('[worker] ' + m) })];
|
||||
if (progressive) {
|
||||
loops.push(runAgentLoop({ ...loopOptions, log: (m) => log('[supervisor] ' + m) }));
|
||||
}
|
||||
agentDone = Promise.all(loops);
|
||||
}
|
||||
agentAbort = new AbortController();
|
||||
agentDone = runAgentLoop({
|
||||
tmp,
|
||||
scriptsDir: SCRIPTS_DIR,
|
||||
port: live.port,
|
||||
token: live.token,
|
||||
agent,
|
||||
wrapTarget,
|
||||
signal: agentAbort.signal,
|
||||
log: (m) => log('[agent] ' + m),
|
||||
steerSourceFile: runtime.steer?.sourceFile,
|
||||
steerTarget: runtime.steer?.target,
|
||||
});
|
||||
|
||||
const scheme = runtime.scheme || 'http';
|
||||
ctx = await browser.newContext({
|
||||
@@ -352,12 +283,10 @@ export async function bootFixtureSession({
|
||||
});
|
||||
|
||||
const pageStartedAt = Date.now();
|
||||
trace('setup.page_load.start', { fixture: name });
|
||||
await page.goto(`${scheme}://127.0.0.1:${devPort}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30_000,
|
||||
});
|
||||
trace('setup.page_load.end', { fixture: name });
|
||||
log(`page loaded in ${formatDuration(Date.now() - pageStartedAt)}`);
|
||||
|
||||
return {
|
||||
@@ -366,7 +295,6 @@ export async function bootFixtureSession({
|
||||
ctx,
|
||||
dev,
|
||||
live,
|
||||
worker: externalWorker,
|
||||
consoleErrors,
|
||||
stopLiveServer: stopLiveForDeferredWork,
|
||||
teardown,
|
||||
|
||||
+4
-58
@@ -424,23 +424,7 @@ export async function pickElement(page, selector, opts = {}) {
|
||||
if (visible) break;
|
||||
await resetPickMode(page);
|
||||
if (attempt === 2) {
|
||||
const snapshot = await page.evaluate(({ selector, barSel, pickSel }) => {
|
||||
const target = document.querySelector(selector);
|
||||
const rect = target?.getBoundingClientRect();
|
||||
const hit = rect ? document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2) : null;
|
||||
const query = window.__impeccableLiveQuery || ((sel) => document.querySelector(sel));
|
||||
const bar = query(barSel);
|
||||
const pick = query(pickSel);
|
||||
return {
|
||||
liveState: window.__IMPECCABLE_LIVE_STATE__ || null,
|
||||
target: target ? { tag: target.tagName, classes: target.className, rect: rect?.toJSON?.() || null } : null,
|
||||
hit: hit ? { tag: hit.tagName, classes: hit.className, text: (hit.textContent || '').slice(0, 80) } : null,
|
||||
pickActive: pick?.dataset.active || null,
|
||||
bar: bar ? { display: bar.style.display, text: bar.textContent } : null,
|
||||
debugState: window.__IMPECCABLE_LIVE_CHROME_CORE__?.debugState?.() || null,
|
||||
};
|
||||
}, { selector, barSel: BAR_ID, pickSel: PICK_TOGGLE_ID }).catch((error) => ({ error: error.message }));
|
||||
throw new Error(`pick did not open configure bar for ${selector}: ${JSON.stringify(snapshot)}`);
|
||||
await page.waitForSelector(BAR_ID, { state: 'visible', timeout: 1 });
|
||||
}
|
||||
}
|
||||
// Wait specifically for the Configure-row submit button to be in the bar.
|
||||
@@ -544,36 +528,6 @@ export async function setCount(page, count) {
|
||||
throw new Error(`could not cycle count to ${count}`);
|
||||
}
|
||||
|
||||
/** Select a named Impeccable sub-command from the configure-row picker. */
|
||||
export async function selectAction(page, action) {
|
||||
const pickerSelector = '#impeccable-live-picker';
|
||||
const opened = await page.evaluate(({ barSel, pickerSel }) => {
|
||||
const query = window.__impeccableLiveQuery || ((selector) => document.querySelector(selector));
|
||||
const bar = query(barSel);
|
||||
const picker = query(pickerSel);
|
||||
const actionControl = [...(bar?.querySelectorAll('button') || [])]
|
||||
.find((button) => (button.textContent || '').includes('\u25BE'));
|
||||
if (!actionControl || !picker) return false;
|
||||
actionControl.click();
|
||||
return true;
|
||||
}, { barSel: BAR_ID, pickerSel: pickerSelector });
|
||||
if (!opened) throw new Error('could not open Live action picker');
|
||||
|
||||
await page.waitForFunction((selector) => {
|
||||
const picker = window.__impeccableLiveQuery(selector);
|
||||
return picker && picker.style.display !== 'none';
|
||||
}, pickerSelector, { timeout: 5_000 });
|
||||
|
||||
const selected = await page.evaluate(({ pickerSel, value }) => {
|
||||
const picker = window.__impeccableLiveQuery(pickerSel);
|
||||
const chip = picker?.querySelector(`button[data-action="${CSS.escape(value)}"]`);
|
||||
if (!chip) return false;
|
||||
chip.click();
|
||||
return true;
|
||||
}, { pickerSel: pickerSelector, value: action });
|
||||
if (!selected) throw new Error(`Live action ${JSON.stringify(action)} is unavailable`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click Go. Browser POSTs the generate event; the agent picks it up. Headed
|
||||
* browser runs can occasionally accept the click without leaving configure
|
||||
@@ -624,14 +578,7 @@ export async function waitForCycling(page, expectedCount, { timeout = 30_000 } =
|
||||
// Counter format: "1/3", "2/3" etc. Look for any "i/N" with N matching.
|
||||
const m = text.match(/(\d+)\s*\/\s*(\d+)/);
|
||||
if (!m) return false;
|
||||
const wrapper = window.__impeccableLiveQuery('[data-impeccable-variants]');
|
||||
const debugState = window.__IMPECCABLE_LIVE_CHROME_CORE__?.debugState?.();
|
||||
const arrived = /^(?:svelte|vue)-component$/.test(wrapper?.dataset.impeccablePreview || '')
|
||||
? Number(debugState?.arrivedVariants || 0)
|
||||
: wrapper
|
||||
? wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])').length
|
||||
: 0;
|
||||
return parseInt(m[2], 10) === expected && arrived >= expected;
|
||||
return parseInt(m[2], 10) === expected;
|
||||
},
|
||||
{ barSel: BAR_ID, expected: expectedCount },
|
||||
{ timeout },
|
||||
@@ -643,7 +590,7 @@ export async function waitForCycling(page, expectedCount, { timeout = 30_000 } =
|
||||
const root = window.__IMPECCABLE_LIVE_CHROME_CORE__?.root?.() || window.__IMPECCABLE_LIVE_UI_ROOT__ || null;
|
||||
const bar = query(barSel);
|
||||
const toast = query('#impeccable-live-toast');
|
||||
const wrapper = query('[data-impeccable-variants]');
|
||||
const wrapper = document.querySelector('[data-impeccable-variants]');
|
||||
return {
|
||||
liveInit: window.__IMPECCABLE_LIVE_INIT__,
|
||||
adapter: window.__IMPECCABLE_LIVE_ADAPTER__,
|
||||
@@ -804,8 +751,7 @@ async function ensureVisibleVariant(page, expectedVariant) {
|
||||
*/
|
||||
export async function clickDiscard(page) {
|
||||
// The discard button has just a "✕" glyph as text content.
|
||||
if (await dispatchBarButton(page, '✕')) return;
|
||||
await clickBarButton(page, '✕');
|
||||
await page.locator(`${BAR_ID} button`, { hasText: '✕' }).click();
|
||||
}
|
||||
|
||||
export async function clickEditCopy(page) {
|
||||
|
||||
Reference in New Issue
Block a user