import { useEffect, useMemo, useState } from 'react'
import type { ReactNode } from 'react'
import { BookOpenText, Database, FileText, FolderGit2, 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 [projectQuery, setProjectQuery] = useState('')
const [fileQuery, setFileQuery] = useState('')
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, projectQuery),
[projectQuery, projects],
)
const filteredFiles = useMemo(
() => filterFiles(files, fileQuery),
[fileQuery, files],
)
const previewContent = stripMarkdownFrontmatter(draftContent)
useEffect(() => {
void fetchProjects(activeCwd)
}, [activeCwd, fetchProjects])
useEffect(() => {
if (!selectedProjectId) return
void fetchFiles(selectedProjectId)
}, [fetchFiles, selectedProjectId])
useEffect(() => {
if (!projectQuery.trim() || filteredProjects.length === 0) return
if (selectedProjectId && filteredProjects.some((project) => project.id === selectedProjectId)) return
selectProject(filteredProjects[0]!.id)
}, [filteredProjects, projectQuery, selectProject, selectedProjectId])
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 handleProjectSelect = (projectId: string) => {
if (projectId === selectedProjectId) return
selectProject(projectId)
}
const handleFileOpen = (file: MemoryFile) => {
if (!selectedProjectId || file.path === selectedFile?.path) return
void openFile(selectedProjectId, file.path)
}
return (
{error && (
{error}
)}
{selectedFile?.path ?? t('settings.memory.noFileSelected')}
{isDirty && {t('settings.memory.unsaved')}}
{lastSavedAt && !isDirty && {t('settings.memory.saved')}}
{selectedProject?.memoryDir ?? t('settings.memory.selectProject')}
{selectedFile ? (
{t('settings.memory.editor')}
{formatBytes(selectedFile.bytes)}
{t('settings.memory.preview')}
{selectedFile.updatedAt ? formatDate(selectedFile.updatedAt) : ''}
) : (
} text={isLoadingFile ? t('common.loading') : t('settings.memory.selectFile')} />
)}
)
}
function SearchField({
value,
onChange,
placeholder,
ariaLabel,
clearLabel,
}: {
value: string
onChange: (value: string) => void
placeholder: string
ariaLabel: string
clearLabel: string
}) {
return (
onChange(event.target.value)}
placeholder={placeholder}
className="h-10 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] pl-9 pr-9 text-sm text-[var(--color-text-primary)] outline-none transition-colors duration-150 placeholder:text-[var(--color-text-tertiary)] focus:border-[var(--color-border-focus)] focus:shadow-[var(--shadow-focus-ring)]"
/>
{value ? (
) : null}
)
}
function PanelHeader({ icon, title, meta }: { icon?: ReactNode; title: string; meta: string }) {
return (
{icon ? {icon} : null}
{title}
{meta}
)
}
function ProjectRow({
project,
active,
onSelect,
}: {
project: MemoryProject
active: boolean
onSelect: () => void
}) {
const t = useTranslation()
const display = projectDisplayName(project.label)
return (
)
}
function FileRow({
file,
active,
onSelect,
}: {
file: MemoryFile
active: boolean
onSelect: () => void
}) {
const t = useTranslation()
return (
)
}
function Badge({ children }: { children: string }) {
return (
{children}
)
}
function EmptyState({ icon, text }: { icon?: ReactNode; text: string }) {
return (
{icon ? (
{icon}
) : null}
{text}
)
}
function normalizeSearch(value: string): string {
return value.toLowerCase().replace(/\\/g, '/').replace(/\/+/g, '/').trim()
}
function filterProjects(projects: MemoryProject[], query: string): MemoryProject[] {
const normalized = normalizeSearch(query)
if (!normalized) return projects
return projects.filter((project) =>
normalizeSearch(`${project.label} ${project.memoryDir} ${project.id}`).includes(normalized),
)
}
function filterFiles(files: MemoryFile[], query: string): MemoryFile[] {
const normalized = normalizeSearch(query)
if (!normalized) return files
return files.filter((file) =>
normalizeSearch(`${file.title} ${file.path} ${file.description ?? ''} ${file.type ?? ''}`).includes(normalized),
)
}
function projectDisplayName(label: string): string {
const normalized = label.replace(/\\/g, '/').replace(/\/+/g, '/').replace(/\/$/, '')
const parts = normalized.split('/').filter(Boolean)
if (parts.length >= 2) return `${parts[parts.length - 2]}/${parts[parts.length - 1]}`
return parts[0] ?? label
}
function stripMarkdownFrontmatter(content: string): string {
if (!content.startsWith('---')) return content
const end = content.indexOf('\n---', 3)
if (end < 0) return content
const after = content.indexOf('\n', end + 4)
return after < 0 ? '' : content.slice(after + 1).trimStart()
}
function normalizeFsPath(value: string): string {
return value.replace(/\\/g, '/').replace(/\/+$/, '')
}
function resolveMemoryFileTarget(projects: MemoryProject[], absolutePath: string): { projectId: string; path: string } | null {
const target = normalizeFsPath(absolutePath)
for (const project of projects) {
const memoryDir = normalizeFsPath(project.memoryDir)
if (!memoryDir) continue
if (target === memoryDir) {
return { projectId: project.id, path: DEFAULT_MEMORY_PATH }
}
if (target.startsWith(`${memoryDir}/`)) {
return {
projectId: project.id,
path: target.slice(memoryDir.length + 1),
}
}
}
return null
}
function formatDate(value: string): string {
const date = new Date(value)
if (Number.isNaN(date.getTime())) return ''
return new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date)
}