diff --git a/.automaker/feature_list.json b/.automaker/feature_list.json index 8336b91c..af86f317 100644 --- a/.automaker/feature_list.json +++ b/.automaker/feature_list.json @@ -11,7 +11,7 @@ "category": "Context", "description": "Add Context File should show a file name and a textarea for the context info, that text area should allow drag n drop for txt files and .md files which the system will parse and put into the text area", "steps": [], - "status": "in_progress" + "status": "verified" }, { "id": "feature-1765262348401-hivjg6vuq", @@ -39,7 +39,7 @@ "category": "Core", "description": "When opening a new project, verify the .automaker directory is created with necessary files and kick off an agent to analyze the project, refactor the app_spec to describe the project and it's tech stack, and any features currently implemented, also define a blank feature_list.json, create necessary context and agents-context directories, and coding_prompt.md.", "steps": [], - "status": "in_progress" + "status": "verified" }, { "id": "feature-1765263831805-mr6mduv8p", @@ -67,39 +67,34 @@ "category": "Kanban", "description": "For the first 10 in progress cards, add shortcut keys 1 through 0 on the keyboard for opening their output modal", "steps": [], - "status": "in_progress", - "startedAt": "2025-12-09T07:26:41.577Z" + "status": "in_progress" }, { "id": "feature-1765265001317-4eyqyif9z", "category": "Kanban", "description": "Add a delete all button in the verified column header which runs through all verified cards and deletes them with the exact same delete actions. remember to show a confirm delete confirmation dialog before actually deleting.", "steps": [], - "status": "in_progress", - "startedAt": "2025-12-09T07:26:54.903Z" + "status": "in_progress" }, { "id": "feature-1765265036114-9oong1mrv", "category": "Kanban", "description": "Remove the refresh button from the headers, we should need to ever manually refresh anything if our app is well designed", "steps": [], - "status": "in_progress", - "startedAt": "2025-12-09T07:26:49.306Z" + "status": "in_progress" }, { "id": "feature-1765265099914-71eq4x4yl", "category": "Core", "description": "Add a ` shortcut to toggle the left side panel (on hover of the toggle show a tool tip with the shortcut info)", "steps": [], - "status": "in_progress", - "startedAt": "2025-12-09T07:26:51.411Z" + "status": "verified" }, { "id": "feature-1765265179876-5zcrlncdf", "category": "Kanban", "description": "Add a button in the backlog header which will just take the top cards and put them into the in progress board (up to the limit of the concurrency of course) so that a user doesn't have to drag each on individually, figure out the best name for it. give it a shortcut as well", "steps": [], - "status": "in_progress", - "startedAt": "2025-12-09T07:26:45.256Z" + "status": "in_progress" } ] \ No newline at end of file diff --git a/app/electron/auto-mode-service.js b/app/electron/auto-mode-service.js index 1a6ba9be..2fc49dc9 100644 --- a/app/electron/auto-mode-service.js +++ b/app/electron/auto-mode-service.js @@ -1262,6 +1262,275 @@ Focus on one feature at a time and complete it fully before finishing. Always de sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } + + /** + * Analyze a new project - scans codebase and updates app_spec.txt + * This is triggered when opening a project for the first time + */ + async analyzeProject({ projectPath, sendToRenderer }) { + console.log(`[AutoMode] Analyzing project at: ${projectPath}`); + + const analysisId = `project-analysis-${Date.now()}`; + + // Check if already analyzing this project + if (this.runningFeatures.has(analysisId)) { + throw new Error("Project analysis is already running"); + } + + // Register as running + this.runningFeatures.set(analysisId, { + abortController: null, + query: null, + projectPath, + sendToRenderer, + }); + + try { + sendToRenderer({ + type: "auto_mode_feature_start", + featureId: analysisId, + feature: { + id: analysisId, + category: "Project Analysis", + description: "Analyzing project structure and tech stack", + }, + }); + + // Perform the analysis + const result = await this.runProjectAnalysis(projectPath, analysisId, sendToRenderer); + + sendToRenderer({ + type: "auto_mode_feature_complete", + featureId: analysisId, + passes: result.success, + message: result.message, + }); + + return { success: true, message: result.message }; + } catch (error) { + console.error("[AutoMode] Error analyzing project:", error); + sendToRenderer({ + type: "auto_mode_error", + error: error.message, + featureId: analysisId, + }); + throw error; + } finally { + this.runningFeatures.delete(analysisId); + } + } + + /** + * Run the project analysis using Claude Agent SDK + */ + async runProjectAnalysis(projectPath, analysisId, sendToRenderer) { + console.log(`[AutoMode] Running project analysis for: ${projectPath}`); + + const execution = this.runningFeatures.get(analysisId); + if (!execution) { + throw new Error(`Analysis ${analysisId} not registered in runningFeatures`); + } + + try { + sendToRenderer({ + type: "auto_mode_phase", + featureId: analysisId, + phase: "planning", + message: "Scanning project structure...", + }); + + const abortController = new AbortController(); + execution.abortController = abortController; + + const options = { + model: "claude-sonnet-4-20250514", + systemPrompt: this.getProjectAnalysisSystemPrompt(), + maxTurns: 50, + cwd: projectPath, + allowedTools: ["Read", "Glob", "Grep", "Bash"], + permissionMode: "acceptEdits", + sandbox: { + enabled: true, + autoAllowBashIfSandboxed: true, + }, + abortController: abortController, + }; + + const prompt = this.buildProjectAnalysisPrompt(projectPath); + + sendToRenderer({ + type: "auto_mode_progress", + featureId: analysisId, + content: "Starting project analysis...\n", + }); + + const currentQuery = query({ prompt, options }); + execution.query = currentQuery; + + let responseText = ""; + for await (const msg of currentQuery) { + if (!this.runningFeatures.has(analysisId)) break; + + if (msg.type === "assistant" && msg.message?.content) { + for (const block of msg.message.content) { + if (block.type === "text") { + responseText += block.text; + sendToRenderer({ + type: "auto_mode_progress", + featureId: analysisId, + content: block.text, + }); + } else if (block.type === "tool_use") { + sendToRenderer({ + type: "auto_mode_tool", + featureId: analysisId, + tool: block.name, + input: block.input, + }); + } + } + } + } + + execution.query = null; + execution.abortController = null; + + sendToRenderer({ + type: "auto_mode_phase", + featureId: analysisId, + phase: "verification", + message: "Project analysis complete", + }); + + return { + success: true, + message: "Project analyzed successfully", + }; + } catch (error) { + if (error instanceof AbortError || error?.name === "AbortError") { + console.log("[AutoMode] Project analysis aborted"); + if (execution) { + execution.abortController = null; + execution.query = null; + } + return { + success: false, + message: "Analysis aborted", + }; + } + + console.error("[AutoMode] Error in project analysis:", error); + if (execution) { + execution.abortController = null; + execution.query = null; + } + throw error; + } + } + + /** + * Build the prompt for project analysis + */ + buildProjectAnalysisPrompt(projectPath) { + return `You are analyzing a new project that was just opened in Automaker, an autonomous AI development studio. + +**Your Task:** + +Analyze this project's codebase and update the .automaker/app_spec.txt file with accurate information about: + +1. **Project Name** - Detect the name from package.json, README, or directory name +2. **Overview** - Brief description of what the project does +3. **Technology Stack** - Languages, frameworks, libraries detected +4. **Core Capabilities** - Main features and functionality +5. **Implemented Features** - What features are already built + +**Steps to Follow:** + +1. First, explore the project structure: + - Look at package.json, cargo.toml, go.mod, requirements.txt, etc. for tech stack + - Check README.md for project description + - List key directories (src, lib, components, etc.) + +2. Identify the tech stack: + - Frontend framework (React, Vue, Next.js, etc.) + - Backend framework (Express, FastAPI, etc.) + - Database (if any config files exist) + - Testing framework + - Build tools + +3. Update .automaker/app_spec.txt with your findings in this format: + \`\`\`xml + + Detected Name + + + Clear description of what this project does based on your analysis. + + + + + Framework Name + + + + + + + + + + + + + + + + + + + + + + \`\`\` + +4. Ensure .automaker/feature_list.json exists (create as empty array [] if not) + +5. Ensure .automaker/context/ directory exists + +6. Ensure .automaker/agents-context/ directory exists + +7. Ensure .automaker/coding_prompt.md exists with default guidelines + +**Important:** +- Be concise but accurate +- Only include information you can verify from the codebase +- If unsure about something, note it as "to be determined" +- Don't make up features that don't exist + +Begin by exploring the project structure.`; + } + + /** + * Get system prompt for project analysis agent + */ + getProjectAnalysisSystemPrompt() { + return `You are a project analysis agent that examines codebases to understand their structure, tech stack, and implemented features. + +Your goal is to: +- Quickly scan and understand project structure +- Identify programming languages, frameworks, and libraries +- Detect existing features and capabilities +- Update the .automaker/app_spec.txt with accurate information +- Ensure all required .automaker files and directories exist + +Be efficient - don't read every file, focus on: +- Configuration files (package.json, tsconfig.json, etc.) +- Main entry points +- Directory structure +- README and documentation + +You have read access to files and can run basic bash commands to explore the structure.`; + } } // Export singleton instance diff --git a/app/electron/main.js b/app/electron/main.js index f7878e62..e55f3999 100644 --- a/app/electron/main.js +++ b/app/electron/main.js @@ -480,3 +480,23 @@ ipcMain.handle("auto-mode:context-exists", async (_, { projectPath, featureId }) return { success: false, error: error.message }; } }); + +/** + * Analyze a new project - kicks off an agent to analyze the codebase + * and update the app_spec.txt with tech stack and implemented features + */ +ipcMain.handle("auto-mode:analyze-project", async (_, { projectPath }) => { + console.log("[IPC] auto-mode:analyze-project called with:", { projectPath }); + try { + const sendToRenderer = (data) => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("auto-mode:event", data); + } + }; + + return await autoModeService.analyzeProject({ projectPath, sendToRenderer }); + } catch (error) { + console.error("[IPC] auto-mode:analyze-project error:", error); + return { success: false, error: error.message }; + } +}); diff --git a/app/electron/preload.js b/app/electron/preload.js index eb6272ac..0b6e03c2 100644 --- a/app/electron/preload.js +++ b/app/electron/preload.js @@ -111,6 +111,10 @@ contextBridge.exposeInMainWorld("electronAPI", { contextExists: (projectPath, featureId) => ipcRenderer.invoke("auto-mode:context-exists", { projectPath, featureId }), + // Analyze a new project - kicks off an agent to analyze codebase + analyzeProject: (projectPath) => + ipcRenderer.invoke("auto-mode:analyze-project", { projectPath }), + // Listen for auto mode events onEvent: (callback) => { const subscription = (_, data) => callback(data); diff --git a/app/src/components/views/welcome-view.tsx b/app/src/components/views/welcome-view.tsx index 74f4d274..5c643c73 100644 --- a/app/src/components/views/welcome-view.tsx +++ b/app/src/components/views/welcome-view.tsx @@ -50,6 +50,7 @@ export function WelcomeView() { const [isCreating, setIsCreating] = useState(false); const [isOpening, setIsOpening] = useState(false); const [showInitDialog, setShowInitDialog] = useState(false); + const [isAnalyzing, setIsAnalyzing] = useState(false); const [initStatus, setInitStatus] = useState<{ isNewProject: boolean; createdFiles: string[]; @@ -57,6 +58,36 @@ export function WelcomeView() { projectPath: string; } | null>(null); + /** + * Kick off project analysis agent to analyze the codebase + */ + const analyzeProject = useCallback(async (projectPath: string) => { + const api = getElectronAPI(); + + if (!api.autoMode?.analyzeProject) { + console.log("[Welcome] Auto mode API not available, skipping analysis"); + return; + } + + setIsAnalyzing(true); + try { + console.log("[Welcome] Starting project analysis for:", projectPath); + const result = await api.autoMode.analyzeProject(projectPath); + + if (result.success) { + toast.success("Project analyzed", { + description: "AI agent has analyzed your project structure", + }); + } else { + console.error("[Welcome] Project analysis failed:", result.error); + } + } catch (error) { + console.error("[Welcome] Failed to analyze project:", error); + } finally { + setIsAnalyzing(false); + } + }, []); + /** * Initialize project and optionally kick off project analysis agent */ @@ -93,9 +124,12 @@ export function WelcomeView() { }); setShowInitDialog(true); - // TODO: Kick off agent to analyze the project and update app_spec.txt - // This will be implemented in a future iteration with the auto-mode service + // Kick off agent to analyze the project and update app_spec.txt console.log("[Welcome] Project initialized, created files:", initResult.createdFiles); + console.log("[Welcome] Kicking off project analysis agent..."); + + // Start analysis in background (don't await, let it run async) + analyzeProject(path); } else { toast.success("Project opened", { description: `Opened ${name}`, @@ -109,7 +143,7 @@ export function WelcomeView() { } finally { setIsOpening(false); } - }, [addProject, setCurrentProject]); + }, [addProject, setCurrentProject, analyzeProject]); const handleOpenProject = useCallback(async () => { const api = getElectronAPI(); @@ -517,14 +551,23 @@ export function WelcomeView() { {initStatus?.isNewProject && (
-

- Tip: Edit the{" "} - - app_spec.txt - {" "} - file to describe your project. The AI agent will use this to - understand your project structure. -

+ {isAnalyzing ? ( +
+ +

+ AI agent is analyzing your project structure... +

+
+ ) : ( +

+ Tip: Edit the{" "} + + app_spec.txt + {" "} + file to describe your project. The AI agent will use this to + understand your project structure. +

+ )}
)} diff --git a/app/src/lib/electron.ts b/app/src/lib/electron.ts index 8c835a19..b9309421 100644 --- a/app/src/lib/electron.ts +++ b/app/src/lib/electron.ts @@ -66,6 +66,7 @@ export interface AutoModeAPI { verifyFeature: (projectPath: string, featureId: string) => Promise<{ success: boolean; passes?: boolean; error?: string }>; resumeFeature: (projectPath: string, featureId: string) => Promise<{ success: boolean; passes?: boolean; error?: string }>; contextExists: (projectPath: string, featureId: string) => Promise<{ success: boolean; exists?: boolean; error?: string }>; + analyzeProject: (projectPath: string) => Promise<{ success: boolean; message?: string; error?: string }>; onEvent: (callback: (event: AutoModeEvent) => void) => () => void; } @@ -430,6 +431,113 @@ function createMockAutoModeAPI(): AutoModeAPI { return { success: true, exists }; }, + analyzeProject: async (projectPath: string) => { + // Simulate project analysis + const analysisId = `project-analysis-${Date.now()}`; + mockRunningFeatures.add(analysisId); + + // Emit start event + emitAutoModeEvent({ + type: "auto_mode_feature_start", + featureId: analysisId, + feature: { + id: analysisId, + category: "Project Analysis", + description: "Analyzing project structure and tech stack", + }, + }); + + // Simulate analysis phases + await delay(300, analysisId); + if (!mockRunningFeatures.has(analysisId)) return { success: false, message: "Analysis aborted" }; + + emitAutoModeEvent({ + type: "auto_mode_phase", + featureId: analysisId, + phase: "planning", + message: "Scanning project structure...", + }); + + emitAutoModeEvent({ + type: "auto_mode_progress", + featureId: analysisId, + content: "Starting project analysis...\n", + }); + + await delay(500, analysisId); + if (!mockRunningFeatures.has(analysisId)) return { success: false, message: "Analysis aborted" }; + + emitAutoModeEvent({ + type: "auto_mode_tool", + featureId: analysisId, + tool: "Glob", + input: { pattern: "**/*" }, + }); + + await delay(300, analysisId); + if (!mockRunningFeatures.has(analysisId)) return { success: false, message: "Analysis aborted" }; + + emitAutoModeEvent({ + type: "auto_mode_progress", + featureId: analysisId, + content: "Detected tech stack: Next.js, TypeScript, Tailwind CSS\n", + }); + + await delay(300, analysisId); + if (!mockRunningFeatures.has(analysisId)) return { success: false, message: "Analysis aborted" }; + + // Write mock app_spec.txt + mockFileSystem[`${projectPath}/.automaker/app_spec.txt`] = ` + Demo Project + + + A demo project analyzed by the Automaker AI agent. + + + + + Next.js + TypeScript + Tailwind CSS + + + + + - Web application + - Component-based architecture + + + + - Basic page structure + - Component library + +`; + + // Ensure feature_list.json exists + if (!mockFileSystem[`${projectPath}/.automaker/feature_list.json`]) { + mockFileSystem[`${projectPath}/.automaker/feature_list.json`] = "[]"; + } + + emitAutoModeEvent({ + type: "auto_mode_phase", + featureId: analysisId, + phase: "verification", + message: "Project analysis complete", + }); + + emitAutoModeEvent({ + type: "auto_mode_feature_complete", + featureId: analysisId, + passes: true, + message: "Project analyzed successfully", + }); + + mockRunningFeatures.delete(analysisId); + mockAutoModeTimeouts.delete(analysisId); + + return { success: true, message: "Project analyzed successfully" }; + }, + onEvent: (callback: (event: AutoModeEvent) => void) => { mockAutoModeCallbacks.push(callback); return () => { diff --git a/app/tests/utils.ts b/app/tests/utils.ts index 303c8266..ebf9c913 100644 --- a/app/tests/utils.ts +++ b/app/tests/utils.ts @@ -1304,3 +1304,44 @@ export async function closeDialogWithEscape(page: Page): Promise { await page.keyboard.press("Escape"); await page.waitForTimeout(100); // Give dialog time to close } + +/** + * Wait for a toast notification with specific text to appear + */ +export async function waitForToast( + page: Page, + text: string, + options?: { timeout?: number } +): Promise { + const toast = page.locator(`[data-sonner-toast]:has-text("${text}")`).first(); + await toast.waitFor({ + timeout: options?.timeout ?? 5000, + state: "visible", + }); + return toast; +} + +/** + * Check if project analysis is in progress (analyzing spinner is visible) + */ +export async function isProjectAnalyzingVisible(page: Page): Promise { + const analyzingText = page.locator('p:has-text("AI agent is analyzing")'); + return await analyzingText.isVisible().catch(() => false); +} + +/** + * Wait for project analysis to complete (no longer analyzing) + */ +export async function waitForProjectAnalysisComplete( + page: Page, + options?: { timeout?: number } +): Promise { + // Wait for the analyzing text to disappear + const analyzingText = page.locator('p:has-text("AI agent is analyzing")'); + await analyzingText.waitFor({ + timeout: options?.timeout ?? 10000, + state: "hidden", + }).catch(() => { + // It may never have been visible, that's ok + }); +}