import { Loader2 } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { MemoryBackendCapabilities, MemoryBackendStatus, MemoryFileInfo, MemoryRetrievalTestResult, } from "../../../api"; import { FileEditor } from "../../FileEditor"; import { SettingsToggleRow } from "../SettingsToggleRow"; import { SettingsNumberRow } from "../SettingsNumberRow"; import { SettingsTextRow } from "../SettingsTextRow"; import { SettingsHelpTip } from "../SettingsHelpTip"; import type { SectionBaseProps } from "./context"; import { LoadingSpinner } from "../../LoadingSpinner"; 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); } export interface MemorySectionMemoryProps { memoryCapabilities: MemoryBackendCapabilities | null; memoryBackendStatus: MemoryBackendStatus | null; memoryBackendLoading: boolean; memoryBackendError: string | null; memoryFiles: MemoryFileInfo[]; selectedMemoryPath: string; setSelectedMemoryPath: (path: string) => void; memoryContent: string; setMemoryContent: (content: string) => void; memoryLoading: boolean; memoryDirty: boolean; setMemoryDirty: (dirty: boolean) => void; memoryTestQuery: string; setMemoryTestQuery: (query: string) => void; memoryTestLoading: boolean; memoryTestResult: MemoryRetrievalTestResult | null; qmdInstallLoading: boolean; dreamRunning: boolean; memoryCompactLoading: boolean; onInstallQmd: () => void; onTestMemoryRetrieval: () => void; onDreamNow: () => void; onCompactMemory: () => void; onSaveMemory: () => void; } export interface MemorySectionProps extends SectionBaseProps { memory: MemorySectionMemoryProps; } export function MemorySection({ form, setForm, memory }: MemorySectionProps) { const { t } = useTranslation("app"); const { memoryCapabilities: capabilities, memoryBackendStatus: backendStatus, memoryBackendLoading: backendLoading, memoryBackendError: backendError, memoryFiles, selectedMemoryPath, setSelectedMemoryPath, memoryContent, setMemoryContent, memoryLoading, memoryDirty, setMemoryDirty, memoryTestQuery, setMemoryTestQuery, memoryTestLoading, memoryTestResult, qmdInstallLoading, dreamRunning, memoryCompactLoading, onInstallQmd, onTestMemoryRetrieval, onDreamNow, onCompactMemory, onSaveMemory, } = memory; // Determine if editing is allowed const isMemoryEnabled = form.memoryEnabled !== false; const backendStatusResolved = !backendLoading && backendStatus !== null; const isBackendWritable = backendStatusResolved ? (capabilities?.writable ?? true) : true; const isEditingAllowed = isMemoryEnabled && isBackendWritable; const selectedMemoryFile = memoryFiles.find((file) => file.path === selectedMemoryPath); const memoryLayerNames: Record = { "long-term": "Long-term", daily: "Daily", dreams: "Dreams", }; return (<> {/* FNXC:SettingsHelp 2026-07-16-12:45: Section intro moved behind the shared "?" beside the heading — operator requirement: no inline description paragraphs in Settings. */}

{t("settings.memory.memory", "Memory")}

{t("settings.memory.memoryLivesIn", " Memory lives in ")}.fusion/memory/{t("settings.memory.agentsSearchWithQmdFirstFallBackTo", ". Agents search with qmd first, fall back to local files when qmd is missing, and open exact line windows only when needed. ")}
setForm((f) => ({ ...f, memoryEnabled: v === true }))} /> {backendLoading ? (
{t("settings.memory.checkingMemoryWriteAccess", "Checking memory write access...")}
) : backendError ? (
{t("settings.memory.failedToLoadBackendStatus", "Failed to load backend status: ")}{backendError}
) : null} {backendStatusResolved && backendStatus.qmdAvailable === false && (
{t("settings.memory.qmdIsNotInstalledSearchWillUseLocal", " qmd is not installed. Search will use local files. Install indexed retrieval: ")}{backendStatus.qmdInstallCommand || "bun install -g @tobilu/qmd"}
)} setForm((f) => ({ ...f, memoryAutoSummarizeEnabled: v === true }))} /> {(form.memoryAutoSummarizeEnabled || false) && (<> {/* FNXC:MemoryCompaction 2026-07-15-17:35: An empty or unparseable threshold falls back to the 50000 schema default rather than persisting undefined: the auto-summarize scheduler reads this value directly, so a blank field must still compact at the documented default instead of disabling compaction silently. */} setForm((f) => ({ ...f, memoryAutoSummarizeThresholdChars: v || 50000 }))} /> setForm((f) => ({ ...f, memoryAutoSummarizeSchedule: v ?? "" }))} /> )} setForm((f) => ({ ...f, insightExtractionEnabled: v === true }))} /> {(form.insightExtractionEnabled || false) && ( setForm((f) => ({ ...f, insightExtractionSchedule: v ?? "" }))} />)}
{/* FNXC:MemoryDreams 2026-07-15-17:35: Dream processing reads the daily memory layer, so the toggle is disabled whenever memory tools are off — there is nothing to synthesize from. The schedule row below stays gated on both flags for the same reason. */} setForm((f) => ({ ...f, memoryDreamsEnabled: v === true }))} /> {isMemoryEnabled && form.memoryDreamsEnabled === true && (<> setForm((f) => ({ ...f, memoryDreamsSchedule: v ?? "" }))} />
{/* FNXC:SettingsHelp 2026-07-16-12:45: Inline help moved behind the shared "?" affordance — operator requirement: no inline description paragraphs in Settings, action buttons included. The tip is a sibling of the button; the button's own label still names the action. */} {t("settings.memory.manuallyTriggerDreamProcessingNow", "Manually trigger dream processing now.")}
)} {/* FNXC:SettingsSearch 2026-07-15-17:35: The retrieval tester, memory-file picker, and editor below stay on plain `form-group` markup on purpose: none of them edits a settings key. They are a transient query box, a file selector gated on unsaved edits, and a document editor, so rendering them as settings rows would file them in the settings search index as configuration an operator can set — which they are not. */}
{/* FNXC:SettingsHelp 2026-07-15-21:40: The tester is not a settings key (see the note above), but its help still hangs off the shared "?" so this section does not mix a help icon and a paragraph in adjacent rows. `settingKey` reuses the input's id — the tip only needs a stable handle for its bubble id, not a real settings key. */}
{t("settings.memory.runsTheSameQmdBackedMemorySearchPath", "Runs the same qmd-backed memory_search path agents use.")}
setMemoryTestQuery(e.target.value)} placeholder={t("settings.memory.searchMemoryWithQmd", "Search memory with qmd")}/>
{memoryTestResult && (
{memoryTestResult.results.length}{t("settings.memory.result", " result")}{memoryTestResult.results.length === 1 ? "" : "s"} {" "}{t("settings.memory.for", "for \"")}{memoryTestResult.query}" {t("settings.memory.qmd", " 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}

  • ))}
) : ({t("settings.memory.noMatchingMemoryFound", "No matching memory found.")})}
)}
{!isMemoryEnabled && (
{t("settings.memory.memoryIsCurrentlyDisabledYouCanViewThe", " Memory is currently disabled. You can view the file, but editing is read-only until memory is re-enabled. ")}
)} {isMemoryEnabled && backendStatusResolved && !isBackendWritable && (
{t("settings.memory.memoryIsConfiguredWithAReadOnlyBackend", " Memory is configured with a read-only backend. You can view the file, but saving is disabled. ")}
)} {memoryLoading ? (
) : (
{/* FNXC:SettingsHelp 2026-07-16-12:45: The descriptive branch moved behind the shared "?" beside the label — operator requirement: no inline description paragraphs in Settings. The dirty-state line below stays inline: it is the live reason the select is DISABLED, not help, and must be visible without opening a tip. */}
Choose any project memory file to view or edit. Dreams is selected by default.
{memoryDirty && (Save or discard the current edits before switching files.)}
{selectedMemoryFile && (
{memoryLayerNames[selectedMemoryFile.layer]} {selectedMemoryFile.path} {selectedMemoryFile.size.toLocaleString()}{t("settings.memory.bytesUpdated", " bytes \u00B7 updated ")}{new Date(selectedMemoryFile.updatedAt).toLocaleString()}
)}
{/* FNXC:SettingsHelp 2026-07-16-12:45: Per-layer orientation copy moved behind the shared "?" beside the editor label — operator requirement: no inline description paragraphs in Settings. The bubble content still tracks the file picker, so it always describes the currently selected file. */}
{selectedMemoryFile?.layer === "long-term" && "Curated durable decisions, conventions, constraints, and pitfalls promoted from dreams."} {selectedMemoryFile?.layer === "daily" && "Raw daily observations, open loops, and running context for dream processing."} {selectedMemoryFile?.layer === "dreams" && "Synthesized patterns and open loops promoted from daily memory."} {!selectedMemoryFile && "Edits the selected memory file."}
{ setMemoryContent(content); setMemoryDirty(true); }} readOnly={!isEditingAllowed} filePath={selectedMemoryPath}/>
)} {!memoryLoading && (
{/* FNXC:SettingsHelp 2026-07-16-12:45: The descriptive branch moved behind the shared "?" beside the action button — operator requirement: no inline description paragraphs in Settings. The dirty-state line stays inline: it is the live reason the button is DISABLED, not help. */} {`Compacts ${selectedMemoryPath} and writes the result back to the same file.`} {memoryDirty && (Save or discard edits before compacting this file.)}
)} {memoryDirty && isEditingAllowed && (
)} {memoryDirty && !isEditingAllowed && (
{t("settings.memory.cannotSave", "Cannot save: ")}{isMemoryEnabled ? "Backend is read-only" : "Memory is disabled"}
)} ); } export default MemorySection;