diff --git a/app/electron/auto-mode-service.js b/app/electron/auto-mode-service.js index 20c75246..cedd6fd6 100644 --- a/app/electron/auto-mode-service.js +++ b/app/electron/auto-mode-service.js @@ -391,8 +391,7 @@ class AutoModeService { featureId, "waiting_approval", projectPath, - null, // no summary - error.message // pass error message + { error: error.message } ); } catch (statusError) { console.error("[AutoMode] Failed to update feature status after error:", statusError); @@ -495,8 +494,7 @@ class AutoModeService { featureId, "waiting_approval", projectPath, - null, // no summary - error.message // pass error message + { error: error.message } ); } catch (statusError) { console.error("[AutoMode] Failed to update feature status after error:", statusError); @@ -662,8 +660,7 @@ class AutoModeService { featureId, "waiting_approval", projectPath, - null, // no summary - error.message // pass error message + { error: error.message } ); } catch (statusError) { console.error("[AutoMode] Failed to update feature status after error:", statusError); @@ -859,8 +856,7 @@ class AutoModeService { featureId, "waiting_approval", projectPath, - null, // no summary - error.message // pass error message + { error: error.message } ); } catch (statusError) { console.error("[AutoMode] Failed to update feature status after error:", statusError); @@ -1102,8 +1098,7 @@ class AutoModeService { featureId, "waiting_approval", projectPath, - null, // no summary - error.message // pass error message + { error: error.message } ); } catch (statusError) { console.error("[AutoMode] Failed to update feature status after error:", statusError); diff --git a/app/electron/main.js b/app/electron/main.js index 41e96442..5fc2b0a1 100644 --- a/app/electron/main.js +++ b/app/electron/main.js @@ -898,7 +898,7 @@ ipcMain.handle( featureId, status, projectPath, - summary + { summary } ); // Notify renderer if window is available @@ -1170,6 +1170,7 @@ ipcMain.handle("spec-regeneration:status", () => { isRunning: specRegenerationExecution !== null && specRegenerationExecution.isActive(), + currentPhase: specRegenerationService.getCurrentPhase(), }; }); @@ -1234,6 +1235,62 @@ ipcMain.handle( } ); +/** + * Generate features from existing app_spec.txt + * This allows users to generate features retroactively without regenerating the spec + */ +ipcMain.handle( + "spec-regeneration:generate-features", + async (_, { projectPath }) => { + try { + // Add project path to allowed paths + addAllowedPath(projectPath); + + // 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 generating features (runs in background) + specRegenerationService + .generateFeaturesOnly(projectPath, sendToRenderer, specRegenerationExecution) + .catch((error) => { + console.error( + "[IPC] spec-regeneration:generate-features 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-features error:", error); + specRegenerationExecution = null; + return { success: false, error: error.message }; + } + } +); + /** * Merge feature worktree changes back to main branch */ diff --git a/app/electron/preload.js b/app/electron/preload.js index aa3d68c1..85a31baa 100644 --- a/app/electron/preload.js +++ b/app/electron/preload.js @@ -285,6 +285,12 @@ contextBridge.exposeInMainWorld("electronAPI", { projectDefinition, }), + // Generate features from existing app_spec.txt + generateFeatures: (projectPath) => + ipcRenderer.invoke("spec-regeneration:generate-features", { + projectPath, + }), + // Stop regenerating spec stop: () => ipcRenderer.invoke("spec-regeneration:stop"), diff --git a/app/electron/services/feature-loader.js b/app/electron/services/feature-loader.js index 1ff3a7c0..d95ba08c 100644 --- a/app/electron/services/feature-loader.js +++ b/app/electron/services/feature-loader.js @@ -170,14 +170,38 @@ class FeatureLoader { const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId); try { + // Read feature.json directly - handle ENOENT in catch block + // This avoids TOCTOU race condition from checking with fs.access first const content = await fs.readFile(featureJsonPath, "utf-8"); const feature = JSON.parse(content); + + // Validate that the feature has required fields + if (!feature.id) { + console.warn( + `[FeatureLoader] Feature ${featureId} missing required 'id' field, skipping` + ); + continue; + } + features.push(feature); } catch (error) { - console.error( - `[FeatureLoader] Failed to load feature ${featureId}:`, - error - ); + // Handle different error types appropriately + if (error.code === "ENOENT") { + // File doesn't exist - this is expected for incomplete feature directories + // Skip silently (feature.json not yet created or was removed) + continue; + } else if (error instanceof SyntaxError) { + // JSON parse error - log as warning since file exists but is malformed + console.warn( + `[FeatureLoader] Failed to parse feature.json for ${featureId}: ${error.message}` + ); + } else { + // Other errors - log as error + console.error( + `[FeatureLoader] Failed to load feature ${featureId}:`, + error.message || error + ); + } // Continue loading other features } } @@ -339,30 +363,93 @@ class FeatureLoader { /** * Update feature status (legacy API) * Features are stored in .automaker/features/{id}/feature.json + * Creates the feature if it doesn't exist. * @param {string} featureId - The ID of the feature to update * @param {string} status - The new status * @param {string} projectPath - Path to the project - * @param {string} [summary] - Optional summary of what was done - * @param {string} [error] - Optional error message if feature errored + * @param {Object} options - Options object for optional parameters + * @param {string} [options.summary] - Optional summary of what was done + * @param {string} [options.error] - Optional error message if feature errored + * @param {string} [options.description] - Optional detailed description + * @param {string} [options.category] - Optional category/phase + * @param {string[]} [options.steps] - Optional array of implementation steps */ - async updateFeatureStatus(featureId, status, projectPath, summary, error) { + async updateFeatureStatus(featureId, status, projectPath, options = {}) { + const { summary, error, description, category, steps } = options; + // Check if feature exists + const existingFeature = await this.get(projectPath, featureId); + + if (!existingFeature) { + // Feature doesn't exist - create it with all required fields + console.log(`[FeatureLoader] Feature ${featureId} not found - creating new feature`); + const newFeature = { + id: featureId, + title: featureId.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '), + description: description || summary || '', // Use provided description, fall back to summary + category: category || "Uncategorized", + steps: steps || [], + status: status, + images: [], + imagePaths: [], + skipTests: false, // Auto-generated features should run tests by default + model: "sonnet", + thinkingLevel: "none", + summary: summary || description || '', + createdAt: new Date().toISOString(), + }; + if (error !== undefined) { + newFeature.error = error; + } + await this.create(projectPath, newFeature); + console.log( + `[FeatureLoader] Created feature ${featureId}: status=${status}, category=${category || "Uncategorized"}, steps=${steps?.length || 0}${ + summary ? `, summary="${summary}"` : "" + }` + ); + return; + } + + // Feature exists - update it const updates = { status }; if (summary !== undefined) { updates.summary = summary; + // Also update description if it's empty or not set + if (!existingFeature.description) { + updates.description = summary; + } + } + if (description !== undefined) { + updates.description = description; + } + if (category !== undefined) { + updates.category = category; + } + if (steps !== undefined && Array.isArray(steps)) { + updates.steps = steps; } if (error !== undefined) { updates.error = error; } else { // Clear error if not provided - const feature = await this.get(projectPath, featureId); - if (feature && feature.error) { + if (existingFeature.error) { updates.error = undefined; } } + + // Ensure required fields exist (for features created before this fix) + if (!existingFeature.category && !updates.category) updates.category = "Uncategorized"; + if (!existingFeature.steps && !updates.steps) updates.steps = []; + if (!existingFeature.images) updates.images = []; + if (!existingFeature.imagePaths) updates.imagePaths = []; + if (existingFeature.skipTests === undefined) updates.skipTests = false; + if (!existingFeature.model) updates.model = "sonnet"; + if (!existingFeature.thinkingLevel) updates.thinkingLevel = "none"; await this.update(projectPath, featureId, updates); console.log( `[FeatureLoader] Updated feature ${featureId}: status=${status}${ + category ? `, category="${category}"` : "" + }${steps ? `, steps=${steps.length}` : ""}${ summary ? `, summary="${summary}"` : "" }` ); diff --git a/app/electron/services/mcp-server-factory.js b/app/electron/services/mcp-server-factory.js index b890dfbe..4caddfe2 100644 --- a/app/electron/services/mcp-server-factory.js +++ b/app/electron/services/mcp-server-factory.js @@ -19,37 +19,58 @@ class McpServerFactory { tools: [ tool( "UpdateFeatureStatus", - "Update the status of a feature. Use this tool instead of directly modifying feature files to safely update feature status. IMPORTANT: If the feature has skipTests=true, you should NOT mark it as verified - instead it will automatically go to waiting_approval status for manual review. Always include a summary of what was done.", + "Create or update a feature. Use this tool to create new features with detailed information or update existing feature status. When creating features, provide comprehensive description, category, and implementation steps.", { - featureId: z.string().describe("The ID of the feature to update"), - status: z.enum(["backlog", "in_progress", "verified"]).describe("The new status for the feature. Note: If skipTests=true, verified will be converted to waiting_approval automatically."), - summary: z.string().optional().describe("A brief summary of what was implemented/changed. This will be displayed on the Kanban card. Example: 'Added dark mode toggle. Modified: settings.tsx, theme-provider.tsx'") + featureId: z.string().describe("The ID of the feature (lowercase, hyphens for spaces). Example: 'user-authentication', 'budget-tracking'"), + status: z.enum(["backlog", "todo", "in_progress", "verified"]).describe("The status for the feature. Use 'backlog' or 'todo' for new features."), + summary: z.string().optional().describe("A brief summary of what was implemented/changed or what the feature does."), + description: z.string().optional().describe("A detailed description of the feature. Be comprehensive - explain what the feature does, its purpose, and key functionality."), + category: z.string().optional().describe("The category/phase for this feature. Example: 'Phase 1: Foundation', 'Phase 2: Core Logic', 'Phase 3: Polish', 'Authentication', 'UI/UX'"), + steps: z.array(z.string()).optional().describe("Array of implementation steps. Each step should be a clear, actionable task. Example: ['Set up database schema', 'Create API endpoints', 'Build UI components', 'Add validation']") }, async (args) => { try { - console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}, summary=${args.summary || "(none)"}`); + console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}, summary=${args.summary || "(none)"}, category=${args.category || "(none)"}, steps=${args.steps?.length || 0}`); + console.log(`[Feature Creation] Creating/updating feature "${args.featureId}" with status "${args.status}"`); // Load the feature to check skipTests flag const features = await featureLoader.loadFeatures(projectPath); const feature = features.find((f) => f.id === args.featureId); if (!feature) { - throw new Error(`Feature ${args.featureId} not found`); + console.log(`[Feature Creation] Feature ${args.featureId} not found - this might be a new feature being created`); + // This might be a new feature - try to proceed anyway } // If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval let finalStatus = args.status; - if (args.status === "verified" && feature.skipTests === true) { + // Convert 'todo' to 'backlog' for consistency, but only for new features + if (!feature && finalStatus === "todo") { + finalStatus = "backlog"; + } + if (feature && args.status === "verified" && feature.skipTests === true) { console.log(`[McpServerFactory] Feature ${args.featureId} has skipTests=true, converting verified -> waiting_approval`); finalStatus = "waiting_approval"; } - // Call the provided callback to update feature status with summary - await updateFeatureStatusCallback(args.featureId, finalStatus, projectPath, args.summary); + // Call the provided callback to update feature status + await updateFeatureStatusCallback( + args.featureId, + finalStatus, + projectPath, + { + summary: args.summary, + description: args.description, + category: args.category, + steps: args.steps, + } + ); const statusMessage = finalStatus !== args.status - ? `Successfully updated feature ${args.featureId} to status "${finalStatus}" (converted from "${args.status}" because skipTests=true)${args.summary ? ` with summary: "${args.summary}"` : ""}` - : `Successfully updated feature ${args.featureId} to status "${finalStatus}"${args.summary ? ` with summary: "${args.summary}"` : ""}`; + ? `Successfully created/updated feature ${args.featureId} to status "${finalStatus}" (converted from "${args.status}")${args.summary ? ` - ${args.summary}` : ""}` + : `Successfully created/updated feature ${args.featureId} to status "${finalStatus}"${args.summary ? ` - ${args.summary}` : ""}`; + + console.log(`[Feature Creation] ✓ ${statusMessage}`); return { content: [{ @@ -59,6 +80,7 @@ class McpServerFactory { }; } catch (error) { console.error("[McpServerFactory] UpdateFeatureStatus tool error:", error); + console.error(`[Feature Creation] ✗ Failed to create/update feature ${args.featureId}: ${error.message}`); return { content: [{ type: "text", diff --git a/app/electron/services/mcp-server-stdio.js b/app/electron/services/mcp-server-stdio.js index 44cdd6b4..798f12d1 100644 --- a/app/electron/services/mcp-server-stdio.js +++ b/app/electron/services/mcp-server-stdio.js @@ -215,7 +215,7 @@ async function handleToolsCall(params, id) { // Call the update callback via IPC or direct call // Since we're in a separate process, we need to use IPC to communicate back // For now, we'll call the feature loader directly since it has the update method - await featureLoader.updateFeatureStatus(featureId, finalStatus, projectPath, summary); + await featureLoader.updateFeatureStatus(featureId, finalStatus, projectPath, { summary }); const statusMessage = finalStatus !== status ? `Successfully updated feature ${featureId} to status "${finalStatus}" (converted from "${status}" because skipTests=true)${summary ? ` with summary: "${summary}"` : ''}` diff --git a/app/electron/services/prompt-builder.js b/app/electron/services/prompt-builder.js index 2c793403..d2e44d9c 100644 --- a/app/electron/services/prompt-builder.js +++ b/app/electron/services/prompt-builder.js @@ -52,11 +52,11 @@ ${memoryContent} **Current Feature to Implement:** ID: ${feature.id} -Category: ${feature.category} -Description: ${feature.description} +Category: ${feature.category || "Uncategorized"} +Description: ${feature.description || feature.summary || feature.title || "No description provided"} ${skipTestsNote}${imagesNote}${contextFilesPreview} **Steps to Complete:** -${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")} +${(feature.steps || []).map((step, i) => `${i + 1}. ${step}`).join("\n") || "No specific steps provided - implement based on description"} **Your Task:** @@ -195,12 +195,12 @@ ${memoryContent} **Feature to Implement/Verify:** ID: ${feature.id} -Category: ${feature.category} -Description: ${feature.description} +Category: ${feature.category || "Uncategorized"} +Description: ${feature.description || feature.summary || feature.title || "No description provided"} Current Status: ${feature.status} ${skipTestsNote}${imagesNote}${contextFilesPreview} **Steps that should be implemented:** -${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")} +${(feature.steps || []).map((step, i) => `${i + 1}. ${step}`).join("\n") || "No specific steps provided - implement based on description"} **Your Task:** @@ -335,11 +335,11 @@ ${memoryContent} **Current Feature:** ID: ${feature.id} -Category: ${feature.category} -Description: ${feature.description} +Category: ${feature.category || "Uncategorized"} +Description: ${feature.description || feature.summary || feature.title || "No description provided"} ${skipTestsNote}${imagesNote}${contextFilesPreview} **Steps to Complete:** -${feature.steps.map((step, i) => `${i + 1}. ${step}`).join("\n")} +${(feature.steps || []).map((step, i) => `${i + 1}. ${step}`).join("\n") || "No specific steps provided - implement based on description"} **Previous Work Context:** diff --git a/app/electron/services/spec-regeneration-service.js b/app/electron/services/spec-regeneration-service.js index 99cad5f1..a977a0a6 100644 --- a/app/electron/services/spec-regeneration-service.js +++ b/app/electron/services/spec-regeneration-service.js @@ -1,6 +1,8 @@ const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk"); const fs = require("fs/promises"); const path = require("path"); +const mcpServerFactory = require("./mcp-server-factory"); +const featureLoader = require("./feature-loader"); /** * XML template for app_spec.txt @@ -84,6 +86,15 @@ const APP_SPEC_XML_TEMPLATE = ` class SpecRegenerationService { constructor() { this.runningRegeneration = null; + this.currentPhase = ""; // Tracks current phase for status queries + } + + /** + * Get the current phase of the regeneration process + * @returns {string} Current phase or empty string if not running + */ + getCurrentPhase() { + return this.currentPhase; } /** @@ -95,18 +106,59 @@ class SpecRegenerationService { * @param {boolean} generateFeatures - Whether to generate feature entries in features folder */ async createInitialSpec(projectPath, projectOverview, sendToRenderer, execution, generateFeatures = true) { - console.log(`[SpecRegeneration] Creating initial spec for: ${projectPath}, generateFeatures: ${generateFeatures}`); + const startTime = Date.now(); + console.log(`[SpecRegeneration] ===== Starting initial spec creation =====`); + console.log(`[SpecRegeneration] Project path: ${projectPath}`); + console.log(`[SpecRegeneration] Generate features: ${generateFeatures}`); + console.log(`[SpecRegeneration] Project overview length: ${projectOverview.length} characters`); try { const abortController = new AbortController(); execution.abortController = abortController; + // Phase tracking - use instance property for status queries + this.currentPhase = "initialization"; + + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Phase: ${this.currentPhase}] Initializing spec generation process...\n`, + }); + console.log(`[SpecRegeneration] Phase: ${this.currentPhase}`); + + // Create custom MCP server with UpdateFeatureStatus tool if generating features + if (generateFeatures) { + console.log("[SpecRegeneration] Setting up feature generation tools..."); + try { + mcpServerFactory.createFeatureToolsServer( + featureLoader.updateFeatureStatus.bind(featureLoader), + projectPath + ); + console.log("[SpecRegeneration] Feature tools server created successfully"); + } catch (error) { + console.error("[SpecRegeneration] ERROR: Failed to create feature tools server:", error); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Failed to initialize feature generation tools: ${error.message}`, + }); + throw error; + } + } + + this.currentPhase = "setup"; + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Phase: ${this.currentPhase}] Configuring AI agent and tools...\n`, + }); + console.log(`[SpecRegeneration] Phase: ${this.currentPhase}`); + + // Phase 1: Generate spec WITHOUT UpdateFeatureStatus tool + // This prevents features from being created before the spec is complete const options = { model: "claude-sonnet-4-20250514", - systemPrompt: this.getInitialCreationSystemPrompt(generateFeatures), + systemPrompt: this.getInitialCreationSystemPrompt(false), // Always false - no feature tools during spec gen maxTurns: 50, cwd: projectPath, - allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"], + allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"], // No UpdateFeatureStatus during spec gen permissionMode: "acceptEdits", sandbox: { enabled: true, @@ -115,54 +167,157 @@ class SpecRegenerationService { abortController: abortController, }; - const prompt = this.buildInitialCreationPrompt(projectOverview, generateFeatures); + const prompt = this.buildInitialCreationPrompt(projectOverview); // No feature generation during spec creation + this.currentPhase = "analysis"; sendToRenderer({ type: "spec_regeneration_progress", - content: "Starting project analysis and spec creation...\n", + content: `[Phase: ${this.currentPhase}] Starting project analysis and spec creation...\n`, }); + console.log(`[SpecRegeneration] Phase: ${this.currentPhase} - Starting AI agent query`); + + if (generateFeatures) { + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Phase: ${this.currentPhase}] Feature generation is enabled - features will be created after spec is complete.\n`, + }); + console.log("[SpecRegeneration] Feature generation enabled - will create features after spec"); + } const currentQuery = query({ prompt, options }); execution.query = currentQuery; let fullResponse = ""; - for await (const msg of currentQuery) { - if (!execution.isActive()) break; + let toolCallCount = 0; + let messageCount = 0; + + try { + for await (const msg of currentQuery) { + if (!execution.isActive()) { + console.log("[SpecRegeneration] Execution aborted by user"); + 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, - }); + if (msg.type === "assistant" && msg.message?.content) { + messageCount++; + for (const block of msg.message.content) { + if (block.type === "text") { + fullResponse += block.text; + const preview = block.text.substring(0, 100).replace(/\n/g, " "); + console.log(`[SpecRegeneration] Agent message #${messageCount}: ${preview}...`); + sendToRenderer({ + type: "spec_regeneration_progress", + content: block.text, + }); + } else if (block.type === "tool_use") { + toolCallCount++; + const toolName = block.name; + console.log(`[SpecRegeneration] Tool call #${toolCallCount}: ${toolName}`); + console.log(`[SpecRegeneration] Tool input: ${JSON.stringify(block.input).substring(0, 200)}...`); + + sendToRenderer({ + type: "spec_regeneration_progress", + content: `\n[Tool] Using ${toolName}...\n`, + }); + + sendToRenderer({ + type: "spec_regeneration_tool", + tool: toolName, + input: block.input, + }); + } } + } else if (msg.type === "tool_result") { + const toolName = msg.toolName || "unknown"; + const result = msg.content?.[0]?.text || JSON.stringify(msg.content); + const resultPreview = result.substring(0, 200).replace(/\n/g, " "); + console.log(`[SpecRegeneration] Tool result (${toolName}): ${resultPreview}...`); + + // During spec generation, UpdateFeatureStatus is not available + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Tool Result] ${toolName} completed successfully\n`, + }); + } else if (msg.type === "error") { + const errorMsg = msg.error?.message || JSON.stringify(msg.error); + console.error(`[SpecRegeneration] ERROR in query stream: ${errorMsg}`); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Error during spec generation: ${errorMsg}`, + }); } } + } catch (streamError) { + console.error("[SpecRegeneration] ERROR in query stream:", streamError); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Stream error: ${streamError.message || String(streamError)}`, + }); + throw streamError; } + + console.log(`[SpecRegeneration] Query completed - ${messageCount} messages, ${toolCallCount} tool calls`); execution.query = null; execution.abortController = null; + const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); + this.currentPhase = "spec_complete"; + console.log(`[SpecRegeneration] Phase: ${this.currentPhase} - Spec creation completed in ${elapsedTime}s`); sendToRenderer({ - type: "spec_regeneration_complete", - message: "Initial spec creation complete!", + type: "spec_regeneration_progress", + content: `\n[Phase: ${this.currentPhase}] ✓ App specification created successfully! (${elapsedTime}s)\n`, }); + + if (generateFeatures) { + // Phase 2: Generate features AFTER spec is complete + console.log(`[SpecRegeneration] Starting Phase 2: Feature generation from app_spec.txt`); + + // Send intermediate completion event for spec creation + sendToRenderer({ + type: "spec_regeneration_complete", + message: "Initial spec creation complete! Features are being generated...", + }); + + // Now start feature generation in a separate query + try { + await this.generateFeaturesFromSpec(projectPath, sendToRenderer, execution, startTime); + console.log(`[SpecRegeneration] Feature generation completed successfully`); + } catch (featureError) { + console.error(`[SpecRegeneration] Feature generation failed:`, featureError); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Feature generation failed: ${featureError.message || String(featureError)}`, + }); + } + } else { + this.currentPhase = "complete"; + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Phase: ${this.currentPhase}] All tasks completed!\n`, + }); + + // Send final completion event + sendToRenderer({ + type: "spec_regeneration_complete", + message: "Initial spec creation complete!", + }); + } + console.log(`[SpecRegeneration] ===== Initial spec creation finished successfully =====`); return { success: true, message: "Initial spec creation complete", }; } catch (error) { + const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); + if (error instanceof AbortError || error?.name === "AbortError") { - console.log("[SpecRegeneration] Creation aborted"); + console.log(`[SpecRegeneration] Creation aborted after ${elapsedTime}s`); + sendToRenderer({ + type: "spec_regeneration_error", + error: "Spec generation was aborted by user", + }); if (execution) { execution.abortController = null; execution.query = null; @@ -173,7 +328,244 @@ class SpecRegenerationService { }; } - console.error("[SpecRegeneration] Error creating initial spec:", error); + const errorMessage = error.message || String(error); + const errorStack = error.stack || ""; + console.error(`[SpecRegeneration] ERROR creating initial spec after ${elapsedTime}s:`); + console.error(`[SpecRegeneration] Error message: ${errorMessage}`); + console.error(`[SpecRegeneration] Error stack: ${errorStack}`); + + sendToRenderer({ + type: "spec_regeneration_error", + error: `Failed to create spec: ${errorMessage}`, + }); + + if (execution) { + execution.abortController = null; + execution.query = null; + } + throw error; + } + } + + /** + * Generate features from the implementation roadmap in app_spec.txt + * This is called AFTER the spec has been created + */ + async generateFeaturesFromSpec(projectPath, sendToRenderer, execution, startTime) { + const featureStartTime = Date.now(); + this.currentPhase = "feature_generation"; + + console.log(`[SpecRegeneration] ===== Starting Phase 2: Feature Generation =====`); + console.log(`[SpecRegeneration] Project path: ${projectPath}`); + + sendToRenderer({ + type: "spec_regeneration_progress", + content: `\n[Phase: ${this.currentPhase}] Starting feature creation from implementation roadmap...\n`, + }); + console.log(`[SpecRegeneration] Phase: ${this.currentPhase} - Starting feature generation query`); + + try { + // Create feature tools server + const featureToolsServer = mcpServerFactory.createFeatureToolsServer( + featureLoader.updateFeatureStatus.bind(featureLoader), + projectPath + ); + + const abortController = new AbortController(); + execution.abortController = abortController; + + const options = { + model: "claude-sonnet-4-20250514", + systemPrompt: `You are a feature management assistant. Your job is to analyze an existing codebase, compare it against the app_spec.txt, and create feature entries for work that still needs to be done. + +**CRITICAL: You must analyze the existing codebase FIRST before creating features.** + +**Your Task:** +1. Read the .automaker/app_spec.txt file thoroughly to understand the planned features +2. **ANALYZE THE EXISTING CODEBASE** - Look at: + - package.json/requirements.txt for installed dependencies + - Source code structure (src/, app/, components/, pages/, etc.) + - Existing components, routes, API endpoints, database schemas + - Configuration files, authentication setup, etc. +3. For EACH feature in the implementation_roadmap: + - Determine if it's ALREADY IMPLEMENTED (fully or partially) + - If fully implemented: Create with status "verified" and note what's done + - If partially implemented: Create with status "in_progress" and note remaining work + - If not started: Create with status "backlog" + +**IMPORTANT - For each feature you MUST provide:** +- **featureId**: A descriptive ID (lowercase, hyphens for spaces). Example: "user-authentication", "budget-tracking" +- **status**: + - "verified" if feature is fully implemented in the codebase + - "in_progress" if partially implemented + - "backlog" if not yet started +- **description**: A DETAILED description (2-4 sentences) explaining what the feature does, its purpose, and key functionality +- **category**: The phase from the roadmap (e.g., "Phase 1: Foundation", "Phase 2: Core Logic", "Phase 3: Polish") +- **steps**: An array of 4-8 clear, actionable implementation steps. For verified features, these are what WAS done. For backlog, these are what NEEDS to be done. +- **summary**: A brief one-line summary. For verified features, describe what's implemented. + +**Example of analyzing existing code:** +If you find NextAuth.js configured in the codebase with working login pages, the user-authentication feature should be "verified" not "backlog". + +**Example of a well-defined feature:** +{ + "featureId": "user-authentication", + "status": "verified", // Because we found it's already implemented + "description": "Secure user authentication system with email/password login and session management. Already implemented using NextAuth.js with email provider.", + "category": "Phase 1: Foundation", + "steps": [ + "Set up authentication provider (NextAuth.js) - DONE", + "Configure email/password authentication - DONE", + "Create login and registration UI components - DONE", + "Implement session management - DONE" + ], + "summary": "Authentication implemented with NextAuth.js email provider" +} + +**Feature Storage:** +Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. +Use the UpdateFeatureStatus tool to create features with ALL the fields above.`, + maxTurns: 50, + cwd: projectPath, + mcpServers: { + "automaker-tools": featureToolsServer, + }, + allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "mcp__automaker-tools__UpdateFeatureStatus"], + permissionMode: "acceptEdits", + sandbox: { + enabled: true, + autoAllowBashIfSandboxed: true, + }, + abortController: abortController, + }; + + const prompt = `Analyze this project and create feature entries based on the app_spec.txt implementation roadmap. + +**IMPORTANT: You must analyze the existing codebase to determine what's already implemented.** + +**Your workflow:** +1. **First, analyze the existing codebase:** + - Read package.json or similar config files to see what's installed + - Explore the source code structure (use Glob to list directories) + - Look at key files: components, pages, API routes, database schemas + - Check for authentication, routing, state management, etc. + +2. **Then, read .automaker/app_spec.txt** to see the implementation roadmap + +3. **For EACH feature in the roadmap, determine its status:** + - Is it ALREADY IMPLEMENTED in the codebase? → status: "verified" + - Is it PARTIALLY IMPLEMENTED? → status: "in_progress" + - Is it NOT STARTED? → status: "backlog" + +4. **Create each feature with UpdateFeatureStatus including ALL fields:** + - featureId: Descriptive ID (lowercase, hyphens) + - status: "verified", "in_progress", or "backlog" based on your analysis + - description: 2-4 sentences explaining the feature + - category: The phase name from the roadmap + - steps: Array of 4-8 implementation steps + - summary: One-line summary (for verified features, note what's implemented) + +**Start by exploring the project structure, then read the app_spec, then create features with accurate statuses.**`; + + const currentQuery = query({ prompt, options }); + execution.query = currentQuery; + + const counters = { toolCallCount: 0, messageCount: 0 }; + + try { + for await (const msg of currentQuery) { + if (!execution.isActive()) { + console.log("[SpecRegeneration] Feature generation aborted by user"); + break; + } + + if (msg.type === "assistant" && msg.message?.content) { + this._handleAssistantMessage(msg, sendToRenderer, counters); + } else if (msg.type === "tool_result") { + this._handleToolResult(msg, sendToRenderer); + } else if (msg.type === "error") { + this._handleStreamError(msg, sendToRenderer); + } + } + } catch (streamError) { + console.error("[SpecRegeneration] ERROR in feature generation stream:", streamError); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Feature generation stream error: ${streamError.message || String(streamError)}`, + }); + throw streamError; + } + + console.log(`[SpecRegeneration] Feature generation completed - ${counters.messageCount} messages, ${counters.toolCallCount} tool calls`); + + execution.query = null; + execution.abortController = null; + + this.currentPhase = "complete"; + const featureElapsedTime = ((Date.now() - featureStartTime) / 1000).toFixed(1); + const totalElapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); + sendToRenderer({ + type: "spec_regeneration_progress", + content: `\n[Phase: ${this.currentPhase}] ✓ All tasks completed! (${totalElapsedTime}s total, ${featureElapsedTime}s for features)\n`, + }); + sendToRenderer({ + type: "spec_regeneration_complete", + message: "All tasks completed!", + }); + console.log(`[SpecRegeneration] All tasks completed including feature generation`); + + } catch (error) { + const errorMessage = error.message || String(error); + console.error(`[SpecRegeneration] ERROR generating features: ${errorMessage}`); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Failed to generate features: ${errorMessage}`, + }); + throw error; + } + } + + /** + * Generate features from existing app_spec.txt + * This is a standalone method that can be called without generating a new spec + * Useful for retroactively generating features from an existing spec + */ + async generateFeaturesOnly(projectPath, sendToRenderer, execution) { + const startTime = Date.now(); + console.log(`[SpecRegeneration] ===== Starting standalone feature generation =====`); + console.log(`[SpecRegeneration] Project path: ${projectPath}`); + + try { + // Verify app_spec.txt exists + const specPath = path.join(projectPath, ".automaker", "app_spec.txt"); + try { + await fs.access(specPath); + } catch { + sendToRenderer({ + type: "spec_regeneration_error", + error: "No app_spec.txt found. Please create a spec first before generating features.", + }); + throw new Error("No app_spec.txt found"); + } + + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Phase: initialization] Starting feature generation from existing app_spec.txt...\n`, + }); + + // Use the existing feature generation method + await this.generateFeaturesFromSpec(projectPath, sendToRenderer, execution, startTime); + + console.log(`[SpecRegeneration] ===== Standalone feature generation finished successfully =====`); + return { + success: true, + message: "Feature generation complete", + }; + } catch (error) { + const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); + const errorMessage = error.message || String(error); + console.error(`[SpecRegeneration] ERROR in standalone feature generation after ${elapsedTime}s: ${errorMessage}`); + if (execution) { execution.abortController = null; execution.query = null; @@ -205,14 +597,12 @@ When analyzing, look at: - Database configurations and schemas - API structures and patterns -**Feature Storage:** -Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. -Do NOT manually create feature files. Use the UpdateFeatureStatus tool to manage features. - 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.`; +You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec. + +**IMPORTANT:** Focus ONLY on creating the app_spec.txt file. Do NOT create any feature files or use any feature management tools during this phase.`; } /** @@ -266,12 +656,29 @@ 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}`); + const startTime = Date.now(); + console.log(`[SpecRegeneration] ===== Starting spec regeneration =====`); + console.log(`[SpecRegeneration] Project path: ${projectPath}`); + console.log(`[SpecRegeneration] Project definition length: ${projectDefinition.length} characters`); try { + this.currentPhase = "initialization"; + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Phase: ${this.currentPhase}] Initializing spec regeneration process...\n`, + }); + console.log(`[SpecRegeneration] Phase: ${this.currentPhase}`); + const abortController = new AbortController(); execution.abortController = abortController; + this.currentPhase = "setup"; + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Phase: ${this.currentPhase}] Configuring AI agent and tools...\n`, + }); + console.log(`[SpecRegeneration] Phase: ${this.currentPhase}`); + const options = { model: "claude-sonnet-4-20250514", systemPrompt: this.getSystemPrompt(), @@ -288,52 +695,118 @@ Begin by exploring the project structure.`; const prompt = this.buildRegenerationPrompt(projectDefinition); + this.currentPhase = "regeneration"; sendToRenderer({ type: "spec_regeneration_progress", - content: "Starting spec regeneration...\n", + content: `[Phase: ${this.currentPhase}] Starting spec regeneration...\n`, }); + console.log(`[SpecRegeneration] Phase: ${this.currentPhase} - Starting AI agent query`); const currentQuery = query({ prompt, options }); execution.query = currentQuery; let fullResponse = ""; - for await (const msg of currentQuery) { - if (!execution.isActive()) break; + let toolCallCount = 0; + let messageCount = 0; + + try { + for await (const msg of currentQuery) { + if (!execution.isActive()) { + console.log("[SpecRegeneration] Execution aborted by user"); + 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, - }); + if (msg.type === "assistant" && msg.message?.content) { + messageCount++; + for (const block of msg.message.content) { + if (block.type === "text") { + fullResponse += block.text; + const preview = block.text.substring(0, 100).replace(/\n/g, " "); + console.log(`[SpecRegeneration] Agent message #${messageCount}: ${preview}...`); + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Agent] ${block.text}`, + }); + } else if (block.type === "tool_use") { + toolCallCount++; + const toolName = block.name; + const toolInput = block.input; + console.log(`[SpecRegeneration] Tool call #${toolCallCount}: ${toolName}`); + console.log(`[SpecRegeneration] Tool input: ${JSON.stringify(toolInput).substring(0, 200)}...`); + + sendToRenderer({ + type: "spec_regeneration_progress", + content: `\n[Tool] Using ${toolName}...\n`, + }); + + sendToRenderer({ + type: "spec_regeneration_tool", + tool: toolName, + input: toolInput, + }); + } } + } else if (msg.type === "tool_result") { + // Log tool results for better visibility + const toolName = msg.toolName || "unknown"; + const result = msg.content?.[0]?.text || JSON.stringify(msg.content); + const resultPreview = result.substring(0, 200).replace(/\n/g, " "); + console.log(`[SpecRegeneration] Tool result (${toolName}): ${resultPreview}...`); + + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Tool Result] ${toolName} completed successfully\n`, + }); + } else if (msg.type === "error") { + const errorMsg = msg.error?.message || JSON.stringify(msg.error); + console.error(`[SpecRegeneration] ERROR in query stream: ${errorMsg}`); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Error during spec regeneration: ${errorMsg}`, + }); } } + } catch (streamError) { + console.error("[SpecRegeneration] ERROR in query stream:", streamError); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Stream error: ${streamError.message || String(streamError)}`, + }); + throw streamError; } + + console.log(`[SpecRegeneration] Query completed - ${messageCount} messages, ${toolCallCount} tool calls`); execution.query = null; execution.abortController = null; + const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); + this.currentPhase = "complete"; + console.log(`[SpecRegeneration] Phase: ${this.currentPhase} - Spec regeneration completed in ${elapsedTime}s`); + sendToRenderer({ + type: "spec_regeneration_progress", + content: `\n[Phase: ${this.currentPhase}] ✓ Spec regeneration complete! (${elapsedTime}s)\n`, + }); + sendToRenderer({ type: "spec_regeneration_complete", message: "Spec regeneration complete!", }); + console.log(`[SpecRegeneration] ===== Spec regeneration finished successfully =====`); return { success: true, message: "Spec regeneration complete", }; } catch (error) { + const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1); + if (error instanceof AbortError || error?.name === "AbortError") { - console.log("[SpecRegeneration] Regeneration aborted"); + console.log(`[SpecRegeneration] Regeneration aborted after ${elapsedTime}s`); + sendToRenderer({ + type: "spec_regeneration_error", + error: "Spec regeneration was aborted by user", + }); if (execution) { execution.abortController = null; execution.query = null; @@ -344,7 +817,17 @@ Begin by exploring the project structure.`; }; } - console.error("[SpecRegeneration] Error regenerating spec:", error); + const errorMessage = error.message || String(error); + const errorStack = error.stack || ""; + console.error(`[SpecRegeneration] ERROR regenerating spec after ${elapsedTime}s:`); + console.error(`[SpecRegeneration] Error message: ${errorMessage}`); + console.error(`[SpecRegeneration] Error stack: ${errorStack}`); + + sendToRenderer({ + type: "spec_regeneration_error", + error: `Failed to regenerate spec: ${errorMessage}`, + }); + if (execution) { execution.abortController = null; execution.query = null; @@ -374,9 +857,8 @@ When analyzing, look at: - Database configurations and schemas - API structures and patterns -**Feature Storage:** -Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder. -Do NOT manually create feature files. Use the UpdateFeatureStatus tool to manage features. +**Note:** Feature files are stored separately in .automaker/features/{id}/feature.json. +Your task is ONLY to update the app_spec.txt file - feature files will be managed separately. You CAN and SHOULD modify: - .automaker/app_spec.txt (this is your primary target) @@ -453,6 +935,94 @@ Use this general structure: Begin by exploring the project structure.`; } + /** + * Handle assistant message in feature generation stream + * @private + */ + _handleAssistantMessage(msg, sendToRenderer, counters) { + counters.messageCount++; + for (const block of msg.message.content) { + if (block.type === "text") { + const preview = block.text.substring(0, 100).replace(/\n/g, " "); + console.log(`[SpecRegeneration] Feature gen message #${counters.messageCount}: ${preview}...`); + sendToRenderer({ + type: "spec_regeneration_progress", + content: block.text, + }); + } else if (block.type === "tool_use") { + this._handleToolUse(block, sendToRenderer, counters); + } + } + } + + /** + * Handle tool use block in feature generation stream + * @private + */ + _handleToolUse(block, sendToRenderer, counters) { + counters.toolCallCount++; + const toolName = block.name; + const toolInput = block.input; + console.log(`[SpecRegeneration] Feature gen tool call #${counters.toolCallCount}: ${toolName}`); + + if (toolName === "mcp__automaker-tools__UpdateFeatureStatus" || toolName === "UpdateFeatureStatus") { + const featureId = toolInput?.featureId || "unknown"; + const status = toolInput?.status || "unknown"; + const summary = toolInput?.summary || ""; + sendToRenderer({ + type: "spec_regeneration_progress", + content: `\n[Feature Creation] Creating feature "${featureId}" with status "${status}"${summary ? `\n Summary: ${summary}` : ""}\n`, + }); + } else { + sendToRenderer({ + type: "spec_regeneration_progress", + content: `\n[Tool] Using ${toolName}...\n`, + }); + } + + sendToRenderer({ + type: "spec_regeneration_tool", + tool: toolName, + input: toolInput, + }); + } + + /** + * Handle tool result in feature generation stream + * @private + */ + _handleToolResult(msg, sendToRenderer) { + const toolName = msg.toolName || "unknown"; + const result = msg.content?.[0]?.text || JSON.stringify(msg.content); + const resultPreview = result.substring(0, 200).replace(/\n/g, " "); + console.log(`[SpecRegeneration] Feature gen tool result (${toolName}): ${resultPreview}...`); + + if (toolName === "mcp__automaker-tools__UpdateFeatureStatus" || toolName === "UpdateFeatureStatus") { + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Feature Creation] ${result}\n`, + }); + } else { + sendToRenderer({ + type: "spec_regeneration_progress", + content: `[Tool Result] ${toolName} completed successfully\n`, + }); + } + } + + /** + * Handle error in feature generation stream + * @private + */ + _handleStreamError(msg, sendToRenderer) { + const errorMsg = msg.error?.message || JSON.stringify(msg.error); + console.error(`[SpecRegeneration] ERROR in feature generation stream: ${errorMsg}`); + sendToRenderer({ + type: "spec_regeneration_error", + error: `Error during feature generation: ${errorMsg}`, + }); + } + /** * Stop the current regeneration */ @@ -461,6 +1031,7 @@ Begin by exploring the project structure.`; this.runningRegeneration.abortController.abort(); } this.runningRegeneration = null; + this.currentPhase = ""; } } diff --git a/app/src/components/ui/log-viewer.tsx b/app/src/components/ui/log-viewer.tsx index 37d8fe21..169c626f 100644 --- a/app/src/components/ui/log-viewer.tsx +++ b/app/src/components/ui/log-viewer.tsx @@ -208,7 +208,19 @@ export function LogViewer({ output, className }: LogViewerProps) { }; if (entries.length === 0) { - return null; + return ( +
+
+ +

No log entries yet. Logs will appear here as the process runs.

+ {output && output.trim() && ( +
+
{output}
+
+ )} +
+
+ ); } // Count entries by type diff --git a/app/src/components/views/kanban-card.tsx b/app/src/components/views/kanban-card.tsx index 71cb4416..7888de3e 100644 --- a/app/src/components/views/kanban-card.tsx +++ b/app/src/components/views/kanban-card.tsx @@ -393,10 +393,10 @@ export const KanbanCard = memo(function KanbanCard({ !isDescriptionExpanded && "line-clamp-3" )} > - {feature.description} + {feature.description || feature.summary || feature.id} {/* Show More/Less toggle - only show when description is likely truncated */} - {feature.description.length > 100 && ( + {(feature.description || feature.summary || "").length > 100 && ( + {errorMessage && ( +
+

