import { IconChevronRight, IconChevronDown, IconFolder, IconFileText, IconFileCode, IconPhoto, IconFile, IconPlus, IconTrash, IconHierarchy2, IconPlugConnected, IconBrowser, IconDeviceDesktop, IconBulb, IconClockHour3, IconLoader2, IconHelpCircle, } from "@tabler/icons-react"; import React, { useState, useRef, useCallback } from "react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "../components/ui/tooltip.js"; import { cn } from "../utils.js"; import type { McpServer } from "./use-mcp-servers.js"; import type { TreeNode, ResourceMeta, JobMetadata } from "./use-resources.js"; function StatusDot({ className, tooltip, }: { className: string; tooltip: string; }) { return ( {tooltip} ); } // ─── Helpers ──────────────────────────────────────────────────────────────── function getFileIcon(node: TreeNode): React.ReactNode { if (node.kind === "agent") { return ( ); } if (node.kind === "remote-agent" || node.kind === "mcp-server") { return ( ); } if (node.kind === "mcp-builtin") { return node.mcpBuiltinMeta?.exclusiveGroup === "browser" ? ( ) : ( ); } if (node.kind === "skill") { return ; } if (node.kind === "job") { return ( ); } const name = node.name; const ext = name.split(".").pop()?.toLowerCase() ?? ""; const iconClass = "h-3.5 w-3.5 shrink-0 text-muted-foreground"; if (ext === "md" || ext === "mdx") return ; if ( ["ts", "tsx", "js", "jsx", "json", "css", "html", "py", "sh"].includes(ext) ) return ; if (["png", "jpg", "jpeg", "gif", "svg", "webp", "ico"].includes(ext)) return ; return ; } // ─── Types ────────────────────────────────────────────────────────────────── export interface ResourceTreeProps { tree: TreeNode[]; /** Presentation mode for the resources: nested tree rows or a flat collection. */ variant?: "tree" | "collection"; selectedId: string | null; onSelect: (resource: ResourceMeta) => void; onCreateFile: (parentPath: string, name: string) => void; onCreateFolder: (parentPath: string, name: string) => void; onDelete: (id: string) => void; onRename: (id: string, newPath: string) => void; onDrop: (files: FileList) => void; /** Section title displayed as heading */ title?: string; /** Tooltip for the section heading */ titleTooltip?: string; /** Whether this section's tree is still loading */ isLoading?: boolean; /** Resource id currently being deleted (shows spinner + muted row) */ deletingId?: string | null; /** When true, hide create/delete/rename/upload affordances. Files stay readable. */ readOnly?: boolean; /** Optional hint shown next to the heading (e.g. "Read only") */ headingHint?: React.ReactNode; /** Scope-specific action shown beside the section heading. */ sectionAction?: React.ReactNode; /** Scope-specific action shown beneath an empty collection message. */ emptyStateAction?: React.ReactNode; } interface CreatingState { parentPath: string; type: "file" | "folder"; } function McpStatusDot({ server }: { server: McpServer }) { const status = server.status ?? { state: "unknown" as const }; if (status.state === "connected") { return ( ); } if (status.state === "error") { return ( ); } return ( ); } function BuiltinStatusDot({ node }: { node: TreeNode }) { const meta = node.mcpBuiltinMeta; if (!meta?.available) { return ( ); } if (!meta.scopeEnabled) { return ( ); } const status = meta.status?.[meta.scope]; if (status?.state === "connected") { return ( ); } if (status?.state === "error") { return ( ); } return ; } function JobStatusDot({ meta }: { meta: JobMetadata }) { if (!meta.enabled) { return ( ); } if (meta.lastStatus === "running") { return ( ); } if (meta.lastStatus === "error") { return ( ); } if (meta.lastStatus === "success") { return ( ); } return ( ); } type LeafResourceNode = TreeNode & { resource: ResourceMeta }; function getLeafResources(nodes: TreeNode[]): LeafResourceNode[] { return nodes.flatMap((node) => { if (node.type === "folder") { return getLeafResources(node.children ?? []); } return node.resource ? [node as LeafResourceNode] : []; }); } function formatResourceSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function getCollectionDescriptor(node: LeafResourceNode): string { if (node.kind === "agent") { return ( node.agentMeta?.description || node.agentMeta?.model || "Custom agent" ); } if (node.kind === "skill") { return node.skillMeta?.description || "Skill"; } if (node.kind === "remote-agent") { return ( node.remoteAgentMeta?.description || node.remoteAgentMeta?.url || "Remote agent" ); } if (node.kind === "job") { return ( node.jobMeta?.scheduleDescription || node.jobMeta?.schedule || "Scheduled job" ); } if (node.kind === "mcp-server") return "MCP server"; if (node.kind === "mcp-builtin") return "Built-in MCP"; return formatResourceSize(node.resource.size); } function CollectionResourceRow({ node, selectedId, deletingId, readOnly, onSelect, onDelete, }: { node: LeafResourceNode; selectedId: string | null; deletingId?: string | null; readOnly?: boolean; onSelect: (resource: ResourceMeta) => void; onDelete: (id: string) => void; }) { const isSelected = node.resource.id === selectedId; const isDeleting = node.resource.id === deletingId; const [confirmingDelete, setConfirmingDelete] = useState(false); const descriptor = getCollectionDescriptor(node); const selectResource = () => { if (!isDeleting) onSelect(node.resource); }; return (
setConfirmingDelete(false)} > {!readOnly && node.kind !== "mcp-builtin" && (
{isDeleting ? ( Deleting… ) : ( {confirmingDelete ? "Click again to delete" : "Delete"} )}
)}
); } // ─── TreeNodeRow ──────────────────────────────────────────────────────────── function TreeNodeRow({ node, depth, expanded, selectedId, deletingId, readOnly, onToggle, onSelect, onDelete, onStartCreate, }: { node: TreeNode; depth: number; expanded: Set; selectedId: string | null; deletingId?: string | null; readOnly?: boolean; onToggle: (path: string) => void; onSelect: (resource: ResourceMeta) => void; onDelete: (id: string) => void; onStartCreate: (parentPath: string, type: "file" | "folder") => void; }) { const isFolder = node.type === "folder"; const isExpanded = expanded.has(node.path); const isSelected = node.resource?.id === selectedId; const isDeleting = !!node.resource && node.resource.id === deletingId; const [confirmingDelete, setConfirmingDelete] = useState(false); return (
{ if (isDeleting) return; if (isFolder) { onToggle(node.path); } else if (node.resource) { onSelect(node.resource); } }} onMouseLeave={() => setConfirmingDelete(false)} > {isFolder ? ( isExpanded ? ( ) : ( ) ) : ( )} {isFolder ? ( ) : ( getFileIcon(node) )} {node.name} {node.jobMeta && } {node.mcpServerMeta && } {node.mcpBuiltinMeta && } {!readOnly && (
{isFolder && ( New file )} {node.resource && node.kind !== "mcp-builtin" && (isDeleting ? ( Deleting… ) : ( {confirmingDelete ? "Click again to delete" : "Delete"} ))}
)}
{isFolder && isExpanded && node.children && (
{node.children.map((child) => ( ))}
)}
); } // ─── InlineInput ──────────────────────────────────────────────────────────── function InlineInput({ depth, onConfirm, onCancel, }: { depth: number; onConfirm: (name: string) => void; onCancel: () => void; }) { const inputRef = useRef(null); const [value, setValue] = useState(""); React.useEffect(() => { inputRef.current?.focus(); }, []); return (
setValue(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && value.trim()) { onConfirm(value.trim()); } else if (e.key === "Escape") { onCancel(); } }} onBlur={() => { if (value.trim()) { onConfirm(value.trim()); } else { onCancel(); } }} className="min-w-0 flex-1 bg-transparent text-[12px] leading-none text-foreground outline-none placeholder:text-muted-foreground/50" placeholder="filename.md" />
); } // ─── ResourceTree ─────────────────────────────────────────────────────────── export function ResourceTree({ tree, variant = "tree", selectedId, onSelect, onCreateFile, onCreateFolder, onDelete, onDrop, title = "Files", titleTooltip, isLoading = false, deletingId = null, readOnly = false, headingHint, sectionAction, emptyStateAction, }: ResourceTreeProps) { const [expanded, setExpanded] = useState>(() => new Set()); const [creating, setCreating] = useState(null); const [dragOver, setDragOver] = useState(false); const leafResources = variant === "collection" ? getLeafResources(tree) : []; const isEmpty = variant === "collection" ? leafResources.length === 0 : tree.length === 0; const toggleExpand = useCallback((path: string) => { setExpanded((prev) => { const next = new Set(prev); if (next.has(path)) { next.delete(path); } else { next.add(path); } return next; }); }, []); const handleStartCreate = useCallback( (parentPath: string, type: "file" | "folder") => { setCreating({ parentPath, type }); // auto-expand the parent folder setExpanded((prev) => { const next = new Set(prev); next.add(parentPath); return next; }); }, [], ); const handleConfirmCreate = useCallback( (name: string) => { if (!creating) return; if (creating.type === "file") { onCreateFile(creating.parentPath, name); } else { onCreateFolder(creating.parentPath, name); } setCreating(null); }, [creating, onCreateFile, onCreateFolder], ); const handleCancelCreate = useCallback(() => { setCreating(null); }, []); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragOver(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragOver(false); }, []); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragOver(false); if (readOnly) return; if (e.dataTransfer.files.length > 0) { onDrop(e.dataTransfer.files); } }, [onDrop, readOnly], ); return (
{/* Section heading */}
{title} {headingHint && ( {headingHint} )} {titleTooltip && ( {titleTooltip} )} {sectionAction ?? (variant === "tree" && !readOnly && ( New file ))}
{variant === "collection" ? leafResources.length > 0 && (
{leafResources.map((node) => ( ))}
) : tree.map((node) => ( ))} {isLoading && isEmpty && (
{Array.from({ length: 3 }).map((_, i) => (
))}
)} {/* Inline input for root-level creation */} {creating && creating.parentPath === "" && ( )} {/* Inline input for folder-level creation */} {creating && creating.parentPath !== "" && ( )} {isEmpty && !creating && !isLoading && (

{variant === "collection" ? "No resources in this collection yet" : "No files yet"}

{emptyStateAction &&
{emptyStateAction}
}
)}
); }