import { useEffect, useId, useMemo, useRef, useState, type KeyboardEvent, type ReactNode, } from "react"; import { useQueries, useQuery } from "@tanstack/react-query"; import { AlertTriangle, ChevronDown, ChevronRight, Cloud, FileCode2, FolderOpen, Loader2, Search } from "lucide-react"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { fileResourcesApi } from "@/api/file-resources"; import { projectsApi } from "@/api/projects"; import { ApiError } from "@/api/client"; import { queryKeys } from "@/lib/queryKeys"; import { parseWorkspaceFileRef } from "@/lib/workspace-file-parser"; import type { Project, WorkspaceFileListItem, WorkspaceFileListFileItem, WorkspaceFileListMode, WorkspaceFileSelector, } from "@paperclipai/shared"; type BrowserSource = "current" | "other"; // Hard list cap. The spec called out ~50 to keep reads cheap; 100 trades a bit // more scan for fewer "refine to narrow" dead-ends on large trees. Footer always // discloses truncation so the cap is never silent. const LIST_LIMIT = 100; function basename(path: string): string { const idx = path.lastIndexOf("/"); return idx < 0 ? path : path.slice(idx + 1); } function normalizeFolderPrefix(path: string | null | undefined): string { if (!path) return ""; const trimmed = path.replace(/^\/+/, "").replace(/\/+$/, ""); return trimmed ? `${trimmed}/` : ""; } function parentFolderPath(path: string | null | undefined): string | null { const trimmed = path?.replace(/^\/+/, "").replace(/\/+$/, ""); if (!trimmed) return null; const index = trimmed.lastIndexOf("/"); return index > 0 ? trimmed.slice(0, index) : null; } function folderKey(path: string | null | undefined): string { return path?.replace(/^\/+/, "").replace(/\/+$/, "") ?? ""; } function selectedAncestorFolders(selectedPath: string | null | undefined, rootPath: string | null | undefined): string[] { const parent = parentFolderPath(selectedPath); if (!parent) return []; const root = folderKey(rootPath); const parentSegments = parent.split("/").filter(Boolean); const rootSegments = root ? root.split("/").filter(Boolean) : []; if (rootSegments.some((segment, index) => parentSegments[index] !== segment)) return []; const paths: string[] = []; for (let index = rootSegments.length; index < parentSegments.length; index += 1) { paths.push(parentSegments.slice(0, index + 1).join("/")); } return paths; } /** * Maps a server `unavailableReason` to a calm, board-readable explanation. * Copy is kept in sync with `describeDenial` in FileViewerSheet so the browse * states and the viewer's error panels read in one voice. Substring matching * keeps it resilient to small reason-string changes on the server. */ export function describeUnavailable(reason: string): { title: string; body: string; icon: ReactNode } { const lower = reason.toLowerCase(); if (lower.includes("remote")) { return { icon: