"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 */}
{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 (
);
})}
);
}