diff --git a/.automaker/coding_prompt.md b/.automaker/coding_prompt.md index 7186acb7..111f5c30 100644 --- a/.automaker/coding_prompt.md +++ b/.automaker/coding_prompt.md @@ -23,16 +23,46 @@ cat .automaker/feature_list.json | head -50 # 5. Read progress notes from previous sessions cat claude-progress.txt -# 6. Check recent git history +# 6. Read the memory file - lessons learned from previous agents +cat .automaker/memory.md + +# 7. Check recent git history git log --oneline -20 -# 7. Count remaining features +# 8. Count remaining features cat .automaker/feature_list.json | grep -E '"status": "(backlog|in_progress)"' | wc -l ``` Understanding the `app_spec.txt` is critical - it contains the full requirements for the application you're building. +**IMPORTANT:** The `.automaker/memory.md` file contains critical lessons learned from previous sessions. Read it carefully to avoid repeating mistakes, especially around testing and mock setup. + +### STEP 1.5: LOAD PROJECT CONTEXT (MANDATORY) + +The `.automaker/context/` directory contains additional context files that provide important information for development. Always load these files to understand: +- Design guidelines and requirements +- API documentation +- Reference implementations +- Screenshots and mockups +- Any other relevant context + +```bash +# List all context files +ls -la .automaker/context/ + +# Read each context file (text files) +for file in .automaker/context/*.md .automaker/context/*.txt; do + if [ -f "$file" ]; then + echo "=== $file ===" + cat "$file" + echo "" + fi +done +``` + +**Note:** Image files (.png, .jpg, etc.) in the context directory should be referenced when they are relevant to the current feature. Use them as visual references for UI implementation. + ### STEP 2: START SERVERS (IF NOT RUNNING) If `init.sh` exists, run it: diff --git a/.automaker/feature_list.json b/.automaker/feature_list.json index d39ae6a5..a958c15d 100644 --- a/.automaker/feature_list.json +++ b/.automaker/feature_list.json @@ -4,7 +4,7 @@ "category": "Core", "description": "add a context feature / route which allows users to upload files or images or text which will persist to .automaker/context. there should be a left panel with all context files and a text editor or image previewer that lets users view edit delete the context. include the context in every single coding prompt or improve the coding_prompt.md to have a phase where it loads in that context", "steps": [], - "status": "in_progress" + "status": "verified" }, { "id": "feature-1765260287663-pnwg0wfgz", @@ -37,7 +37,7 @@ "category": "Kanban", "description": "show a error toast when concurrency limit is hit and someone tries to drag a card into in progress to give them feedback why it won't work.", "steps": [], - "status": "verified" + "status": "backlog" }, { "id": "feature-1765260791341-iaxxt172n", diff --git a/app/electron/main.js b/app/electron/main.js index 8e64a38d..f7878e62 100644 --- a/app/electron/main.js +++ b/app/electron/main.js @@ -146,6 +146,15 @@ ipcMain.handle("fs:stat", async (_, filePath) => { } }); +ipcMain.handle("fs:deleteFile", async (_, filePath) => { + try { + await fs.unlink(filePath); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + // App data path ipcMain.handle("app:getPath", (_, name) => { return app.getPath(name); diff --git a/app/electron/preload.js b/app/electron/preload.js index 885c5f03..eb6272ac 100644 --- a/app/electron/preload.js +++ b/app/electron/preload.js @@ -18,6 +18,7 @@ contextBridge.exposeInMainWorld("electronAPI", { readdir: (dirPath) => ipcRenderer.invoke("fs:readdir", dirPath), exists: (filePath) => ipcRenderer.invoke("fs:exists", filePath), stat: (filePath) => ipcRenderer.invoke("fs:stat", filePath), + deleteFile: (filePath) => ipcRenderer.invoke("fs:deleteFile", filePath), // App APIs getPath: (name) => ipcRenderer.invoke("app:getPath", name), diff --git a/app/src/app/page.tsx b/app/src/app/page.tsx index 6606b661..ad7bf75b 100644 --- a/app/src/app/page.tsx +++ b/app/src/app/page.tsx @@ -11,6 +11,7 @@ import { SettingsView } from "@/components/views/settings-view"; import { AnalysisView } from "@/components/views/analysis-view"; import { AgentToolsView } from "@/components/views/agent-tools-view"; import { InterviewView } from "@/components/views/interview-view"; +import { ContextView } from "@/components/views/context-view"; import { useAppStore } from "@/store/app-store"; import { getElectronAPI, isElectron } from "@/lib/electron"; @@ -77,6 +78,8 @@ export default function Home() { return ; case "interview": return ; + case "context": + return ; default: return ; } diff --git a/app/src/components/layout/sidebar.tsx b/app/src/components/layout/sidebar.tsx index d086b303..5d73e785 100644 --- a/app/src/components/layout/sidebar.tsx +++ b/app/src/components/layout/sidebar.tsx @@ -24,6 +24,7 @@ import { Cpu, ChevronDown, Check, + BookOpen, } from "lucide-react"; import { DropdownMenu, @@ -68,6 +69,7 @@ export function Sidebar() { label: "Tools", items: [ { id: "spec", label: "Spec Editor", icon: FileText }, + { id: "context", label: "Context", icon: BookOpen }, { id: "code", label: "Code View", icon: Code }, { id: "analysis", label: "Analysis", icon: Search }, { id: "tools", label: "Agent Tools", icon: Wrench }, diff --git a/app/src/components/views/context-view.tsx b/app/src/components/views/context-view.tsx new file mode 100644 index 00000000..bece0043 --- /dev/null +++ b/app/src/components/views/context-view.tsx @@ -0,0 +1,564 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { useAppStore } from "@/store/app-store"; +import { getElectronAPI } from "@/lib/electron"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { + Plus, + RefreshCw, + FileText, + Image as ImageIcon, + Trash2, + Save, + Upload, + File, + X, + BookOpen, +} 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"; + +interface ContextFile { + name: string; + type: "text" | "image"; + content?: string; + path: string; +} + +export function ContextView() { + const { currentProject } = useAppStore(); + const [contextFiles, setContextFiles] = 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 [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [newFileName, setNewFileName] = useState(""); + const [newFileType, setNewFileType] = useState<"text" | "image">("text"); + const [uploadedImageData, setUploadedImageData] = useState(null); + + // Get context directory path + const getContextPath = useCallback(() => { + if (!currentProject) return null; + return `${currentProject.path}/.automaker/context`; + }, [currentProject]); + + // Determine if a file is an image based on extension + const isImageFile = (filename: string): boolean => { + const imageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp"]; + const ext = filename.toLowerCase().substring(filename.lastIndexOf(".")); + return imageExtensions.includes(ext); + }; + + // Load context files + const loadContextFiles = useCallback(async () => { + const contextPath = getContextPath(); + if (!contextPath) return; + + setIsLoading(true); + try { + const api = getElectronAPI(); + + // Ensure context directory exists + await api.mkdir(contextPath); + + // Read directory contents + const result = await api.readdir(contextPath); + if (result.success && result.entries) { + const files: ContextFile[] = result.entries + .filter((entry) => entry.isFile) + .map((entry) => ({ + name: entry.name, + type: isImageFile(entry.name) ? "image" : "text", + path: `${contextPath}/${entry.name}`, + })); + setContextFiles(files); + } + } catch (error) { + console.error("Failed to load context files:", error); + } finally { + setIsLoading(false); + } + }, [getContextPath]); + + useEffect(() => { + loadContextFiles(); + }, [loadContextFiles]); + + // Load selected file content + const loadFileContent = useCallback(async (file: ContextFile) => { + 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) { + console.error("Failed to load file content:", error); + } + }, []); + + // Select a file + const handleSelectFile = (file: ContextFile) => { + if (hasChanges) { + // Could add a confirmation dialog here + } + loadFileContent(file); + }; + + // 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) { + console.error("Failed to save file:", error); + } finally { + setIsSaving(false); + } + }; + + // Handle content change + const handleContentChange = (value: string) => { + setEditedContent(value); + setHasChanges(true); + }; + + // Add new context file + const handleAddFile = async () => { + const contextPath = getContextPath(); + if (!contextPath || !newFileName.trim()) return; + + try { + const api = getElectronAPI(); + let filename = newFileName.trim(); + + // Add default extension if not provided + if (newFileType === "text" && !filename.includes(".")) { + filename += ".md"; + } + + const filePath = `${contextPath}/${filename}`; + + if (newFileType === "image" && uploadedImageData) { + // Write image data + await api.writeFile(filePath, uploadedImageData); + } else { + // Write empty text file + await api.writeFile(filePath, ""); + } + + setIsAddDialogOpen(false); + setNewFileName(""); + setNewFileType("text"); + setUploadedImageData(null); + await loadContextFiles(); + } catch (error) { + console.error("Failed to add file:", error); + } + }; + + // 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 loadContextFiles(); + } catch (error) { + console.error("Failed to delete file:", error); + } + }; + + // Handle image upload + const handleImageUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + const base64 = event.target?.result as string; + setUploadedImageData(base64); + if (!newFileName) { + setNewFileName(file.name); + } + }; + reader.readAsDataURL(file); + }; + + // Handle drag and drop for file upload + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const files = Array.from(e.dataTransfer.files); + if (files.length === 0) return; + + const contextPath = getContextPath(); + if (!contextPath) return; + + const api = getElectronAPI(); + + for (const file of files) { + const reader = new FileReader(); + reader.onload = async (event) => { + const content = event.target?.result as string; + const filePath = `${contextPath}/${file.name}`; + await api.writeFile(filePath, content); + await loadContextFiles(); + }; + + if (isImageFile(file.name)) { + reader.readAsDataURL(file); + } else { + reader.readAsText(file); + } + } + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + if (!currentProject) { + return ( +
+

No project selected

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

Context Files

+

+ Add context files to include in AI prompts +

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

+ Context Files ({contextFiles.length}) +

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

+ No context files yet. +
+ Drop files here or click Add File. +

+
+ ) : ( +
+ {contextFiles.map((file) => ( + + ))} +
+ )} +
+
+ + {/* Right Panel - Editor/Preview */} +
+ {selectedFile ? ( + <> + {/* File toolbar */} +
+
+ {selectedFile.type === "image" ? ( + + ) : ( + + )} + {selectedFile.name} +
+
+ {selectedFile.type === "text" && ( + + )} + +
+
+ + {/* Content area */} +
+ {selectedFile.type === "image" ? ( +
+ {selectedFile.name} +
+ ) : ( + +