/** * RightPanel — Manus-style tabbed right panel. * Tabs: Actions (tool timeline), Files (explorer), Preview (live viewport). */ import { useState, useCallback, useEffect, useRef } from "react"; import { Activity, FolderTree, Monitor, Folder, File, ChevronRight, ChevronDown, RefreshCw, Home, ArrowUp, FileText, FileCode, FileJson, Image as ImageIcon, Loader2, ExternalLink, } from "lucide-react"; import { ActionTree } from "./ActionTree"; import type { ChatMessage } from "@/hooks/useChat"; import { cn } from "@/lib/utils"; // ─── Tab definitions ────────────────────────────────────────────────── type TabId = "actions" | "files" | "preview"; const TABS: { id: TabId; label: string; icon: typeof Activity }[] = [ { id: "actions", label: "Actions", icon: Activity }, { id: "files", label: "Files", icon: FolderTree }, { id: "preview", label: "Preview", icon: Monitor }, ]; // ─── File Explorer types ────────────────────────────────────────────── interface FileNode { name: string; path: string; isDirectory: boolean; size?: number; } function getFileIcon(name: string) { const ext = name.split(".").pop()?.toLowerCase() || ""; if (["ts", "tsx", "js", "jsx", "py", "rs", "go", "java", "c", "cpp", "rb", "sh"].includes(ext)) return FileCode; if (["json", "yaml", "yml", "toml", "xml", "env"].includes(ext)) return FileJson; if (["png", "jpg", "jpeg", "gif", "svg", "webp", "ico"].includes(ext)) return ImageIcon; if (["md", "txt", "log", "csv"].includes(ext)) return FileText; return File; } function formatSize(bytes?: number): string { if (bytes === undefined) return ""; if (bytes < 1024) return `${bytes}B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`; return `${(bytes / (1024 * 1024)).toFixed(1)}M`; } // ─── Inline File Explorer ───────────────────────────────────────────── function FileExplorer() { const [currentPath, setCurrentPath] = useState("/home/user"); const [files, setFiles] = useState([]); const [loading, setLoading] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [fileContent, setFileContent] = useState(null); const [loadingContent, setLoadingContent] = useState(false); 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) { // Parse text listing const lines = data.result.split("\n").filter((l: string) => l.trim()); const parsed: FileNode[] = lines .filter((l: string) => !l.startsWith("##") && !l.startsWith("---")) .map((l: string) => { const isDir = l.includes("[DIR]") || l.endsWith("/"); const name = l.replace("[DIR]", "").replace(/\/$/, "").trim().split(/\s+/).pop() || l.trim(); return { name, path: `${dirPath}/${name}`.replace("//", "/"), isDirectory: isDir }; }) .filter((f: FileNode) => f.name && f.name !== "." && f.name !== ".."); setFiles(parsed); } } catch (err) { console.error("Failed to fetch files:", err); setFiles([]); } finally { setLoading(false); } }, []); const fetchFileContent = useCallback(async (filePath: string) => { setLoadingContent(true); 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(); setFileContent(data.content || data.result || "Unable to read file"); } catch { setFileContent("Error reading file"); } finally { setLoadingContent(false); } }, []); useEffect(() => { fetchFiles(currentPath); }, [currentPath, fetchFiles]); const navigateUp = () => { const parent = currentPath.split("/").slice(0, -1).join("/") || "/"; setCurrentPath(parent); setSelectedFile(null); setFileContent(null); }; const handleClick = (file: FileNode) => { if (file.isDirectory) { setCurrentPath(file.path); setSelectedFile(null); setFileContent(null); } else { setSelectedFile(file.path); fetchFileContent(file.path); } }; return (
{/* Path bar */}
{currentPath}
{/* File list or content preview */} {selectedFile && fileContent !== null ? (
{selectedFile.split("/").pop()}
{loadingContent ? (
) : (
                {fileContent.length > 10000
                  ? fileContent.slice(0, 10000) + "\n\n[...truncated]"
                  : fileContent}
              
)}
) : (
{loading ? (
) : files.length === 0 ? (
Empty directory
) : (
{files .sort((a, b) => { if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1; return a.name.localeCompare(b.name); }) .map((file) => { const Icon = file.isDirectory ? Folder : getFileIcon(file.name); return ( ); })}
)}
)}
); } // ─── Preview Panel ──────────────────────────────────────────────────── function PreviewPanel({ messages }: { messages: ChatMessage[] }) { // Extract the latest tool output that could be previewed // (bash output, web fetch results, file contents) const latestPreviewable = (() => { for (let i = messages.length - 1; i >= 0; i--) { const msg = messages[i]; if (msg.role === "assistant" && msg.toolCalls) { for (let j = msg.toolCalls.length - 1; j >= 0; j--) { const tc = msg.toolCalls[j]; if (tc.result && !tc.isError) { if (tc.name === "bash" || tc.name === "PowerShell") { return { type: "terminal" as const, name: tc.name, content: tc.result, command: (() => { try { return JSON.parse(tc.arguments).command; } catch { return ""; } })() }; } if (tc.name === "WebFetch" || tc.name === "WebSearch") { return { type: "web" as const, name: tc.name, content: tc.result, url: (() => { try { return JSON.parse(tc.arguments).url || JSON.parse(tc.arguments).query; } catch { return ""; } })() }; } if (tc.name === "read_file") { return { type: "file" as const, name: tc.name, content: tc.result, path: (() => { try { return JSON.parse(tc.arguments).path; } catch { return ""; } })() }; } } } } } return null; })(); if (!latestPreviewable) { return (

Live preview will appear here when the agent runs commands or fetches content

); } return (
{/* Preview header */}
{latestPreviewable.type === "terminal" && ( <>
$ {latestPreviewable.command?.slice(0, 60)} )} {latestPreviewable.type === "web" && ( <> {latestPreviewable.url} )} {latestPreviewable.type === "file" && ( <> {latestPreviewable.path} )}
{/* Preview content */}
          {latestPreviewable.content.length > 15000
            ? latestPreviewable.content.slice(0, 15000) + "\n\n[...truncated]"
            : latestPreviewable.content}
        
); } // ─── Main RightPanel ────────────────────────────────────────────────── interface RightPanelProps { messages: ChatMessage[]; isStreaming: boolean; } export function RightPanel({ messages, isStreaming }: RightPanelProps) { const [activeTab, setActiveTab] = useState("actions"); return (
{/* Tab bar */}
{TABS.map((tab) => { const Icon = tab.icon; const isActive = activeTab === tab.id; return ( ); })}
{/* Tab content */}
{activeTab === "actions" && ( )} {activeTab === "files" && ( )} {activeTab === "preview" && ( )}
); }