mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 20:03:37 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da682e3993 | ||
|
|
299b838400 | ||
|
|
c5d0a8be7d |
@@ -32,7 +32,7 @@
|
|||||||
"@automaker/prompts": "1.0.0",
|
"@automaker/prompts": "1.0.0",
|
||||||
"@automaker/types": "1.0.0",
|
"@automaker/types": "1.0.0",
|
||||||
"@automaker/utils": "1.0.0",
|
"@automaker/utils": "1.0.0",
|
||||||
"@modelcontextprotocol/sdk": "1.25.1",
|
"@modelcontextprotocol/sdk": "1.25.2",
|
||||||
"@openai/codex-sdk": "^0.77.0",
|
"@openai/codex-sdk": "^0.77.0",
|
||||||
"cookie-parser": "1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
GitPullRequest,
|
GitPullRequest,
|
||||||
Zap,
|
Zap,
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
|
Brain,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { NavSection, NavItem } from '../types';
|
import type { NavSection, NavItem } from '../types';
|
||||||
import type { KeyboardShortcut } from '@/hooks/use-keyboard-shortcuts';
|
import type { KeyboardShortcut } from '@/hooks/use-keyboard-shortcuts';
|
||||||
@@ -26,6 +27,7 @@ interface UseNavigationProps {
|
|||||||
cycleNextProject: string;
|
cycleNextProject: string;
|
||||||
spec: string;
|
spec: string;
|
||||||
context: string;
|
context: string;
|
||||||
|
memory: string;
|
||||||
profiles: string;
|
profiles: string;
|
||||||
board: string;
|
board: string;
|
||||||
agent: string;
|
agent: string;
|
||||||
@@ -114,6 +116,12 @@ export function useNavigation({
|
|||||||
icon: BookOpen,
|
icon: BookOpen,
|
||||||
shortcut: shortcuts.context,
|
shortcut: shortcuts.context,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'memory',
|
||||||
|
label: 'Memory',
|
||||||
|
icon: Brain,
|
||||||
|
shortcut: shortcuts.memory,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'profiles',
|
id: 'profiles',
|
||||||
label: 'AI Profiles',
|
label: 'AI Profiles',
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ const SHORTCUT_LABELS: Record<keyof KeyboardShortcuts, string> = {
|
|||||||
agent: 'Agent Runner',
|
agent: 'Agent Runner',
|
||||||
spec: 'Spec Editor',
|
spec: 'Spec Editor',
|
||||||
context: 'Context',
|
context: 'Context',
|
||||||
|
memory: 'Memory',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
profiles: 'AI Profiles',
|
profiles: 'AI Profiles',
|
||||||
terminal: 'Terminal',
|
terminal: 'Terminal',
|
||||||
@@ -115,6 +116,7 @@ const SHORTCUT_CATEGORIES: Record<keyof KeyboardShortcuts, 'navigation' | 'ui' |
|
|||||||
agent: 'navigation',
|
agent: 'navigation',
|
||||||
spec: 'navigation',
|
spec: 'navigation',
|
||||||
context: 'navigation',
|
context: 'navigation',
|
||||||
|
memory: 'navigation',
|
||||||
settings: 'navigation',
|
settings: 'navigation',
|
||||||
profiles: 'navigation',
|
profiles: 'navigation',
|
||||||
terminal: 'navigation',
|
terminal: 'navigation',
|
||||||
|
|||||||
627
apps/ui/src/components/views/memory-view.tsx
Normal file
627
apps/ui/src/components/views/memory-view.tsx
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
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<MemoryFile[]>([]);
|
||||||
|
const [selectedFile, setSelectedFile] = useState<MemoryFile | null>(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 (
|
||||||
|
<div
|
||||||
|
className="flex-1 flex items-center justify-center"
|
||||||
|
data-testid="memory-view-no-project"
|
||||||
|
>
|
||||||
|
<p className="text-muted-foreground">No project selected</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex items-center justify-center" data-testid="memory-view-loading">
|
||||||
|
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="memory-view">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Brain className="w-5 h-5 text-muted-foreground" />
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-bold">Memory Layer</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
View and edit AI memory files for this project
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={loadMemoryFiles}
|
||||||
|
data-testid="refresh-memory-button"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsCreateMemoryOpen(true)}
|
||||||
|
data-testid="create-memory-button"
|
||||||
|
>
|
||||||
|
<FilePlus className="w-4 h-4 mr-2" />
|
||||||
|
Create Memory File
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content area with file list and editor */}
|
||||||
|
<div className="flex-1 flex overflow-hidden">
|
||||||
|
{/* Left Panel - File List */}
|
||||||
|
<div className="w-64 border-r border-border flex flex-col overflow-hidden">
|
||||||
|
<div className="p-3 border-b border-border">
|
||||||
|
<h2 className="text-sm font-semibold text-muted-foreground">
|
||||||
|
Memory Files ({memoryFiles.length})
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto p-2" data-testid="memory-file-list">
|
||||||
|
{memoryFiles.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-center p-4">
|
||||||
|
<Brain className="w-8 h-8 text-muted-foreground mb-2" />
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
No memory files yet.
|
||||||
|
<br />
|
||||||
|
Create a memory file to get started.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{memoryFiles.map((file) => (
|
||||||
|
<div
|
||||||
|
key={file.path}
|
||||||
|
onClick={() => 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}`}
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4 flex-shrink-0" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<span className="truncate text-sm block">{file.name}</span>
|
||||||
|
</div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-accent rounded transition-opacity"
|
||||||
|
data-testid={`memory-file-menu-${file.name}`}
|
||||||
|
>
|
||||||
|
<MoreVertical className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setRenameFileName(file.name);
|
||||||
|
setSelectedFile(file);
|
||||||
|
setIsRenameDialogOpen(true);
|
||||||
|
}}
|
||||||
|
data-testid={`rename-memory-file-${file.name}`}
|
||||||
|
>
|
||||||
|
<Pencil className="w-4 h-4 mr-2" />
|
||||||
|
Rename
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleDeleteFromList(file)}
|
||||||
|
className="text-red-500 focus:text-red-500"
|
||||||
|
data-testid={`delete-memory-file-${file.name}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Panel - Editor/Preview */}
|
||||||
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
{selectedFile ? (
|
||||||
|
<>
|
||||||
|
{/* File toolbar */}
|
||||||
|
<div className="flex items-center justify-between p-3 border-b border-border bg-card">
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<FileText className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
||||||
|
<span className="text-sm font-medium truncate">{selectedFile.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsPreviewMode(!isPreviewMode)}
|
||||||
|
data-testid="toggle-preview-mode"
|
||||||
|
>
|
||||||
|
{isPreviewMode ? (
|
||||||
|
<>
|
||||||
|
<Pencil className="w-4 h-4 mr-2" />
|
||||||
|
Edit
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Eye className="w-4 h-4 mr-2" />
|
||||||
|
Preview
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={saveFile}
|
||||||
|
disabled={!hasChanges || isSaving}
|
||||||
|
data-testid="save-memory-file"
|
||||||
|
>
|
||||||
|
<Save className="w-4 h-4 mr-2" />
|
||||||
|
{isSaving ? 'Saving...' : hasChanges ? 'Save' : 'Saved'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsDeleteDialogOpen(true)}
|
||||||
|
className="text-red-500 hover:text-red-400 hover:border-red-500/50"
|
||||||
|
data-testid="delete-memory-file"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content area */}
|
||||||
|
<div className="flex-1 overflow-hidden p-4">
|
||||||
|
{isPreviewMode ? (
|
||||||
|
<Card className="h-full overflow-auto p-4" data-testid="markdown-preview">
|
||||||
|
<Markdown>{editedContent}</Markdown>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card className="h-full overflow-hidden">
|
||||||
|
<textarea
|
||||||
|
className="w-full h-full p-4 font-mono text-sm bg-transparent resize-none focus:outline-none"
|
||||||
|
value={editedContent}
|
||||||
|
onChange={(e) => handleContentChange(e.target.value)}
|
||||||
|
placeholder="Enter memory content here..."
|
||||||
|
spellCheck={false}
|
||||||
|
data-testid="memory-editor"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Brain className="w-12 h-12 text-muted-foreground mx-auto mb-3" />
|
||||||
|
<p className="text-foreground-secondary">Select a file to view or edit</p>
|
||||||
|
<p className="text-muted-foreground text-sm mt-1">
|
||||||
|
Memory files help AI agents learn from past interactions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Create Memory Dialog */}
|
||||||
|
<Dialog open={isCreateMemoryOpen} onOpenChange={setIsCreateMemoryOpen}>
|
||||||
|
<DialogContent
|
||||||
|
data-testid="create-memory-dialog"
|
||||||
|
className="w-[60vw] max-w-[60vw] max-h-[80vh] flex flex-col"
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create Memory File</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a new memory file to store learnings and patterns for AI agents.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-4 flex-1 overflow-auto">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="memory-filename">File Name</Label>
|
||||||
|
<Input
|
||||||
|
id="memory-filename"
|
||||||
|
value={newMemoryName}
|
||||||
|
onChange={(e) => setNewMemoryName(e.target.value)}
|
||||||
|
placeholder="my-learnings.md"
|
||||||
|
data-testid="new-memory-name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="memory-content">Content</Label>
|
||||||
|
<textarea
|
||||||
|
id="memory-content"
|
||||||
|
value={newMemoryContent}
|
||||||
|
onChange={(e) => setNewMemoryContent(e.target.value)}
|
||||||
|
placeholder="Enter your memory content here..."
|
||||||
|
className="w-full h-60 p-3 font-mono text-sm bg-background border border-border rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent"
|
||||||
|
spellCheck={false}
|
||||||
|
data-testid="new-memory-content"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setIsCreateMemoryOpen(false);
|
||||||
|
setNewMemoryName('');
|
||||||
|
setNewMemoryContent('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateMemory}
|
||||||
|
disabled={!newMemoryName.trim()}
|
||||||
|
data-testid="confirm-create-memory"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Dialog */}
|
||||||
|
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||||
|
<DialogContent data-testid="delete-memory-dialog">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete Memory File</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to delete "{selectedFile?.name}"? This action cannot be undone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsDeleteDialogOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDeleteFile}
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
data-testid="confirm-delete-file"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Rename Dialog */}
|
||||||
|
<Dialog open={isRenameDialogOpen} onOpenChange={setIsRenameDialogOpen}>
|
||||||
|
<DialogContent data-testid="rename-memory-dialog">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Rename Memory File</DialogTitle>
|
||||||
|
<DialogDescription>Enter a new name for "{selectedFile?.name}".</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="rename-filename">File Name</Label>
|
||||||
|
<Input
|
||||||
|
id="rename-filename"
|
||||||
|
value={renameFileName}
|
||||||
|
onChange={(e) => setRenameFileName(e.target.value)}
|
||||||
|
placeholder="Enter new filename"
|
||||||
|
data-testid="rename-file-input"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && renameFileName.trim()) {
|
||||||
|
handleRenameFile();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setIsRenameDialogOpen(false);
|
||||||
|
setRenameFileName('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleRenameFile}
|
||||||
|
disabled={!renameFileName.trim() || renameFileName === selectedFile?.name}
|
||||||
|
data-testid="confirm-rename-file"
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
apps/ui/src/routes/memory.tsx
Normal file
6
apps/ui/src/routes/memory.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
import { MemoryView } from '@/components/views/memory-view';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/memory')({
|
||||||
|
component: MemoryView,
|
||||||
|
});
|
||||||
@@ -217,6 +217,7 @@ export interface KeyboardShortcuts {
|
|||||||
agent: string;
|
agent: string;
|
||||||
spec: string;
|
spec: string;
|
||||||
context: string;
|
context: string;
|
||||||
|
memory: string;
|
||||||
settings: string;
|
settings: string;
|
||||||
profiles: string;
|
profiles: string;
|
||||||
terminal: string;
|
terminal: string;
|
||||||
@@ -252,6 +253,7 @@ export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = {
|
|||||||
agent: 'A',
|
agent: 'A',
|
||||||
spec: 'D',
|
spec: 'D',
|
||||||
context: 'C',
|
context: 'C',
|
||||||
|
memory: 'Y',
|
||||||
settings: 'S',
|
settings: 'S',
|
||||||
profiles: 'M',
|
profiles: 'M',
|
||||||
terminal: 'T',
|
terminal: 'T',
|
||||||
|
|||||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"apps/server": {
|
"apps/server": {
|
||||||
"name": "@automaker/server",
|
"name": "@automaker/server",
|
||||||
"version": "0.8.0",
|
"version": "0.9.0",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "0.1.76",
|
"@anthropic-ai/claude-agent-sdk": "0.1.76",
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"@automaker/prompts": "1.0.0",
|
"@automaker/prompts": "1.0.0",
|
||||||
"@automaker/types": "1.0.0",
|
"@automaker/types": "1.0.0",
|
||||||
"@automaker/utils": "1.0.0",
|
"@automaker/utils": "1.0.0",
|
||||||
"@modelcontextprotocol/sdk": "1.25.1",
|
"@modelcontextprotocol/sdk": "1.25.2",
|
||||||
"@openai/codex-sdk": "^0.77.0",
|
"@openai/codex-sdk": "^0.77.0",
|
||||||
"cookie-parser": "1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
},
|
},
|
||||||
"apps/ui": {
|
"apps/ui": {
|
||||||
"name": "@automaker/ui",
|
"name": "@automaker/ui",
|
||||||
"version": "0.8.0",
|
"version": "0.9.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3723,9 +3723,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.25.1",
|
"version": "1.25.2",
|
||||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz",
|
||||||
"integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==",
|
"integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/node-server": "^1.19.7",
|
"@hono/node-server": "^1.19.7",
|
||||||
|
|||||||
Reference in New Issue
Block a user