import { useState, useMemo, useCallback, useEffect } from "react"; import { Loader2 } from "lucide-react"; import "./MemoryView.css"; import "./SettingsModal.css"; import type { MemoryFileInfo, MemoryRetrievalTestResult } from "../api"; import { FileEditor } from "./FileEditor"; import { useMemoryData } from "../hooks/useMemoryData"; 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", }; const MEMORY_LAYER_NAMES: Record = { "long-term": "Long-term", daily: "Daily", dreams: "Dreams", }; const MEMORY_LAYER_DESCRIPTIONS: Record = { "long-term": "Curated durable decisions, conventions, constraints, and pitfalls promoted from dreams.", daily: "Raw daily observations, open loops, and running context for dream processing.", dreams: "Synthesized patterns and open loops promoted from daily memory.", }; const MEMORY_FILE_OPTION_LABEL_MAX_CHARS = 72; function truncateMiddle(value: string, maxChars: number): string { if (value.length <= maxChars) { return value; } const visibleChars = Math.max(1, maxChars - 1); const startChars = Math.ceil(visibleChars / 2); const endChars = Math.floor(visibleChars / 2); return `${value.slice(0, startChars)}…${value.slice(value.length - endChars)}`; } function formatMemoryFileOptionLabel(file: MemoryFileInfo): string { const fullLabel = `${file.label} — ${file.path}`; return truncateMiddle(fullLabel, MEMORY_FILE_OPTION_LABEL_MAX_CHARS); } 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/, agent//memory/)"; 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 [memorySettingsDraft, setMemorySettingsDraft] = useState({ memoryEnabled: true, memoryAutoSummarizeEnabled: false, memoryAutoSummarizeThresholdChars: 50_000, memoryAutoSummarizeSchedule: "0 3 * * *", memoryDreamsEnabled: false, memoryDreamsSchedule: "0 4 * * *", }); const [memoryTestQuery, setMemoryTestQuery] = useState(""); const [memoryTestLoading, setMemoryTestLoading] = useState(false); const [memoryTestResult, setMemoryTestResult] = useState(null); const { insightsContent, insightsLoading, insightsExists, saveInsights, memorySettings, settingsLoading, saveMemorySettings, savingMemorySettings, backendStatus, backendLoading, extractInsights, extracting, auditReport, auditLoading, refreshAudit, compactMemory, compacting, installQmdAction, installingQmd, testRetrieval, memoryFiles, memoryFilesLoading, selectedFilePath, selectedFileContent, selectedFileLoading, selectedFileDirty, setSelectedFileContent, selectFile, saveSelectedFile, savingSelectedFile, reloadMemoryFiles, triggerDreamNow, dreamRunning, } = useMemoryData({ projectId }); useEffect(() => { setMemorySettingsDraft(memorySettings); }, [memorySettings]); const memorySettingsDirty = useMemo(() => ( memorySettingsDraft.memoryEnabled !== memorySettings.memoryEnabled || memorySettingsDraft.memoryAutoSummarizeEnabled !== memorySettings.memoryAutoSummarizeEnabled || memorySettingsDraft.memoryAutoSummarizeThresholdChars !== memorySettings.memoryAutoSummarizeThresholdChars || memorySettingsDraft.memoryAutoSummarizeSchedule !== memorySettings.memoryAutoSummarizeSchedule || memorySettingsDraft.memoryDreamsEnabled !== memorySettings.memoryDreamsEnabled || memorySettingsDraft.memoryDreamsSchedule !== memorySettings.memoryDreamsSchedule ), [memorySettingsDraft, memorySettings]); const selectedMemoryFile = useMemo( () => memoryFiles.find((file) => file.path === selectedFilePath), [memoryFiles, selectedFilePath], ); const selectedLayerDescription = selectedMemoryFile ? MEMORY_LAYER_DESCRIPTIONS[selectedMemoryFile.layer] : "Edits the selected memory file."; // 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; }); }, []); const handleSelectMemoryFile = useCallback(async (path: string) => { try { await selectFile(path); } catch { addToast("Failed to load memory file", "error"); } }, [selectFile, addToast]); const handleSaveSelectedFile = useCallback(async () => { try { await saveSelectedFile(); addToast("Memory saved", "success"); } catch { addToast("Failed to save memory", "error"); } }, [saveSelectedFile, addToast]); const handleSaveMemorySettings = useCallback(async () => { if (!memorySettingsDirty) { return; } const patch: Partial = {}; if (memorySettingsDraft.memoryEnabled !== memorySettings.memoryEnabled) { patch.memoryEnabled = memorySettingsDraft.memoryEnabled; } if (memorySettingsDraft.memoryAutoSummarizeEnabled !== memorySettings.memoryAutoSummarizeEnabled) { patch.memoryAutoSummarizeEnabled = memorySettingsDraft.memoryAutoSummarizeEnabled; } if (memorySettingsDraft.memoryAutoSummarizeThresholdChars !== memorySettings.memoryAutoSummarizeThresholdChars) { patch.memoryAutoSummarizeThresholdChars = memorySettingsDraft.memoryAutoSummarizeThresholdChars; } if (memorySettingsDraft.memoryAutoSummarizeSchedule !== memorySettings.memoryAutoSummarizeSchedule) { patch.memoryAutoSummarizeSchedule = memorySettingsDraft.memoryAutoSummarizeSchedule; } if (memorySettingsDraft.memoryDreamsEnabled !== memorySettings.memoryDreamsEnabled) { patch.memoryDreamsEnabled = memorySettingsDraft.memoryDreamsEnabled; } if (memorySettingsDraft.memoryDreamsSchedule !== memorySettings.memoryDreamsSchedule) { patch.memoryDreamsSchedule = memorySettingsDraft.memoryDreamsSchedule; } try { await saveMemorySettings(patch); addToast("Memory settings saved", "success"); } catch { addToast("Failed to save memory settings", "error"); } }, [memorySettingsDirty, memorySettingsDraft, memorySettings, saveMemorySettings, addToast]); const handleInstallQmd = useCallback(async () => { try { const result = await installQmdAction(); addToast( result.qmdAvailable ? "qmd installed successfully" : "qmd install finished, but qmd is still unavailable", result.qmdAvailable ? "success" : "info", ); } catch { addToast("Failed to install qmd", "error"); } }, [installQmdAction, addToast]); const handleTestRetrieval = useCallback(async () => { setMemoryTestLoading(true); setMemoryTestResult(null); try { const result = await testRetrieval(memoryTestQuery); setMemoryTestResult(result); addToast( result.qmdAvailable ? "Memory retrieval test complete" : "qmd is not installed; local fallback was used", result.qmdAvailable ? "success" : "info", ); } catch { addToast("Failed to test memory retrieval", "error"); } finally { setMemoryTestLoading(false); } }, [memoryTestQuery, testRetrieval, addToast]); const handleDreamNow = useCallback(async () => { try { await triggerDreamNow(); addToast("Dream processing completed", "success"); await reloadMemoryFiles(); } catch (error) { addToast(error instanceof Error ? error.message : "Failed to run dream processing", "error"); } }, [triggerDreamNow, reloadMemoryFiles, addToast]); // Handle compact memory const handleCompactMemory = useCallback(async () => { try { await compactMemory(selectedFilePath); addToast("Memory file compacted", "success"); } catch { addToast("Failed to compact memory", "error"); } }, [compactMemory, selectedFilePath, 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 backendStatusResolved = !backendLoading && backendStatus !== 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" && (
{backendStatusResolved && !isWritable && (
This memory backend is read-only. Changes cannot be saved.
)} {memoryFilesLoading || selectedFileLoading ? (
Loading memory file…
) : ( <>
{selectedFileDirty ? "Save or discard the current edits before switching files." : "Choose any project memory file to view or edit."}
{selectedMemoryFile && (
{MEMORY_LAYER_NAMES[selectedMemoryFile.layer]} {selectedMemoryFile.path} {selectedMemoryFile.size.toLocaleString()} bytes · updated {new Date(selectedMemoryFile.updatedAt).toLocaleString()}
)}
{selectedLayerDescription}
{selectedFileContent.length} characters
{isWritable && selectedFileContent.length > 0 && ( )} {selectedFileDirty && isWritable && ( )}
Turns daily notes into DREAMS.md and promotes reusable lessons into MEMORY.md.
{memorySettingsDraft.memoryEnabled && memorySettingsDraft.memoryDreamsEnabled && ( <>
{ setMemorySettingsDraft((prev) => ({ ...prev, memoryDreamsSchedule: event.target.value, })); }} placeholder="0 4 * * *" disabled={settingsLoading} /> Cron expression for dream processing.
Manually trigger dream processing now.
)}
Automatically compact memory when it exceeds the threshold on a schedule
{memorySettingsDraft.memoryEnabled && memorySettingsDraft.memoryAutoSummarizeEnabled && ( <>
{ setMemorySettingsDraft((prev) => ({ ...prev, memoryAutoSummarizeThresholdChars: parseInt(event.target.value, 10) || 50000, })); }} min={1000} disabled={settingsLoading} /> Memory will be compacted when it exceeds this character count
{ setMemorySettingsDraft((prev) => ({ ...prev, memoryAutoSummarizeSchedule: event.target.value, })); }} placeholder="0 3 * * *" disabled={settingsLoading} /> Cron expression for auto-summarize schedule (default: daily at 3 AM)
)}
{!memorySettingsDraft.memoryEnabled && (
Memory is currently disabled. Enable memory tools in Settings to edit these automations.
)} {memorySettingsDirty && (
)}
)}
)} {/* 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…
) : ( <> {/* QMD Integration Card */}

