diff --git a/.automaker/feature_list.json b/.automaker/feature_list.json index 128e17c4..0637a088 100644 --- a/.automaker/feature_list.json +++ b/.automaker/feature_list.json @@ -1,20 +1 @@ -[ - { - "id": "feature-1765335919754-r010d1fw5", - "category": "Uncategorized", - "description": "what does the text in the button say?\n", - "steps": [], - "status": "waiting_approval", - "startedAt": "2025-12-10T03:05:34.894Z", - "imagePaths": [ - { - "id": "img-1765335919132-0x3t37l1r", - "path": "/Users/webdevcody/Library/Application Support/automaker/images/1765335919131-g4qvs053g_Screenshot_2025-12-09_at_10.05.17_PM.png", - "filename": "Screenshot 2025-12-09 at 10.05.17 PM.png", - "mimeType": "image/png" - } - ], - "skipTests": true, - "summary": "Investigated button text in the app. Main buttons found in welcome-view.tsx: \"Create Project\" (primary action), \"Browse Folder\" (secondary action), \"Browse\" (directory selector), \"Cancel\", \"Get Started\". No code changes made - this was an investigative question." - } -] \ No newline at end of file +[] \ No newline at end of file diff --git a/app/electron/main.js b/app/electron/main.js index eeb0a76f..c100da40 100644 --- a/app/electron/main.js +++ b/app/electron/main.js @@ -7,6 +7,8 @@ const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron"); const fs = require("fs/promises"); const agentService = require("./agent-service"); const autoModeService = require("./auto-mode-service"); +const featureSuggestionsService = require("./services/feature-suggestions-service"); +const specRegenerationService = require("./services/spec-regeneration-service"); let mainWindow = null; @@ -669,3 +671,224 @@ ipcMain.handle( } } ); + +// ============================================================================ +// Feature Suggestions IPC Handlers +// ============================================================================ + +// Track running suggestions analysis +let suggestionsExecution = null; + +/** + * Generate feature suggestions by analyzing the project + */ +ipcMain.handle( + "suggestions:generate", + async (_, { projectPath }) => { + console.log("[IPC] suggestions:generate called with:", { projectPath }); + + try { + // Check if already running + if (suggestionsExecution && suggestionsExecution.isActive()) { + return { success: false, error: "Suggestions generation is already running" }; + } + + // Create execution context + suggestionsExecution = { + abortController: null, + query: null, + isActive: () => suggestionsExecution !== null, + }; + + const sendToRenderer = (data) => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("suggestions:event", data); + } + }; + + // Start generating suggestions (runs in background) + featureSuggestionsService + .generateSuggestions(projectPath, sendToRenderer, suggestionsExecution) + .catch((error) => { + console.error("[IPC] suggestions:generate background error:", error); + sendToRenderer({ + type: "suggestions_error", + error: error.message, + }); + }) + .finally(() => { + suggestionsExecution = null; + }); + + // Return immediately + return { success: true }; + } catch (error) { + console.error("[IPC] suggestions:generate error:", error); + suggestionsExecution = null; + return { success: false, error: error.message }; + } + } +); + +/** + * Stop the current suggestions generation + */ +ipcMain.handle("suggestions:stop", async () => { + console.log("[IPC] suggestions:stop called"); + try { + if (suggestionsExecution && suggestionsExecution.abortController) { + suggestionsExecution.abortController.abort(); + } + suggestionsExecution = null; + return { success: true }; + } catch (error) { + console.error("[IPC] suggestions:stop error:", error); + return { success: false, error: error.message }; + } +}); + +/** + * Get suggestions generation status + */ +ipcMain.handle("suggestions:status", () => { + return { + success: true, + isRunning: suggestionsExecution !== null && suggestionsExecution.isActive(), + }; +}); + +// ============================================================================ +// Spec Regeneration IPC Handlers +// ============================================================================ + +// Track running spec regeneration +let specRegenerationExecution = null; + +/** + * Regenerate the app spec based on project definition + */ +ipcMain.handle( + "spec-regeneration:generate", + async (_, { projectPath, projectDefinition }) => { + console.log("[IPC] spec-regeneration:generate called with:", { projectPath }); + + try { + // Check if already running + if (specRegenerationExecution && specRegenerationExecution.isActive()) { + return { success: false, error: "Spec regeneration is already running" }; + } + + // Create execution context + specRegenerationExecution = { + abortController: null, + query: null, + isActive: () => specRegenerationExecution !== null, + }; + + const sendToRenderer = (data) => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("spec-regeneration:event", data); + } + }; + + // Start regenerating spec (runs in background) + specRegenerationService + .regenerateSpec(projectPath, projectDefinition, sendToRenderer, specRegenerationExecution) + .catch((error) => { + console.error("[IPC] spec-regeneration:generate background error:", error); + sendToRenderer({ + type: "spec_regeneration_error", + error: error.message, + }); + }) + .finally(() => { + specRegenerationExecution = null; + }); + + // Return immediately + return { success: true }; + } catch (error) { + console.error("[IPC] spec-regeneration:generate error:", error); + specRegenerationExecution = null; + return { success: false, error: error.message }; + } + } +); + +/** + * Stop the current spec regeneration + */ +ipcMain.handle("spec-regeneration:stop", async () => { + console.log("[IPC] spec-regeneration:stop called"); + try { + if (specRegenerationExecution && specRegenerationExecution.abortController) { + specRegenerationExecution.abortController.abort(); + } + specRegenerationExecution = null; + return { success: true }; + } catch (error) { + console.error("[IPC] spec-regeneration:stop error:", error); + return { success: false, error: error.message }; + } +}); + +/** + * Get spec regeneration status + */ +ipcMain.handle("spec-regeneration:status", () => { + return { + success: true, + isRunning: specRegenerationExecution !== null && specRegenerationExecution.isActive(), + }; +}); + +/** + * Create initial app spec for a new project + */ +ipcMain.handle( + "spec-regeneration:create", + async (_, { projectPath, projectOverview, generateFeatures = true }) => { + console.log("[IPC] spec-regeneration:create called with:", { projectPath, generateFeatures }); + + try { + // Check if already running + if (specRegenerationExecution && specRegenerationExecution.isActive()) { + return { success: false, error: "Spec creation is already running" }; + } + + // Create execution context + specRegenerationExecution = { + abortController: null, + query: null, + isActive: () => specRegenerationExecution !== null, + }; + + const sendToRenderer = (data) => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("spec-regeneration:event", data); + } + }; + + // Start creating spec (runs in background) + specRegenerationService + .createInitialSpec(projectPath, projectOverview, sendToRenderer, specRegenerationExecution, generateFeatures) + .catch((error) => { + console.error("[IPC] spec-regeneration:create background error:", error); + sendToRenderer({ + type: "spec_regeneration_error", + error: error.message, + }); + }) + .finally(() => { + specRegenerationExecution = null; + }); + + // Return immediately + return { success: true }; + } catch (error) { + console.error("[IPC] spec-regeneration:create error:", error); + specRegenerationExecution = null; + return { success: false, error: error.message }; + } + } +); diff --git a/app/electron/preload.js b/app/electron/preload.js index 18a1eaeb..d98190a3 100644 --- a/app/electron/preload.js +++ b/app/electron/preload.js @@ -139,6 +139,58 @@ contextBridge.exposeInMainWorld("electronAPI", { }; }, }, + + // Feature Suggestions API + suggestions: { + // Generate feature suggestions + generate: (projectPath) => + ipcRenderer.invoke("suggestions:generate", { projectPath }), + + // Stop generating suggestions + stop: () => ipcRenderer.invoke("suggestions:stop"), + + // Get suggestions status + status: () => ipcRenderer.invoke("suggestions:status"), + + // Listen for suggestions events + onEvent: (callback) => { + const subscription = (_, data) => callback(data); + ipcRenderer.on("suggestions:event", subscription); + + // Return unsubscribe function + return () => { + ipcRenderer.removeListener("suggestions:event", subscription); + }; + }, + }, + + // Spec Regeneration API + specRegeneration: { + // Create initial app spec for a new project + create: (projectPath, projectOverview, generateFeatures = true) => + ipcRenderer.invoke("spec-regeneration:create", { projectPath, projectOverview, generateFeatures }), + + // Regenerate the app spec + generate: (projectPath, projectDefinition) => + ipcRenderer.invoke("spec-regeneration:generate", { projectPath, projectDefinition }), + + // Stop regenerating spec + stop: () => ipcRenderer.invoke("spec-regeneration:stop"), + + // Get regeneration status + status: () => ipcRenderer.invoke("spec-regeneration:status"), + + // Listen for regeneration events + onEvent: (callback) => { + const subscription = (_, data) => callback(data); + ipcRenderer.on("spec-regeneration:event", subscription); + + // Return unsubscribe function + return () => { + ipcRenderer.removeListener("spec-regeneration:event", subscription); + }; + }, + }, }); // Also expose a flag to detect if we're in Electron diff --git a/app/electron/services/feature-suggestions-service.js b/app/electron/services/feature-suggestions-service.js new file mode 100644 index 00000000..28063e66 --- /dev/null +++ b/app/electron/services/feature-suggestions-service.js @@ -0,0 +1,269 @@ +const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk"); +const promptBuilder = require("./prompt-builder"); + +/** + * Feature Suggestions Service - Analyzes project and generates feature suggestions + */ +class FeatureSuggestionsService { + constructor() { + this.runningAnalysis = null; + } + + /** + * Generate feature suggestions by analyzing the project + */ + async generateSuggestions(projectPath, sendToRenderer, execution) { + console.log( + `[FeatureSuggestions] Generating suggestions for: ${projectPath}` + ); + + try { + const abortController = new AbortController(); + execution.abortController = abortController; + + const options = { + model: "claude-sonnet-4-20250514", + systemPrompt: this.getSystemPrompt(), + maxTurns: 50, + cwd: projectPath, + allowedTools: ["Read", "Glob", "Grep", "Bash"], + permissionMode: "acceptEdits", + sandbox: { + enabled: true, + autoAllowBashIfSandboxed: true, + }, + abortController: abortController, + }; + + const prompt = this.buildAnalysisPrompt(); + + sendToRenderer({ + type: "suggestions_progress", + content: "Starting project analysis...\n", + }); + + const currentQuery = query({ prompt, options }); + execution.query = currentQuery; + + let fullResponse = ""; + for await (const msg of currentQuery) { + if (!execution.isActive()) break; + + if (msg.type === "assistant" && msg.message?.content) { + for (const block of msg.message.content) { + if (block.type === "text") { + fullResponse += block.text; + sendToRenderer({ + type: "suggestions_progress", + content: block.text, + }); + } else if (block.type === "tool_use") { + sendToRenderer({ + type: "suggestions_tool", + tool: block.name, + input: block.input, + }); + } + } + } + } + + execution.query = null; + execution.abortController = null; + + // Parse the suggestions from the response + const suggestions = this.parseSuggestions(fullResponse); + + sendToRenderer({ + type: "suggestions_complete", + suggestions: suggestions, + }); + + return { + success: true, + suggestions: suggestions, + }; + } catch (error) { + if (error instanceof AbortError || error?.name === "AbortError") { + console.log("[FeatureSuggestions] Analysis aborted"); + if (execution) { + execution.abortController = null; + execution.query = null; + } + return { + success: false, + message: "Analysis aborted", + suggestions: [], + }; + } + + console.error( + "[FeatureSuggestions] Error generating suggestions:", + error + ); + if (execution) { + execution.abortController = null; + execution.query = null; + } + throw error; + } + } + + /** + * Parse suggestions from the LLM response + * Looks for JSON array in the response + */ + parseSuggestions(response) { + try { + // Try to find JSON array in the response + // Look for ```json ... ``` blocks first + const jsonBlockMatch = response.match(/```json\s*([\s\S]*?)```/); + if (jsonBlockMatch) { + const parsed = JSON.parse(jsonBlockMatch[1].trim()); + if (Array.isArray(parsed)) { + return this.validateSuggestions(parsed); + } + } + + // Try to find a raw JSON array + const jsonArrayMatch = response.match(/\[\s*\{[\s\S]*\}\s*\]/); + if (jsonArrayMatch) { + const parsed = JSON.parse(jsonArrayMatch[0]); + if (Array.isArray(parsed)) { + return this.validateSuggestions(parsed); + } + } + + console.warn( + "[FeatureSuggestions] Could not parse suggestions from response" + ); + return []; + } catch (error) { + console.error("[FeatureSuggestions] Error parsing suggestions:", error); + return []; + } + } + + /** + * Validate and normalize suggestions + */ + validateSuggestions(suggestions) { + return suggestions + .filter((s) => s && typeof s === "object") + .map((s, index) => ({ + id: `suggestion-${Date.now()}-${index}`, + category: s.category || "Uncategorized", + description: s.description || s.title || "No description", + steps: Array.isArray(s.steps) ? s.steps : [], + priority: typeof s.priority === "number" ? s.priority : index + 1, + reasoning: s.reasoning || "", + })) + .sort((a, b) => a.priority - b.priority); + } + + /** + * Get the system prompt for feature suggestion analysis + */ + getSystemPrompt() { + return `You are an expert software architect and product manager. Your job is to analyze a codebase and suggest missing features that would improve the application. + +You should: +1. Thoroughly analyze the project structure, code, and any existing documentation +2. Identify what the application does and what features it currently has (look at the .automaker/app_spec.txt file as well if it exists) +3. Generate a comprehensive list of missing features that would be valuable to users +4. Prioritize features by impact and complexity +5. Provide clear, actionable descriptions and implementation steps + +When analyzing, look at: +- README files and documentation +- Package.json, cargo.toml, or similar config files for tech stack +- Source code structure and organization +- Existing features and their implementation patterns +- Common patterns in similar applications +- User experience improvements +- Developer experience improvements +- Performance optimizations +- Security enhancements + +You have access to file reading and search tools. Use them to understand the codebase.`; + } + + /** + * Build the prompt for analyzing the project + */ + buildAnalysisPrompt() { + return `Analyze this project and generate a list of suggested features that are missing or would improve the application. + +**Your Task:** + +1. First, explore the project structure: + - Read README.md, package.json, or similar config files + - Scan the source code directory structure + - Identify the tech stack and frameworks used + - Look at existing features and how they're implemented + +2. Identify what the application does: + - What is the main purpose? + - What features are already implemented? + - What patterns and conventions are used? + +3. Generate feature suggestions: + - Think about what's missing compared to similar applications + - Consider user experience improvements + - Consider developer experience improvements + - Think about performance, security, and reliability + - Consider testing and documentation improvements + +4. **CRITICAL: Output your suggestions as a JSON array** at the end of your response, formatted like this: + +\`\`\`json +[ + { + "category": "User Experience", + "description": "Add dark mode support with system preference detection", + "steps": [ + "Create a ThemeProvider context to manage theme state", + "Add a toggle component in the settings or header", + "Implement CSS variables for theme colors", + "Add localStorage persistence for user preference" + ], + "priority": 1, + "reasoning": "Dark mode is a standard feature that improves accessibility and user comfort" + }, + { + "category": "Performance", + "description": "Implement lazy loading for heavy components", + "steps": [ + "Identify components that are heavy or rarely used", + "Use React.lazy() and Suspense for code splitting", + "Add loading states for lazy-loaded components" + ], + "priority": 2, + "reasoning": "Improves initial load time and reduces bundle size" + } +] +\`\`\` + +**Important Guidelines:** +- Generate at least 10-20 feature suggestions +- Order them by priority (1 = highest priority) +- Each feature should have clear, actionable steps +- Categories should be meaningful (e.g., "User Experience", "Performance", "Security", "Testing", "Documentation", "Developer Experience", "Accessibility", etc.) +- Be specific about what files might need to be created or modified +- Consider the existing tech stack and patterns when suggesting implementation steps + +Begin by exploring the project structure.`; + } + + /** + * Stop the current analysis + */ + stop() { + if (this.runningAnalysis && this.runningAnalysis.abortController) { + this.runningAnalysis.abortController.abort(); + } + this.runningAnalysis = null; + } +} + +module.exports = new FeatureSuggestionsService(); diff --git a/app/electron/services/spec-regeneration-service.js b/app/electron/services/spec-regeneration-service.js new file mode 100644 index 00000000..f0aa116b --- /dev/null +++ b/app/electron/services/spec-regeneration-service.js @@ -0,0 +1,519 @@ +const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk"); +const fs = require("fs/promises"); +const path = require("path"); + +/** + * XML template for app_spec.txt + */ +const APP_SPEC_XML_TEMPLATE = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +/** + * Spec Regeneration Service - Regenerates app spec based on project description and tech stack + */ +class SpecRegenerationService { + constructor() { + this.runningRegeneration = null; + } + + /** + * Create initial app spec for a new project + * @param {string} projectPath - Path to the project + * @param {string} projectOverview - User's project description + * @param {Function} sendToRenderer - Function to send events to renderer + * @param {Object} execution - Execution context with abort controller + * @param {boolean} generateFeatures - Whether to generate feature_list.json entries + */ + async createInitialSpec(projectPath, projectOverview, sendToRenderer, execution, generateFeatures = true) { + console.log(`[SpecRegeneration] Creating initial spec for: ${projectPath}, generateFeatures: ${generateFeatures}`); + + try { + const abortController = new AbortController(); + execution.abortController = abortController; + + const options = { + model: "claude-sonnet-4-20250514", + systemPrompt: this.getInitialCreationSystemPrompt(generateFeatures), + maxTurns: 50, + cwd: projectPath, + allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"], + permissionMode: "acceptEdits", + sandbox: { + enabled: true, + autoAllowBashIfSandboxed: true, + }, + abortController: abortController, + }; + + const prompt = this.buildInitialCreationPrompt(projectOverview, generateFeatures); + + sendToRenderer({ + type: "spec_regeneration_progress", + content: "Starting project analysis and spec creation...\n", + }); + + const currentQuery = query({ prompt, options }); + execution.query = currentQuery; + + let fullResponse = ""; + for await (const msg of currentQuery) { + if (!execution.isActive()) break; + + if (msg.type === "assistant" && msg.message?.content) { + for (const block of msg.message.content) { + if (block.type === "text") { + fullResponse += block.text; + sendToRenderer({ + type: "spec_regeneration_progress", + content: block.text, + }); + } else if (block.type === "tool_use") { + sendToRenderer({ + type: "spec_regeneration_tool", + tool: block.name, + input: block.input, + }); + } + } + } + } + + execution.query = null; + execution.abortController = null; + + sendToRenderer({ + type: "spec_regeneration_complete", + message: "Initial spec creation complete!", + }); + + return { + success: true, + message: "Initial spec creation complete", + }; + } catch (error) { + if (error instanceof AbortError || error?.name === "AbortError") { + console.log("[SpecRegeneration] Creation aborted"); + if (execution) { + execution.abortController = null; + execution.query = null; + } + return { + success: false, + message: "Creation aborted", + }; + } + + console.error("[SpecRegeneration] Error creating initial spec:", error); + if (execution) { + execution.abortController = null; + execution.query = null; + } + throw error; + } + } + + /** + * Get the system prompt for initial spec creation + * @param {boolean} generateFeatures - Whether features should be generated + */ + getInitialCreationSystemPrompt(generateFeatures = true) { + const featureListInstructions = generateFeatures + ? ` +**FEATURE LIST GENERATION** + +After creating the app_spec.txt, you MUST also update the .automaker/feature_list.json file with all features from the implementation_roadmap section. + +For EACH feature in each phase of the implementation_roadmap: +1. Read the app_spec.txt you just created +2. Extract every single feature from each phase (phase_1, phase_2, phase_3, phase_4, etc.) +3. Write ALL features to .automaker/feature_list.json in order + +The feature_list.json format should be: +\`\`\`json +[ + { + "id": "feature--", + "category": "", + "description": "", + "status": "backlog", + "steps": ["Step 1", "Step 2", "..."], + "skipTests": true + } +] +\`\`\` + +IMPORTANT: Include EVERY feature from the implementation_roadmap. Do not skip any.` + : ` +**CRITICAL FILE PROTECTION** + +THE FOLLOWING FILE IS ABSOLUTELY FORBIDDEN FROM DIRECT MODIFICATION: +- .automaker/feature_list.json + +**YOU MUST NEVER:** +- Use the Write tool on .automaker/feature_list.json +- Use the Edit tool on .automaker/feature_list.json +- Use any Bash command that writes to .automaker/feature_list.json`; + + return `You are an expert software architect and product manager. Your job is to analyze an existing codebase and generate a comprehensive application specification based on a user's project overview. + +You should: +1. First, thoroughly analyze the project structure to understand the existing tech stack +2. Read key configuration files (package.json, tsconfig.json, Cargo.toml, requirements.txt, etc.) to understand dependencies and frameworks +3. Understand the current architecture and patterns used +4. Based on the user's project overview, create a comprehensive app specification +5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application +6. Use the XML template format provided +7. Write the specification to .automaker/app_spec.txt + +When analyzing, look at: +- package.json, cargo.toml, requirements.txt or similar config files for tech stack +- Source code structure and organization +- Framework-specific patterns (Next.js, React, Django, etc.) +- Database configurations and schemas +- API structures and patterns +${featureListInstructions} + +You CAN and SHOULD modify: +- .automaker/app_spec.txt (this is your primary target)${generateFeatures ? '\n- .automaker/feature_list.json (to populate features from implementation_roadmap)' : ''} + +You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec.`; + } + + /** + * Build the prompt for initial spec creation + * @param {string} projectOverview - User's project description + * @param {boolean} generateFeatures - Whether to generate feature_list.json entries + */ + buildInitialCreationPrompt(projectOverview, generateFeatures = true) { + const featureGenerationStep = generateFeatures + ? ` +5. **IMPORTANT - GENERATE FEATURE LIST**: After writing the app_spec.txt: + - Read back the app_spec.txt file you just created + - Look at the implementation_roadmap section + - For EVERY feature listed in each phase (phase_1, phase_2, phase_3, phase_4, etc.), create an entry + - Write ALL these features to \`.automaker/feature_list.json\` in the order they appear + - Each feature should have: id (feature-timestamp-index), category (phase name), description, status: "backlog", steps array, and skipTests: true + - Do NOT skip any features - include every single one from the roadmap` + : ''; + + return `I need you to create an initial application specification for my project. I haven't set up an app_spec.txt yet, so this will be the first one. + +**My Project Overview:** +${projectOverview} + +**Your Task:** + +1. First, explore the project to understand the existing tech stack: + - Read package.json, Cargo.toml, requirements.txt, or similar config files + - Identify all frameworks and libraries being used + - Understand the current project structure and architecture + - Note any database, authentication, or other infrastructure in use + +2. Based on my project overview and the existing tech stack, create a comprehensive app specification using this XML template: + +\`\`\`xml +${APP_SPEC_XML_TEMPLATE} +\`\`\` + +3. Fill out the template with: + - **project_name**: Extract from the project or derive from overview + - **overview**: A clear description based on my project overview + - **technology_stack**: All technologies you discover in the project (fill out the relevant sections, remove irrelevant ones) + - **core_capabilities**: List all the major capabilities the app should have based on my overview + - **ui_layout**: Describe the UI structure if relevant + - **development_workflow**: Note any testing or development patterns + - **implementation_roadmap**: Break down the features into phases - be VERY detailed here, listing every feature that needs to be built + +4. **IMPORTANT**: Write the complete specification to the file \`.automaker/app_spec.txt\` +${featureGenerationStep} + +**Guidelines:** +- Be comprehensive! Include ALL features needed for a complete application +- Only include technology_stack sections that are relevant (e.g., skip desktop_shell if it's a web-only app) +- Add new sections to core_capabilities as needed for the specific project +- The implementation_roadmap should reflect logical phases for building out the app - list EVERY feature individually +- Consider user flows, error states, and edge cases when defining features +- Each phase should have multiple specific, actionable features + +Begin by exploring the project structure.`; + } + + /** + * Regenerate the app spec based on user's project definition + */ + async regenerateSpec(projectPath, projectDefinition, sendToRenderer, execution) { + console.log(`[SpecRegeneration] Regenerating spec for: ${projectPath}`); + + try { + const abortController = new AbortController(); + execution.abortController = abortController; + + const options = { + model: "claude-sonnet-4-20250514", + systemPrompt: this.getSystemPrompt(), + maxTurns: 50, + cwd: projectPath, + allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"], + permissionMode: "acceptEdits", + sandbox: { + enabled: true, + autoAllowBashIfSandboxed: true, + }, + abortController: abortController, + }; + + const prompt = this.buildRegenerationPrompt(projectDefinition); + + sendToRenderer({ + type: "spec_regeneration_progress", + content: "Starting spec regeneration...\n", + }); + + const currentQuery = query({ prompt, options }); + execution.query = currentQuery; + + let fullResponse = ""; + for await (const msg of currentQuery) { + if (!execution.isActive()) break; + + if (msg.type === "assistant" && msg.message?.content) { + for (const block of msg.message.content) { + if (block.type === "text") { + fullResponse += block.text; + sendToRenderer({ + type: "spec_regeneration_progress", + content: block.text, + }); + } else if (block.type === "tool_use") { + sendToRenderer({ + type: "spec_regeneration_tool", + tool: block.name, + input: block.input, + }); + } + } + } + } + + execution.query = null; + execution.abortController = null; + + sendToRenderer({ + type: "spec_regeneration_complete", + message: "Spec regeneration complete!", + }); + + return { + success: true, + message: "Spec regeneration complete", + }; + } catch (error) { + if (error instanceof AbortError || error?.name === "AbortError") { + console.log("[SpecRegeneration] Regeneration aborted"); + if (execution) { + execution.abortController = null; + execution.query = null; + } + return { + success: false, + message: "Regeneration aborted", + }; + } + + console.error("[SpecRegeneration] Error regenerating spec:", error); + if (execution) { + execution.abortController = null; + execution.query = null; + } + throw error; + } + } + + /** + * Get the system prompt for spec regeneration + */ + getSystemPrompt() { + return `You are an expert software architect and product manager. Your job is to analyze an existing codebase and generate a comprehensive application specification based on a user's project definition. + +You should: +1. First, thoroughly analyze the project structure to understand the existing tech stack +2. Read key configuration files (package.json, tsconfig.json, etc.) to understand dependencies and frameworks +3. Understand the current architecture and patterns used +4. Based on the user's project definition, create a comprehensive app specification that includes ALL features needed to realize their vision +5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application +6. Write the specification to .automaker/app_spec.txt + +When analyzing, look at: +- package.json, cargo.toml, or similar config files for tech stack +- Source code structure and organization +- Framework-specific patterns (Next.js, React, etc.) +- Database configurations and schemas +- API structures and patterns + +**CRITICAL FILE PROTECTION** + +THE FOLLOWING FILE IS ABSOLUTELY FORBIDDEN FROM DIRECT MODIFICATION: +- .automaker/feature_list.json + +**YOU MUST NEVER:** +- Use the Write tool on .automaker/feature_list.json +- Use the Edit tool on .automaker/feature_list.json +- Use any Bash command that writes to .automaker/feature_list.json + +You CAN and SHOULD modify: +- .automaker/app_spec.txt (this is your primary target) + +You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec.`; + } + + /** + * Build the prompt for regenerating the spec + */ + buildRegenerationPrompt(projectDefinition) { + return `I need you to regenerate my application specification based on the following project definition. Be very comprehensive and liberal when defining features - I want a complete, polished application. + +**My Project Definition:** +${projectDefinition} + +**Your Task:** + +1. First, explore the project to understand the existing tech stack: + - Read package.json or similar config files + - Identify all frameworks and libraries being used + - Understand the current project structure and architecture + - Note any database, authentication, or other infrastructure in use + +2. Based on my project definition and the existing tech stack, create a comprehensive app specification that includes: + - Product Overview: A clear description of what the app does + - Tech Stack: All technologies currently in use + - Features: A COMPREHENSIVE list of all features needed to realize my vision + - Be liberal! Include all features that would make this a complete, production-ready application + - Include core features, supporting features, and nice-to-have features + - Think about user experience, error handling, edge cases, etc. + - Architecture Notes: Any important architectural decisions or patterns + +3. **IMPORTANT**: Write the complete specification to the file \`.automaker/app_spec.txt\` + +**Format Guidelines for the Spec:** + +Use this general structure: + +\`\`\` +# [App Name] - Application Specification + +## Product Overview +[Description of what the app does and its purpose] + +## Tech Stack +- Frontend: [frameworks, libraries] +- Backend: [frameworks, APIs] +- Database: [if applicable] +- Other: [other relevant tech] + +## Features + +### [Category 1] +- **[Feature Name]**: [Detailed description of the feature] +- **[Feature Name]**: [Detailed description] +... + +### [Category 2] +- **[Feature Name]**: [Detailed description] +... + +## Architecture Notes +[Any important architectural notes, patterns, or conventions] +\`\`\` + +**Remember:** +- Be comprehensive! Include ALL features needed for a complete application +- Consider user flows, error states, loading states, etc. +- Include authentication, authorization if relevant +- Think about what would make this a polished, production-ready app +- The more detailed and complete the spec, the better + +Begin by exploring the project structure.`; + } + + /** + * Stop the current regeneration + */ + stop() { + if (this.runningRegeneration && this.runningRegeneration.abortController) { + this.runningRegeneration.abortController.abort(); + } + this.runningRegeneration = null; + } +} + +module.exports = new SpecRegenerationService(); diff --git a/app/src/components/layout/sidebar.tsx b/app/src/components/layout/sidebar.tsx index 43021508..45b10e2c 100644 --- a/app/src/components/layout/sidebar.tsx +++ b/app/src/components/layout/sidebar.tsx @@ -16,9 +16,12 @@ import { PanelLeft, PanelLeftClose, ChevronDown, + Redo2, Check, BookOpen, GripVertical, + RotateCw, + RotateCcw, Trash2, Undo2, } from "lucide-react"; @@ -44,8 +47,15 @@ import { KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; import { getElectronAPI, Project, TrashedProject } from "@/lib/electron"; -import { initializeProject } from "@/lib/project-init"; +import { + initializeProject, + hasAppSpec, + hasAutomakerDir, +} from "@/lib/project-init"; import { toast } from "sonner"; +import { Sparkles, Loader2 } from "lucide-react"; +import { Checkbox } from "@/components/ui/checkbox"; +import type { SpecRegenerationEvent } from "@/types/electron"; import { DndContext, DragEndEvent, @@ -155,6 +165,7 @@ export function Sidebar() { currentProject, currentView, sidebarOpen, + projectHistory, addProject, setCurrentProject, setCurrentView, @@ -163,6 +174,8 @@ export function Sidebar() { deleteTrashedProject, emptyTrash, reorderProjects, + cyclePrevProject, + cycleNextProject, } = useAppStore(); // State for project picker dropdown @@ -171,6 +184,17 @@ export function Sidebar() { const [activeTrashId, setActiveTrashId] = useState(null); const [isEmptyingTrash, setIsEmptyingTrash] = useState(false); + // State for new project setup dialog + const [showSetupDialog, setShowSetupDialog] = useState(false); + const [setupProjectPath, setSetupProjectPath] = useState(""); + const [projectOverview, setProjectOverview] = useState(""); + const [isCreatingSpec, setIsCreatingSpec] = useState(false); + const [creatingSpecProjectPath, setCreatingSpecProjectPath] = useState< + string | null + >(null); + const [generateFeatures, setGenerateFeatures] = useState(true); + const [showSpecIndicator, setShowSpecIndicator] = useState(true); + // Sensors for drag-and-drop const sensors = useSensors( useSensor(PointerSensor, { @@ -197,6 +221,93 @@ export function Sidebar() { [projects, reorderProjects] ); + // Subscribe to spec regeneration events + useEffect(() => { + const api = getElectronAPI(); + if (!api.specRegeneration) return; + + const unsubscribe = api.specRegeneration.onEvent( + (event: SpecRegenerationEvent) => { + console.log("[Sidebar] Spec regeneration event:", event.type); + + if (event.type === "spec_regeneration_complete") { + setIsCreatingSpec(false); + setCreatingSpecProjectPath(null); + setShowSetupDialog(false); + setProjectOverview(""); + setSetupProjectPath(""); + toast.success("App specification created", { + description: "Your project is now set up and ready to go!", + }); + // Navigate to spec view to show the new spec + setCurrentView("spec"); + } else if (event.type === "spec_regeneration_error") { + setIsCreatingSpec(false); + setCreatingSpecProjectPath(null); + toast.error("Failed to create specification", { + description: event.error, + }); + } + } + ); + + return () => { + unsubscribe(); + }; + }, [setCurrentView]); + + // Handle creating initial spec for new project + const handleCreateInitialSpec = useCallback(async () => { + if (!setupProjectPath || !projectOverview.trim()) return; + + setIsCreatingSpec(true); + setCreatingSpecProjectPath(setupProjectPath); + setShowSpecIndicator(true); + setShowSetupDialog(false); + + try { + const api = getElectronAPI(); + if (!api.specRegeneration) { + toast.error("Spec regeneration not available"); + setIsCreatingSpec(false); + setCreatingSpecProjectPath(null); + return; + } + const result = await api.specRegeneration.create( + setupProjectPath, + projectOverview.trim(), + generateFeatures + ); + + if (!result.success) { + console.error("[Sidebar] Failed to start spec creation:", result.error); + setIsCreatingSpec(false); + setCreatingSpecProjectPath(null); + toast.error("Failed to create specification", { + description: result.error, + }); + } + // If successful, we'll wait for the events to update the state + } catch (error) { + console.error("[Sidebar] Failed to create spec:", error); + setIsCreatingSpec(false); + setCreatingSpecProjectPath(null); + toast.error("Failed to create specification", { + description: error instanceof Error ? error.message : "Unknown error", + }); + } + }, [setupProjectPath, projectOverview]); + + // Handle skipping setup + const handleSkipSetup = useCallback(() => { + setShowSetupDialog(false); + setProjectOverview(""); + setSetupProjectPath(""); + toast.info("Setup skipped", { + description: "You can set up your app_spec.txt later from the Spec view.", + }); + }, []); + /** * Opens the system folder selection dialog and initializes the selected project. * Used by both the 'O' keyboard shortcut and the folder icon button. @@ -210,6 +321,9 @@ export function Sidebar() { const name = path.split("/").pop() || "Untitled Project"; try { + // Check if this is a brand new project (no .automaker directory) + const hadAutomakerDir = await hasAutomakerDir(path); + // Initialize the .automaker directory structure const initResult = await initializeProject(path); @@ -230,7 +344,20 @@ export function Sidebar() { addProject(project); setCurrentProject(project); - if (initResult.createdFiles && initResult.createdFiles.length > 0) { + // Check if app_spec.txt exists + const specExists = await hasAppSpec(path); + + if (!hadAutomakerDir && !specExists) { + // This is a brand new project - show setup dialog + setSetupProjectPath(path); + setShowSetupDialog(true); + toast.success("Project opened", { + description: `Opened ${name}. Let's set up your app specification!`, + }); + } else if ( + initResult.createdFiles && + initResult.createdFiles.length > 0 + ) { toast.success( initResult.isNewProject ? "Project initialized" : "Project updated", { @@ -422,6 +549,20 @@ export function Sidebar() { }); } + // Project cycling shortcuts - only when we have project history + if (projectHistory.length > 1) { + shortcuts.push({ + key: ACTION_SHORTCUTS.cyclePrevProject, + action: () => cyclePrevProject(), + description: "Cycle to previous project (MRU)", + }); + shortcuts.push({ + key: ACTION_SHORTCUTS.cycleNextProject, + action: () => cycleNextProject(), + description: "Cycle to next project (LRU)", + }); + } + // Only enable nav shortcuts if there's a current project if (currentProject) { navSections.forEach((section) => { @@ -451,6 +592,9 @@ export function Sidebar() { toggleSidebar, projects.length, handleOpenFolder, + projectHistory.length, + cyclePrevProject, + cycleNextProject, ]); // Register keyboard shortcuts @@ -464,7 +608,7 @@ export function Sidebar() {