import { RefreshIcon } from "~/components/ui/refresh-icon"; import { Spinner } from "~/components/ui/spinner"; import type { EnvironmentId } from "@t3tools/contracts"; import { ArrowLeftIcon, ChevronRightIcon } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { PierreEntryIcon } from "~/components/chat/PierreEntryIcon"; import { Menu, MenuGroup, MenuItem, MenuPopup, MenuSeparator, MenuTrigger, } from "~/components/ui/menu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip"; import { useTheme } from "~/hooks/useTheme"; import { useWorkspaceMutationRefresh } from "~/hooks/useWorkspaceMutationRefresh"; import { cn } from "~/lib/utils"; import { isAbsolutePath } from "~/terminal-links"; import { type FileBreadcrumb, fileBreadcrumbChildren, fileBreadcrumbParent, fileBreadcrumbs, } from "./filePath"; import { useProjectEntriesQuery } from "./projectFilesQueryState"; interface FileBreadcrumbsProps { readonly cwd: string; readonly environmentId: EnvironmentId; readonly onOpenFile: (relativePath: string) => void; readonly projectName: string; readonly relativePath: string; readonly workspaceMutationId: string | null; } function pathLabel(path: string, projectName: string): string { return path.slice(path.lastIndexOf("/") + 1) || projectName; } function BreadcrumbLabel(props: { readonly current?: boolean; readonly label: string; readonly pathLabel: string; }) { return ( } > {props.label} {props.pathLabel} ); } function BreadcrumbMenuContent(props: { readonly cwd: string; readonly currentFilePath: string; readonly directoryPath: string; readonly environmentId: EnvironmentId; readonly onDirectoryChange: (path: string) => void; readonly onOpenChange: (open: boolean) => void; readonly onOpenFile: (path: string) => void; readonly projectName: string; readonly rootPath: string; readonly workspaceMutationId: string | null; }) { const entriesQuery = useProjectEntriesQuery(props.environmentId, props.cwd); useWorkspaceMutationRefresh({ mutationId: props.workspaceMutationId, refresh: entriesQuery.refresh, resourceKey: `files:${props.environmentId}:${props.cwd}`, }); const { resolvedTheme } = useTheme(); const entries = entriesQuery.data?.entries ?? []; const entriesTruncated = entriesQuery.data?.truncated ?? false; const children = useMemo( () => fileBreadcrumbChildren(entries, props.directoryPath), [entries, props.directoryPath], ); const directoryAvailable = props.directoryPath === "" || entries.some((entry) => entry.kind === "directory" && entry.path === props.directoryPath); const parentPath = fileBreadcrumbParent(props.directoryPath); const canGoBack = props.directoryPath !== props.rootPath && parentPath !== null && (props.rootPath === "" || parentPath === props.rootPath || parentPath.startsWith(`${props.rootPath}/`)); return ( { if (event.key !== "ArrowLeft" || !canGoBack || parentPath === null) return; event.preventDefault(); event.stopPropagation(); props.onDirectoryChange(parentPath); }} > {canGoBack && parentPath !== null ? ( <> props.onDirectoryChange(parentPath)}> Back to {pathLabel(parentPath, props.projectName)} ) : null} {entriesQuery.isPending && entriesQuery.data === null ? ( Loading folder… ) : entriesQuery.error && entriesQuery.data === null ? ( Retry loading folder ) : !directoryAvailable && !entriesTruncated ? ( This folder is no longer available. ) : children.length === 0 ? ( {entriesTruncated ? "No entries from this folder are available in the partial workspace index." : "This folder is empty."} ) : ( children.map((entry) => { const isCurrentFile = entry.kind === "file" && entry.path === props.currentFilePath; return ( { if (entry.kind === "directory") { props.onDirectoryChange(entry.path); return; } props.onOpenChange(false); props.onOpenFile(entry.path); }} > }> {entry.label} {entry.path} {entry.kind === "directory" ? : null} ); }) )} {entriesQuery.error && entriesQuery.data !== null ? ( <> Refresh failed — retry ) : null} {entriesTruncated ? ( <> Some workspace entries are not shown. ) : null} ); } function DirectoryBreadcrumb(props: FileBreadcrumbsProps & { readonly crumb: FileBreadcrumb }) { const [open, setOpen] = useState(false); const [directoryPath, setDirectoryPath] = useState(props.crumb.path); useEffect(() => { setOpen(false); setDirectoryPath(props.crumb.path); }, [props.crumb.path, props.relativePath]); const handleOpenChange = (nextOpen: boolean) => { setOpen(nextOpen); if (nextOpen) setDirectoryPath(props.crumb.path); }; return ( } /> } > {props.crumb.label} {props.crumb.path || props.projectName} {open ? ( ) : null} ); } export function FileBreadcrumbs(props: FileBreadcrumbsProps) { const hostPath = isAbsolutePath(props.relativePath); const breadcrumbs = useMemo( () => fileBreadcrumbs(props.projectName, props.relativePath), [props.projectName, props.relativePath], ); return breadcrumbs.map((crumb, index) => (
{index > 0 ? ( ) : null} {crumb.kind === "file" ? ( ) : hostPath ? ( ) : ( )}
)); }