code
Diff Summary Card
A collapsed summary of a batch of file edits, with undo and an overflow list.
Edited 11 files
now
"use client";
import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { ChevronDown, ChevronRight, Code2, Copy, Pin, Volume2 } from "lucide-react";
import { cn } from "@/lib/utils";
export interface DiffFile {
id: string;
name: string;
additions: number;
deletions: number;
}
export interface DiffSummaryCardProps {
files: DiffFile[];
visibleCount?: number;
timestamp?: string;
onUndo?: () => void;
onViewChanges?: () => void;
className?: string;
}
export function DiffSummaryCard({
files,
visibleCount = 5,
timestamp = "now",
onUndo,
onViewChanges,
className,
}: DiffSummaryCardProps) {
const [expanded, setExpanded] = React.useState(false);
const primary = files.slice(0, visibleCount);
const rest = files.slice(visibleCount);
return (
<div className={cn("w-full overflow-hidden rounded-2xl border bg-card", className)}>
<div className="flex items-center justify-between gap-3 border-b px-4 py-3">
<p className="text-xs font-medium">Edited {files.length} files</p>
<div className="flex items-center gap-2">
<button
type="button"
onClick={onUndo}
className="text-xs text-muted-foreground transition-colors hover:text-foreground"
>
Undo
</button>
<button
type="button"
onClick={onViewChanges}
className="rounded-full border px-3 py-1 text-xs font-medium transition-colors hover:bg-accent"
>
View changes
</button>
</div>
</div>
<ul className="divide-y">
{primary.map((file) => (
<FileRow key={file.id} file={file} />
))}
</ul>
<AnimatePresence initial={false}>
{expanded && rest.length > 0 && (
<motion.ul
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2, ease: "easeInOut" }}
className="divide-y overflow-hidden border-t"
>
{rest.map((file) => (
<FileRow key={file.id} file={file} />
))}
</motion.ul>
)}
</AnimatePresence>
{rest.length > 0 && (
<button
type="button"
onClick={() => setExpanded((v) => !v)}
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"
>
{expanded ? "Show less" : `Show ${rest.length} more`}
<ChevronDown className={cn("size-4 transition-transform", expanded && "rotate-180")} />
</button>
)}
<div className="flex items-center justify-between border-t px-4 py-2.5">
<div className="flex items-center gap-1 text-muted-foreground">
<button
type="button"
aria-label="Copy"
className="rounded-md p-1.5 transition-colors hover:bg-accent hover:text-foreground"
>
<Copy className="size-3.5" />
</button>
<button
type="button"
aria-label="Pin"
className="rounded-md p-1.5 transition-colors hover:bg-accent hover:text-foreground"
>
<Pin className="size-3.5" />
</button>
<button
type="button"
aria-label="Read aloud"
className="rounded-md p-1.5 transition-colors hover:bg-accent hover:text-foreground"
>
<Volume2 className="size-3.5" />
</button>
</div>
<span className="text-xs text-muted-foreground">{timestamp}</span>
</div>
</div>
);
}
function FileRow({ file }: { file: DiffFile }) {
return (
<li>
<button
type="button"
className="flex w-full items-center gap-2.5 px-4 py-2.5 text-left transition-colors hover:bg-accent/50"
>
<Code2 className="size-4 shrink-0 text-muted-foreground" />
<span className="flex-1 truncate text-xs">{file.name}</span>
<span className="shrink-0 font-mono text-xs">
<span className="text-emerald-600 dark:text-emerald-400">+{file.additions}</span>
<span className="ml-1.5 text-red-600 dark:text-red-400">-{file.deletions}</span>
</span>
<ChevronRight className="size-4 shrink-0 text-muted-foreground" />
</button>
</li>
);
}
A UX spec for this pattern — written for agents implementing or reusing it, not the code.
# Diff Summary Card
## Summary
A 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.
## When to use
- Immediately after an agent finishes a multi-file edit (a coding assistant, a batch content update, a config migration).
- When individual files have a clear, quantifiable diff (lines added/removed) worth surfacing at a glance.
## When not to use
- For a single-file edit — a one-line inline confirmation is enough; a whole card is overkill for one file.
- 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.
- 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.
## Anatomy
- Header: file count ("Edited N files"), an Undo action, a View changes action.
- 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.
- Overflow control: "Show N more" beneath the first handful of rows, expanding in place.
- Footer: secondary actions (e.g. copy, pin, read aloud) and a timestamp.
## Behavior
- 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.
- Expanding the overflow list animates height smoothly rather than snapping; it does not replace or reflow the already-visible rows.
- 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.
- Undo should act on the whole batch, not per file — this card represents one atomic change.
- Clicking a file row should open that file's specific diff, not the whole batch's.
## Content guidelines
- 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.
- The header count and the actual number of rows must always agree, including after expanding.
## Accessibility
- Each file row and the overflow toggle must be reachable and operable by keyboard, not just by mouse.
- Additions/deletions must not rely on color alone — the +/- sign already carries the meaning, so keep it even if you restyle the colors.
- The overflow toggle should update its accessible name/state (e.g. `aria-expanded`) when toggled.
## Related patterns
- Often appears as the terminal state of an agent turn, after a Thinking Loader/Expandable Trace sequence — the "here's what I actually changed" summary once work completes.