import { useEffect, useMemo, useState } from 'react' import type { MouseEvent, ReactNode } from 'react' import { BookOpenText, ChevronDown, ChevronRight, Database, FileText, Folder, FolderGit2, FolderOpen, MoreHorizontal, Pin, PinOff, 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 { useOpenTargetStore } from '../stores/openTargetStore' import { useSessionStore } from '../stores/sessionStore' import { useUIStore } from '../stores/uiStore' import type { MemoryFile, MemoryProject } from '../types/memory' const DEFAULT_MEMORY_PATH = 'MEMORY.md' const PINNED_MEMORY_PROJECTS_STORAGE_KEY = 'cc-haha-memory-pinned-projects' 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 addToast = useUIStore((s) => s.addToast) 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 [pinnedProjectIds, setPinnedProjectIds] = useState>(() => readPinnedMemoryProjects()) const [projectMenu, setProjectMenu] = useState<{ projectId: string; x: number; y: number } | null>(null) 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 orderedProjects = useMemo( () => orderMemoryProjects(filteredProjects, pinnedProjectIds), [filteredProjects, pinnedProjectIds], ) const pinnedProjects = useMemo( () => orderedProjects.filter((project) => pinnedProjectIds.has(project.id)), [orderedProjects, pinnedProjectIds], ) const regularProjects = useMemo( () => orderedProjects.filter((project) => !pinnedProjectIds.has(project.id)), [orderedProjects, pinnedProjectIds], ) const filteredFiles = useMemo( () => filterFiles(files, resourceQuery), [files, resourceQuery], ) const fileTree = useMemo( () => buildMemoryFileTree(filteredFiles), [filteredFiles], ) const previewContent = stripMarkdownFrontmatter(draftContent) useEffect(() => { void fetchProjects(activeCwd) }, [activeCwd, fetchProjects]) useEffect(() => { if (!selectedProjectId) return void fetchFiles(selectedProjectId) }, [fetchFiles, selectedProjectId]) useEffect(() => { if (!selectedProjectId) return setExpandedProjectId(selectedProjectId) }, [selectedProjectId]) useEffect(() => { if (!projectMenu) return const close = () => setProjectMenu(null) const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') setProjectMenu(null) } document.addEventListener('click', close) document.addEventListener('keydown', handleKeyDown) return () => { document.removeEventListener('click', close) document.removeEventListener('keydown', handleKeyDown) } }, [projectMenu]) 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 handleRefresh = () => { void fetchProjects(activeCwd) if (selectedProjectId) { void fetchFiles(selectedProjectId) } } const handleProjectToggle = (projectId: string) => { if (expandedProjectId === projectId) { setExpandedProjectId(null) return } setExpandedProjectId(projectId) if (projectId !== selectedProjectId) { selectProject(projectId) } } const handleFileOpen = (file: MemoryFile) => { if (!selectedProjectId || file.path === selectedFile?.path) return void openFile(selectedProjectId, file.path) } const togglePinnedProject = (projectId: string) => { setProjectMenu(null) setPinnedProjectIds((previous) => { const next = new Set(previous) if (next.has(projectId)) { next.delete(projectId) } else { next.add(projectId) } writePinnedMemoryProjects(next) return next }) } const openProjectInFinder = async (project: MemoryProject) => { setProjectMenu(null) try { const store = useOpenTargetStore.getState() await store.ensureTargets() const latest = useOpenTargetStore.getState() const target = latest.targets.find((item) => item.id === 'finder') ?? latest.targets.find((item) => item.kind === 'file_manager') if (!target) { throw new Error(t('settings.memory.openInFinderUnavailable')) } await latest.openTarget(target.id, resolveMemoryProjectOpenPath(project)) } catch (error) { addToast({ type: 'error', message: error instanceof Error ? error.message : t('settings.memory.openInFinderFailed'), }) } } 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()) 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 ? (
{t('settings.memory.editor')} MARKDOWN