import { useState, useMemo, useCallback, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Brain, Loader2 } from "lucide-react"; import "./MemoryView.css"; import "./SettingsModal.css"; import type { MemoryFileInfo, MemoryRetrievalTestResult } from "../api"; import { FileEditor } from "./FileEditor"; import { ViewHeader } from "./ViewHeader"; import { useMemoryData } from "../hooks/useMemoryData"; interface MemoryViewProps { projectId?: string; addToast: (message: string, type: "success" | "error" | "info") => void; onSendSelectionToTask?: (description: string) => 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_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); } export function MemoryView({ projectId, addToast, onSendSelectionToTask }: MemoryViewProps) { const { t } = useTranslation("app"); 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 ? (selectedMemoryFile.layer === "long-term" ? t("memory.layerDescLongTerm", "Curated durable decisions, conventions, constraints, and pitfalls promoted from dreams.") : selectedMemoryFile.layer === "daily" ? t("memory.layerDescDaily", "Raw daily observations, open loops, and running context for dream processing.") : t("memory.layerDescDreams", "Synthesized patterns and open loops promoted from daily memory.")) : t("memory.editorDefaultDescription", "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(t("memory.loadFileFailed", "Failed to load memory file"), "error"); } }, [selectFile, addToast]); const handleSaveSelectedFile = useCallback(async () => { try { await saveSelectedFile(); addToast(t("memory.memorySaved", "Memory saved"), "success"); } catch { addToast(t("memory.saveMemoryFailed", "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(t("memory.settingsSaved", "Memory settings saved"), "success"); } catch { addToast(t("memory.saveSettingsFailed", "Failed to save memory settings"), "error"); } }, [memorySettingsDirty, memorySettingsDraft, memorySettings, saveMemorySettings, addToast]); const handleInstallQmd = useCallback(async () => { try { const result = await installQmdAction(); addToast( result.qmdAvailable ? t("memory.qmdInstallSuccess", "qmd installed successfully") : t("memory.qmdInstallUnavailable", "qmd install finished, but qmd is still unavailable"), result.qmdAvailable ? "success" : "info", ); } catch { addToast(t("memory.installQmdFailed", "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 ? t("memory.retrievalTestComplete", "Memory retrieval test complete") : t("memory.retrievalTestFallback", "qmd is not installed; local fallback was used"), result.qmdAvailable ? "success" : "info", ); } catch { addToast(t("memory.retrievalTestFailed", "Failed to test memory retrieval"), "error"); } finally { setMemoryTestLoading(false); } }, [memoryTestQuery, testRetrieval, addToast]); const handleDreamNow = useCallback(async () => { try { await triggerDreamNow(); addToast(t("memory.dreamProcessingComplete", "Dream processing completed"), "success"); await reloadMemoryFiles(); } catch (error) { addToast(error instanceof Error ? error.message : t("memory.dreamProcessingFailed", "Failed to run dream processing"), "error"); } }, [triggerDreamNow, reloadMemoryFiles, addToast]); // Handle compact memory const handleCompactMemory = useCallback(async () => { try { await compactMemory(selectedFilePath); addToast(t("memory.fileCompacted", "Memory file compacted"), "success"); } catch { addToast(t("memory.compactFailed", "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 : t("memory.extractInsightsFailed", "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(t("memory.insightsSaved", "Insights saved"), "success"); } catch { addToast(t("memory.saveInsightsFailed", "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 (
{/* FNXC:Navigation 2026-06-22-01:10: Memory adopts the shared ViewHeader (CC-modeled) for a consistent main-content title row. FNXC:Memory 2026-06-22-12:00: The Memory view header should be title-only; remove the "Working memory, long-term insights, and engine status" subtitle so the tab bar becomes the first content under the header. */} {/* Tab bar */}
{/* Content area */}
{/* Working Memory Tab */} {activeTab === "working" && (
{backendStatusResolved && !isWritable && (
{t("memory.readOnlyBanner", "This memory backend is read-only. Changes cannot be saved.")}
)} {memoryFilesLoading || selectedFileLoading ? (
{t("memory.loadingFile", "Loading memory file…")}
) : ( <>
{selectedFileDirty ? t("memory.fileSwitchDirtyHint", "Save or discard the current edits before switching files.") : t("memory.fileSwitchHint", "Choose any project memory file to view or edit.")}
{selectedMemoryFile && (
{selectedMemoryFile.layer === "long-term" ? t("memory.layerLongTerm", "Long-term") : selectedMemoryFile.layer === "daily" ? t("memory.layerDaily", "Daily") : t("memory.layerDreams", "Dreams")} {selectedMemoryFile.path} {t("memory.fileSummary", "{{size}} bytes · updated {{updatedAt}}", { size: selectedMemoryFile.size.toLocaleString(), updatedAt: new Date(selectedMemoryFile.updatedAt).toLocaleString() })}
)}
{selectedLayerDescription}
{t("memory.charCount", "{{count}} characters", { count: selectedFileContent.length })}
{isWritable && selectedFileContent.length > 0 && ( )} {selectedFileDirty && isWritable && ( )}
{t("memory.dreamsEnabledHint", "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} /> {t("memory.dreamsScheduleHint", "Cron expression for dream processing.")}
{t("memory.dreamNowHint", "Manually trigger dream processing now.")}
)}
{t("memory.autoSummarizeHint", "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} /> {t("memory.compactionThresholdHint", "Memory will be compacted when it exceeds this character count")}
{ setMemorySettingsDraft((prev) => ({ ...prev, memoryAutoSummarizeSchedule: event.target.value, })); }} placeholder="0 3 * * *" disabled={settingsLoading} /> {t("memory.autoSummarizeScheduleHint", "Cron expression for auto-summarize schedule (default: daily at 3 AM)")}
)}
{!memorySettingsDraft.memoryEnabled && (
{t("memory.disabledMessage", "Memory is currently disabled. Enable memory tools in Settings to edit these automations.")}
)} {memorySettingsDirty && (
)}
)}
)} {/* Insights Tab */} {activeTab === "insights" && (
{insightsLoading ? (
{t("memory.loadingInsights", "Loading insights…")}
) : editingInsights ? ( // Raw editor mode
) : !insightsExists || parsedCategories.length === 0 ? ( // Empty state

{t("memory.noInsights", "No insights extracted yet.")}

{t("memory.noInsightsHint", "Insights are automatically extracted from working memory. Click \"Extract Now\" to trigger extraction manually.")}

) : ( // Parsed insights view <>
{totalInsights}
{t("memory.totalInsights", "Total Insights")}
{parsedCategories.length}
{t("memory.categories", "Categories")}
{lastUpdated && (
{lastUpdated}
{t("memory.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 ? (
{t("memory.loadingEngineStatus", "Loading engine status…")}
) : ( <> {/* QMD Integration Card */}

{t("memory.qmdIntegrationTitle", "QMD Integration")}

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

{t("memory.testMemorySearchTitle", "Test Memory Search")}

setMemoryTestQuery(event.target.value)} placeholder={t("memory.searchPlaceholder", "Search memory with qmd")} />
{t("memory.testSearchHint", "Runs the same qmd-backed memory_search path agents use.")} {memoryTestResult && (
{t("memory.testResultCount", "{{count}} result for \"{{query}}\"", { count: memoryTestResult.results.length, query: memoryTestResult.query, defaultValue_one: "{{count}} result for \"{{query}}\"", defaultValue_other: "{{count}} results for \"{{query}}\"" })} {t("memory.testResultStatus", "qmd {{qmdStatus}} · {{fallbackStatus}}", { qmdStatus: memoryTestResult.qmdAvailable ? t("memory.qmdStatusAvailable", "available") : t("memory.qmdStatusMissing", "missing"), fallbackStatus: memoryTestResult.usedFallback ? t("memory.localFallbackUsed", "local fallback used") : t("memory.qmdPathUsed", "qmd path used") })} {memoryTestResult.results.length > 0 ? (
    {memoryTestResult.results.map((result, index) => (
  • {result.path}:{result.lineStart}

    {result.snippet}

  • ))}
) : ( {t("memory.noMatchingMemory", "No matching memory found.")} )}
)}
{/* Backend Card */}

{t("memory.currentBackendTitle", "Current Backend")}

{ backendStatus?.currentBackend === "file" ? t("memory.backendFile", "File (.fusion/memory/, agent//memory/)") : backendStatus?.currentBackend === "readonly" ? t("memory.backendReadonly", "Read-Only") : backendStatus?.currentBackend === "qmd" ? t("memory.backendQmd", "QMD (Quantized Memory Distillation)") : (backendStatus?.currentBackend ?? "unknown") }
{backendStatus?.capabilities?.readable && ( {t("memory.capReadable", "Readable")} )} {backendStatus?.capabilities?.writable && ( {t("memory.capWritable", "Writable")} )} {backendStatus?.capabilities?.supportsAtomicWrite && ( {t("memory.capAtomicWrites", "Atomic Writes")} )} {backendStatus?.capabilities?.persistent && ( {t("memory.capPersistent", "Persistent")} )}
{/* Health Status Card */} {auditReport && (

{t("memory.healthStatusTitle", "Health Status")}

{auditReport.health === "healthy" ? t("memory.healthHealthy", "Healthy") : auditReport.health === "warning" ? t("memory.healthWarning", "Warning") : t("memory.healthIssues", "Issues Found")}
{t("memory.workingMemoryLabel", "Working Memory")}
{t("memory.sizeChars", "{{size}} chars", { size: auditReport.workingMemory.size })}
{t("memory.sectionCount", "{{count}} sections", { count: auditReport.workingMemory.sectionCount })}
{t("memory.insightsMemoryLabel", "Insights Memory")}
{t("memory.sizeChars", "{{size}} chars", { size: auditReport.insightsMemory.size })}
{t("memory.insightCount", "{{count}} insights", { count: auditReport.insightsMemory.insightCount })}
{t("memory.lastExtractionLabel", "Last Extraction")}
{auditReport.extraction.success ? ( {t("memory.extractionSuccess", "Success")} ) : ( {t("memory.extractionFailed", "Failed")} )}
{auditReport.extraction.summary || t("memory.insightsExtracted", "{{count}} insights extracted", { count: auditReport.extraction.insightCount })}
{t("memory.pruningLabel", "Pruning")}
{auditReport.pruning.applied ? ( {t("memory.pruningApplied", "Applied")} ) : ( {t("memory.pruningNotNeeded", "Not needed")} )}
{auditReport.pruning.applied && (
{auditReport.pruning.reason}
)}
)} {/* Audit Checks */} {auditReport && auditReport.checks.length > 0 && (

{t("memory.auditChecksTitle", "Audit Checks")}

{auditReport.checks.map((check) => (
{check.passed ? "✓" : "✗"}
{check.name}
{check.details}
))}
)} {/* Actions */}
{/* Note about Settings */}
{t("memory.settingsNote", "Note: Change backend type in")}
)}
)}
); }