mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 08:36:25 +03:00
fix(live): land valid TSX through wrap → preview → accept → carbonize
Closes #114. Three orthogonal bugs that surfaced together when live mode picked an element inside a Vite React/TSX component with sibling branches: 1. JSX wrapper insertion produced invalid TSX - Replacing a single picked JSX child with [comment, <div>, comment] yields three adjacent siblings, which oxc rejects with "Adjacent JSX elements must be wrapped in an enclosing tag." - A Fragment `<></>` solves the adjacency case but breaks `cloneElement`-using parents (Radix `asChild`, Headless UI, etc.) with "Invalid prop supplied to React.Fragment." - Fix: keep the wrapper `<div data-impeccable-variants="ID">` as the single JSX-slot child and tuck both marker comments INSIDE it. accept/discard now expands its replacement range to include the wrapper's `<div>` open/close lines via div-depth tracking. 2. carbonize produced nested template literals in TSX `<style>` - extractCss captured `{` / `` `} `` lines from the agent's existing `<style>{`…`}</style>` template, then handleAccept re-wrapped with another pair, producing `<style>{`{`@scope…`}`}</style>` which oxc rejects with "Expected `}` but found `@`". - Fix: extractCss now strips a leading `{` and trailing `` `} `` wherever they appear in the captured content (own line OR attached to the first/last CSS line), so re-wrapping always yields exactly one `{` ` … ` `}` pair. 3. Ambiguous source matching for repeated JSX branches - `findElement` returned the first substring match. Multiple `<aside className="card">` siblings all matched the same query, so wrap silently landed on the first regardless of which one the user picked. - Fix: live-wrap accepts `--text TEXT` (the picked element's textContent), collects ALL candidates via `findAllElements`, and narrows by a tag-stripped, JSX-expression-stripped substring match. Returns `element_ambiguous + candidates[]` when multiple branches match equally; falls back to first-match when source uses dynamic content (`<h1>{title}</h1>`) so existing flows aren't broken. - The fake e2e agent now forwards `event.element.textContent` to wrap, and live.md tells the agent to do the same. Test coverage: - New `vite8-react-tsx-repeated-aside` e2e fixture: three identical `<aside>` branches, picks the second card's <h1>, runs the full wrap → Go → cycle → accept → carbonize cycle on a real Vite + TSX dev server, asserts that Hero One and Hero Three survive untouched (proving wrap landed on the correct branch). - Six new unit tests across live-wrap.test.mjs and live-accept.test.mjs covering the Fragment-replacement design, both leading/trailing template-literal placements, --text disambiguation, the dynamic- content fallback, and the element_ambiguous error shape. - New `runtime.assertSourceContains` fixture hook so other regression fixtures can assert sibling-branch survivability cheaply. All 186 unit + static-fixture tests pass; all 21 live e2e fixtures (20 prior + new TSX) pass with no console errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
638af20566
commit
54d9f05ea5
@@ -323,12 +323,17 @@ export async function runAgentLoop({
|
||||
// sessions: the agent must derive the selector from the picked
|
||||
// element on the fly).
|
||||
const target = typeof wrapTarget === 'function' ? wrapTarget(event) : wrapTarget;
|
||||
// Pull textContent from the picker event so wrap can disambiguate
|
||||
// when sibling elements share classes/tag (issue #114). Fixtures can
|
||||
// still override by including `text` in their wrapTarget.
|
||||
const text = target.text ?? (event.element?.textContent || '').trim();
|
||||
const wrapInfo = await runWrap({
|
||||
tmp,
|
||||
scriptsDir,
|
||||
id: event.id,
|
||||
count: event.count,
|
||||
...target,
|
||||
text,
|
||||
});
|
||||
log(`wrapped: ${wrapInfo.file} insertLine=${wrapInfo.insertLine}`);
|
||||
|
||||
@@ -426,11 +431,12 @@ export async function runAgentLoop({
|
||||
}
|
||||
}
|
||||
|
||||
async function runWrap({ tmp, scriptsDir, id, count, classes, tag, elementId }) {
|
||||
async function runWrap({ tmp, scriptsDir, id, count, classes, tag, elementId, text }) {
|
||||
const args = [path.join(scriptsDir, 'live-wrap.mjs'), '--id', id, '--count', String(count)];
|
||||
if (elementId) args.push('--element-id', elementId);
|
||||
if (classes) args.push('--classes', classes);
|
||||
if (tag) args.push('--tag', tag);
|
||||
if (text) args.push('--text', text);
|
||||
const { stdout } = await execFileP(process.execPath, args, { cwd: tmp });
|
||||
const last = stdout.trim().split('\n').filter(Boolean).pop();
|
||||
return JSON.parse(last);
|
||||
|
||||
Reference in New Issue
Block a user