mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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"),
|
||||
|
||||
|
||||
@@ -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}"` : ""
|
||||
}`
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}"` : ''}`
|
||||
|
||||
@@ -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:**
|
||||
|
||||
|
||||
@@ -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 = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
6
app/src/types/electron.d.ts
vendored
6
app/src/types/electron.d.ts
vendored
@@ -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;
|
||||
}>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user