import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode, } from "react"; import { useQuery, type UseQueryResult } from "@tanstack/react-query"; import { AlertTriangle, ArrowLeft, Ban, Check, Cloud, Copy, Eye, FileCode2, FileSearch, FolderOpen, FolderSearch, Link2, Loader2, Lock, RefreshCcw, X, } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { fileResourcesApi } from "@/api/file-resources"; import { ApiError } from "@/api/client"; import { queryKeys } from "@/lib/queryKeys"; import { useRequiredFileViewer, type FileViewerUrlState, } from "@/context/FileViewerContext"; import { WorkspaceFileBrowser } from "@/components/WorkspaceFileBrowser"; import { WorkspaceFileMarkdownBody } from "@/components/WorkspaceFileMarkdownBody"; import type { ResolvedWorkspaceResource, WorkspaceFileContent, WorkspaceFileSelector, } from "@paperclipai/shared"; const FILE_VIEWER_LABELLED_BY_ID = "paperclip-file-viewer-title"; const FILE_VIEWER_DESCRIBED_BY_ID = "paperclip-file-viewer-description"; const MIN_FILE_TREE_WIDTH = 220; const MAX_FILE_TREE_WIDTH = 520; interface FileViewerErrorShape { status: number; code: string; message: string; } function normalizeError(error: unknown): FileViewerErrorShape { if (error instanceof ApiError) { const body = (error.body ?? null) as { error?: string; code?: string } | null; const code = typeof body?.code === "string" ? body.code : ""; return { status: error.status, code, message: typeof body?.error === "string" ? body.error : error.message, }; } if (error instanceof Error) { return { status: 0, code: "", message: error.message }; } return { status: 0, code: "", message: "Something went wrong." }; } function formatBytes(size: number | null | undefined): string | null { if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null; if (size < 1024) return `${size} B`; if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`; return `${(size / (1024 * 1024)).toFixed(1)} MB`; } function splitContentIntoLines(data: string): string[] { if (data === "") return [""]; const normalized = data.replace(/\r\n?/g, "\n"); const lines = normalized.split("\n"); if (lines.length > 0 && lines[lines.length - 1] === "") { lines.pop(); } return lines.length > 0 ? lines : [""]; } function basename(path: string): string { const idx = path.lastIndexOf("/"); return idx < 0 ? path : path.slice(idx + 1); } function middleTruncatePath(path: string, maxLen = 80): string { if (path.length <= maxLen) return path; const head = path.slice(0, Math.floor(maxLen / 2) - 1); const tail = path.slice(path.length - (maxLen - head.length - 1)); return `${head}…${tail}`; } function isMarkdownResource(resource: ResolvedWorkspaceResource): boolean { const contentType = resource.contentType?.toLowerCase() ?? ""; if (contentType.includes("markdown")) return true; const path = (resource.displayPath || resource.title).toLowerCase(); return /\.(md|markdown|mdown|mkdn|mkd)$/.test(path); } async function copyTextWithFallback(text: string) { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); return; } const textarea = document.createElement("textarea"); textarea.value = text; textarea.style.position = "fixed"; textarea.style.left = "-9999px"; document.body.appendChild(textarea); try { textarea.select(); const success = document.execCommand("copy"); if (!success) throw new Error("execCommand copy failed"); } finally { document.body.removeChild(textarea); } } export function describeDenial(code: string, fallback: string): { title: string; body: string; icon: ReactNode } { const lower = code.toLowerCase(); if (lower.includes("policy") || lower.includes("denied") || lower.includes("sensitive")) { return { icon: