diff --git a/.agents/skills/impeccable/scripts/live-accept.mjs b/.agents/skills/impeccable/scripts/live-accept.mjs index 6fcf40940..b1e0b035c 100644 --- a/.agents/skills/impeccable/scripts/live-accept.mjs +++ b/.agents/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/.claude/skills/impeccable/scripts/live-accept.mjs b/.claude/skills/impeccable/scripts/live-accept.mjs index 6fcf40940..b1e0b035c 100644 --- a/.claude/skills/impeccable/scripts/live-accept.mjs +++ b/.claude/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/.cursor/skills/impeccable/scripts/live-accept.mjs b/.cursor/skills/impeccable/scripts/live-accept.mjs index 6fcf40940..b1e0b035c 100644 --- a/.cursor/skills/impeccable/scripts/live-accept.mjs +++ b/.cursor/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/.gemini/skills/impeccable/scripts/live-accept.mjs b/.gemini/skills/impeccable/scripts/live-accept.mjs index 6fcf40940..b1e0b035c 100644 --- a/.gemini/skills/impeccable/scripts/live-accept.mjs +++ b/.gemini/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/.github/skills/impeccable/scripts/live-accept.mjs b/.github/skills/impeccable/scripts/live-accept.mjs index 6fcf40940..b1e0b035c 100644 --- a/.github/skills/impeccable/scripts/live-accept.mjs +++ b/.github/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/.kiro/skills/impeccable/scripts/live-accept.mjs b/.kiro/skills/impeccable/scripts/live-accept.mjs index 6fcf40940..b1e0b035c 100644 --- a/.kiro/skills/impeccable/scripts/live-accept.mjs +++ b/.kiro/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/.opencode/skills/impeccable/scripts/live-accept.mjs b/.opencode/skills/impeccable/scripts/live-accept.mjs index 6fcf40940..b1e0b035c 100644 --- a/.opencode/skills/impeccable/scripts/live-accept.mjs +++ b/.opencode/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/.pi/skills/impeccable/scripts/live-accept.mjs b/.pi/skills/impeccable/scripts/live-accept.mjs index 6fcf40940..b1e0b035c 100644 --- a/.pi/skills/impeccable/scripts/live-accept.mjs +++ b/.pi/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/.rovodev/skills/impeccable/scripts/live-accept.mjs b/.rovodev/skills/impeccable/scripts/live-accept.mjs index 6fcf40940..b1e0b035c 100644 --- a/.rovodev/skills/impeccable/scripts/live-accept.mjs +++ b/.rovodev/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/.trae-cn/skills/impeccable/scripts/live-accept.mjs b/.trae-cn/skills/impeccable/scripts/live-accept.mjs index 6fcf40940..b1e0b035c 100644 --- a/.trae-cn/skills/impeccable/scripts/live-accept.mjs +++ b/.trae-cn/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/.trae/skills/impeccable/scripts/live-accept.mjs b/.trae/skills/impeccable/scripts/live-accept.mjs index 6fcf40940..b1e0b035c 100644 --- a/.trae/skills/impeccable/scripts/live-accept.mjs +++ b/.trae/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/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, /^