feat: show visual diffs for Edit/Write tool returns (#392)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -5,14 +5,17 @@ import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||
import { colors } from "./colors";
|
||||
|
||||
// Helper to format path as relative with ../
|
||||
function formatRelativePath(filePath: string): string {
|
||||
/**
|
||||
* Formats a file path for display (matches Claude Code style):
|
||||
* - Files within cwd: relative path without ./ prefix
|
||||
* - Files outside cwd: full absolute path
|
||||
*/
|
||||
function formatDisplayPath(filePath: string): string {
|
||||
const cwd = process.cwd();
|
||||
const relativePath = relative(cwd, filePath);
|
||||
|
||||
// If file is outside cwd, it will start with ..
|
||||
// If file is in cwd, add ./ prefix
|
||||
if (!relativePath.startsWith("..")) {
|
||||
return `./${relativePath}`;
|
||||
// If path goes outside cwd (starts with ..), show full absolute path
|
||||
if (relativePath.startsWith("..")) {
|
||||
return filePath;
|
||||
}
|
||||
return relativePath;
|
||||
}
|
||||
@@ -30,6 +33,7 @@ interface DiffLineProps {
|
||||
content: string;
|
||||
compareContent?: string; // The other version to compare against for word diff
|
||||
columns: number;
|
||||
showLineNumbers?: boolean; // Whether to show line numbers (default true)
|
||||
}
|
||||
|
||||
function DiffLine({
|
||||
@@ -38,6 +42,7 @@ function DiffLine({
|
||||
content,
|
||||
compareContent,
|
||||
columns,
|
||||
showLineNumbers = true,
|
||||
}: DiffLineProps) {
|
||||
const prefix = type === "add" ? "+" : "-";
|
||||
const lineBg =
|
||||
@@ -45,8 +50,13 @@ function DiffLine({
|
||||
const wordBg =
|
||||
type === "add" ? colors.diff.addedWordBg : colors.diff.removedWordBg;
|
||||
|
||||
const prefixWidth = 1; // Single space prefix
|
||||
const contentWidth = Math.max(0, columns - prefixWidth);
|
||||
const gutterWidth = 4; // " " indent to align with tool return prefix
|
||||
const contentWidth = Math.max(0, columns - gutterWidth);
|
||||
|
||||
// Build the line prefix (with or without line number)
|
||||
const linePrefix = showLineNumbers
|
||||
? `${lineNumber} ${prefix} `
|
||||
: `${prefix} `;
|
||||
|
||||
// If we have something to compare against, do word-level diff
|
||||
if (compareContent !== undefined && content.trim() && compareContent.trim()) {
|
||||
@@ -57,13 +67,13 @@ function DiffLine({
|
||||
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth} flexShrink={0}>
|
||||
<Text> </Text>
|
||||
<Box width={gutterWidth} flexShrink={0}>
|
||||
<Text>{" "}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} width={contentWidth}>
|
||||
<Text wrap="wrap">
|
||||
<Text backgroundColor={lineBg} color={colors.diff.textOnDark}>
|
||||
{`${lineNumber} ${prefix} `}
|
||||
{linePrefix}
|
||||
</Text>
|
||||
{wordDiffs.map((part, i) => {
|
||||
if (part.added && type === "add") {
|
||||
@@ -112,8 +122,8 @@ function DiffLine({
|
||||
// No comparison, just show the whole line with one background
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth} flexShrink={0}>
|
||||
<Text> </Text>
|
||||
<Box width={gutterWidth} flexShrink={0}>
|
||||
<Text>{" "}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} width={contentWidth}>
|
||||
<Text
|
||||
@@ -121,7 +131,7 @@ function DiffLine({
|
||||
color={colors.diff.textOnDark}
|
||||
wrap="wrap"
|
||||
>
|
||||
{`${lineNumber} ${prefix} ${content}`}
|
||||
{`${linePrefix}${content}`}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -135,23 +145,33 @@ interface WriteRendererProps {
|
||||
|
||||
export function WriteRenderer({ filePath, content }: WriteRendererProps) {
|
||||
const columns = useTerminalWidth();
|
||||
const relativePath = formatRelativePath(filePath);
|
||||
const relativePath = formatDisplayPath(filePath);
|
||||
const lines = content.split("\n");
|
||||
const lineCount = lines.length;
|
||||
|
||||
const prefixWidth = 1; // Single space prefix
|
||||
const contentWidth = Math.max(0, columns - prefixWidth);
|
||||
const gutterWidth = 4; // " " indent to align with tool return prefix
|
||||
const contentWidth = Math.max(0, columns - gutterWidth);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
{" "}
|
||||
⎿ Wrote {lineCount} line{lineCount !== 1 ? "s" : ""} to {relativePath}
|
||||
</Text>
|
||||
<Box flexDirection="row">
|
||||
<Box width={gutterWidth} flexShrink={0}>
|
||||
<Text>
|
||||
{" "}
|
||||
<Text dimColor>⎿</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} width={contentWidth}>
|
||||
<Text wrap="wrap">
|
||||
Wrote <Text bold>{lineCount}</Text> line
|
||||
{lineCount !== 1 ? "s" : ""} to <Text bold>{relativePath}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{lines.map((line, i) => (
|
||||
<Box key={`line-${i}-${line.substring(0, 20)}`} flexDirection="row">
|
||||
<Box width={prefixWidth} flexShrink={0}>
|
||||
<Text> </Text>
|
||||
<Box width={gutterWidth} flexShrink={0}>
|
||||
<Text>{" "}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} width={contentWidth}>
|
||||
<Text wrap="wrap">{line}</Text>
|
||||
@@ -166,15 +186,17 @@ interface EditRendererProps {
|
||||
filePath: string;
|
||||
oldString: string;
|
||||
newString: string;
|
||||
showLineNumbers?: boolean; // Whether to show line numbers (default true)
|
||||
}
|
||||
|
||||
export function EditRenderer({
|
||||
filePath,
|
||||
oldString,
|
||||
newString,
|
||||
showLineNumbers = true,
|
||||
}: EditRendererProps) {
|
||||
const columns = useTerminalWidth();
|
||||
const relativePath = formatRelativePath(filePath);
|
||||
const relativePath = formatDisplayPath(filePath);
|
||||
const oldLines = oldString.split("\n");
|
||||
const newLines = newString.split("\n");
|
||||
|
||||
@@ -187,14 +209,28 @@ export function EditRenderer({
|
||||
// For multi-line, we could do more sophisticated matching
|
||||
const singleLineEdit = oldLines.length === 1 && newLines.length === 1;
|
||||
|
||||
const gutterWidth = 4; // " " indent to align with tool return prefix
|
||||
const contentWidth = Math.max(0, columns - gutterWidth);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
{" "}
|
||||
⎿ Updated {relativePath} with {additions} addition
|
||||
{additions !== 1 ? "s" : ""} and {removals} removal
|
||||
{removals !== 1 ? "s" : ""}
|
||||
</Text>
|
||||
<Box flexDirection="row">
|
||||
<Box width={gutterWidth} flexShrink={0}>
|
||||
<Text>
|
||||
{" "}
|
||||
<Text dimColor>⎿</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} width={contentWidth}>
|
||||
<Text wrap="wrap">
|
||||
Updated <Text bold>{relativePath}</Text> with{" "}
|
||||
<Text bold>{additions}</Text> addition
|
||||
{additions !== 1 ? "s" : ""} and <Text bold>{removals}</Text>{" "}
|
||||
removal
|
||||
{removals !== 1 ? "s" : ""}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Show removals */}
|
||||
{oldLines.map((line, i) => (
|
||||
@@ -205,6 +241,7 @@ export function EditRenderer({
|
||||
content={line}
|
||||
compareContent={singleLineEdit ? newLines[0] : undefined}
|
||||
columns={columns}
|
||||
showLineNumbers={showLineNumbers}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -217,6 +254,7 @@ export function EditRenderer({
|
||||
content={line}
|
||||
compareContent={singleLineEdit ? oldLines[0] : undefined}
|
||||
columns={columns}
|
||||
showLineNumbers={showLineNumbers}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
@@ -229,11 +267,16 @@ interface MultiEditRendererProps {
|
||||
old_string: string;
|
||||
new_string: string;
|
||||
}>;
|
||||
showLineNumbers?: boolean; // Whether to show line numbers (default true)
|
||||
}
|
||||
|
||||
export function MultiEditRenderer({ filePath, edits }: MultiEditRendererProps) {
|
||||
export function MultiEditRenderer({
|
||||
filePath,
|
||||
edits,
|
||||
showLineNumbers = true,
|
||||
}: MultiEditRendererProps) {
|
||||
const columns = useTerminalWidth();
|
||||
const relativePath = formatRelativePath(filePath);
|
||||
const relativePath = formatDisplayPath(filePath);
|
||||
|
||||
// Count total additions and removals
|
||||
let totalAdditions = 0;
|
||||
@@ -244,14 +287,28 @@ export function MultiEditRenderer({ filePath, edits }: MultiEditRendererProps) {
|
||||
totalRemovals += countLines(edit.old_string);
|
||||
});
|
||||
|
||||
const gutterWidth = 4; // " " indent to align with tool return prefix
|
||||
const contentWidth = Math.max(0, columns - gutterWidth);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
{" "}
|
||||
⎿ Updated {relativePath} with {totalAdditions} addition
|
||||
{totalAdditions !== 1 ? "s" : ""} and {totalRemovals} removal
|
||||
{totalRemovals !== 1 ? "s" : ""}
|
||||
</Text>
|
||||
<Box flexDirection="row">
|
||||
<Box width={gutterWidth} flexShrink={0}>
|
||||
<Text>
|
||||
{" "}
|
||||
<Text dimColor>⎿</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} width={contentWidth}>
|
||||
<Text wrap="wrap">
|
||||
Updated <Text bold>{relativePath}</Text> with{" "}
|
||||
<Text bold>{totalAdditions}</Text> addition
|
||||
{totalAdditions !== 1 ? "s" : ""} and{" "}
|
||||
<Text bold>{totalRemovals}</Text> removal
|
||||
{totalRemovals !== 1 ? "s" : ""}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* For multi-edit, show each edit sequentially */}
|
||||
{edits.map((edit, index) => {
|
||||
@@ -267,25 +324,27 @@ export function MultiEditRenderer({ filePath, edits }: MultiEditRendererProps) {
|
||||
{oldLines.map((line, i) => (
|
||||
<DiffLine
|
||||
key={`old-${index}-${i}-${line.substring(0, 20)}`}
|
||||
lineNumber={i + 1} // TODO: This should be actual file line numbers
|
||||
lineNumber={i + 1}
|
||||
type="remove"
|
||||
content={line}
|
||||
compareContent={
|
||||
singleLineEdit && i === 0 ? newLines[0] : undefined
|
||||
}
|
||||
columns={columns}
|
||||
showLineNumbers={showLineNumbers}
|
||||
/>
|
||||
))}
|
||||
{newLines.map((line, i) => (
|
||||
<DiffLine
|
||||
key={`new-${index}-${i}-${line.substring(0, 20)}`}
|
||||
lineNumber={i + 1} // TODO: This should be actual file line numbers
|
||||
lineNumber={i + 1}
|
||||
type="add"
|
||||
content={line}
|
||||
compareContent={
|
||||
singleLineEdit && i === 0 ? oldLines[0] : undefined
|
||||
}
|
||||
columns={columns}
|
||||
showLineNumbers={showLineNumbers}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user