Merge pull request #26 from AutoMaker-Org/bugfix/appspec-complete-overhaul

Complete overhaul for app spec system. Created logic to auto generate…
This commit is contained in:
Web Dev Cody
2025-12-11 17:28:16 -05:00
committed by GitHub
14 changed files with 1562 additions and 184 deletions

View File

@@ -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);

View File

@@ -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
*/

View File

@@ -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"),

View File

@@ -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}"` : ""
}`
);

View File

@@ -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",

View File

@@ -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}"` : ''}`

View File

@@ -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:**

View File

@@ -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 = `<project_specification>
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 = "";
}
}

View File

@@ -208,7 +208,19 @@ export function LogViewer({ output, className }: LogViewerProps) {
};
if (entries.length === 0) {
return null;
return (
<div className="flex items-center justify-center p-8 text-muted-foreground">
<div className="text-center">
<Info className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No log entries yet. Logs will appear here as the process runs.</p>
{output && output.trim() && (
<div className="mt-4 p-3 bg-zinc-900/50 rounded text-xs font-mono text-left max-h-40 overflow-auto">
<pre className="whitespace-pre-wrap">{output}</pre>
</div>
)}
</div>
</div>
);
}
// Count entries by type

View File

@@ -393,10 +393,10 @@ export const KanbanCard = memo(function KanbanCard({
!isDescriptionExpanded && "line-clamp-3"
)}
>
{feature.description}
{feature.description || feature.summary || feature.id}
</CardTitle>
{/* Show More/Less toggle - only show when description is likely truncated */}
{feature.description.length > 100 && (
{(feature.description || feature.summary || "").length > 100 && (
<button
onClick={(e) => {
e.stopPropagation();
@@ -427,7 +427,7 @@ export const KanbanCard = memo(function KanbanCard({
</CardHeader>
<CardContent className="p-3 pt-0">
{/* Steps Preview - Show in Standard and Detailed modes */}
{showSteps && feature.steps.length > 0 && (
{showSteps && feature.steps && feature.steps.length > 0 && (
<div className="mb-3 space-y-1">
{feature.steps.slice(0, 3).map((step, index) => (
<div
@@ -844,10 +844,13 @@ export const KanbanCard = memo(function KanbanCard({
<Sparkles className="w-5 h-5 text-green-400" />
Implementation Summary
</DialogTitle>
<DialogDescription className="text-sm" title={feature.description}>
{feature.description.length > 100
? `${feature.description.slice(0, 100)}...`
: feature.description}
<DialogDescription className="text-sm" title={feature.description || feature.summary || ""}>
{(() => {
const displayText = feature.description || feature.summary || "No description";
return displayText.length > 100
? `${displayText.slice(0, 100)}...`
: displayText;
})()}
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto p-4 bg-card rounded-lg border border-border">

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { useEffect, useState, useCallback, useRef } from "react";
import { useAppStore } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import { Button } from "@/components/ui/button";
@@ -14,11 +14,17 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2 } from "lucide-react";
import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2, AlertCircle, ListPlus } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor";
import type { SpecRegenerationEvent } from "@/types/electron";
// Delay before reloading spec file to ensure it's written to disk
const SPEC_FILE_WRITE_DELAY = 500;
// Interval for polling backend status during generation
const STATUS_CHECK_INTERVAL_MS = 2000;
export function SpecView() {
const { currentProject, appSpec, setAppSpec } = useAppStore();
const [isLoading, setIsLoading] = useState(true);
@@ -36,6 +42,19 @@ export function SpecView() {
const [projectOverview, setProjectOverview] = useState("");
const [isCreating, setIsCreating] = useState(false);
const [generateFeatures, setGenerateFeatures] = useState(true);
// Generate features only state
const [isGeneratingFeatures, setIsGeneratingFeatures] = useState(false);
// Logs state (kept for internal tracking, but UI removed)
const [logs, setLogs] = useState<string>("");
const logsRef = useRef<string>("");
// Phase tracking and status
const [currentPhase, setCurrentPhase] = useState<string>("");
const [errorMessage, setErrorMessage] = useState<string>("");
const statusCheckRef = useRef<boolean>(false);
const stateRestoredRef = useRef<boolean>(false);
// Load spec from file
const loadSpec = useCallback(async () => {
@@ -69,6 +88,143 @@ export function SpecView() {
loadSpec();
}, [loadSpec]);
// Check if spec regeneration is running when component mounts or project changes
useEffect(() => {
const checkStatus = async () => {
if (!currentProject || statusCheckRef.current) return;
statusCheckRef.current = true;
try {
const api = getElectronAPI();
if (!api.specRegeneration) {
statusCheckRef.current = false;
return;
}
const status = await api.specRegeneration.status();
console.log("[SpecView] Status check on mount:", status);
if (status.success && status.isRunning) {
// Something is running - restore state using backend's authoritative phase
console.log("[SpecView] Spec generation is running - restoring state", { phase: status.currentPhase });
if (!stateRestoredRef.current) {
setIsCreating(true);
setIsRegenerating(true);
stateRestoredRef.current = true;
}
// Use the backend's currentPhase directly - single source of truth
if (status.currentPhase) {
setCurrentPhase(status.currentPhase);
} else {
setCurrentPhase("in progress");
}
// Add resume message to logs if needed
if (!logsRef.current) {
const resumeMessage = "[Status] Resumed monitoring existing spec generation process...\n";
logsRef.current = resumeMessage;
setLogs(resumeMessage);
} else if (!logsRef.current.includes("Resumed monitoring")) {
const resumeMessage = "\n[Status] Resumed monitoring existing spec generation process...\n";
logsRef.current = logsRef.current + resumeMessage;
setLogs(logsRef.current);
}
} else if (status.success && !status.isRunning) {
// Not running - clear all state
setIsCreating(false);
setIsRegenerating(false);
setCurrentPhase("");
stateRestoredRef.current = false;
}
} catch (error) {
console.error("[SpecView] Failed to check status:", error);
} finally {
statusCheckRef.current = false;
}
};
// Reset restoration flag when project changes
stateRestoredRef.current = false;
checkStatus();
}, [currentProject]);
// Sync state when tab becomes visible (user returns to spec editor)
useEffect(() => {
const handleVisibilityChange = async () => {
if (!document.hidden && currentProject && (isCreating || isRegenerating || isGeneratingFeatures)) {
// Tab became visible and we think we're still generating - verify status from backend
try {
const api = getElectronAPI();
if (!api.specRegeneration) return;
const status = await api.specRegeneration.status();
console.log("[SpecView] Visibility change - status check:", status);
if (!status.isRunning) {
// Backend says not running - clear state
console.log("[SpecView] Visibility change: Backend indicates generation complete - clearing state");
setIsCreating(false);
setIsRegenerating(false);
setIsGeneratingFeatures(false);
setCurrentPhase("");
stateRestoredRef.current = false;
loadSpec();
} else if (status.currentPhase) {
// Still running - update phase from backend
setCurrentPhase(status.currentPhase);
}
} catch (error) {
console.error("[SpecView] Failed to check status on visibility change:", error);
}
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, loadSpec]);
// Periodic status check to ensure state stays in sync (only when we think we're running)
useEffect(() => {
if (!currentProject || (!isCreating && !isRegenerating && !isGeneratingFeatures)) return;
const intervalId = setInterval(async () => {
try {
const api = getElectronAPI();
if (!api.specRegeneration) return;
const status = await api.specRegeneration.status();
if (!status.isRunning) {
// Backend says not running - clear state
console.log("[SpecView] Periodic check: Backend indicates generation complete - clearing state");
setIsCreating(false);
setIsRegenerating(false);
setIsGeneratingFeatures(false);
setCurrentPhase("");
stateRestoredRef.current = false;
loadSpec();
} else if (status.currentPhase && status.currentPhase !== currentPhase) {
// Still running but phase changed - update from backend
console.log("[SpecView] Periodic check: Phase updated from backend", {
old: currentPhase,
new: status.currentPhase
});
setCurrentPhase(status.currentPhase);
}
} catch (error) {
console.error("[SpecView] Periodic status check error:", error);
}
}, STATUS_CHECK_INTERVAL_MS);
return () => {
clearInterval(intervalId);
};
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, currentPhase, loadSpec]);
// Subscribe to spec regeneration events
useEffect(() => {
const api = getElectronAPI();
@@ -77,18 +233,132 @@ export function SpecView() {
const unsubscribe = api.specRegeneration.onEvent((event: SpecRegenerationEvent) => {
console.log("[SpecView] Regeneration event:", event.type);
if (event.type === "spec_regeneration_complete") {
setIsRegenerating(false);
setIsCreating(false);
setShowRegenerateDialog(false);
setShowCreateDialog(false);
setProjectDefinition("");
setProjectOverview("");
// Reload the spec to show the new content
loadSpec();
if (event.type === "spec_regeneration_progress") {
// Extract phase from content if present
const phaseMatch = event.content.match(/\[Phase:\s*([^\]]+)\]/);
if (phaseMatch) {
const phase = phaseMatch[1];
setCurrentPhase(phase);
console.log(`[SpecView] Phase updated: ${phase}`);
// If phase is "complete", clear running state immediately
if (phase === "complete") {
console.log("[SpecView] Phase is complete - clearing state");
setIsCreating(false);
setIsRegenerating(false);
stateRestoredRef.current = false;
// Small delay to ensure spec file is written
setTimeout(() => {
loadSpec();
}, SPEC_FILE_WRITE_DELAY);
}
}
// Check for completion indicators in content
if (event.content.includes("All tasks completed") ||
event.content.includes("✓ All tasks completed")) {
// This indicates everything is done - clear state immediately
console.log("[SpecView] Detected completion in progress message - clearing state");
setIsCreating(false);
setIsRegenerating(false);
setCurrentPhase("");
stateRestoredRef.current = false;
setTimeout(() => {
loadSpec();
}, SPEC_FILE_WRITE_DELAY);
}
// Append progress to logs
const newLog = logsRef.current + event.content;
logsRef.current = newLog;
setLogs(newLog);
console.log("[SpecView] Progress:", event.content.substring(0, 100));
// Clear error message when we get new progress
if (errorMessage) {
setErrorMessage("");
}
} else if (event.type === "spec_regeneration_tool") {
// Check if this is a feature creation tool
const isFeatureTool = event.tool === "mcp__automaker-tools__UpdateFeatureStatus" ||
event.tool === "UpdateFeatureStatus" ||
event.tool?.includes("Feature");
if (isFeatureTool) {
// Ensure we're in feature generation phase
if (currentPhase !== "feature_generation") {
setCurrentPhase("feature_generation");
setIsCreating(true);
setIsRegenerating(true);
console.log("[SpecView] Detected feature creation tool - setting phase to feature_generation");
}
}
// Log tool usage with details
const toolInput = event.input ? ` (${JSON.stringify(event.input).substring(0, 100)}...)` : "";
const toolLog = `\n[Tool] ${event.tool}${toolInput}\n`;
const newLog = logsRef.current + toolLog;
logsRef.current = newLog;
setLogs(newLog);
console.log("[SpecView] Tool:", event.tool, event.input);
} else if (event.type === "spec_regeneration_complete") {
// Add completion message to logs first
const completionLog = logsRef.current + `\n[Complete] ${event.message}\n`;
logsRef.current = completionLog;
setLogs(completionLog);
// --- Completion Detection Logic ---
// The backend sends explicit signals for completion:
// 1. "All tasks completed" in the message
// 2. [Phase: complete] marker in logs
const isFinalCompletionMessage = event.message?.includes("All tasks completed") ||
event.message === "All tasks completed!" ||
event.message === "All tasks completed";
const hasCompletePhase = logsRef.current.includes("[Phase: complete]");
// Rely solely on explicit backend signals
const shouldComplete = isFinalCompletionMessage || hasCompletePhase;
if (shouldComplete) {
// Fully complete - clear all states immediately
console.log("[SpecView] Final completion detected - clearing state", {
isFinalCompletionMessage,
hasCompletePhase,
message: event.message
});
setIsRegenerating(false);
setIsCreating(false);
setIsGeneratingFeatures(false);
setCurrentPhase("");
setShowRegenerateDialog(false);
setShowCreateDialog(false);
setProjectDefinition("");
setProjectOverview("");
setErrorMessage("");
stateRestoredRef.current = false;
// Reload the spec to show the new content
loadSpec();
} else {
// Intermediate completion - keep state active for feature generation
setIsCreating(true);
setIsRegenerating(true);
setCurrentPhase("feature_generation");
console.log("[SpecView] Intermediate completion, continuing with feature generation");
}
console.log("[SpecView] Spec generation event:", event.message);
} else if (event.type === "spec_regeneration_error") {
setIsRegenerating(false);
setIsCreating(false);
setIsGeneratingFeatures(false);
setCurrentPhase("error");
setErrorMessage(event.error);
stateRestoredRef.current = false; // Reset restoration flag
// Add error to logs
const errorLog = logsRef.current + `\n\n[ERROR] ${event.error}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
console.error("[SpecView] Regeneration error:", event.error);
}
});
@@ -126,6 +396,12 @@ export function SpecView() {
if (!currentProject || !projectDefinition.trim()) return;
setIsRegenerating(true);
setCurrentPhase("initialization");
setErrorMessage("");
// Reset logs when starting new regeneration
logsRef.current = "";
setLogs("");
console.log("[SpecView] Starting spec regeneration");
try {
const api = getElectronAPI();
if (!api.specRegeneration) {
@@ -139,13 +415,25 @@ export function SpecView() {
);
if (!result.success) {
console.error("[SpecView] Failed to start regeneration:", result.error);
const errorMsg = result.error || "Unknown error";
console.error("[SpecView] Failed to start regeneration:", errorMsg);
setIsRegenerating(false);
setCurrentPhase("error");
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to start regeneration: ${errorMsg}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
}
// If successful, we'll wait for the events to update the state
} catch (error) {
console.error("[SpecView] Failed to regenerate spec:", error);
const errorMsg = error instanceof Error ? error.message : String(error);
console.error("[SpecView] Failed to regenerate spec:", errorMsg);
setIsRegenerating(false);
setCurrentPhase("error");
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to regenerate spec: ${errorMsg}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
}
};
@@ -154,6 +442,12 @@ export function SpecView() {
setIsCreating(true);
setShowCreateDialog(false);
setCurrentPhase("initialization");
setErrorMessage("");
// Reset logs when starting new generation
logsRef.current = "";
setLogs("");
console.log("[SpecView] Starting spec creation, generateFeatures:", generateFeatures);
try {
const api = getElectronAPI();
if (!api.specRegeneration) {
@@ -168,13 +462,70 @@ export function SpecView() {
);
if (!result.success) {
console.error("[SpecView] Failed to start spec creation:", result.error);
const errorMsg = result.error || "Unknown error";
console.error("[SpecView] Failed to start spec creation:", errorMsg);
setIsCreating(false);
setCurrentPhase("error");
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to start spec creation: ${errorMsg}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
}
// If successful, we'll wait for the events to update the state
} catch (error) {
console.error("[SpecView] Failed to create spec:", error);
const errorMsg = error instanceof Error ? error.message : String(error);
console.error("[SpecView] Failed to create spec:", errorMsg);
setIsCreating(false);
setCurrentPhase("error");
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to create spec: ${errorMsg}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
}
};
const handleGenerateFeatures = async () => {
if (!currentProject) return;
setIsGeneratingFeatures(true);
setShowRegenerateDialog(false);
setCurrentPhase("initialization");
setErrorMessage("");
// Reset logs when starting feature generation
logsRef.current = "";
setLogs("");
console.log("[SpecView] Starting feature generation from existing spec");
try {
const api = getElectronAPI();
if (!api.specRegeneration) {
console.error("[SpecView] Spec regeneration not available");
setIsGeneratingFeatures(false);
return;
}
const result = await api.specRegeneration.generateFeatures(
currentProject.path
);
if (!result.success) {
const errorMsg = result.error || "Unknown error";
console.error("[SpecView] Failed to start feature generation:", errorMsg);
setIsGeneratingFeatures(false);
setCurrentPhase("error");
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to start feature generation: ${errorMsg}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
}
// If successful, we'll wait for the events to update the state
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error("[SpecView] Failed to generate features:", errorMsg);
setIsGeneratingFeatures(false);
setCurrentPhase("error");
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to generate features: ${errorMsg}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
}
};
@@ -218,6 +569,36 @@ export function SpecView() {
</p>
</div>
</div>
{(isCreating || isRegenerating) && (
<div className="flex items-center gap-3 px-6 py-3.5 rounded-xl bg-gradient-to-r from-primary/15 to-primary/5 border border-primary/30 shadow-lg backdrop-blur-md">
<div className="relative">
<Loader2 className="w-5 h-5 animate-spin text-primary flex-shrink-0" />
<div className="absolute inset-0 w-5 h-5 animate-ping text-primary/20" />
</div>
<div className="flex flex-col gap-1 min-w-0">
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
{isCreating ? "Generating Specification" : "Regenerating Specification"}
</span>
{currentPhase && (
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
{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}
</span>
)}
</div>
</div>
)}
{errorMessage && (
<div className="flex items-center gap-2 text-destructive">
<span className="text-sm font-medium">Error: {errorMessage}</span>
</div>
)}
</div>
{/* Empty State */}
@@ -225,26 +606,74 @@ export function SpecView() {
<div className="text-center max-w-md">
<div className="mb-6 flex justify-center">
<div className="p-4 rounded-full bg-primary/10">
<FilePlus2 className="w-12 h-12 text-primary" />
{isCreating ? (
<Loader2 className="w-12 h-12 text-primary animate-spin" />
) : (
<FilePlus2 className="w-12 h-12 text-primary" />
)}
</div>
</div>
<h2 className="text-2xl font-semibold mb-3">No App Specification Found</h2>
<h2 className="text-2xl font-semibold mb-4">
{isCreating ? (
<>
<div className="mb-4">
<span>Generating App Specification</span>
</div>
{currentPhase && (
<div className="px-6 py-3.5 rounded-xl bg-gradient-to-r from-primary/15 to-primary/5 border border-primary/30 shadow-lg backdrop-blur-md inline-flex items-center justify-center">
<span className="text-sm font-semibold text-primary text-center tracking-tight">
{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}
</span>
</div>
)}
</>
) : (
"No App Specification Found"
)}
</h2>
<p className="text-muted-foreground mb-6">
Create an app specification to help our system understand your project.
We&apos;ll analyze your codebase and generate a comprehensive spec based on your description.
{isCreating
? currentPhase === "feature_generation"
? "The app specification has been created! Now generating features from the implementation roadmap..."
: "We're analyzing your project and generating a comprehensive specification. This may take a few moments..."
: "Create an app specification to help our system understand your project. We'll analyze your codebase and generate a comprehensive spec based on your description."}
</p>
<Button
size="lg"
onClick={() => setShowCreateDialog(true)}
>
<FilePlus2 className="w-5 h-5 mr-2" />
Create app_spec
</Button>
{errorMessage && (
<div className="mb-4 p-3 rounded-md bg-destructive/10 border border-destructive/20">
<p className="text-sm text-destructive font-medium">Error:</p>
<p className="text-sm text-destructive">{errorMessage}</p>
</div>
)}
{!isCreating && (
<div className="flex gap-2 justify-center">
<Button
size="lg"
onClick={() => setShowCreateDialog(true)}
>
<FilePlus2 className="w-5 h-5 mr-2" />
Create app_spec
</Button>
</div>
)}
</div>
</div>
{/* Create Dialog */}
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<Dialog
open={showCreateDialog}
onOpenChange={(open) => {
if (!open && !isCreating) {
setShowCreateDialog(false);
}
}}
>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Create App Specification</DialogTitle>
@@ -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}
/>
</div>
@@ -278,11 +708,12 @@ export function SpecView() {
id="generate-features"
checked={generateFeatures}
onCheckedChange={(checked) => setGenerateFeatures(checked === true)}
disabled={isCreating}
/>
<div className="space-y-1">
<label
htmlFor="generate-features"
className="text-sm font-medium cursor-pointer"
className={`text-sm font-medium ${isCreating ? "" : "cursor-pointer"}`}
>
Generate feature list
</label>
@@ -298,17 +729,27 @@ export function SpecView() {
<Button
variant="ghost"
onClick={() => setShowCreateDialog(false)}
disabled={isCreating}
>
Cancel
</Button>
<HotkeyButton
onClick={handleCreateSpec}
disabled={!projectOverview.trim()}
disabled={!projectOverview.trim() || isCreating}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={showCreateDialog}
hotkeyActive={showCreateDialog && !isCreating}
>
<Sparkles className="w-4 h-4 mr-2" />
Generate Spec
{isCreating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Generating...
</>
) : (
<>
<Sparkles className="w-4 h-4 mr-2" />
Generate Spec
</>
)}
</HotkeyButton>
</DialogFooter>
</DialogContent>
@@ -333,30 +774,66 @@ export function SpecView() {
</p>
</div>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setShowRegenerateDialog(true)}
disabled={isRegenerating}
data-testid="regenerate-spec"
>
{isRegenerating ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Sparkles className="w-4 h-4 mr-2" />
)}
{isRegenerating ? "Regenerating..." : "Regenerate"}
</Button>
<Button
size="sm"
onClick={saveSpec}
disabled={!hasChanges || isSaving}
data-testid="save-spec"
>
<Save className="w-4 h-4 mr-2" />
{isSaving ? "Saving..." : hasChanges ? "Save Changes" : "Saved"}
</Button>
<div className="flex items-center gap-3">
{(isRegenerating || isCreating || isGeneratingFeatures) && (
<div className="flex items-center gap-3 px-6 py-3.5 rounded-xl bg-gradient-to-r from-primary/15 to-primary/5 border border-primary/30 shadow-lg backdrop-blur-md">
<div className="relative">
<Loader2 className="w-5 h-5 animate-spin text-primary flex-shrink-0" />
<div className="absolute inset-0 w-5 h-5 animate-ping text-primary/20" />
</div>
<div className="flex flex-col gap-1 min-w-0">
<span className="text-sm font-semibold text-primary leading-tight tracking-tight">
{isGeneratingFeatures ? "Generating Features" : isCreating ? "Generating Specification" : "Regenerating Specification"}
</span>
{currentPhase && (
<span className="text-xs text-muted-foreground/90 leading-tight font-medium">
{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}
</span>
)}
</div>
</div>
)}
{errorMessage && (
<div className="flex items-center gap-3 px-6 py-3.5 rounded-xl bg-gradient-to-r from-destructive/15 to-destructive/5 border border-destructive/30 shadow-lg backdrop-blur-md">
<AlertCircle className="w-5 h-5 text-destructive flex-shrink-0" />
<div className="flex flex-col gap-1 min-w-0">
<span className="text-sm font-semibold text-destructive leading-tight tracking-tight">Error</span>
<span className="text-xs text-destructive/90 leading-tight font-medium">{errorMessage}</span>
</div>
</div>
)}
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setShowRegenerateDialog(true)}
disabled={isRegenerating || isCreating || isGeneratingFeatures}
data-testid="regenerate-spec"
>
{isRegenerating ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Sparkles className="w-4 h-4 mr-2" />
)}
{isRegenerating ? "Regenerating..." : "Regenerate"}
</Button>
<Button
size="sm"
onClick={saveSpec}
disabled={!hasChanges || isSaving || isCreating || isRegenerating || isGeneratingFeatures}
data-testid="save-spec"
>
<Save className="w-4 h-4 mr-2" />
{isSaving ? "Saving..." : hasChanges ? "Save Changes" : "Saved"}
</Button>
</div>
</div>
</div>
@@ -373,7 +850,14 @@ export function SpecView() {
</div>
{/* Regenerate Dialog */}
<Dialog open={showRegenerateDialog} onOpenChange={setShowRegenerateDialog}>
<Dialog
open={showRegenerateDialog}
onOpenChange={(open) => {
if (!open && !isRegenerating) {
setShowRegenerateDialog(false);
}
}}
>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Regenerate App Specification</DialogTitle>
@@ -403,35 +887,56 @@ export function SpecView() {
</div>
</div>
<DialogFooter>
<DialogFooter className="flex justify-between sm:justify-between">
<Button
variant="ghost"
onClick={() => setShowRegenerateDialog(false)}
disabled={isRegenerating}
variant="outline"
onClick={handleGenerateFeatures}
disabled={isRegenerating || isGeneratingFeatures}
title="Generate features from the existing app_spec.txt without regenerating the spec"
>
Cancel
</Button>
<HotkeyButton
onClick={handleRegenerate}
disabled={!projectDefinition.trim() || isRegenerating}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={showRegenerateDialog}
>
{isRegenerating ? (
{isGeneratingFeatures ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Regenerating...
Generating...
</>
) : (
<>
<Sparkles className="w-4 h-4 mr-2" />
Regenerate Spec
<ListPlus className="w-4 h-4 mr-2" />
Generate Features
</>
)}
</HotkeyButton>
</Button>
<div className="flex gap-2">
<Button
variant="ghost"
onClick={() => setShowRegenerateDialog(false)}
disabled={isRegenerating || isGeneratingFeatures}
>
Cancel
</Button>
<HotkeyButton
onClick={handleRegenerate}
disabled={!projectDefinition.trim() || isRegenerating || isGeneratingFeatures}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={showRegenerateDialog && !isRegenerating && !isGeneratingFeatures}
>
{isRegenerating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Regenerating...
</>
) : (
<>
<Sparkles className="w-4 h-4 mr-2" />
Regenerate Spec
</>
)}
</HotkeyButton>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -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(
</core_capabilities>
</project_specification>`;
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;
}

View File

@@ -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");

View File

@@ -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;
}>;