Elements

29 / 122 · Tool use

File tree

Everything a run touched, as a tree, with the churn spelled out per file.

4 files changed+78 9
packages/core/src

Installation

npx shadcn@latest add "@assistant-ui/elements-file-tree"

Usage

import { FileTree } from "@/components/elements/file-tree";

<FileTree
  nodes={nodes}
  visibleCount={nodes.length}
  totalAdditions={78}
  totalDeletions={9}
/>

Props

nodes*FileTreeNode[]Flat list in display order. Nesting comes from each node's depth, not from children arrays.
visibleCount*numberHow many nodes have landed. Raise it as the run touches more files.
totalAdditions*numberAdded lines across the whole change.
totalDeletions*numberRemoved lines across the whole change.
classNamestringExtra classes merged onto the root.

Source

file-tree.tsx
"use client";

import { ChevronDownIcon, FileIcon, FolderIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { mono, paper } from "./surfaces";

export interface FileTreeNode {
  path: string;
  name: string;
  depth: number;
  kind: "folder" | "file";
  additions?: number;
  deletions?: number;
}

export function FileTree({
  nodes,
  visibleCount,
  totalAdditions,
  totalDeletions,
  className,
}: {
  nodes: readonly FileTreeNode[];
  visibleCount: number;
  totalAdditions: number;
  totalDeletions: number;
  className?: string;
}) {
  const files = nodes.filter((node) => node.kind === "file").length;

  return (
    <div
      className={cn(
        paper,
        "flex w-full max-w-sm flex-col gap-2 rounded-2xl p-3.5",
        className,
      )}
    >
      <div className="flex items-baseline justify-between px-1">
        <span className="text-[13.5px] font-medium">{files} files changed</span>
        <span className={cn(mono, "tabular-nums")}>
          <span className="text-emerald-600 dark:text-emerald-400">
            +{totalAdditions}
          </span>{" "}
          <span className="text-red-600 dark:text-red-400">
            −{totalDeletions}
          </span>
        </span>
      </div>

      <div className="flex flex-col">
        {nodes.slice(0, visibleCount).map((node) => (
          <div
            key={node.path}
            className="fade-in slide-in-from-left-1 animate-in fill-mode-both hover:bg-foreground/[0.03] flex items-center gap-2 rounded-lg px-1 py-1 text-[13px] transition-colors duration-300"
            style={{ paddingInlineStart: `${0.25 + node.depth * 0.85}rem` }}
          >
            {node.kind === "folder" ? (
              <>
                <ChevronDownIcon className="text-foreground/25 size-3 shrink-0" />
                <FolderIcon className="text-foreground/35 size-3.5 shrink-0" />
                <span className="text-foreground/60 min-w-0 flex-1 truncate">
                  {node.name}
                </span>
              </>
            ) : (
              <>
                <FileIcon className="text-foreground/30 ms-3 size-3.5 shrink-0" />
                <span className="text-foreground/85 min-w-0 flex-1 truncate">
                  {node.name}
                </span>
                <span className={cn(mono, "shrink-0 tabular-nums")}>
                  {node.additions ? (
                    <span className="text-emerald-600 dark:text-emerald-400">
                      +{node.additions}
                    </span>
                  ) : null}{" "}
                  {node.deletions ? (
                    <span className="text-red-600 dark:text-red-400">
                      −{node.deletions}
                    </span>
                  ) : null}
                </span>
              </>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}