Error:

+

{errorMessage}

+
+ )} + {!isCreating && ( +
+ +
+ )} {/* Create Dialog */} - + { + if (!open && !isCreating) { + setShowCreateDialog(false); + } + }} + > Create App Specification @@ -270,6 +699,7 @@ export function SpecView() { onChange={(e) => setProjectOverview(e.target.value)} placeholder="e.g., A project management tool that allows teams to track tasks, manage sprints, and visualize progress through kanban boards. It should support user authentication, real-time updates, and file attachments..." autoFocus + disabled={isCreating} /> @@ -278,11 +708,12 @@ export function SpecView() { id="generate-features" checked={generateFeatures} onCheckedChange={(checked) => setGenerateFeatures(checked === true)} + disabled={isCreating} />
@@ -298,17 +729,27 @@ export function SpecView() { - - Generate Spec + {isCreating ? ( + <> + + Generating... + + ) : ( + <> + + Generate Spec + + )} @@ -333,30 +774,66 @@ export function SpecView() {

-
- - +
+ {(isRegenerating || isCreating || isGeneratingFeatures) && ( +
+
+ +
+
+
+ + {isGeneratingFeatures ? "Generating Features" : isCreating ? "Generating Specification" : "Regenerating Specification"} + + {currentPhase && ( + + {currentPhase === "initialization" && "Initializing..."} + {currentPhase === "setup" && "Setting up tools..."} + {currentPhase === "analysis" && "Analyzing project structure..."} + {currentPhase === "spec_complete" && "Spec created! Generating features..."} + {currentPhase === "feature_generation" && "Creating features from roadmap..."} + {currentPhase === "complete" && "Complete!"} + {currentPhase === "error" && "Error occurred"} + {!["initialization", "setup", "analysis", "spec_complete", "feature_generation", "complete", "error"].includes(currentPhase) && currentPhase} + + )} +
+
+ )} + {errorMessage && ( +
+ +
+ Error + {errorMessage} +
+
+ )} +
+ + +
@@ -373,7 +850,14 @@ export function SpecView() {
{/* Regenerate Dialog */} - + { + if (!open && !isRegenerating) { + setShowRegenerateDialog(false); + } + }} + > Regenerate App Specification @@ -403,35 +887,56 @@ export function SpecView() { - + - - {isRegenerating ? ( + {isGeneratingFeatures ? ( <> - Regenerating... + Generating... ) : ( <> - - Regenerate Spec + + Generate Features )} - + +
+ + + {isRegenerating ? ( + <> + + Regenerating... + + ) : ( + <> + + Regenerate Spec + + )} + +
+ ); } diff --git a/app/src/lib/electron.ts b/app/src/lib/electron.ts index 04d7625f..54e9309a 100644 --- a/app/src/lib/electron.ts +++ b/app/src/lib/electron.ts @@ -147,10 +147,15 @@ export interface SpecRegenerationAPI { projectPath: string, projectDefinition: string ) => Promise<{ success: boolean; error?: string }>; + generateFeatures: (projectPath: string) => Promise<{ + success: boolean; + error?: string; + }>; stop: () => Promise<{ success: boolean; error?: string }>; status: () => Promise<{ success: boolean; isRunning?: boolean; + currentPhase?: string; error?: string; }>; onEvent: (callback: (event: SpecRegenerationEvent) => void) => () => void; @@ -1800,6 +1805,7 @@ async function simulateSuggestionsGeneration( // Mock Spec Regeneration state and implementation let mockSpecRegenerationRunning = false; +let mockSpecRegenerationPhase = ""; let mockSpecRegenerationCallbacks: ((event: SpecRegenerationEvent) => void)[] = []; let mockSpecRegenerationTimeout: NodeJS.Timeout | null = null; @@ -1843,8 +1849,26 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI { return { success: true }; }, + generateFeatures: async (projectPath: string) => { + if (mockSpecRegenerationRunning) { + return { + success: false, + error: "Feature generation is already running", + }; + } + + mockSpecRegenerationRunning = true; + console.log(`[Mock] Generating features from existing spec for: ${projectPath}`); + + // Simulate async feature generation + simulateFeatureGeneration(projectPath); + + return { success: true }; + }, + stop: async () => { mockSpecRegenerationRunning = false; + mockSpecRegenerationPhase = ""; if (mockSpecRegenerationTimeout) { clearTimeout(mockSpecRegenerationTimeout); mockSpecRegenerationTimeout = null; @@ -1856,6 +1880,7 @@ function createMockSpecRegenerationAPI(): SpecRegenerationAPI { return { success: true, isRunning: mockSpecRegenerationRunning, + currentPhase: mockSpecRegenerationPhase, }; }, @@ -1879,9 +1904,10 @@ async function simulateSpecCreation( projectOverview: string, generateFeatures = true ) { + mockSpecRegenerationPhase = "initialization"; emitSpecRegenerationEvent({ type: "spec_regeneration_progress", - content: "Starting project analysis...\n", + content: "[Phase: initialization] Starting project analysis...\n", }); await new Promise((resolve) => { @@ -1889,6 +1915,7 @@ async function simulateSpecCreation( }); if (!mockSpecRegenerationRunning) return; + mockSpecRegenerationPhase = "setup"; emitSpecRegenerationEvent({ type: "spec_regeneration_tool", tool: "Glob", @@ -1900,9 +1927,10 @@ async function simulateSpecCreation( }); if (!mockSpecRegenerationRunning) return; + mockSpecRegenerationPhase = "analysis"; emitSpecRegenerationEvent({ type: "spec_regeneration_progress", - content: "Detecting tech stack...\n", + content: "[Phase: analysis] Detecting tech stack...\n", }); await new Promise((resolve) => { @@ -1942,12 +1970,14 @@ async function simulateSpecCreation( // The generateFeatures parameter is kept for API compatibility but features // should be created through the features API + mockSpecRegenerationPhase = "complete"; emitSpecRegenerationEvent({ type: "spec_regeneration_complete", - message: "Initial spec creation complete!", + message: "All tasks completed!", }); mockSpecRegenerationRunning = false; + mockSpecRegenerationPhase = ""; mockSpecRegenerationTimeout = null; } @@ -1955,9 +1985,10 @@ async function simulateSpecRegeneration( projectPath: string, projectDefinition: string ) { + mockSpecRegenerationPhase = "initialization"; emitSpecRegenerationEvent({ type: "spec_regeneration_progress", - content: "Starting spec regeneration...\n", + content: "[Phase: initialization] Starting spec regeneration...\n", }); await new Promise((resolve) => { @@ -1965,9 +1996,10 @@ async function simulateSpecRegeneration( }); if (!mockSpecRegenerationRunning) return; + mockSpecRegenerationPhase = "analysis"; emitSpecRegenerationEvent({ type: "spec_regeneration_progress", - content: "Analyzing codebase...\n", + content: "[Phase: analysis] Analyzing codebase...\n", }); await new Promise((resolve) => { @@ -1998,12 +2030,63 @@ async function simulateSpecRegeneration(
`; + mockSpecRegenerationPhase = "complete"; emitSpecRegenerationEvent({ type: "spec_regeneration_complete", - message: "Spec regeneration complete!", + message: "All tasks completed!", }); mockSpecRegenerationRunning = false; + mockSpecRegenerationPhase = ""; + mockSpecRegenerationTimeout = null; +} + +async function simulateFeatureGeneration(projectPath: string) { + mockSpecRegenerationPhase = "initialization"; + emitSpecRegenerationEvent({ + type: "spec_regeneration_progress", + content: "[Phase: initialization] Starting feature generation from existing app_spec.txt...\n", + }); + + await new Promise((resolve) => { + mockSpecRegenerationTimeout = setTimeout(resolve, 500); + }); + if (!mockSpecRegenerationRunning) return; + + emitSpecRegenerationEvent({ + type: "spec_regeneration_progress", + content: "[Phase: feature_generation] Reading implementation roadmap...\n", + }); + + await new Promise((resolve) => { + mockSpecRegenerationTimeout = setTimeout(resolve, 500); + }); + if (!mockSpecRegenerationRunning) return; + + mockSpecRegenerationPhase = "feature_generation"; + emitSpecRegenerationEvent({ + type: "spec_regeneration_progress", + content: "[Phase: feature_generation] Creating features from roadmap...\n", + }); + + await new Promise((resolve) => { + mockSpecRegenerationTimeout = setTimeout(resolve, 1000); + }); + if (!mockSpecRegenerationRunning) return; + + mockSpecRegenerationPhase = "complete"; + emitSpecRegenerationEvent({ + type: "spec_regeneration_progress", + content: "[Phase: complete] All tasks completed!\n", + }); + + emitSpecRegenerationEvent({ + type: "spec_regeneration_complete", + message: "All tasks completed!", + }); + + mockSpecRegenerationRunning = false; + mockSpecRegenerationPhase = ""; mockSpecRegenerationTimeout = null; } diff --git a/app/src/lib/log-parser.ts b/app/src/lib/log-parser.ts index a2e6cad1..872b814d 100644 --- a/app/src/lib/log-parser.ts +++ b/app/src/lib/log-parser.ts @@ -72,10 +72,21 @@ function detectEntryType(content: string): LogEntryType { trimmed.startsWith("📋") || trimmed.startsWith("⚡") || trimmed.startsWith("✅") || - trimmed.match(/^(Planning|Action|Verification)/i) + trimmed.match(/^(Planning|Action|Verification)/i) || + trimmed.match(/\[Phase:\s*([^\]]+)\]/) || + trimmed.match(/Phase:\s*\w+/i) ) { return "phase"; } + + // Feature creation events + if ( + trimmed.match(/\[Feature Creation\]/i) || + trimmed.match(/Feature Creation/i) || + trimmed.match(/Creating feature/i) + ) { + return "success"; + } // Errors if (trimmed.startsWith("❌") || trimmed.toLowerCase().includes("error:")) { @@ -138,6 +149,12 @@ function extractPhase(content: string): string | undefined { if (content.includes("⚡")) return "action"; if (content.includes("✅")) return "verification"; + // Extract from [Phase: ...] format + const phaseMatch = content.match(/\[Phase:\s*([^\]]+)\]/); + if (phaseMatch) { + return phaseMatch[1].toLowerCase(); + } + const match = content.match(/^(Planning|Action|Verification)/i); return match?.[1]?.toLowerCase(); } @@ -155,7 +172,14 @@ function generateTitle(type: LogEntryType, content: string): string { return "Tool Input/Result"; case "phase": { const phase = extractPhase(content); - return phase ? `Phase: ${phase.charAt(0).toUpperCase() + phase.slice(1)}` : "Phase Change"; + if (phase) { + // Capitalize first letter of each word + const formatted = phase.split(/\s+/).map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(" "); + return `Phase: ${formatted}`; + } + return "Phase Change"; } case "error": return "Error"; @@ -224,6 +248,13 @@ export function parseLogOutput(rawOutput: string): LogEntry[] { trimmedLine.startsWith("❌") || trimmedLine.startsWith("⚠️") || trimmedLine.startsWith("🧠") || + trimmedLine.match(/\[Phase:\s*([^\]]+)\]/) || + trimmedLine.match(/\[Feature Creation\]/i) || + trimmedLine.match(/\[Tool\]/i) || + trimmedLine.match(/\[Agent\]/i) || + trimmedLine.match(/\[Complete\]/i) || + trimmedLine.match(/\[ERROR\]/i) || + trimmedLine.match(/\[Status\]/i) || trimmedLine.toLowerCase().includes("ultrathink preparation") || trimmedLine.toLowerCase().includes("thinking level") || (trimmedLine.startsWith("Input:") && currentEntry?.type === "tool_call"); diff --git a/app/src/types/electron.d.ts b/app/src/types/electron.d.ts index 213b793d..a816218c 100644 --- a/app/src/types/electron.d.ts +++ b/app/src/types/electron.d.ts @@ -257,6 +257,11 @@ export interface SpecRegenerationAPI { error?: string; }>; + generateFeatures: (projectPath: string) => Promise<{ + success: boolean; + error?: string; + }>; + stop: () => Promise<{ success: boolean; error?: string; @@ -265,6 +270,7 @@ export interface SpecRegenerationAPI { status: () => Promise<{ success: boolean; isRunning?: boolean; + currentPhase?: string; error?: string; }>;