import { useEffect, useMemo, useState } from 'react' import type { ReactNode } from 'react' import { BookOpenText, ChevronDown, ChevronRight, Database, FileText, Folder, FolderGit2, PencilLine, RefreshCw, RotateCcw, Save, Search, X } from 'lucide-react' import { Button } from '../components/shared/Button' import { MarkdownRenderer } from '../components/markdown/MarkdownRenderer' import { useTranslation } from '../i18n' import { formatBytes } from '../lib/formatBytes' import { useMemoryStore } from '../stores/memoryStore' import { useSessionStore } from '../stores/sessionStore' import { useUIStore } from '../stores/uiStore' import type { MemoryFile, MemoryProject } from '../types/memory' const DEFAULT_MEMORY_PATH = 'MEMORY.md' export function MemorySettings() { const t = useTranslation() const { projects, files, selectedProjectId, selectedFile, draftContent, isLoadingProjects, isLoadingFiles, isLoadingFile, isSaving, error, lastSavedAt, fetchProjects, selectProject, fetchFiles, openFile, updateDraft, saveFile, } = useMemoryStore() const sessions = useSessionStore((s) => s.sessions) const activeSessionId = useSessionStore((s) => s.activeSessionId) const pendingMemoryPath = useUIStore((s) => s.pendingMemoryPath) const setPendingMemoryPath = useUIStore((s) => s.setPendingMemoryPath) const [resourceQuery, setResourceQuery] = useState('') const [expandedProjectId, setExpandedProjectId] = useState(null) const [collapsedFolders, setCollapsedFolders] = useState>(new Set()) const [isEditing, setIsEditing] = useState(false) const activeSession = useMemo( () => sessions.find((session) => session.id === activeSessionId), [activeSessionId, sessions], ) const activeCwd = activeSession?.workDir || activeSession?.projectPath || undefined const selectedProject = projects.find((project) => project.id === selectedProjectId) ?? null const isDirty = Boolean(selectedFile && draftContent !== selectedFile.content) const filteredProjects = useMemo( () => filterProjects(projects, resourceQuery, selectedProjectId, files), [files, projects, resourceQuery, selectedProjectId], ) const filteredFiles = useMemo( () => filterFiles(files, resourceQuery), [files, resourceQuery], ) const fileTree = useMemo( () => buildMemoryFileTree(filteredFiles), [filteredFiles], ) const previewContent = stripMarkdownFrontmatter(draftContent) const selectedFilePath = selectedFile?.path ?? null useEffect(() => { void fetchProjects(activeCwd) }, [activeCwd, fetchProjects]) useEffect(() => { if (!selectedProjectId) return void fetchFiles(selectedProjectId) }, [fetchFiles, selectedProjectId]) useEffect(() => { if (!selectedProjectId) return setExpandedProjectId(selectedProjectId) }, [selectedProjectId]) useEffect(() => { setIsEditing(false) }, [selectedFilePath]) useEffect(() => { if (!selectedProjectId || selectedFile || isLoadingFiles || isLoadingFile) return if (pendingMemoryPath) return const firstFile = files[0] if (firstFile) { void openFile(selectedProjectId, firstFile.path) } }, [files, isLoadingFile, isLoadingFiles, openFile, pendingMemoryPath, selectedFile, selectedProjectId]) useEffect(() => { if (!pendingMemoryPath || isLoadingProjects || projects.length === 0) return const target = resolveMemoryFileTarget(projects, pendingMemoryPath) if (!target) { setPendingMemoryPath(null) return } if (selectedProjectId !== target.projectId) { selectProject(target.projectId) return } if (selectedFile?.path === target.path && !isLoadingFile) { setPendingMemoryPath(null) return } void openFile(target.projectId, target.path).then(() => { setPendingMemoryPath(null) }) }, [ isLoadingFile, isLoadingProjects, openFile, pendingMemoryPath, projects, selectProject, selectedFile?.path, selectedProjectId, setPendingMemoryPath, ]) const canLeaveDirtyEdit = () => { if (!isEditing || !isDirty) return true return window.confirm(t('settings.memory.discardUnsavedConfirm')) } const handleRefresh = () => { if (!canLeaveDirtyEdit()) return void fetchProjects(activeCwd) if (selectedProjectId) { void fetchFiles(selectedProjectId) } } const handleProjectToggle = (projectId: string) => { if (expandedProjectId === projectId) { setExpandedProjectId(null) return } if (projectId !== selectedProjectId && !canLeaveDirtyEdit()) return setExpandedProjectId(projectId) if (projectId !== selectedProjectId) { selectProject(projectId) } } const handleFileOpen = (file: MemoryFile) => { if (!selectedProjectId || file.path === selectedFile?.path) return if (!canLeaveDirtyEdit()) return void openFile(selectedProjectId, file.path) } const handleSave = async () => { if (!selectedFile) return if (!isDirty) { setIsEditing(false) return } const saved = await saveFile() if (saved) { setIsEditing(false) } } useEffect(() => { if (!isEditing || !selectedFile) return const handleKeyDown = (event: KeyboardEvent) => { if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 's') return event.preventDefault() void handleSave() } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }) const handlePreviewLinkClick = (href: string): boolean => { if (!selectedProjectId || !selectedFile) return false const targetPath = resolveMarkdownMemoryLink( href, selectedFile.path, selectedProject?.memoryDir, files, ) if (!targetPath || targetPath === selectedFile.path) return false if (!canLeaveDirtyEdit()) return true void openFile(selectedProjectId, targetPath) return true } const toggleFolder = (path: string) => { setCollapsedFolders((previous) => { const next = new Set(previous) if (next.has(path)) { next.delete(path) } else { next.add(path) } return next }) } const forceExpandFiles = Boolean(resourceQuery.trim()) const handleCancelEdit = () => { if (selectedFile) { updateDraft(selectedFile.content) } setIsEditing(false) } return (

{t('settings.memory.title')}

{t('settings.memory.projects')}

{error && (
{error}
)}

{selectedFile?.path ? fileNameFromPath(selectedFile.path) : t('settings.memory.noFileSelected')}

{isDirty && {t('settings.memory.unsaved')}} {lastSavedAt && !isDirty && {t('settings.memory.saved')}}

{selectedProject?.memoryDir ?? t('settings.memory.selectProject')}

{selectedFile ? ( <> {formatBytes(selectedFile.bytes)} {selectedFile.updatedAt ? {formatDate(selectedFile.updatedAt)} : null} ) : null}
{selectedFile ? ( isEditing ? (
{t('settings.memory.editor')} MARKDOWN