File Tree

A directory tree over a flat file list — git status colours, +/- counts, keyboard navigation, type-to-filter and per-node menus, virtualized by @pierre/trees.

Open in
Files10

components/ui/diff-view.tsx

Installation

npx shadcn@latest add miskibin/chat-components/file-tree

Also installs change-summary.

Usage

"use client"

import { FileTree, type FileTreeEntry } from "@/components/ui/file-tree"

export function ChangedFiles({ entries }: { entries: FileTreeEntry[] }) {
  const [selected, setSelected] = useState<string | null>(null)

  // The tree owns its scroll container, so give it a height.
  return (
    <div className="h-full min-h-0 w-72">
      <FileTree
        entries={entries}
        selectedPath={selected}
        onSelect={setSelected}
      />
    </div>
  )
}

Examples

One level at a time

A whole checkout is too much to send at once, so hand the tree a loadChildren instead of a list: it asks for the root, then for each folder the first time it opens. An unloaded folder is a path ending in / with nothing under it yet, which is why empty directories are not folded away in this mode.

No files.

Open a folder to fetch its level.

  • fetched /

Notes

API reference

FileTree

entriesFileTreeEntry[]
The files, flat: { path, status?, additions?, deletions? }. Directories are inferred from the slashes, so a git status list is a valid tree on its own. A path ending in / is a directory.
loadChildren(dir: string) => Promise<FileTreeEntry[]>
Lazy mode. Called with "" for the root, and with a directory path — trailing slash included — the first time the reader opens it. Return that level only. A level that rejects is asked for again on the next open.
selectedPath / revealNoncestring | null / number
The file the host is showing, kept selected and scrolled into view. Revealing happens once per path and nonce, so a list that changes underneath never drags the reader back.
onSelect(path: string) => void
Fires when the reader picks a file — never for a directory, and never for the echo of selectedPath.
searchboolean = true
The type-to-filter box above the tree. Matches expand; everything else folds away.
fileActionsFileActionItem[]
Right-click menu per node, from the same shape change-summary and the file panel use — so every file in the app answers a right-click the same way.
renderNodeMenu(node, context) => React.ReactNode
Full control of that menu. Outranks fileActions; call context.close() when an item is chosen.
initialExpansion"closed" | "open" | number = "open"
A diff is a short list and the reader came for the files, so it opens; a whole checkout usually wants "closed" or a level count.
iconsFileTreeIcons = { set: "complete", colored: true }
Pierre's built-in icon sets, or a custom sprite sheet with per-name and per-extension rules. complete is the coloured set; standard assigns type tokens but does not paint them.
density"compact" | "default" | "relaxed" | number = "compact"
Row height preset, or a scale factor.
headerReact.ReactNode
A row above the filter box — a title, a count, a collapse-all button.

Data slots

Every part below is addressable from a parent, so you can restyle internals without forking the component.

file-treefile-tree-headerfile-tree-searchfile-tree-search-clearfile-tree-emptyfile-tree-menufile-tree-menu-item