`, so
+ // `block.start` sits 2 spaces deeper than the original element. Using that
+ // as the deindent base would push the restored content 2 spaces too far
+ // right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
+ // line, which is at the original element's indent for both HTML and JSX.
+ const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
const restored = deindentContent(original, indent);
const newLines = [
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
const block = findMarkerBlock(id, lines);
if (!block) return { handled: false, error: 'Markers not found' };
- const indent = lines[block.start].match(/^(\s*)/)[1];
const commentSyntax = detectCommentSyntax(targetFile);
const isJsx = commentSyntax.open === '{/*';
+ // Anchor indent on the line we're replacing FROM (the outer wrapper),
+ // not on `block.start` — for JSX that's the marker comment 2 spaces
+ // deeper than the original element. See handleDiscard for the full
+ // rationale.
+ const replaceRange = expandReplaceRange(block, lines, isJsx);
+ const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
// Extract the chosen variant's inner content
const variantContent = extractVariant(lines, block, variantNum);
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
replacement.push(...restored);
}
- const replaceRange = expandReplaceRange(block, lines, isJsx);
const newLines = [
...lines.slice(0, replaceRange.start),
...replacement,
diff --git a/plugin/skills/impeccable/scripts/live-accept.mjs b/plugin/skills/impeccable/scripts/live-accept.mjs
index 6fcf40940..b1e0b035c 100644
--- a/plugin/skills/impeccable/scripts/live-accept.mjs
+++ b/plugin/skills/impeccable/scripts/live-accept.mjs
@@ -105,11 +105,16 @@ function handleDiscard(id, lines, targetFile) {
if (!block) return { handled: false, error: 'Markers not found' };
const original = extractOriginal(lines, block);
- const indent = lines[block.start].match(/^(\s*)/)[1];
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
const replaceRange = expandReplaceRange(block, lines, isJsx);
- // De-indent the original content back to the marker's indentation level
+ // Restore at the line we're actually replacing FROM, not the marker line.
+ // For JSX wrappers the marker comments live INSIDE the outer `
`, so
+ // `block.start` sits 2 spaces deeper than the original element. Using that
+ // as the deindent base would push the restored content 2 spaces too far
+ // right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
+ // line, which is at the original element's indent for both HTML and JSX.
+ const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
const restored = deindentContent(original, indent);
const newLines = [
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
const block = findMarkerBlock(id, lines);
if (!block) return { handled: false, error: 'Markers not found' };
- const indent = lines[block.start].match(/^(\s*)/)[1];
const commentSyntax = detectCommentSyntax(targetFile);
const isJsx = commentSyntax.open === '{/*';
+ // Anchor indent on the line we're replacing FROM (the outer wrapper),
+ // not on `block.start` — for JSX that's the marker comment 2 spaces
+ // deeper than the original element. See handleDiscard for the full
+ // rationale.
+ const replaceRange = expandReplaceRange(block, lines, isJsx);
+ const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
// Extract the chosen variant's inner content
const variantContent = extractVariant(lines, block, variantNum);
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
replacement.push(...restored);
}
- const replaceRange = expandReplaceRange(block, lines, isJsx);
const newLines = [
...lines.slice(0, replaceRange.start),
...replacement,
diff --git a/source/skills/impeccable/scripts/live-accept.mjs b/source/skills/impeccable/scripts/live-accept.mjs
index 6fcf40940..b1e0b035c 100644
--- a/source/skills/impeccable/scripts/live-accept.mjs
+++ b/source/skills/impeccable/scripts/live-accept.mjs
@@ -105,11 +105,16 @@ function handleDiscard(id, lines, targetFile) {
if (!block) return { handled: false, error: 'Markers not found' };
const original = extractOriginal(lines, block);
- const indent = lines[block.start].match(/^(\s*)/)[1];
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
const replaceRange = expandReplaceRange(block, lines, isJsx);
- // De-indent the original content back to the marker's indentation level
+ // Restore at the line we're actually replacing FROM, not the marker line.
+ // For JSX wrappers the marker comments live INSIDE the outer `
`, so
+ // `block.start` sits 2 spaces deeper than the original element. Using that
+ // as the deindent base would push the restored content 2 spaces too far
+ // right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
+ // line, which is at the original element's indent for both HTML and JSX.
+ const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
const restored = deindentContent(original, indent);
const newLines = [
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
const block = findMarkerBlock(id, lines);
if (!block) return { handled: false, error: 'Markers not found' };
- const indent = lines[block.start].match(/^(\s*)/)[1];
const commentSyntax = detectCommentSyntax(targetFile);
const isJsx = commentSyntax.open === '{/*';
+ // Anchor indent on the line we're replacing FROM (the outer wrapper),
+ // not on `block.start` — for JSX that's the marker comment 2 spaces
+ // deeper than the original element. See handleDiscard for the full
+ // rationale.
+ const replaceRange = expandReplaceRange(block, lines, isJsx);
+ const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
// Extract the chosen variant's inner content
const variantContent = extractVariant(lines, block, variantNum);
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
replacement.push(...restored);
}
- const replaceRange = expandReplaceRange(block, lines, isJsx);
const newLines = [
...lines.slice(0, replaceRange.start),
...replacement,
diff --git a/tests/live-accept.test.mjs b/tests/live-accept.test.mjs
index 16baab0b2..bf94b9831 100644
--- a/tests/live-accept.test.mjs
+++ b/tests/live-accept.test.mjs
@@ -9,7 +9,7 @@ import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
-import { execFileSync } from 'node:child_process';
+import { execFileSync, execSync } from 'node:child_process';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ACCEPT = resolve(__dirname, '..', 'source/skills/impeccable/scripts/live-accept.mjs');
@@ -206,6 +206,74 @@ describe('live-accept — style-element edge cases', () => {
assert.ok(inner.includes('@scope ([data-impeccable-variant="1"])'), 'variant-1 scope kept');
});
+ // Cursor Bugbot regression (PR #118 review): the JSX wrapper places
+ // marker comments INSIDE the outer
, so block.start sits 2 spaces
+ // deeper than the original element. Using block.start as the deindent
+ // base on JSX accept/discard pushes every restored line 2 spaces too far
+ // right. The fix anchors the indent on `replaceRange.start` (the outer
+ // wrapper line), which is at the original element's indent level for
+ // both HTML and JSX.
+ it('discard restores JSX content at the original indent (no 2-space drift from marker-inside layout)', () => {
+ // Run the real wrap CLI so we exercise the JSX-marker-inside-wrapper
+ // layout end to end, not a hand-rolled approximation.
+ const tsx = `export default function App() {
+ return (
+
+
+
+ );
+}`;
+ writeFileSync(join(tmp, 'App.tsx'), tsx);
+
+ execSync(
+ `node source/skills/impeccable/scripts/live-wrap.mjs --id INDENTDISC --count 3 --classes "card" --tag "aside" --file "${join(tmp, 'App.tsx')}"`,
+ { cwd: process.cwd(), encoding: 'utf-8' }
+ );
+
+ runAccept(tmp, ['--id', 'INDENTDISC', '--discard']);
+ const after = readFileSync(join(tmp, 'App.tsx'), 'utf-8');
+ // The aside opener should land at exactly 6 spaces — same as the
+ // original. Any deeper indent is the bug Bugbot flagged. (Inner indent
+ // loss inside the element is a separate, pre-existing wrap behavior;
+ // not asserted here.)
+ assert.match(after, /^