QMD Integration

{backendStatus?.qmdAvailable === true ? (
Installed qmd is available on PATH.
) : backendStatus?.qmdAvailable === false ? (
qmd is not installed. Search will use local files. Install indexed retrieval: {backendStatus.qmdInstallCommand || "bun install -g @tobilu/qmd"}
) : (
Checking Checking qmd availability…
)}
{backendStatus?.capabilities?.readable && ( Readable )} {backendStatus?.capabilities?.writable && ( Writable )} {backendStatus?.capabilities?.supportsAtomicWrite && ( Atomic Writes )} {backendStatus?.capabilities?.persistent && ( Persistent )}
{/* Memory Retrieval Test Card */}

Test Memory Search

setMemoryTestQuery(event.target.value)} placeholder="Search memory with qmd" />
Runs the same qmd-backed memory_search path agents use. {memoryTestResult && (
{memoryTestResult.results.length} result{memoryTestResult.results.length === 1 ? "" : "s"} {" "}for "{memoryTestResult.query}" qmd {memoryTestResult.qmdAvailable ? "available" : "missing"} · {memoryTestResult.usedFallback ? "local fallback used" : "qmd path used"} {memoryTestResult.results.length > 0 ? (
    {memoryTestResult.results.map((result, index) => (
  • {result.path}:{result.lineStart}

    {result.snippet}

  • ))}
) : ( No matching memory found. )}
)}
{/* 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
)}
)}
); }