import { useState, useMemo, useCallback } from "react"; import { FileEditor } from "./FileEditor"; import { useMemoryData } from "../hooks/useMemoryData"; import { Loader2 } from "lucide-react"; interface MemoryViewProps { projectId?: string; addToast: (message: string, type: "success" | "error" | "info") => void; } type Tab = "working" | "insights" | "engines"; /** Known category headers in the insights file */ const CATEGORY_HEADERS: Record = { "Patterns": "pattern", "Principles": "principle", "Conventions": "convention", "Pitfalls": "pitfall", "Context": "context", }; interface ParsedInsightCategory { name: string; key: string; items: string[]; expanded: boolean; } /** Parse insights markdown content into categorized sections */ function parseInsightsContent(content: string | null): ParsedInsightCategory[] { if (!content) return []; const categories: ParsedInsightCategory[] = []; const sections = content.split(/(?=^## )/m); for (const section of sections) { const trimmed = section.trim(); if (!trimmed) continue; // Check if this is a category header const match = trimmed.match(/^##\s+(.+?)(\n|$)/); if (match) { const header = match[1].trim(); const key = CATEGORY_HEADERS[header] ?? header.toLowerCase(); const body = trimmed.slice(match[0].length).trim(); // Extract bullet points const items = body .split("\n") .map((line) => line.replace(/^-\s+/, "").trim()) .filter((line) => line.length > 0 && (line.startsWith("- ") || line.startsWith("* "))); if (items.length > 0 || body.length > 0) { categories.push({ name: header, key, items: items.length > 0 ? items : (body.length > 0 ? [body] : []), expanded: true, }); } } } return categories; } /** Parse the "Last Updated" timestamp from insights content */ function parseLastUpdated(content: string | null): string | null { if (!content) return null; const match = content.match(/##\s+Last\s+Updated:\s*(\d{4}-\d{2}-\d{2})/i); return match ? match[1] : null; } /** Count total insights from parsed categories */ function countTotalInsights(categories: ParsedInsightCategory[]): number { return categories.reduce((sum, cat) => sum + cat.items.length, 0); } /** Get backend display name */ function getBackendDisplayName(backend: string): string { switch (backend) { case "file": return "File (.fusion/memory.md)"; case "readonly": return "Read-Only"; case "qmd": return "QMD (Quantized Memory Distillation)"; default: return backend; } } /** Get health badge text */ function getHealthBadgeText(health: "healthy" | "warning" | "issues"): string { switch (health) { case "healthy": return "Healthy"; case "warning": return "Warning"; case "issues": return "Issues Found"; } } export function MemoryView({ projectId, addToast }: MemoryViewProps) { const [activeTab, setActiveTab] = useState("working"); const [expandedCategories, setExpandedCategories] = useState>(new Set()); const [editingInsights, setEditingInsights] = useState(false); const [insightsEditorContent, setInsightsEditorContent] = useState(null); const { workingMemory, workingMemoryLoading, workingMemoryDirty, setWorkingMemory, saveWorkingMemory, savingWorkingMemory, insightsContent, insightsLoading, insightsExists, refreshInsights, saveInsights, backendStatus, backendLoading, extractInsights, extracting, auditReport, auditLoading, refreshAudit, compactMemory, compacting, } = useMemoryData({ projectId }); // Parse insights content const parsedCategories = useMemo( () => parseInsightsContent(insightsContent), [insightsContent] ); const totalInsights = useMemo( () => countTotalInsights(parsedCategories), [parsedCategories] ); const lastUpdated = useMemo( () => parseLastUpdated(insightsContent), [insightsContent] ); // Toggle category expansion const toggleCategory = useCallback((key: string) => { setExpandedCategories((prev) => { const next = new Set(prev); if (next.has(key)) { next.delete(key); } else { next.add(key); } return next; }); }, []); // Handle save working memory const handleSaveWorkingMemory = useCallback(async () => { try { await saveWorkingMemory(); addToast("Working memory saved", "success"); } catch { addToast("Failed to save working memory", "error"); } }, [saveWorkingMemory, addToast]); // Handle compact memory const handleCompactMemory = useCallback(async () => { try { await compactMemory(); addToast("Memory compacted successfully", "success"); } catch { addToast("Failed to compact memory", "error"); } }, [compactMemory, addToast]); // Handle extract insights const handleExtractInsights = useCallback(async () => { try { const result = await extractInsights(); addToast(result.summary, "success"); } catch (err) { addToast(err instanceof Error ? err.message : "Failed to extract insights", "error"); } }, [extractInsights, addToast]); // Handle save insights (from raw editor) const handleSaveInsights = useCallback(async () => { if (insightsEditorContent === null) return; try { await saveInsights(insightsEditorContent); setEditingInsights(false); setInsightsEditorContent(null); addToast("Insights saved", "success"); } catch { addToast("Failed to save insights", "error"); } }, [insightsEditorContent, saveInsights, addToast]); // Start editing insights const handleStartEditingInsights = useCallback(() => { setInsightsEditorContent(insightsContent ?? ""); setEditingInsights(true); }, [insightsContent]); // Cancel editing insights const handleCancelEditingInsights = useCallback(() => { setEditingInsights(false); setInsightsEditorContent(null); }, []); const isWritable = backendStatus?.capabilities?.writable ?? false; return (
{/* Header */}

Memory

Working memory, long-term insights, and engine status

{/* Tab bar */}
{/* Content area */}
{/* Working Memory Tab */} {activeTab === "working" && (
{!isWritable && (
This memory backend is read-only. Changes cannot be saved.
)} {workingMemoryLoading ? (
Loading working memory…
) : ( <>
{workingMemory.length} characters
{isWritable && workingMemory.length > 0 && ( )} {workingMemoryDirty && isWritable && ( )}
)}
)} {/* Insights Tab */} {activeTab === "insights" && (
{insightsLoading ? (
Loading insights…
) : editingInsights ? ( // Raw editor mode <>
) : !insightsExists || parsedCategories.length === 0 ? ( // Empty state

No insights extracted yet.

Insights are automatically extracted from working memory. Click "Extract Now" to trigger extraction manually.

) : ( // Parsed insights view <>
{totalInsights}
Total Insights
{parsedCategories.length}
Categories
{lastUpdated && (
{lastUpdated}
Last Updated
)}
{parsedCategories.map((category) => { const isExpanded = !expandedCategories.has(category.key); return (
toggleCategory(category.key)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleCategory(category.key); } }} >

{category.name}

{category.items.length}
{isExpanded && (
{category.items.map((item, index) => (
{item.replace(/^-\s+/, "").replace(/^\*\s+/, "")}
))}
)}
); })}
)}
)} {/* Engines Tab */} {activeTab === "engines" && (
{backendLoading || auditLoading ? (
Loading engine status…
) : ( <> {/* Backend Card */}

Current Backend

{getBackendDisplayName(backendStatus?.currentBackend ?? "unknown")}
{backendStatus?.capabilities?.readable && ( Readable )} {backendStatus?.capabilities?.writable && ( Writable )} {backendStatus?.capabilities?.supportsAtomicWrite && ( Atomic Writes )} {backendStatus?.capabilities?.persistent && ( Persistent )}
{/* Health Status Card */} {auditReport && (

Health Status

{getHealthBadgeText(auditReport.health)}
Working Memory
{auditReport.workingMemory.size} chars
{auditReport.workingMemory.sectionCount} sections
Insights Memory
{auditReport.insightsMemory.size} chars
{auditReport.insightsMemory.insightCount} insights
Last Extraction
{auditReport.extraction.success ? ( Success ) : ( Failed )}
{auditReport.extraction.summary || `${auditReport.extraction.insightCount} insights extracted`}
Pruning
{auditReport.pruning.applied ? ( Applied ) : ( Not needed )}
{auditReport.pruning.applied && (
{auditReport.pruning.reason}
)}
)} {/* Audit Checks */} {auditReport && auditReport.checks.length > 0 && (

Audit Checks

{auditReport.checks.map((check) => (
{check.passed ? "✓" : "✗"}
{check.name}
{check.details}
))}
)} {/* Actions */}
{/* Note about Settings */}
Note: Change backend type in{' '} { // This would open the settings modal with memory section focused // For now, just add a toast hint addToast("Open Settings → Memory to change backend type", "info"); }} > Settings → Memory
)}
)}
); }