Elements

14 / 37 · Tool use

Code diff

A unified diff with tinted additions and removals, sized for chat.

composer.tsx+2 1
export function Composer() {
const threadId = useThreadId();
const composer = useComposer();
const [draft, setDraft] = useState("");
+ const draft = useDraft(threadId);
+ useEffect(() => hydrate(draft), [threadId]);
return (
<form onSubmit={composer.send}>
{/* … */}

Installation

npx shadcn@latest add "@assistant-ui/elements-code-diff"

Usage

import { CodeDiff } from "@/components/elements/code-diff";

<CodeDiff
  filename="thread.tsx"
  additions={2}
  deletions={1}
  lines={[
    { kind: "removed", text: "const x = 1" },
    { kind: "added", text: "const x = 2" },
  ]}
  cycle={0}
/>

Props

filename*stringFile path shown in the diff header.
additions*numberAddition count rendered in green next to the filename.
deletions*numberDeletion count rendered in red next to the filename.
lines*readonly DiffLine[]Unified diff lines with context, added, or removed kind.
cycle*numberIdentity key that remounts the body so entrance animation can replay.
classNamestringExtra classes merged onto the root.

Source

code-diff.tsx
"use client";

import { cn } from "@/lib/utils";
import { mono, paper } from "./surfaces";

export type DiffKind = "context" | "added" | "removed";

export interface DiffLine {
  kind: DiffKind;
  text: string;
}

const GUTTER: Record<DiffKind, string> = {
  context: "",
  added: "+",
  removed: "−",
};

export function CodeDiff({
  filename,
  additions,
  deletions,
  lines,
  cycle,
  className,
}: {
  filename: string;
  additions: number;
  deletions: number;
  lines: readonly DiffLine[];
  cycle: number;
  className?: string;
}) {
  return (
    <div
      className={cn(
        paper,
        "w-full max-w-md overflow-hidden rounded-2xl font-mono text-xs",
        className,
      )}
    >
      <div className="flex items-center justify-between px-4 pt-3 pb-2">
        <span className="text-foreground/90">{filename}</span>
        <span className={cn(mono, "tabular-nums")}>
          <span className="text-emerald-600 dark:text-emerald-400">
            +{additions}
          </span>{" "}
          <span className="text-red-600 dark:text-red-400">−{deletions}</span>
        </span>
      </div>
      <div>
        {lines.map((line, i) => (
          <div
            key={`${cycle}-${i}-${line.text}`}
            className={cn(
              "fade-in animate-in fill-mode-both flex px-4 py-0.5 leading-relaxed whitespace-pre duration-300",
              line.kind === "context" && "text-foreground/45",
              line.kind === "added" &&
                "bg-emerald-500/10 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-300",
              line.kind === "removed" &&
                "bg-red-500/10 text-red-700 dark:text-red-300",
            )}
            style={{ animationDelay: `${i * 60}ms` }}
          >
            <span className="w-4 shrink-0 select-none">
              {GUTTER[line.kind]}
            </span>
            <span>{line.text}</span>
          </div>
        ))}
      </div>
    </div>
  );
}