import { useEffect, useState, useCallback } from 'react'; import { createLogger } from '@automaker/utils/logger'; import { useAppStore } from '@/store/app-store'; import { getElectronAPI } from '@/lib/electron'; import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; import { RefreshCw, FileText, Trash2, Save, Brain, Eye, Pencil, FilePlus, MoreVertical, } from 'lucide-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; import { Markdown } from '../ui/markdown'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; const logger = createLogger('MemoryView'); interface MemoryFile { name: string; content?: string; path: string; } export function MemoryView() { const { currentProject } = useAppStore(); const [memoryFiles, setMemoryFiles] = useState([]); 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} ) : (