import { useState, useCallback, useEffect } from "react"; import { cn } from "@/lib/utils"; import { X, Folder, File, RefreshCw, Plus, Trash2, Edit3, FolderPlus, Copy, Check, Loader2, Home, ChevronUp, Save, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { toast } from "sonner"; interface FileNode { name: string; path: string; isDirectory: boolean; size?: number; } interface FileManagerPanelProps { open: boolean; onClose: () => void; } // Language detection for syntax highlighting class function getLanguage(name: string): string { const ext = name.split(".").pop()?.toLowerCase() || ""; const langMap: Record = { ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx", json: "json", md: "markdown", css: "css", html: "html", py: "python", sh: "bash", yml: "yaml", yaml: "yaml", sql: "sql", env: "bash", toml: "toml", xml: "xml", svg: "xml", rs: "rust", go: "go", rb: "ruby", java: "java", c: "c", cpp: "cpp", h: "c", hpp: "cpp", }; return langMap[ext] || "text"; } // File icon color based on extension function getFileColor(name: string) { const ext = name.split(".").pop()?.toLowerCase() || ""; const colors: Record = { ts: "text-blue-400", tsx: "text-blue-400", js: "text-yellow-400", jsx: "text-yellow-400", json: "text-green-400", md: "text-gray-400", css: "text-pink-400", html: "text-orange-400", py: "text-green-500", sh: "text-green-300", yml: "text-red-400", yaml: "text-red-400", sql: "text-cyan-400", env: "text-yellow-300", lock: "text-gray-500", }; return colors[ext] || "text-muted-foreground"; } export function FileManagerPanel({ open, onClose }: FileManagerPanelProps) { const [currentPath, setCurrentPath] = useState("/home/ubuntu"); const [files, setFiles] = useState([]); const [loading, setLoading] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [fileContent, setFileContent] = useState(null); const [contentLoading, setContentLoading] = useState(false); const [creating, setCreating] = useState<"file" | "folder" | null>(null); const [newName, setNewName] = useState(""); const [copied, setCopied] = useState(false); const [renaming, setRenaming] = useState(null); const [renameValue, setRenameValue] = useState(""); const [editing, setEditing] = useState(false); const [editContent, setEditContent] = useState(""); // Fetch directory listing const fetchFiles = useCallback(async (dirPath: string) => { setLoading(true); try { const response = await fetch("/api/chat/slash", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command: "/files", args: `list ${dirPath}`, }), }); const data = await response.json(); if (data.files) { setFiles(data.files); } else if (data.result) { const lines = data.result.split("\n").filter((l: string) => l.trim()); const parsed: FileNode[] = lines .map((line: string) => { const isDir = line.endsWith("/"); const name = isDir ? line.slice(0, -1) : line; return { name: name.split("/").pop() || name, path: `${dirPath}/${name.split("/").pop() || name}`, isDirectory: isDir, }; }) .filter((f: FileNode) => f.name); setFiles(parsed); } } catch { toast.error("Failed to load files"); } finally { setLoading(false); } }, []); // Fetch file content const fetchFileContent = useCallback(async (filePath: string) => { setContentLoading(true); setSelectedFile(filePath); setEditing(false); try { const response = await fetch("/api/chat/slash", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command: "/files", args: `read ${filePath}`, }), }); const data = await response.json(); const content = data.result || data.content || "(empty)"; setFileContent(content); setEditContent(content); } catch { setFileContent("Error reading file"); } finally { setContentLoading(false); } }, []); // Create file or folder const handleCreate = useCallback(async () => { if (!newName.trim()) return; const fullPath = `${currentPath}/${newName.trim()}`; try { const response = await fetch("/api/chat/slash", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command: "/files", args: `${creating === "folder" ? "mkdir" : "touch"} ${fullPath}`, }), }); const data = await response.json(); toast.success(data.result || `Created ${newName}`); setCreating(null); setNewName(""); fetchFiles(currentPath); } catch { toast.error("Failed to create"); } }, [newName, currentPath, creating, fetchFiles]); // Delete file const handleDelete = useCallback( async (filePath: string) => { if (!confirm(`Delete ${filePath}?`)) return; try { const response = await fetch("/api/chat/slash", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command: "/files", args: `delete ${filePath}`, }), }); const data = await response.json(); toast.success(data.result || "Deleted"); fetchFiles(currentPath); if (selectedFile === filePath) { setSelectedFile(null); setFileContent(null); } } catch { toast.error("Failed to delete"); } }, [currentPath, selectedFile, fetchFiles] ); // Rename file const handleRename = useCallback( async (oldPath: string) => { if (!renameValue.trim()) { setRenaming(null); return; } const dir = oldPath.substring(0, oldPath.lastIndexOf("/")); const newPath = `${dir}/${renameValue.trim()}`; try { const response = await fetch("/api/chat/slash", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command: "/terminal", args: `${dir} mv "${oldPath}" "${newPath}"`, }), }); const data = await response.json(); if (data.error) { toast.error(data.error); } else { toast.success(`Renamed to ${renameValue}`); fetchFiles(currentPath); if (selectedFile === oldPath) { setSelectedFile(newPath); } } } catch { toast.error("Failed to rename"); } setRenaming(null); }, [renameValue, currentPath, selectedFile, fetchFiles] ); // Save edited file const handleSaveFile = useCallback(async () => { if (!selectedFile) return; try { const response = await fetch("/api/chat/slash", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command: "/terminal", args: `/home/ubuntu cat > "${selectedFile}" << 'CLAW_EOF'\n${editContent}\nCLAW_EOF`, }), }); const data = await response.json(); if (data.error) { toast.error(data.error); } else { toast.success("File saved!"); setFileContent(editContent); setEditing(false); } } catch { toast.error("Failed to save file"); } }, [selectedFile, editContent]); useEffect(() => { if (open) { fetchFiles(currentPath); } }, [open, currentPath, fetchFiles]); const copyContent = () => { if (fileContent) { navigator.clipboard.writeText(fileContent); setCopied(true); setTimeout(() => setCopied(false), 2000); } }; const navigateUp = () => { const parent = currentPath.split("/").slice(0, -1).join("/") || "/"; setCurrentPath(parent); }; // Breadcrumbs const pathParts = currentPath.split("/").filter(Boolean); if (!open) return null; return (
{/* Header */}

File Manager

{/* Breadcrumb path bar */}
{pathParts.map((part, i) => { const path = "/" + pathParts.slice(0, i + 1).join("/"); return (
/
); })}
{/* Create form */} {creating && (
New {creating}: setNewName(e.target.value)} placeholder={creating === "folder" ? "folder-name" : "file.txt"} className="h-7 text-xs font-mono flex-1" autoFocus onKeyDown={(e) => { if (e.key === "Enter") handleCreate(); if (e.key === "Escape") { setCreating(null); setNewName(""); } }} />
)} {/* Content area */}
{/* File list */}
{loading ? (
) : files.length === 0 ? (
Empty directory
) : (
{files .sort((a, b) => { if (a.isDirectory && !b.isDirectory) return -1; if (!a.isDirectory && b.isDirectory) return 1; return a.name.localeCompare(b.name); }) .map((file) => (
{ if (file.isDirectory) { setCurrentPath(file.path); setSelectedFile(null); setFileContent(null); } else { fetchFileContent(file.path); } }} > {file.isDirectory ? ( ) : ( )} {renaming === file.path ? ( setRenameValue(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleRename(file.path); if (e.key === "Escape") setRenaming(null); }} onBlur={() => handleRename(file.path)} className="flex-1 bg-background text-xs rounded px-1.5 py-0.5 outline-none border border-border font-mono" autoFocus onClick={(e) => e.stopPropagation()} /> ) : ( {file.name} )}
))}
)}
{/* File content viewer */}
{selectedFile ? ( <>
{selectedFile.split("/").pop()} {getLanguage(selectedFile)}
{editing ? ( ) : ( )}
{contentLoading ? (
) : editing ? (