From da682e39935bd573f9c14ed6e7c8aeb088db26bf Mon Sep 17 00:00:00 2001 From: webdevcody Date: Sat, 10 Jan 2026 20:07:50 -0500 Subject: [PATCH] feat: add memory management feature with UI components - Introduced a new MemoryView component for viewing and editing AI memory files. - Updated navigation hooks and keyboard shortcuts to include memory functionality. - Added memory file creation, deletion, and renaming capabilities. - Enhanced the sidebar navigation to support memory as a new section. - Implemented loading and saving of memory files with a markdown editor. - Integrated dialogs for creating, deleting, and renaming memory files. --- .../layout/sidebar/hooks/use-navigation.ts | 8 + apps/ui/src/components/ui/keyboard-map.tsx | 2 + apps/ui/src/components/views/memory-view.tsx | 627 ++++++++++++++++++ apps/ui/src/routes/memory.tsx | 6 + apps/ui/src/store/app-store.ts | 2 + 5 files changed, 645 insertions(+) create mode 100644 apps/ui/src/components/views/memory-view.tsx create mode 100644 apps/ui/src/routes/memory.tsx diff --git a/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts b/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts index 350bd2f8..2f1a0aa6 100644 --- a/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts +++ b/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts @@ -11,6 +11,7 @@ import { GitPullRequest, Zap, Lightbulb, + Brain, } from 'lucide-react'; import type { NavSection, NavItem } from '../types'; import type { KeyboardShortcut } from '@/hooks/use-keyboard-shortcuts'; @@ -26,6 +27,7 @@ interface UseNavigationProps { cycleNextProject: string; spec: string; context: string; + memory: string; profiles: string; board: string; agent: string; @@ -114,6 +116,12 @@ export function useNavigation({ icon: BookOpen, shortcut: shortcuts.context, }, + { + id: 'memory', + label: 'Memory', + icon: Brain, + shortcut: shortcuts.memory, + }, { id: 'profiles', label: 'AI Profiles', diff --git a/apps/ui/src/components/ui/keyboard-map.tsx b/apps/ui/src/components/ui/keyboard-map.tsx index 2e00c1e2..2ee3606b 100644 --- a/apps/ui/src/components/ui/keyboard-map.tsx +++ b/apps/ui/src/components/ui/keyboard-map.tsx @@ -87,6 +87,7 @@ const SHORTCUT_LABELS: Record = { agent: 'Agent Runner', spec: 'Spec Editor', context: 'Context', + memory: 'Memory', settings: 'Settings', profiles: 'AI Profiles', terminal: 'Terminal', @@ -115,6 +116,7 @@ const SHORTCUT_CATEGORIES: Record([]); + const [selectedFile, setSelectedFile] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + const [editedContent, setEditedContent] = useState(''); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); + const [renameFileName, setRenameFileName] = useState(''); + const [isPreviewMode, setIsPreviewMode] = useState(true); + + // Create Memory file modal state + const [isCreateMemoryOpen, setIsCreateMemoryOpen] = useState(false); + const [newMemoryName, setNewMemoryName] = useState(''); + const [newMemoryContent, setNewMemoryContent] = useState(''); + + // Get memory directory path + const getMemoryPath = useCallback(() => { + if (!currentProject) return null; + return `${currentProject.path}/.automaker/memory`; + }, [currentProject]); + + const isMarkdownFile = (filename: string): boolean => { + const ext = filename.toLowerCase().substring(filename.lastIndexOf('.')); + return ext === '.md' || ext === '.markdown'; + }; + + // Load memory files + const loadMemoryFiles = useCallback(async () => { + const memoryPath = getMemoryPath(); + if (!memoryPath) return; + + setIsLoading(true); + try { + const api = getElectronAPI(); + + // Ensure memory directory exists + await api.mkdir(memoryPath); + + // Read directory contents + const result = await api.readdir(memoryPath); + if (result.success && result.entries) { + const files: MemoryFile[] = result.entries + .filter((entry) => entry.isFile && isMarkdownFile(entry.name)) + .map((entry) => ({ + name: entry.name, + path: `${memoryPath}/${entry.name}`, + })); + setMemoryFiles(files); + } + } catch (error) { + logger.error('Failed to load memory files:', error); + } finally { + setIsLoading(false); + } + }, [getMemoryPath]); + + useEffect(() => { + loadMemoryFiles(); + }, [loadMemoryFiles]); + + // Load selected file content + const loadFileContent = useCallback(async (file: MemoryFile) => { + try { + const api = getElectronAPI(); + const result = await api.readFile(file.path); + if (result.success && result.content !== undefined) { + setEditedContent(result.content); + setSelectedFile({ ...file, content: result.content }); + setHasChanges(false); + } + } catch (error) { + logger.error('Failed to load file content:', error); + } + }, []); + + // Select a file + const handleSelectFile = (file: MemoryFile) => { + if (hasChanges) { + // Could add a confirmation dialog here + } + loadFileContent(file); + setIsPreviewMode(true); + }; + + // Save current file + const saveFile = async () => { + if (!selectedFile) return; + + setIsSaving(true); + try { + const api = getElectronAPI(); + await api.writeFile(selectedFile.path, editedContent); + setSelectedFile({ ...selectedFile, content: editedContent }); + setHasChanges(false); + } catch (error) { + logger.error('Failed to save file:', error); + } finally { + setIsSaving(false); + } + }; + + // Handle content change + const handleContentChange = (value: string) => { + setEditedContent(value); + setHasChanges(true); + }; + + // Handle create memory file + const handleCreateMemory = async () => { + const memoryPath = getMemoryPath(); + if (!memoryPath || !newMemoryName.trim()) return; + + try { + const api = getElectronAPI(); + let filename = newMemoryName.trim(); + + // Add .md extension if not provided + if (!filename.includes('.')) { + filename += '.md'; + } + + const filePath = `${memoryPath}/${filename}`; + + // Write memory file + await api.writeFile(filePath, newMemoryContent); + + // Reload files + await loadMemoryFiles(); + + // Reset and close modal + setIsCreateMemoryOpen(false); + setNewMemoryName(''); + setNewMemoryContent(''); + } catch (error) { + logger.error('Failed to create memory file:', error); + setIsCreateMemoryOpen(false); + setNewMemoryName(''); + setNewMemoryContent(''); + } + }; + + // Delete selected file + const handleDeleteFile = async () => { + if (!selectedFile) return; + + try { + const api = getElectronAPI(); + await api.deleteFile(selectedFile.path); + + setIsDeleteDialogOpen(false); + setSelectedFile(null); + setEditedContent(''); + setHasChanges(false); + await loadMemoryFiles(); + } catch (error) { + logger.error('Failed to delete file:', error); + } + }; + + // Rename selected file + const handleRenameFile = async () => { + const memoryPath = getMemoryPath(); + if (!selectedFile || !memoryPath || !renameFileName.trim()) return; + + let newName = renameFileName.trim(); + // Add .md extension if not provided + if (!newName.includes('.')) { + newName += '.md'; + } + + if (newName === selectedFile.name) { + setIsRenameDialogOpen(false); + return; + } + + try { + const api = getElectronAPI(); + const newPath = `${memoryPath}/${newName}`; + + // Check if file with new name already exists + const exists = await api.exists(newPath); + if (exists) { + logger.error('A file with this name already exists'); + return; + } + + // Read current file content + const result = await api.readFile(selectedFile.path); + if (!result.success || result.content === undefined) { + logger.error('Failed to read file for rename'); + return; + } + + // Write to new path + await api.writeFile(newPath, result.content); + + // Delete old file + await api.deleteFile(selectedFile.path); + + setIsRenameDialogOpen(false); + setRenameFileName(''); + + // Reload files and select the renamed file + await loadMemoryFiles(); + + // Update selected file with new name and path + const renamedFile: MemoryFile = { + name: newName, + path: newPath, + content: result.content, + }; + setSelectedFile(renamedFile); + } catch (error) { + logger.error('Failed to rename file:', error); + } + }; + + // Delete file from list (used by dropdown) + const handleDeleteFromList = async (file: MemoryFile) => { + try { + const api = getElectronAPI(); + await api.deleteFile(file.path); + + // Clear selection if this was the selected file + if (selectedFile?.path === file.path) { + setSelectedFile(null); + setEditedContent(''); + setHasChanges(false); + } + + await loadMemoryFiles(); + } catch (error) { + logger.error('Failed to delete file:', error); + } + }; + + if (!currentProject) { + return ( +
+

No project selected

+
+ ); + } + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+ +
+

Memory Layer

+

+ View and edit AI memory files for this project +

+
+
+
+ + +
+
+ + {/* Main content area with file list and editor */} +
+ {/* Left Panel - File List */} +
+
+

+ Memory Files ({memoryFiles.length}) +

+
+
+ {memoryFiles.length === 0 ? ( +
+ +

+ No memory files yet. +
+ Create a memory file to get started. +

+
+ ) : ( +
+ {memoryFiles.map((file) => ( +
handleSelectFile(file)} + className={cn( + 'group w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-colors cursor-pointer', + selectedFile?.path === file.path + ? 'bg-primary/20 text-foreground border border-primary/30' + : 'text-muted-foreground hover:bg-accent hover:text-foreground' + )} + data-testid={`memory-file-${file.name}`} + > + +
+ {file.name} +
+ + + + + + { + setRenameFileName(file.name); + setSelectedFile(file); + setIsRenameDialogOpen(true); + }} + data-testid={`rename-memory-file-${file.name}`} + > + + Rename + + handleDeleteFromList(file)} + className="text-red-500 focus:text-red-500" + data-testid={`delete-memory-file-${file.name}`} + > + + Delete + + + +
+ ))} +
+ )} +
+
+ + {/* Right Panel - Editor/Preview */} +
+ {selectedFile ? ( + <> + {/* File toolbar */} +
+
+ + {selectedFile.name} +
+
+ + + +
+
+ + {/* Content area */} +
+ {isPreviewMode ? ( + + {editedContent} + + ) : ( + +