@@ -306,6 +328,36 @@ export function MemorySettings() {
)
}
+function Breadcrumb({
+ project,
+ filePath,
+ fallbackProject,
+ fallbackFile,
+}: {
+ project: MemoryProject | null
+ filePath?: string
+ fallbackProject: string
+ fallbackFile: string
+}) {
+ const projectLabel = project ? projectDisplayName(project.label) : fallbackProject
+ const parts = filePath ? [projectLabel, ...filePath.split('/').filter(Boolean)] : [projectLabel, fallbackFile]
+ return (
+
+ )
+}
+
function SearchField({
value,
onChange,
@@ -359,38 +411,80 @@ function PanelHeader({ icon, title, meta }: { icon?: ReactNode; title: string; m
)
}
-function ProjectRow({
+function ProjectTreeRow({
project,
+ expanded,
active,
- onSelect,
+ loading,
+ fileTree,
+ activePath,
+ collapsedFolders,
+ forceExpanded,
+ onToggle,
+ onToggleFolder,
+ onFileSelect,
+ emptyText,
}: {
project: MemoryProject
+ expanded: boolean
active: boolean
- onSelect: () => void
+ loading: boolean
+ fileTree: MemoryTreeNode[]
+ activePath: string | null
+ collapsedFolders: Set
+ forceExpanded: boolean
+ onToggle: () => void
+ onToggleFolder: (path: string) => void
+ onFileSelect: (file: MemoryFile) => void
+ emptyText: string
}) {
const t = useTranslation()
const display = projectDisplayName(project.label)
return (
-
+
+
+
+ {expanded ? (
+
+ {loading ? (
+
{t('common.loading')}
+ ) : fileTree.length === 0 ? (
+
{emptyText}
+ ) : (
+ fileTree.map((node) => (
+
+ ))
+ )}
+
+ ) : null}
+
)
}
@@ -398,46 +492,95 @@ function FileRow({
file,
active,
onSelect,
+ depth = 0,
}: {
file: MemoryFile
active: boolean
onSelect: () => void
+ depth?: number
}) {
- const t = useTranslation()
return (
)
}
+function MemoryTreeRow({
+ node,
+ depth,
+ activePath,
+ collapsedFolders,
+ forceExpanded,
+ onToggleFolder,
+ onFileSelect,
+}: {
+ node: MemoryTreeNode
+ depth: number
+ activePath: string | null
+ collapsedFolders: Set
+ forceExpanded: boolean
+ onToggleFolder: (path: string) => void
+ onFileSelect: (file: MemoryFile) => void
+}) {
+ const t = useTranslation()
+ if (node.kind === 'file') {
+ return (
+ onFileSelect(node.file)}
+ />
+ )
+ }
+
+ const isCollapsed = !forceExpanded && collapsedFolders.has(node.path)
+ return (
+
+
+ {!isCollapsed ? (
+
+ {node.children.map((child) => (
+
+ ))}
+
+ ) : null}
+
+ )
+}
+
function Badge({ children }: { children: string }) {
return (
@@ -463,11 +606,19 @@ function normalizeSearch(value: string): string {
return value.toLowerCase().replace(/\\/g, '/').replace(/\/+/g, '/').trim()
}
-function filterProjects(projects: MemoryProject[], query: string): MemoryProject[] {
+function filterProjects(
+ projects: MemoryProject[],
+ query: string,
+ selectedProjectId: string | null,
+ selectedProjectFiles: MemoryFile[],
+): MemoryProject[] {
const normalized = normalizeSearch(query)
if (!normalized) return projects
return projects.filter((project) =>
- normalizeSearch(`${project.label} ${project.memoryDir} ${project.id}`).includes(normalized),
+ normalizeSearch(`${project.label} ${project.memoryDir} ${project.id}`).includes(normalized) ||
+ (project.id === selectedProjectId && selectedProjectFiles.some((file) =>
+ normalizeSearch(`${file.title} ${file.path} ${file.description ?? ''} ${file.type ?? ''}`).includes(normalized),
+ )),
)
}
@@ -516,6 +667,88 @@ function resolveMemoryFileTarget(projects: MemoryProject[], absolutePath: string
return null
}
+type MemoryTreeNode =
+ | {
+ kind: 'folder'
+ id: string
+ name: string
+ path: string
+ fileCount: number
+ children: MemoryTreeNode[]
+ }
+ | {
+ kind: 'file'
+ id: string
+ name: string
+ path: string
+ file: MemoryFile
+ }
+
+type MutableFolderNode = Extract
+
+function buildMemoryFileTree(files: MemoryFile[]): MemoryTreeNode[] {
+ const root: MutableFolderNode = {
+ kind: 'folder',
+ id: '__root__',
+ name: '__root__',
+ path: '',
+ fileCount: 0,
+ children: [],
+ }
+
+ const folders = new Map([['', root]])
+ for (const file of files) {
+ const parts = file.path.split('/').filter(Boolean)
+ let parent = root
+ parts.slice(0, -1).forEach((part, index) => {
+ const folderPath = parts.slice(0, index + 1).join('/')
+ let folder = folders.get(folderPath)
+ if (!folder) {
+ folder = {
+ kind: 'folder',
+ id: `folder:${folderPath}`,
+ name: part,
+ path: folderPath,
+ fileCount: 0,
+ children: [],
+ }
+ folders.set(folderPath, folder)
+ parent.children.push(folder)
+ }
+ folder.fileCount += 1
+ parent = folder
+ })
+ parent.children.push({
+ kind: 'file',
+ id: `file:${file.path}`,
+ name: parts[parts.length - 1] ?? file.name,
+ path: file.path,
+ file,
+ })
+ }
+
+ sortMemoryTree(root.children)
+ return root.children
+}
+
+function sortMemoryTree(nodes: MemoryTreeNode[]): void {
+ nodes.sort((a, b) => {
+ if (a.kind !== b.kind) return a.kind === 'folder' ? -1 : 1
+ const aIndex = a.kind === 'file' ? a.file.isIndex : false
+ const bIndex = b.kind === 'file' ? b.file.isIndex : false
+ if (aIndex !== bIndex) return aIndex ? -1 : 1
+ return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
+ })
+ for (const node of nodes) {
+ if (node.kind === 'folder') sortMemoryTree(node.children)
+ }
+}
+
+function fileNameFromPath(path: string): string {
+ const parts = path.split('/').filter(Boolean)
+ return parts[parts.length - 1] ?? path
+}
+
function formatDate(value: string): string {
const date = new Date(value)
if (Number.isNaN(date.getTime())) return ''