"use client"; import { useState, useCallback, useRef, useEffect } from "react"; import { useCanvasStore, type PanelTab } from "@/store/canvas"; import { showToast } from "@/components/Toaster"; import { StatusDot } from "./StatusDot"; import { Tooltip } from "./Tooltip"; import { DetailsTab } from "./tabs/DetailsTab"; import { SkillsTab } from "./tabs/SkillsTab"; import { ChatTab } from "./tabs/ChatTab"; import { ConfigTab } from "./tabs/ConfigTab"; import { TerminalTab } from "./tabs/TerminalTab"; import { FilesTab } from "./tabs/FilesTab"; import { MemoryTab } from "./tabs/MemoryTab"; import { TracesTab } from "./tabs/TracesTab"; import { EventsTab } from "./tabs/EventsTab"; import { ActivityTab } from "./tabs/ActivityTab"; import { ScheduleTab } from "./tabs/ScheduleTab"; import { ChannelsTab } from "./tabs/ChannelsTab"; import { summarizeWorkspaceCapabilities } from "@/store/canvas"; const SIDEPANEL_WIDTH_KEY = "molecule:sidepanel-width"; const SIDEPANEL_DEFAULT_WIDTH = 480; const SIDEPANEL_MIN_WIDTH = 320; const TABS: { id: PanelTab; label: string; icon: string }[] = [ { id: "chat", label: "Chat", icon: "◈" }, { id: "activity", label: "Activity", icon: "⊙" }, { id: "details", label: "Details", icon: "◉" }, { id: "skills", label: "Skills", icon: "✦" }, { id: "terminal", label: "Terminal", icon: "▸" }, { id: "config", label: "Config", icon: "⚙" }, { id: "schedule", label: "Schedule", icon: "⏲" }, { id: "channels", label: "Channels", icon: "⇌" }, { id: "files", label: "Files", icon: "⊞" }, { id: "memory", label: "Memory", icon: "◇" }, { id: "traces", label: "Traces", icon: "◎" }, { id: "events", label: "Events", icon: "◊" }, ]; export function SidePanel() { const selectedNodeId = useCanvasStore((s) => s.selectedNodeId); const panelTab = useCanvasStore((s) => s.panelTab); const setPanelTab = useCanvasStore((s) => s.setPanelTab); const selectNode = useCanvasStore((s) => s.selectNode); const node = useCanvasStore((s) => s.nodes.find((n) => n.id === s.selectedNodeId) ); // Resizable panel width — persisted across node selections via localStorage const [width, setWidth] = useState(() => { if (typeof window === "undefined") return SIDEPANEL_DEFAULT_WIDTH; const saved = localStorage.getItem(SIDEPANEL_WIDTH_KEY); const parsed = saved ? parseInt(saved, 10) : NaN; return Number.isFinite(parsed) && parsed >= SIDEPANEL_MIN_WIDTH ? parsed : SIDEPANEL_DEFAULT_WIDTH; }); const widthRef = useRef(width); // tracks live drag value for the mouseup handler const dragging = useRef(false); const startX = useRef(0); const startWidth = useRef(SIDEPANEL_DEFAULT_WIDTH); const onMouseDown = useCallback((e: React.MouseEvent) => { e.preventDefault(); dragging.current = true; startX.current = e.clientX; startWidth.current = width; document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; }, [width]); useEffect(() => { const onMouseMove = (e: MouseEvent) => { if (!dragging.current) return; const delta = startX.current - e.clientX; const newWidth = Math.min( Math.max(startWidth.current + delta, SIDEPANEL_MIN_WIDTH), window.innerWidth * 0.8, ); setWidth(newWidth); widthRef.current = newWidth; // keep ref in sync so mouseUp can persist it }; const onMouseUp = () => { if (!dragging.current) return; dragging.current = false; document.body.style.cursor = ""; document.body.style.userSelect = ""; // Persist the final dragged width so it survives node re-selection localStorage.setItem(SIDEPANEL_WIDTH_KEY, String(widthRef.current)); }; window.addEventListener("mousemove", onMouseMove); window.addEventListener("mouseup", onMouseUp); return () => { window.removeEventListener("mousemove", onMouseMove); window.removeEventListener("mouseup", onMouseUp); }; }, []); if (!selectedNodeId || !node) return null; const isOnline = node.data.status === "online"; const capability = summarizeWorkspaceCapabilities(node.data); return (
{/* Resize handle */}
{/* Header */}

{node.data.name}

{node.data.role && ( {node.data.role} )} T{node.data.tier}
{/* Capability summary */}
0 ? `${capability.skillCount}` : "none"} />
{/* Tabs — relative wrapper lets the fade gradient position against the scroll container */}
{/* Right-edge fade: signals more tabs are hidden off-screen when the bar overflows */} {/* Needs Restart Banner */} {node.data.needsRestart && !node.data.currentTask && selectedNodeId && (
Config changed — restart to apply
)} {/* Current Task Banner */} {node.data.currentTask && (
{node.data.currentTask}
)} {/* Tab Content */}
{panelTab === "details" && } {panelTab === "skills" && } {panelTab === "activity" && } {panelTab === "chat" && } {panelTab === "terminal" && } {panelTab === "config" && } {panelTab === "schedule" && } {panelTab === "channels" && } {panelTab === "files" && } {panelTab === "memory" && } {panelTab === "traces" && } {panelTab === "events" && }
{/* Footer — workspace ID */}
{selectedNodeId}
); } function MetaPill({ label, value, tone = "zinc" }: { label: string; value: string; tone?: "zinc" | "emerald" | "amber" }) { const toneClasses = { zinc: "border-zinc-700/50 bg-zinc-900/70 text-zinc-400", emerald: "border-emerald-500/20 bg-emerald-950/20 text-emerald-300", amber: "border-amber-500/20 bg-amber-950/20 text-amber-300", }[tone]; return ( {label} {value} ); }