From 8255e8acca3ee76d2e55ffa18544e3b72bd3a3c4 Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Thu, 18 Dec 2025 18:05:43 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20improve=20visibility=20of=20memory=20to?= =?UTF-8?q?ol=20with=20colored=20name=20and=20diff=20ou=E2=80=A6=20(#311)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vedant020000 Co-authored-by: Letta --- src/cli/components/MemoryDiffRenderer.tsx | 373 +++++++++++++++++++++ src/cli/components/ToolCallMessageRich.tsx | 32 +- src/cli/components/colors.ts | 1 + src/cli/helpers/toolNameMapping.ts | 7 + 4 files changed, 411 insertions(+), 2 deletions(-) create mode 100644 src/cli/components/MemoryDiffRenderer.tsx diff --git a/src/cli/components/MemoryDiffRenderer.tsx b/src/cli/components/MemoryDiffRenderer.tsx new file mode 100644 index 0000000..8d7443f --- /dev/null +++ b/src/cli/components/MemoryDiffRenderer.tsx @@ -0,0 +1,373 @@ +import * as Diff from "diff"; +import { Box, Text } from "ink"; +import { colors } from "./colors"; + +interface MemoryDiffRendererProps { + argsText: string; + toolName: string; +} + +/** + * Renders a diff view for memory tool operations. + * Handles both `memory` (command-based) and `memory_apply_patch` (unified diff) tools. + */ +export function MemoryDiffRenderer({ + argsText, + toolName, +}: MemoryDiffRendererProps) { + try { + const args = JSON.parse(argsText); + + // Handle memory_apply_patch tool (unified diff format) + if (toolName === "memory_apply_patch") { + const label = args.label || "unknown"; + const patch = args.patch || ""; + return ; + } + + // Handle memory tool (command-based) + const command = args.command as string; + const path = args.path || args.old_path || "unknown"; + + // Extract just the block name from the path (e.g., "/memories/project" -> "project") + const blockName = path.split("/").pop() || path; + + switch (command) { + case "str_replace": { + const oldStr = args.old_str || ""; + const newStr = args.new_str || ""; + return ( + + ); + } + + case "insert": { + const insertText = args.insert_text || ""; + const insertLine = args.insert_line; + return ( + + + {" "} + Inserted into memory block{" "} + {blockName} + {insertLine !== undefined && ` at line ${insertLine}`} + + {insertText.split("\n").map((line: string, i: number) => ( + + + + {`+ ${line}`} + + + ))} + + ); + } + + case "create": { + const description = args.description || ""; + const fileText = args.file_text || ""; + return ( + + + {" "} + Created memory block{" "} + {blockName} + {description && ( + - {truncate(description, 40)} + )} + + {fileText + ?.split("\n") + .slice(0, 3) + .map((line: string, i: number) => ( + + + + {`+ ${truncate(line, 60)}`} + + + ))} + {fileText && fileText.split("\n").length > 3 && ( + + {" "}... and {fileText.split("\n").length - 3} more lines + + )} + + ); + } + + case "delete": { + return ( + + {" "} + Deleted memory block{" "} + {blockName} + + ); + } + + case "rename": { + const newPath = args.new_path || ""; + const newBlockName = newPath.split("/").pop() || newPath; + const description = args.description; + if (description) { + return ( + + {" "} + Updated description of{" "} + {blockName} + + ); + } + return ( + + {" "} + Renamed{" "} + {blockName} to{" "} + {newBlockName} + + ); + } + + default: + return ( + + {" "} + Memory operation: {command} on{" "} + {blockName} + + ); + } + } catch { + // If parsing fails, return null to fall through to regular handling + return null; + } +} + +/** + * Renders a str_replace diff with word-level highlighting + */ +function MemoryStrReplaceDiff({ + blockName, + oldStr, + newStr, +}: { + blockName: string; + oldStr: string; + newStr: string; +}) { + const oldLines = oldStr.split("\n"); + const newLines = newStr.split("\n"); + const singleLine = oldLines.length === 1 && newLines.length === 1; + + // Limit display to avoid huge diffs + const maxLines = 5; + const oldTruncated = oldLines.slice(0, maxLines); + const newTruncated = newLines.slice(0, maxLines); + const hasMore = oldLines.length > maxLines || newLines.length > maxLines; + + return ( + + + {" "} + Updated memory block{" "} + {blockName} + + + {/* Removals */} + {oldTruncated.map((line, i) => ( + + ))} + + {/* Additions */} + {newTruncated.map((line, i) => ( + + ))} + + {hasMore && {" "}... diff truncated} + + ); +} + +/** + * Single diff line with word-level highlighting + */ +function DiffLine({ + type, + content, + compareContent, +}: { + type: "add" | "remove"; + content: string; + compareContent?: string; +}) { + const prefix = type === "add" ? "+" : "-"; + const lineBg = + type === "add" ? colors.diff.addedLineBg : colors.diff.removedLineBg; + const wordBg = + type === "add" ? colors.diff.addedWordBg : colors.diff.removedWordBg; + + // Word-level diff if we have something to compare + if (compareContent !== undefined && content.trim() && compareContent.trim()) { + const wordDiffs = + type === "add" + ? Diff.diffWords(compareContent, content) + : Diff.diffWords(content, compareContent); + + return ( + + {" "} + + {`${prefix} `} + + {wordDiffs.map((part, i) => { + if (part.added && type === "add") { + return ( + + {part.value} + + ); + } else if (part.removed && type === "remove") { + return ( + + {part.value} + + ); + } else if (!part.added && !part.removed) { + return ( + + {part.value} + + ); + } + return null; + })} + + ); + } + + // Simple line without word diff + return ( + + {" "} + + {`${prefix} ${content}`} + + + ); +} + +function truncate(str: string, maxLen: number): string { + if (str.length <= maxLen) return str; + return `${str.slice(0, maxLen - 1)}…`; +} + +/** + * Renders a unified-diff patch from memory_apply_patch tool + */ +function PatchDiffRenderer({ label, patch }: { label: string; patch: string }) { + const lines = patch.split("\n"); + const maxLines = 8; + const displayLines = lines.slice(0, maxLines); + const hasMore = lines.length > maxLines; + + return ( + + + {" "} + Patched memory block{" "} + {label} + + {displayLines.map((line, i) => { + // Skip @@ hunk headers + if (line.startsWith("@@")) { + return null; + } + + const firstChar = line[0]; + const content = line.slice(1); // Remove the prefix character + + if (firstChar === "+") { + return ( + + {" "} + + {`+ ${content}`} + + + ); + } else if (firstChar === "-") { + return ( + + {" "} + + {`- ${content}`} + + + ); + } else if (firstChar === " ") { + // Context line - show dimmed + return ( + + + {" "} + {content} + + + ); + } + // Unknown format, show as-is + return ( + + {" "} + {line} + + ); + })} + {hasMore && ( + + {" "}... {lines.length - maxLines} more lines + + )} + + ); +} diff --git a/src/cli/components/ToolCallMessageRich.tsx b/src/cli/components/ToolCallMessageRich.tsx index 3e0d1bd..8f3f085 100644 --- a/src/cli/components/ToolCallMessageRich.tsx +++ b/src/cli/components/ToolCallMessageRich.tsx @@ -4,6 +4,7 @@ import { clipToolReturn } from "../../tools/manager.js"; import { formatArgsDisplay } from "../helpers/formatArgsDisplay.js"; import { getDisplayToolName, + isMemoryTool, isPlanTool, isTaskTool, isTodoTool, @@ -12,6 +13,7 @@ import { useTerminalWidth } from "../hooks/useTerminalWidth"; import { BlinkDot } from "./BlinkDot.js"; import { colors } from "./colors.js"; import { MarkdownDisplay } from "./MarkdownDisplay.js"; +import { MemoryDiffRenderer } from "./MemoryDiffRenderer.js"; import { PlanRenderer } from "./PlanRenderer.js"; import { TodoRenderer } from "./TodoRenderer.js"; @@ -207,6 +209,17 @@ export const ToolCallMessage = memo(({ line }: { line: ToolCallLine }) => { } } + // Check if this is a memory tool - show diff instead of raw result + if (isMemoryTool(rawName) && line.resultOk !== false && line.argsText) { + const memoryDiff = ( + + ); + if (memoryDiff) { + return memoryDiff; + } + // If MemoryDiffRenderer returns null, fall through to regular handling + } + // Regular result handling const isError = line.resultOk === false; @@ -255,10 +268,25 @@ export const ToolCallMessage = memo(({ line }: { line: ToolCallLine }) => { {fallback ? ( - {`${displayName}${args}`} + + {isMemoryTool(rawName) ? ( + <> + {displayName} + {args} + + ) : ( + `${displayName}${args}` + )} + ) : ( - {displayName} + + {displayName} + {args ? (