diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 341ed41b5..de08b9d74 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## [Unreleased] +### Added + +- Visualize leading whitespace (indentation) in diff output with dim glyphs—tabs display as ` → ` and spaces as `·` for improved readability ### Fixed diff --git a/packages/coding-agent/src/modes/components/diff.ts b/packages/coding-agent/src/modes/components/diff.ts index 926780626..76e099dfc 100644 --- a/packages/coding-agent/src/modes/components/diff.ts +++ b/packages/coding-agent/src/modes/components/diff.ts @@ -2,6 +2,38 @@ import * as Diff from "diff"; import { theme } from "../../modes/theme/theme"; import { replaceTabs } from "../../tools/render-utils"; +/** SGR dim on / normal intensity — additive, preserves fg/bg colors. */ +const DIM = "\x1b[2m"; +const DIM_OFF = "\x1b[22m"; + +/** + * Visualize leading whitespace (indentation) with dim glyphs. + * Tabs become ` → ` and spaces become `·`. Only affects whitespace + * before the first non-whitespace character; remaining tabs in code + * content are replaced with spaces (like replaceTabs). + */ +function visualizeIndent(text: string): string { + const match = text.match(/^([ \t]+)/); + if (!match) return replaceTabs(text); + const indent = match[1]; + const rest = text.slice(indent.length); + // Normalize: collapse 3-space groups (tab-width) into tab arrows, + // then handle remaining tabs and lone spaces. + const normalized = indent.replaceAll("\t", " "); + let visible = ""; + let pos = 0; + while (pos < normalized.length) { + if (pos + 3 <= normalized.length && normalized.slice(pos, pos + 3) === " ") { + visible += `${DIM} → ${DIM_OFF}`; + pos += 3; + } else { + visible += `${DIM}·${DIM_OFF}`; + pos++; + } + } + return `${visible}${replaceTabs(rest)}`; +} + /** * Parse diff line to extract prefix, line number, and content. * Format: "+123 content" or "-123 content" or " 123 content" or " ..." @@ -122,24 +154,38 @@ export function renderDiff(diffText: string, _options: RenderDiffOptions = {}): replaceTabs(added.content), ); - result.push(theme.fg("toolDiffRemoved", formatLine("-", removed.lineNum, removedLine))); - result.push(theme.fg("toolDiffAdded", formatLine("+", added.lineNum, addedLine))); + result.push(theme.fg("toolDiffRemoved", formatLine("-", removed.lineNum, visualizeIndent(removedLine)))); + result.push(theme.fg("toolDiffAdded", formatLine("+", added.lineNum, visualizeIndent(addedLine)))); } else { // Show all removed lines first, then all added lines for (const removed of removedLines) { - result.push(theme.fg("toolDiffRemoved", formatLine("-", removed.lineNum, replaceTabs(removed.content)))); + result.push( + theme.fg( + "toolDiffRemoved", + formatLine("-", removed.lineNum, visualizeIndent(removed.content)), + ), + ); } for (const added of addedLines) { - result.push(theme.fg("toolDiffAdded", formatLine("+", added.lineNum, replaceTabs(added.content)))); + result.push( + theme.fg( + "toolDiffAdded", + formatLine("+", added.lineNum, visualizeIndent(added.content)), + ), + ); } } } else if (parsed.prefix === "+") { // Standalone added line - result.push(theme.fg("toolDiffAdded", formatLine("+", parsed.lineNum, replaceTabs(parsed.content)))); + result.push( + theme.fg("toolDiffAdded", formatLine("+", parsed.lineNum, visualizeIndent(parsed.content))), + ); i++; } else { // Context line - result.push(theme.fg("toolDiffContext", formatLine(" ", parsed.lineNum, replaceTabs(parsed.content)))); + result.push( + theme.fg("toolDiffContext", formatLine(" ", parsed.lineNum, visualizeIndent(parsed.content))), + ); i++; } }