import { type TurnId } from "@t3tools/contracts"; import { memo, useCallback, useMemo, useState } from "react"; import { type TurnDiffFileChange } from "../../types"; import { buildTurnDiffTree, summarizeTurnDiffStats, type TurnDiffTreeNode, } from "../../lib/turnDiffTree"; import { ChevronsDownUpIcon, ChevronsUpDownIcon, ChevronRightIcon, FileDiffIcon, FolderIcon, FolderClosedIcon, } from "lucide-react"; import { cn } from "~/lib/utils"; import { DiffStatLabel, hasNonZeroStat } from "./DiffStatLabel"; import { PierreEntryIcon } from "./PierreEntryIcon"; import { Button } from "../ui/button"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; const EMPTY_DIRECTORY_OVERRIDES: Record = {}; export const ChangedFilesCard = memo(function ChangedFilesCard(props: { turnId: TurnId; files: ReadonlyArray; allDirectoriesExpanded: boolean; resolvedTheme: "light" | "dark"; onToggleAllDirectories: () => void; onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void; }) { const { turnId, files, allDirectoriesExpanded, resolvedTheme, onToggleAllDirectories, onOpenTurnDiff, } = props; const summaryStat = useMemo(() => summarizeTurnDiffStats(files), [files]); const hasDirectories = files.some((file) => /[/\\]/.test(file.path)); return (
{files.length} changed file{files.length === 1 ? "" : "s"} {hasNonZeroStat(summaryStat) && ( )}
{hasDirectories && ( } > {allDirectoriesExpanded ? ( ) : ( )} {allDirectoriesExpanded ? "Collapse all folders" : "Expand all folders"} )} onOpenTurnDiff(turnId, files[0]?.path)} /> } > Open diff Open the full diff
); }); export const ChangedFilesTree = memo(function ChangedFilesTree(props: { turnId: TurnId; files: ReadonlyArray; allDirectoriesExpanded: boolean; resolvedTheme: "light" | "dark"; onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void; }) { const { files, allDirectoriesExpanded, onOpenTurnDiff, resolvedTheme, turnId } = props; const treeNodes = useMemo(() => buildTurnDiffTree(files), [files]); const directoryPathsKey = useMemo( () => collectDirectoryPaths(treeNodes).join("\u0000"), [treeNodes], ); const hasDirectoryNodes = directoryPathsKey.length > 0; const expansionStateKey = `${allDirectoriesExpanded ? "expanded" : "collapsed"}\u0000${directoryPathsKey}`; const [directoryExpansionState, setDirectoryExpansionState] = useState<{ key: string; overrides: Record; }>(() => ({ key: expansionStateKey, overrides: {}, })); const expandedDirectories = directoryExpansionState.key === expansionStateKey ? directoryExpansionState.overrides : EMPTY_DIRECTORY_OVERRIDES; const toggleDirectory = useCallback( (pathValue: string) => { setDirectoryExpansionState((current) => { const nextOverrides = current.key === expansionStateKey ? current.overrides : {}; return { key: expansionStateKey, overrides: { ...nextOverrides, [pathValue]: !(nextOverrides[pathValue] ?? allDirectoriesExpanded), }, }; }); }, [allDirectoriesExpanded, expansionStateKey], ); const renderTreeNode = (node: TurnDiffTreeNode, depth: number) => { const leftPadding = 8 + depth * 14; if (node.kind === "directory") { const isExpanded = expandedDirectories[node.path] ?? allDirectoriesExpanded; return (
{isExpanded && (
{node.children.map((childNode) => renderTreeNode(childNode, depth + 1))}
)}
); } return ( ); }; return
{treeNodes.map((node) => renderTreeNode(node, 0))}
; }); function collectDirectoryPaths(nodes: ReadonlyArray): string[] { const paths: string[] = []; for (const node of nodes) { if (node.kind !== "directory") continue; paths.push(node.path); paths.push(...collectDirectoryPaths(node.children)); } return paths; }