"use client"; import { useEffect, useState, useCallback, useRef } from "react"; import { useApp } from "@/lib/store"; import { readFile, saveFile, listSkills, getFileTokenCounts } from "@/lib/api"; import { Save, FileText, Loader2, CheckCircle2, AlertCircle, Brain, Zap, Sparkles, Maximize2, Minimize2, } from "lucide-react"; import dynamic from "next/dynamic"; const MonacoEditor = dynamic(() => import("@monaco-editor/react"), { ssr: false, loading: () => (
Loading editor...
), }); // ── Memory files config ───────────────────────────────── const MEMORY_FILES = [ { label: "MEMORY.md", path: "memory/MEMORY.md", icon: Brain, color: "#7c3aed" }, { label: "SOUL.md", path: "workspace/SOUL.md", icon: Sparkles, color: "#f59e0b" }, { label: "IDENTITY.md", path: "workspace/IDENTITY.md", icon: FileText, color: "#6b7280" }, { label: "USER.md", path: "workspace/USER.md", icon: FileText, color: "#6b7280" }, { label: "AGENTS.md", path: "workspace/AGENTS.md", icon: FileText, color: "#10b981" }, { label: "SKILLS_SNAPSHOT.md", path: "SKILLS_SNAPSHOT.md", icon: Zap, color: "#f59e0b" }, ]; export default function InspectorPanel() { const { inspectorFile, setInspectorFile, rightTab, setRightTab, expandedFile, setExpandedFile, } = useApp(); const [content, setContent] = useState(""); const [originalContent, setOriginalContent] = useState(""); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [saveStatus, setSaveStatus] = useState<"idle" | "saved" | "error">("idle"); const [skills, setSkills] = useState>([]); const [tokenCounts, setTokenCounts] = useState>({}); const editorRef = useRef(null); // Load skills on mount useEffect(() => { listSkills() .then(setSkills) .catch(() => setSkills([])); }, []); // Load token counts for memory files useEffect(() => { if (rightTab === "memory") { const paths = MEMORY_FILES.map((f) => f.path); getFileTokenCounts(paths) .then((data) => { const counts: Record = {}; for (const f of data.files) { counts[f.path] = f.tokens; } setTokenCounts(counts); }) .catch(() => {}); } }, [rightTab]); // Load file content when selected useEffect(() => { if (!inspectorFile) { setContent(""); setOriginalContent(""); return; } setLoading(true); setSaveStatus("idle"); readFile(inspectorFile) .then((t) => { setContent(t); setOriginalContent(t); }) .catch(() => { setContent("# Error loading file"); setOriginalContent(""); }) .finally(() => setLoading(false)); }, [inspectorFile]); const handleSave = useCallback(async () => { if (!inspectorFile || saving) return; setSaving(true); setSaveStatus("idle"); try { await saveFile(inspectorFile, content); setOriginalContent(content); setSaveStatus("saved"); setTimeout(() => setSaveStatus("idle"), 2000); } catch { setSaveStatus("error"); } finally { setSaving(false); } }, [inspectorFile, content, saving]); // Ctrl+S shortcut useEffect(() => { const h = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === "s") { e.preventDefault(); handleSave(); } }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [handleSave]); const isDirty = content !== originalContent; const fileExt = inspectorFile?.split(".").pop() || "md"; const language = fileExt === "md" ? "markdown" : fileExt === "json" ? "json" : "markdown"; const fileName = inspectorFile?.split("/").pop() || ""; return (
{/* Tab bar */}
{(["memory", "skills"] as const).map((tab) => { const active = rightTab === tab; const Icon = tab === "memory" ? Brain : Zap; const color = tab === "memory" ? "#7c3aed" : "#f59e0b"; return ( ); })}
{/* File list (hidden when expanded) */} {!expandedFile && (
{rightTab === "memory" ? ( ) : ( )}
)} {/* Editor area */}
{inspectorFile ? ( <> {/* Editor toolbar */}
{fileName} {isDirty && }
{saveStatus === "saved" && } {saveStatus === "error" && }
{/* Monaco editor */}
{loading ? (
) : ( setContent(val || "")} onMount={(editor) => { editorRef.current = editor; }} options={{ minimap: { enabled: false }, fontSize: 13, lineNumbers: "on", wordWrap: "on", scrollBeyondLastLine: false, padding: { top: 10, bottom: 10 }, renderLineHighlight: "none", overviewRulerBorder: false, hideCursorInOverviewRuler: true, automaticLayout: true, fontFamily: "'SF Mono','JetBrains Mono','Fira Code',Consolas,monospace", lineHeight: 20, cursorBlinking: "smooth", smoothScrolling: true, }} /> )}
) : (

No file selected

Choose from the list above

)}
); } // ── File List Component ───────────────────────────────── function FileList({ files, onSelect, selectedPath, tokenCounts, }: { files: Array<{ label: string; path: string; icon: React.ElementType; color: string }>; onSelect: (path: string) => void; selectedPath: string | null; tokenCounts?: Record; }) { return (

Workspace

{files.map((f) => { const Icon = f.icon; const active = selectedPath === f.path; const count = tokenCounts?.[f.path]; return ( ); })}
); } // ── Skills File List ──────────────────────────────────── function SkillsFileList({ skills, onSelect, selectedPath, }: { skills: Array<{ name: string; path: string; description: string }>; onSelect: (path: string) => void; selectedPath: string | null; }) { return (

Available Skills

{skills.length === 0 && (

No skills found

)} {skills.map((s) => { const active = selectedPath === s.path; return ( ); })}
); }