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 + '
', - originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'), + reindentOriginal(' '), 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 + '
', - originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'), + reindentOriginal(' '), 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 + '
', - originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'), + reindentOriginal(' '), 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 + '
', - originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'), + reindentOriginal(' '), 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 + '
', - originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'), + reindentOriginal(' '), 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 + '
', - originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'), + reindentOriginal(' '), 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 + '
', - originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'), + reindentOriginal(' '), 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 + '
', - originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'), + reindentOriginal(' '), 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 + '
', - originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'), + reindentOriginal(' '), 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 + '
', - originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'), + reindentOriginal(' '), 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 + '
', - originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'), + reindentOriginal(' '), 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 + '
', - originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'), + reindentOriginal(' '), 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 + '
', - originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'), + reindentOriginal(' '), 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, /^