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 ? (