diff --git a/.agents/skills/impeccable/scripts/live-wrap.mjs b/.agents/skills/impeccable/scripts/live-wrap.mjs
index 62acf4bf1..2baca58ea 100644
--- a/.agents/skills/impeccable/scripts/live-wrap.mjs
+++ b/.agents/skills/impeccable/scripts/live-wrap.mjs
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
- // Extract the original element
+ // Extract the original element. Reindent under the wrapper while preserving
+ // the relative depth between lines — `l.trimStart()` would strip ALL leading
+ // whitespace and collapse e.g. `` (6/8/6 spaces)
+ // to a single uniform indent, so on accept/discard the round-trip restores
+ // the inner element at its parent's depth instead of nested inside it.
+ // Strip only the COMMON minimum leading whitespace across the picked lines;
+ // `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
- const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
+ const originalBaseIndent = minLeadingSpaces(originalLines);
+ const reindentOriginal = (extra) => originalLines
+ .map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
+ .join('\n');
+ const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + '
',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
+/**
+ * Return the smallest leading-whitespace count across a set of lines,
+ * ignoring blank lines (whose indent isn't load-bearing). Used to compute
+ * the common base indent of a multi-line picked element so reindenting
+ * under the wrapper preserves the relative depth between lines.
+ */
+function minLeadingSpaces(lines) {
+ let min = Infinity;
+ for (const l of lines) {
+ if (l.trim() === '') continue;
+ const m = l.match(/^(\s*)/);
+ if (m && m[1].length < min) min = m[1].length;
+ }
+ return min === Infinity ? 0 : min;
+}
+
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
diff --git a/.claude/skills/impeccable/scripts/live-wrap.mjs b/.claude/skills/impeccable/scripts/live-wrap.mjs
index 62acf4bf1..2baca58ea 100644
--- a/.claude/skills/impeccable/scripts/live-wrap.mjs
+++ b/.claude/skills/impeccable/scripts/live-wrap.mjs
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
- // Extract the original element
+ // Extract the original element. Reindent under the wrapper while preserving
+ // the relative depth between lines — `l.trimStart()` would strip ALL leading
+ // whitespace and collapse e.g. `` (6/8/6 spaces)
+ // to a single uniform indent, so on accept/discard the round-trip restores
+ // the inner element at its parent's depth instead of nested inside it.
+ // Strip only the COMMON minimum leading whitespace across the picked lines;
+ // `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
- const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
+ const originalBaseIndent = minLeadingSpaces(originalLines);
+ const reindentOriginal = (extra) => originalLines
+ .map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
+ .join('\n');
+ const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + '
',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
+/**
+ * Return the smallest leading-whitespace count across a set of lines,
+ * ignoring blank lines (whose indent isn't load-bearing). Used to compute
+ * the common base indent of a multi-line picked element so reindenting
+ * under the wrapper preserves the relative depth between lines.
+ */
+function minLeadingSpaces(lines) {
+ let min = Infinity;
+ for (const l of lines) {
+ if (l.trim() === '') continue;
+ const m = l.match(/^(\s*)/);
+ if (m && m[1].length < min) min = m[1].length;
+ }
+ return min === Infinity ? 0 : min;
+}
+
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
diff --git a/.cursor/skills/impeccable/scripts/live-wrap.mjs b/.cursor/skills/impeccable/scripts/live-wrap.mjs
index 62acf4bf1..2baca58ea 100644
--- a/.cursor/skills/impeccable/scripts/live-wrap.mjs
+++ b/.cursor/skills/impeccable/scripts/live-wrap.mjs
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
- // Extract the original element
+ // Extract the original element. Reindent under the wrapper while preserving
+ // the relative depth between lines — `l.trimStart()` would strip ALL leading
+ // whitespace and collapse e.g. `` (6/8/6 spaces)
+ // to a single uniform indent, so on accept/discard the round-trip restores
+ // the inner element at its parent's depth instead of nested inside it.
+ // Strip only the COMMON minimum leading whitespace across the picked lines;
+ // `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
- const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
+ const originalBaseIndent = minLeadingSpaces(originalLines);
+ const reindentOriginal = (extra) => originalLines
+ .map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
+ .join('\n');
+ const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + '
',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
+/**
+ * Return the smallest leading-whitespace count across a set of lines,
+ * ignoring blank lines (whose indent isn't load-bearing). Used to compute
+ * the common base indent of a multi-line picked element so reindenting
+ * under the wrapper preserves the relative depth between lines.
+ */
+function minLeadingSpaces(lines) {
+ let min = Infinity;
+ for (const l of lines) {
+ if (l.trim() === '') continue;
+ const m = l.match(/^(\s*)/);
+ if (m && m[1].length < min) min = m[1].length;
+ }
+ return min === Infinity ? 0 : min;
+}
+
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
diff --git a/.gemini/skills/impeccable/scripts/live-wrap.mjs b/.gemini/skills/impeccable/scripts/live-wrap.mjs
index 62acf4bf1..2baca58ea 100644
--- a/.gemini/skills/impeccable/scripts/live-wrap.mjs
+++ b/.gemini/skills/impeccable/scripts/live-wrap.mjs
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
- // Extract the original element
+ // Extract the original element. Reindent under the wrapper while preserving
+ // the relative depth between lines — `l.trimStart()` would strip ALL leading
+ // whitespace and collapse e.g. `` (6/8/6 spaces)
+ // to a single uniform indent, so on accept/discard the round-trip restores
+ // the inner element at its parent's depth instead of nested inside it.
+ // Strip only the COMMON minimum leading whitespace across the picked lines;
+ // `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
- const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
+ const originalBaseIndent = minLeadingSpaces(originalLines);
+ const reindentOriginal = (extra) => originalLines
+ .map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
+ .join('\n');
+ const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + '
',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
+/**
+ * Return the smallest leading-whitespace count across a set of lines,
+ * ignoring blank lines (whose indent isn't load-bearing). Used to compute
+ * the common base indent of a multi-line picked element so reindenting
+ * under the wrapper preserves the relative depth between lines.
+ */
+function minLeadingSpaces(lines) {
+ let min = Infinity;
+ for (const l of lines) {
+ if (l.trim() === '') continue;
+ const m = l.match(/^(\s*)/);
+ if (m && m[1].length < min) min = m[1].length;
+ }
+ return min === Infinity ? 0 : min;
+}
+
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
diff --git a/.github/skills/impeccable/scripts/live-wrap.mjs b/.github/skills/impeccable/scripts/live-wrap.mjs
index 62acf4bf1..2baca58ea 100644
--- a/.github/skills/impeccable/scripts/live-wrap.mjs
+++ b/.github/skills/impeccable/scripts/live-wrap.mjs
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
- // Extract the original element
+ // Extract the original element. Reindent under the wrapper while preserving
+ // the relative depth between lines — `l.trimStart()` would strip ALL leading
+ // whitespace and collapse e.g. `` (6/8/6 spaces)
+ // to a single uniform indent, so on accept/discard the round-trip restores
+ // the inner element at its parent's depth instead of nested inside it.
+ // Strip only the COMMON minimum leading whitespace across the picked lines;
+ // `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
- const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
+ const originalBaseIndent = minLeadingSpaces(originalLines);
+ const reindentOriginal = (extra) => originalLines
+ .map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
+ .join('\n');
+ const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + '
',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
+/**
+ * Return the smallest leading-whitespace count across a set of lines,
+ * ignoring blank lines (whose indent isn't load-bearing). Used to compute
+ * the common base indent of a multi-line picked element so reindenting
+ * under the wrapper preserves the relative depth between lines.
+ */
+function minLeadingSpaces(lines) {
+ let min = Infinity;
+ for (const l of lines) {
+ if (l.trim() === '') continue;
+ const m = l.match(/^(\s*)/);
+ if (m && m[1].length < min) min = m[1].length;
+ }
+ return min === Infinity ? 0 : min;
+}
+
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
diff --git a/.kiro/skills/impeccable/scripts/live-wrap.mjs b/.kiro/skills/impeccable/scripts/live-wrap.mjs
index 62acf4bf1..2baca58ea 100644
--- a/.kiro/skills/impeccable/scripts/live-wrap.mjs
+++ b/.kiro/skills/impeccable/scripts/live-wrap.mjs
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
- // Extract the original element
+ // Extract the original element. Reindent under the wrapper while preserving
+ // the relative depth between lines — `l.trimStart()` would strip ALL leading
+ // whitespace and collapse e.g. `` (6/8/6 spaces)
+ // to a single uniform indent, so on accept/discard the round-trip restores
+ // the inner element at its parent's depth instead of nested inside it.
+ // Strip only the COMMON minimum leading whitespace across the picked lines;
+ // `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
- const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
+ const originalBaseIndent = minLeadingSpaces(originalLines);
+ const reindentOriginal = (extra) => originalLines
+ .map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
+ .join('\n');
+ const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + '
',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
+/**
+ * Return the smallest leading-whitespace count across a set of lines,
+ * ignoring blank lines (whose indent isn't load-bearing). Used to compute
+ * the common base indent of a multi-line picked element so reindenting
+ * under the wrapper preserves the relative depth between lines.
+ */
+function minLeadingSpaces(lines) {
+ let min = Infinity;
+ for (const l of lines) {
+ if (l.trim() === '') continue;
+ const m = l.match(/^(\s*)/);
+ if (m && m[1].length < min) min = m[1].length;
+ }
+ return min === Infinity ? 0 : min;
+}
+
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
diff --git a/.opencode/skills/impeccable/scripts/live-wrap.mjs b/.opencode/skills/impeccable/scripts/live-wrap.mjs
index 62acf4bf1..2baca58ea 100644
--- a/.opencode/skills/impeccable/scripts/live-wrap.mjs
+++ b/.opencode/skills/impeccable/scripts/live-wrap.mjs
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
- // Extract the original element
+ // Extract the original element. Reindent under the wrapper while preserving
+ // the relative depth between lines — `l.trimStart()` would strip ALL leading
+ // whitespace and collapse e.g. `` (6/8/6 spaces)
+ // to a single uniform indent, so on accept/discard the round-trip restores
+ // the inner element at its parent's depth instead of nested inside it.
+ // Strip only the COMMON minimum leading whitespace across the picked lines;
+ // `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
- const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
+ const originalBaseIndent = minLeadingSpaces(originalLines);
+ const reindentOriginal = (extra) => originalLines
+ .map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
+ .join('\n');
+ const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + '
',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
+/**
+ * Return the smallest leading-whitespace count across a set of lines,
+ * ignoring blank lines (whose indent isn't load-bearing). Used to compute
+ * the common base indent of a multi-line picked element so reindenting
+ * under the wrapper preserves the relative depth between lines.
+ */
+function minLeadingSpaces(lines) {
+ let min = Infinity;
+ for (const l of lines) {
+ if (l.trim() === '') continue;
+ const m = l.match(/^(\s*)/);
+ if (m && m[1].length < min) min = m[1].length;
+ }
+ return min === Infinity ? 0 : min;
+}
+
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
diff --git a/.pi/skills/impeccable/scripts/live-wrap.mjs b/.pi/skills/impeccable/scripts/live-wrap.mjs
index 62acf4bf1..2baca58ea 100644
--- a/.pi/skills/impeccable/scripts/live-wrap.mjs
+++ b/.pi/skills/impeccable/scripts/live-wrap.mjs
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
- // Extract the original element
+ // Extract the original element. Reindent under the wrapper while preserving
+ // the relative depth between lines — `l.trimStart()` would strip ALL leading
+ // whitespace and collapse e.g. `` (6/8/6 spaces)
+ // to a single uniform indent, so on accept/discard the round-trip restores
+ // the inner element at its parent's depth instead of nested inside it.
+ // Strip only the COMMON minimum leading whitespace across the picked lines;
+ // `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
- const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
+ const originalBaseIndent = minLeadingSpaces(originalLines);
+ const reindentOriginal = (extra) => originalLines
+ .map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
+ .join('\n');
+ const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + '
',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
+/**
+ * Return the smallest leading-whitespace count across a set of lines,
+ * ignoring blank lines (whose indent isn't load-bearing). Used to compute
+ * the common base indent of a multi-line picked element so reindenting
+ * under the wrapper preserves the relative depth between lines.
+ */
+function minLeadingSpaces(lines) {
+ let min = Infinity;
+ for (const l of lines) {
+ if (l.trim() === '') continue;
+ const m = l.match(/^(\s*)/);
+ if (m && m[1].length < min) min = m[1].length;
+ }
+ return min === Infinity ? 0 : min;
+}
+
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
diff --git a/.rovodev/skills/impeccable/scripts/live-wrap.mjs b/.rovodev/skills/impeccable/scripts/live-wrap.mjs
index 62acf4bf1..2baca58ea 100644
--- a/.rovodev/skills/impeccable/scripts/live-wrap.mjs
+++ b/.rovodev/skills/impeccable/scripts/live-wrap.mjs
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
- // Extract the original element
+ // Extract the original element. Reindent under the wrapper while preserving
+ // the relative depth between lines — `l.trimStart()` would strip ALL leading
+ // whitespace and collapse e.g. `` (6/8/6 spaces)
+ // to a single uniform indent, so on accept/discard the round-trip restores
+ // the inner element at its parent's depth instead of nested inside it.
+ // Strip only the COMMON minimum leading whitespace across the picked lines;
+ // `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
- const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
+ const originalBaseIndent = minLeadingSpaces(originalLines);
+ const reindentOriginal = (extra) => originalLines
+ .map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
+ .join('\n');
+ const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + '
',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
+/**
+ * Return the smallest leading-whitespace count across a set of lines,
+ * ignoring blank lines (whose indent isn't load-bearing). Used to compute
+ * the common base indent of a multi-line picked element so reindenting
+ * under the wrapper preserves the relative depth between lines.
+ */
+function minLeadingSpaces(lines) {
+ let min = Infinity;
+ for (const l of lines) {
+ if (l.trim() === '') continue;
+ const m = l.match(/^(\s*)/);
+ if (m && m[1].length < min) min = m[1].length;
+ }
+ return min === Infinity ? 0 : min;
+}
+
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
diff --git a/.trae-cn/skills/impeccable/scripts/live-wrap.mjs b/.trae-cn/skills/impeccable/scripts/live-wrap.mjs
index 62acf4bf1..2baca58ea 100644
--- a/.trae-cn/skills/impeccable/scripts/live-wrap.mjs
+++ b/.trae-cn/skills/impeccable/scripts/live-wrap.mjs
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
- // Extract the original element
+ // Extract the original element. Reindent under the wrapper while preserving
+ // the relative depth between lines — `l.trimStart()` would strip ALL leading
+ // whitespace and collapse e.g. `` (6/8/6 spaces)
+ // to a single uniform indent, so on accept/discard the round-trip restores
+ // the inner element at its parent's depth instead of nested inside it.
+ // Strip only the COMMON minimum leading whitespace across the picked lines;
+ // `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
- const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
+ const originalBaseIndent = minLeadingSpaces(originalLines);
+ const reindentOriginal = (extra) => originalLines
+ .map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
+ .join('\n');
+ const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + '
',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
+/**
+ * Return the smallest leading-whitespace count across a set of lines,
+ * ignoring blank lines (whose indent isn't load-bearing). Used to compute
+ * the common base indent of a multi-line picked element so reindenting
+ * under the wrapper preserves the relative depth between lines.
+ */
+function minLeadingSpaces(lines) {
+ let min = Infinity;
+ for (const l of lines) {
+ if (l.trim() === '') continue;
+ const m = l.match(/^(\s*)/);
+ if (m && m[1].length < min) min = m[1].length;
+ }
+ return min === Infinity ? 0 : min;
+}
+
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
diff --git a/.trae/skills/impeccable/scripts/live-wrap.mjs b/.trae/skills/impeccable/scripts/live-wrap.mjs
index 62acf4bf1..2baca58ea 100644
--- a/.trae/skills/impeccable/scripts/live-wrap.mjs
+++ b/.trae/skills/impeccable/scripts/live-wrap.mjs
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
- // Extract the original element
+ // Extract the original element. Reindent under the wrapper while preserving
+ // the relative depth between lines — `l.trimStart()` would strip ALL leading
+ // whitespace and collapse e.g. `` (6/8/6 spaces)
+ // to a single uniform indent, so on accept/discard the round-trip restores
+ // the inner element at its parent's depth instead of nested inside it.
+ // Strip only the COMMON minimum leading whitespace across the picked lines;
+ // `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
- const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
+ const originalBaseIndent = minLeadingSpaces(originalLines);
+ const reindentOriginal = (extra) => originalLines
+ .map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
+ .join('\n');
+ const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + '
',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
+/**
+ * Return the smallest leading-whitespace count across a set of lines,
+ * ignoring blank lines (whose indent isn't load-bearing). Used to compute
+ * the common base indent of a multi-line picked element so reindenting
+ * under the wrapper preserves the relative depth between lines.
+ */
+function minLeadingSpaces(lines) {
+ let min = Infinity;
+ for (const l of lines) {
+ if (l.trim() === '') continue;
+ const m = l.match(/^(\s*)/);
+ if (m && m[1].length < min) min = m[1].length;
+ }
+ return min === Infinity ? 0 : min;
+}
+
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
diff --git a/plugin/skills/impeccable/scripts/live-wrap.mjs b/plugin/skills/impeccable/scripts/live-wrap.mjs
index 62acf4bf1..2baca58ea 100644
--- a/plugin/skills/impeccable/scripts/live-wrap.mjs
+++ b/plugin/skills/impeccable/scripts/live-wrap.mjs
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
- // Extract the original element
+ // Extract the original element. Reindent under the wrapper while preserving
+ // the relative depth between lines — `l.trimStart()` would strip ALL leading
+ // whitespace and collapse e.g. `` (6/8/6 spaces)
+ // to a single uniform indent, so on accept/discard the round-trip restores
+ // the inner element at its parent's depth instead of nested inside it.
+ // Strip only the COMMON minimum leading whitespace across the picked lines;
+ // `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
- const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
+ const originalBaseIndent = minLeadingSpaces(originalLines);
+ const reindentOriginal = (extra) => originalLines
+ .map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
+ .join('\n');
+ const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + '
',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
+/**
+ * Return the smallest leading-whitespace count across a set of lines,
+ * ignoring blank lines (whose indent isn't load-bearing). Used to compute
+ * the common base indent of a multi-line picked element so reindenting
+ * under the wrapper preserves the relative depth between lines.
+ */
+function minLeadingSpaces(lines) {
+ let min = Infinity;
+ for (const l of lines) {
+ if (l.trim() === '') continue;
+ const m = l.match(/^(\s*)/);
+ if (m && m[1].length < min) min = m[1].length;
+ }
+ return min === Infinity ? 0 : min;
+}
+
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
diff --git a/source/skills/impeccable/scripts/live-wrap.mjs b/source/skills/impeccable/scripts/live-wrap.mjs
index 62acf4bf1..2baca58ea 100644
--- a/source/skills/impeccable/scripts/live-wrap.mjs
+++ b/source/skills/impeccable/scripts/live-wrap.mjs
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
- // Extract the original element
+ // Extract the original element. Reindent under the wrapper while preserving
+ // the relative depth between lines — `l.trimStart()` would strip ALL leading
+ // whitespace and collapse e.g. `` (6/8/6 spaces)
+ // to a single uniform indent, so on accept/discard the round-trip restores
+ // the inner element at its parent's depth instead of nested inside it.
+ // Strip only the COMMON minimum leading whitespace across the picked lines;
+ // `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
- const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
+ const originalBaseIndent = minLeadingSpaces(originalLines);
+ const reindentOriginal = (extra) => originalLines
+ .map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
+ .join('\n');
+ const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + '
',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
+/**
+ * Return the smallest leading-whitespace count across a set of lines,
+ * ignoring blank lines (whose indent isn't load-bearing). Used to compute
+ * the common base indent of a multi-line picked element so reindenting
+ * under the wrapper preserves the relative depth between lines.
+ */
+function minLeadingSpaces(lines) {
+ let min = Infinity;
+ for (const l of lines) {
+ if (l.trim() === '') continue;
+ const m = l.match(/^(\s*)/);
+ if (m && m[1].length < min) min = m[1].length;
+ }
+ return min === Infinity ? 0 : min;
+}
+
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
diff --git a/tests/live-accept.test.mjs b/tests/live-accept.test.mjs
index bf94b9831..59dabc670 100644
--- a/tests/live-accept.test.mjs
+++ b/tests/live-accept.test.mjs
@@ -235,11 +235,17 @@ describe('live-accept — style-element edge cases', () => {
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.)
+ // original — and the
child at 8 (preserved relative depth).
+ // The earlier 6/6/6 collapse was caused by `originalLines.map(l =>
+ // indent + ' ' + l.trimStart())` in live-wrap stripping ALL
+ // leading whitespace before reindenting; the fix strips only the
+ // COMMON minimum so the relative structure is preserved.
assert.match(after, /^