{"slug":"diff-summary","category":"code","job":"trust","added":"2026-09-11T20:48Z","title":"Diff Summary Card","description":"A collapsed summary of a batch of file edits, with undo and an overflow list.","url":"https://ai-patterns.app/patterns/code/diff-summary","markdown":"https://ai-patterns.app/patterns/code/diff-summary.md","json":"https://ai-patterns.app/api/patterns/code/diff-summary","seenIn":[{"team":"Cursor","source":"https://docs.cursor.com/agent/review"},{"team":"Zed Industries","source":"https://zed.dev/docs/ai/agent-panel"}],"spec":"# Diff Summary Card\n\n## Summary\nA collapsed summary of a batch of file edits an agent just made: a count, an undo action, a \"view changes\" action, a per-file list of names with add/delete line counts, and an overflow expander for large batches. It lets the user trust that changes happened and skim their scope without reading every diff.\n\n## When to use\n- Immediately after an agent finishes a multi-file edit (a coding assistant, a batch content update, a config migration).\n- When individual files have a clear, quantifiable diff (lines added/removed) worth surfacing at a glance.\n\n## When not to use\n- For a single-file edit — a one-line inline confirmation is enough; a whole card is overkill for one file.\n- As the only way to inspect what changed. \"View changes\" must lead somewhere real (a diff viewer); this card is a summary, not a substitute for the actual diff.\n- For edits that can't be undone. Only offer \"Undo\" when it actually reverts the change — a decorative Undo that doesn't work erodes trust fast.\n\n## Anatomy\n- Header: file count (\"Edited N files\"), an Undo action, a View changes action.\n- File list: one row per file — an icon, the file name (truncated, not wrapped), additions/deletions counts, and an affordance that the row is clickable.\n- Overflow control: \"Show N more\" beneath the first handful of rows, expanding in place.\n- Footer: secondary actions (e.g. copy, pin, read aloud) and a timestamp.\n\n## Behavior\n- Show only the first handful of files (5–8) by default; collapse the rest behind \"Show N more\" so the card doesn't dominate the screen for large batches.\n- Expanding the overflow list animates height smoothly rather than snapping; it does not replace or reflow the already-visible rows.\n- Additions and deletions are always shown together (+N in one color, -N in another) so scanning the list gives an at-a-glance sense of which files grew, shrank, or were rewritten.\n- Undo should act on the whole batch, not per file — this card represents one atomic change.\n- Clicking a file row should open that file's specific diff, not the whole batch's.\n\n## Content guidelines\n- File names show their path when it disambiguates (e.g. two files with the same basename in different folders) — don't silently truncate to just the basename.\n- The header count and the actual number of rows must always agree, including after expanding.\n\n## Accessibility\n- Each file row and the overflow toggle must be reachable and operable by keyboard, not just by mouse.\n- Additions/deletions must not rely on color alone — the +/- sign already carries the meaning, so keep it even if you restyle the colors.\n- The overflow toggle should update its accessible name/state (e.g. `aria-expanded`) when toggled.\n\n## Related patterns\n- **Diff Tabs** — Use it instead when people should read the changes inline rather than scan a list of files.\n- **Expandable Trace** — Use it as well above the summary when people want the steps that led to the changes.\n- **Tool Approval** — Use it instead before the edits run, when they need approval rather than a summary.\n","code":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { ChevronDown, ChevronRight, Code2, Copy, Pin, Volume2 } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface DiffFile {\n  id: string;\n  name: string;\n  additions: number;\n  deletions: number;\n}\n\nexport interface DiffSummaryCardProps {\n  files: DiffFile[];\n  visibleCount?: number;\n  timestamp?: string;\n  onUndo?: () => void;\n  onViewChanges?: () => void;\n  className?: string;\n}\n\nexport function DiffSummaryCard({\n  files,\n  visibleCount = 5,\n  timestamp = \"now\",\n  onUndo,\n  onViewChanges,\n  className,\n}: DiffSummaryCardProps) {\n  const [expanded, setExpanded] = React.useState(false);\n  const primary = files.slice(0, visibleCount);\n  const rest = files.slice(visibleCount);\n\n  return (\n    <div className={cn(\"w-full overflow-hidden rounded-2xl border bg-card\", className)}>\n      <div className=\"flex items-center justify-between gap-3 border-b px-4 py-3\">\n        <p className=\"text-xs font-medium\">Edited {files.length} files</p>\n        <div className=\"flex items-center gap-2\">\n          <button\n            type=\"button\"\n            onClick={onUndo}\n            className=\"text-xs text-muted-foreground transition-colors hover:text-foreground\"\n          >\n            Undo\n          </button>\n          <button\n            type=\"button\"\n            onClick={onViewChanges}\n            className=\"rounded-full border px-3 py-1 text-xs font-medium transition-colors hover:bg-accent\"\n          >\n            View changes\n          </button>\n        </div>\n      </div>\n\n      <ul className=\"divide-y\">\n        {primary.map((file) => (\n          <FileRow key={file.id} file={file} />\n        ))}\n      </ul>\n\n      <AnimatePresence initial={false}>\n        {expanded && rest.length > 0 && (\n          <motion.ul\n            initial={{ height: 0, opacity: 0 }}\n            animate={{ height: \"auto\", opacity: 1 }}\n            exit={{ height: 0, opacity: 0 }}\n            transition={{ duration: 0.2, ease: \"easeInOut\" }}\n            className=\"divide-y overflow-hidden border-t\"\n          >\n            {rest.map((file) => (\n              <FileRow key={file.id} file={file} />\n            ))}\n          </motion.ul>\n        )}\n      </AnimatePresence>\n\n      {rest.length > 0 && (\n        <button\n          type=\"button\"\n          onClick={() => setExpanded((v) => !v)}\n          className=\"flex w-full items-center gap-1.5 border-t px-4 py-2.5 text-sm text-muted-foreground transition-colors hover:text-foreground\"\n        >\n          {expanded ? \"Show less\" : `Show ${rest.length} more`}\n          <ChevronDown className={cn(\"size-4 transition-transform\", expanded && \"rotate-180\")} />\n        </button>\n      )}\n\n      <div className=\"flex items-center justify-between border-t px-4 py-2.5\">\n        <div className=\"flex items-center gap-1 text-muted-foreground\">\n          <button\n            type=\"button\"\n            aria-label=\"Copy\"\n            className=\"rounded-md p-1.5 transition-colors hover:bg-accent hover:text-foreground\"\n          >\n            <Copy className=\"size-3.5\" />\n          </button>\n          <button\n            type=\"button\"\n            aria-label=\"Pin\"\n            className=\"rounded-md p-1.5 transition-colors hover:bg-accent hover:text-foreground\"\n          >\n            <Pin className=\"size-3.5\" />\n          </button>\n          <button\n            type=\"button\"\n            aria-label=\"Read aloud\"\n            className=\"rounded-md p-1.5 transition-colors hover:bg-accent hover:text-foreground\"\n          >\n            <Volume2 className=\"size-3.5\" />\n          </button>\n        </div>\n        <span className=\"text-xs text-muted-foreground\">{timestamp}</span>\n      </div>\n    </div>\n  );\n}\n\nfunction FileRow({ file }: { file: DiffFile }) {\n  return (\n    <li>\n      <button\n        type=\"button\"\n        className=\"flex w-full items-center gap-2.5 px-4 py-2.5 text-left transition-colors hover:bg-accent/50\"\n      >\n        <Code2 className=\"size-4 shrink-0 text-muted-foreground\" />\n        <span className=\"flex-1 truncate text-xs\">{file.name}</span>\n        <span className=\"shrink-0 font-mono text-xs\">\n          <span className=\"text-emerald-600 dark:text-emerald-400\">+{file.additions}</span>\n          <span className=\"ml-1.5 text-red-600 dark:text-red-400\">-{file.deletions}</span>\n        </span>\n        <ChevronRight className=\"size-4 shrink-0 text-muted-foreground\" />\n      </button>\n    </li>\n  );\n}\n","dependencies":["lucide-react","motion"],"localImports":["@/lib/utils"]}