import type { EnvironmentId, ProjectEntry } from "@t3tools/contracts"; import { FileTree, useFileTree } from "@pierre/trees/react"; import { RefreshCw, Search } from "lucide-react"; import { useEffect, useMemo, useRef } from "react"; import { useTheme } from "~/hooks/useTheme"; import { cn } from "~/lib/utils"; import { T3_PIERRE_ICONS } from "~/pierre-icons"; import { useProjectEntriesQuery } from "./projectFilesQueryState"; interface FileBrowserPanelProps { environmentId: EnvironmentId; cwd: string; projectName: string; onOpenFile: (relativePath: string) => void; } const TREE_UNSAFE_CSS = ` :host { --trees-bg-override: transparent; --trees-selected-bg-override: color-mix(in srgb, currentColor 12%, transparent); --trees-hover-bg-override: color-mix(in srgb, currentColor 7%, transparent); --trees-border-color-override: color-mix(in srgb, currentColor 14%, transparent); --trees-font-family-override: var(--font-sans); --trees-font-size-override: 12px; } button[data-type='item'] { border-radius: 5px; } `; function treePath(entry: ProjectEntry): string { return entry.kind === "directory" ? `${entry.path}/` : entry.path; } export default function FileBrowserPanel({ environmentId, cwd, projectName, onOpenFile, }: FileBrowserPanelProps) { const { resolvedTheme } = useTheme(); const entriesQuery = useProjectEntriesQuery(environmentId, cwd); const entries = entriesQuery.data?.entries ?? []; const entryKinds = useMemo( () => new Map(entries.map((entry) => [entry.path, entry.kind] as const)), [entries], ); const entryKindsRef = useRef>(entryKinds); const treePaths = useMemo(() => entries.map(treePath), [entries]); const previousTreePathsRef = useRef([]); const { model } = useFileTree({ density: "compact", fileTreeSearchMode: "hide-non-matches", flattenEmptyDirectories: true, initialExpansion: 1, icons: T3_PIERRE_ICONS, onSelectionChange: (selectedPaths) => { const selectedPath = selectedPaths.at(-1)?.replace(/\/$/, ""); if (selectedPath && entryKindsRef.current.get(selectedPath) === "file") { onOpenFile(selectedPath); } }, paths: [], search: true, unsafeCSS: TREE_UNSAFE_CSS, }); useEffect(() => { if (previousTreePathsRef.current === treePaths) return; entryKindsRef.current = entryKinds; previousTreePathsRef.current = treePaths; model.resetPaths(treePaths); }, [entryKinds, model, treePaths]); const fileCount = useMemo( () => entries.reduce((count, entry) => count + (entry.kind === "file" ? 1 : 0), 0), [entries], ); return (
{projectName}
{entriesQuery.isPending && entriesQuery.data === null ? "Indexing…" : `${fileCount.toLocaleString()} files`} {entriesQuery.data?.truncated ? " · partial" : ""}
{entriesQuery.error && entriesQuery.data === null ? (
{entriesQuery.error}
) : ( )}
); }