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.
Files10
components/ui/diff-view.tsx
Installation
npx shadcn@latest add miskibin/chat-components/file-treeAlso 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-summaryand 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