From 72cc43d02ff8c49ed45f9b24434e05de526b3aba Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Wed, 10 Dec 2025 08:51:33 -0500 Subject: [PATCH] feat(feature-suggestions): implement feature suggestions and spec regeneration functionality - Introduced a new `FeatureSuggestionsService` to analyze projects and generate feature suggestions based on the project structure and existing features. - Added IPC handlers for generating and stopping feature suggestions, as well as checking their status. - Implemented a `SpecRegenerationService` to create and regenerate application specifications based on user-defined project overviews and definitions. - Enhanced the UI with a `FeatureSuggestionsDialog` for displaying generated suggestions and allowing users to import them into their project. - Updated the sidebar and board view components to integrate feature suggestions and spec regeneration functionalities, improving project management capabilities. These changes significantly enhance the application's ability to assist users in feature planning and specification management. --- .automaker/feature_list.json | 21 +- app/electron/main.js | 223 ++++++++ app/electron/preload.js | 52 ++ .../services/feature-suggestions-service.js | 269 +++++++++ .../services/spec-regeneration-service.js | 519 ++++++++++++++++++ app/src/components/layout/sidebar.tsx | 281 +++++++++- app/src/components/views/board-view.tsx | 105 +++- app/src/components/views/context-view.tsx | 53 +- .../views/feature-suggestions-dialog.tsx | 433 +++++++++++++++ app/src/components/views/spec-view.tsx | 303 +++++++++- app/src/hooks/use-auto-mode.ts | 146 +++-- app/src/hooks/use-keyboard-shortcuts.ts | 6 +- app/src/lib/electron.ts | 430 ++++++++++++++- app/src/lib/project-init.ts | 62 ++- app/src/store/app-store.ts | 146 ++++- app/src/types/electron.d.ts | 61 ++ 16 files changed, 2923 insertions(+), 187 deletions(-) create mode 100644 app/electron/services/feature-suggestions-service.js create mode 100644 app/electron/services/spec-regeneration-service.js create mode 100644 app/src/components/views/feature-suggestions-dialog.tsx 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() {