mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
Complete overhaul for app spec system. Created logic to auto generate kanban stories after the fact as well as added logging logic and visual aids to tell what stage of the process the app spec creation is in. May need refinement for state-based updates as the menu doesnt update as dynamicly as id like
This commit is contained in:
@@ -1234,6 +1234,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,53 @@ class FeatureLoader {
|
||||
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
|
||||
|
||||
try {
|
||||
// Check if feature.json exists before trying to read it
|
||||
try {
|
||||
await fs.access(featureJsonPath);
|
||||
} catch (accessError) {
|
||||
// File doesn't exist - this is expected for incomplete feature directories
|
||||
// Skip silently or log at debug level only
|
||||
if (accessError.code !== "ENOENT") {
|
||||
console.warn(
|
||||
`[FeatureLoader] Cannot access feature.json for ${featureId}:`,
|
||||
accessError.message
|
||||
);
|
||||
}
|
||||
// Skip this directory - it doesn't have a valid feature.json
|
||||
continue;
|
||||
}
|
||||
|
||||
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 was deleted between access check and read - skip silently
|
||||
console.debug(
|
||||
`[FeatureLoader] Feature ${featureId} was removed, skipping`
|
||||
);
|
||||
} 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,6 +378,7 @@ 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
|
||||
@@ -346,16 +386,46 @@ class FeatureLoader {
|
||||
* @param {string} [error] - Optional error message if feature errored
|
||||
*/
|
||||
async updateFeatureStatus(featureId, status, projectPath, summary, error) {
|
||||
// Check if feature exists
|
||||
const existingFeature = await this.get(projectPath, featureId);
|
||||
|
||||
if (!existingFeature) {
|
||||
// Feature doesn't exist - create it
|
||||
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: summary || '', // Use summary as description for display
|
||||
status: status,
|
||||
summary: summary || '',
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
if (error !== undefined) {
|
||||
newFeature.error = error;
|
||||
}
|
||||
await this.create(projectPath, newFeature);
|
||||
console.log(
|
||||
`[FeatureLoader] Created feature ${featureId}: status=${status}${
|
||||
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 (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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,18 +28,20 @@ class McpServerFactory {
|
||||
async (args) => {
|
||||
try {
|
||||
console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}, summary=${args.summary || "(none)"}`);
|
||||
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) {
|
||||
if (feature && args.status === "verified" && feature.skipTests === true) {
|
||||
console.log(`[McpServerFactory] Feature ${args.featureId} has skipTests=true, converting verified -> waiting_approval`);
|
||||
finalStatus = "waiting_approval";
|
||||
}
|
||||
@@ -51,6 +53,8 @@ class McpServerFactory {
|
||||
? `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}"` : ""}`;
|
||||
|
||||
console.log(`[Feature Creation] ✓ ${statusMessage}`);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
@@ -59,6 +63,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",
|
||||
|
||||
@@ -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
|
||||
@@ -95,18 +97,60 @@ 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
|
||||
let currentPhase = "initialization";
|
||||
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_progress",
|
||||
content: `[Phase: ${currentPhase}] Initializing spec generation process...\n`,
|
||||
});
|
||||
console.log(`[SpecRegeneration] Phase: ${currentPhase}`);
|
||||
|
||||
// Create custom MCP server with UpdateFeatureStatus tool if generating features
|
||||
let featureToolsServer = null;
|
||||
if (generateFeatures) {
|
||||
console.log("[SpecRegeneration] Setting up feature generation tools...");
|
||||
try {
|
||||
featureToolsServer = 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;
|
||||
}
|
||||
}
|
||||
|
||||
currentPhase = "setup";
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_progress",
|
||||
content: `[Phase: ${currentPhase}] Configuring AI agent and tools...\n`,
|
||||
});
|
||||
console.log(`[SpecRegeneration] Phase: ${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 +159,157 @@ class SpecRegenerationService {
|
||||
abortController: abortController,
|
||||
};
|
||||
|
||||
const prompt = this.buildInitialCreationPrompt(projectOverview, generateFeatures);
|
||||
const prompt = this.buildInitialCreationPrompt(projectOverview); // No feature generation during spec creation
|
||||
|
||||
currentPhase = "analysis";
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_progress",
|
||||
content: "Starting project analysis and spec creation...\n",
|
||||
content: `[Phase: ${currentPhase}] Starting project analysis and spec creation...\n`,
|
||||
});
|
||||
console.log(`[SpecRegeneration] Phase: ${currentPhase} - Starting AI agent query`);
|
||||
|
||||
if (generateFeatures) {
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_progress",
|
||||
content: `[Phase: ${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);
|
||||
currentPhase = "spec_complete";
|
||||
console.log(`[SpecRegeneration] Phase: ${currentPhase} - Spec creation completed in ${elapsedTime}s`);
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_complete",
|
||||
message: "Initial spec creation complete!",
|
||||
type: "spec_regeneration_progress",
|
||||
content: `\n[Phase: ${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 {
|
||||
currentPhase = "complete";
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_progress",
|
||||
content: `[Phase: ${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 +320,252 @@ 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();
|
||||
let 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: ${currentPhase}] Starting feature creation from implementation roadmap...\n`,
|
||||
});
|
||||
console.log(`[SpecRegeneration] Phase: ${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 read the app_spec.txt file and create feature entries based on the implementation_roadmap section.
|
||||
|
||||
**Your Task:**
|
||||
1. Read the .automaker/app_spec.txt file
|
||||
2. Parse the implementation_roadmap section (it contains phases with features listed)
|
||||
3. For each feature listed in the roadmap, use the UpdateFeatureStatus tool to create a feature entry
|
||||
4. Set the initial status to "todo" for all features
|
||||
5. Extract a meaningful summary/description for each feature from the roadmap
|
||||
|
||||
**Feature Storage:**
|
||||
Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
|
||||
Use the UpdateFeatureStatus tool to create features. The tool will handle creating the directory structure and feature.json file.
|
||||
|
||||
**Important:**
|
||||
- Create features ONLY from the implementation_roadmap section
|
||||
- Use the UpdateFeatureStatus tool for each feature
|
||||
- Set status to "todo" initially
|
||||
- Use a descriptive featureId based on the feature name (lowercase, hyphens for spaces)`,
|
||||
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 = `Please read the .automaker/app_spec.txt file and create feature entries for all features listed in the implementation_roadmap section.
|
||||
|
||||
For each feature in the roadmap:
|
||||
1. Use the UpdateFeatureStatus tool to create the feature
|
||||
2. Set status to "todo"
|
||||
3. Provide a clear summary/description based on what's in the roadmap
|
||||
4. Use a featureId that's descriptive and follows the pattern: lowercase with hyphens (e.g., "user-authentication", "payment-processing")
|
||||
|
||||
Start by reading the app_spec.txt file to see the implementation roadmap.`;
|
||||
|
||||
const currentQuery = query({ prompt, options });
|
||||
execution.query = currentQuery;
|
||||
|
||||
let toolCallCount = 0;
|
||||
let 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) {
|
||||
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 #${messageCount}: ${preview}...`);
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_progress",
|
||||
content: block.text,
|
||||
});
|
||||
} else if (block.type === "tool_use") {
|
||||
toolCallCount++;
|
||||
const toolName = block.name;
|
||||
const toolInput = block.input;
|
||||
console.log(`[SpecRegeneration] Feature gen tool call #${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,
|
||||
});
|
||||
}
|
||||
}
|
||||
} 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] 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`,
|
||||
});
|
||||
}
|
||||
} else if (msg.type === "error") {
|
||||
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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} 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 - ${messageCount} messages, ${toolCallCount} tool calls`);
|
||||
|
||||
execution.query = null;
|
||||
execution.abortController = null;
|
||||
|
||||
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: ${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 {
|
||||
let currentPhase = "initialization";
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_progress",
|
||||
content: `[Phase: ${currentPhase}] Initializing spec regeneration process...\n`,
|
||||
});
|
||||
console.log(`[SpecRegeneration] Phase: ${currentPhase}`);
|
||||
|
||||
const abortController = new AbortController();
|
||||
execution.abortController = abortController;
|
||||
|
||||
currentPhase = "setup";
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_progress",
|
||||
content: `[Phase: ${currentPhase}] Configuring AI agent and tools...\n`,
|
||||
});
|
||||
console.log(`[SpecRegeneration] Phase: ${currentPhase}`);
|
||||
|
||||
const options = {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
systemPrompt: this.getSystemPrompt(),
|
||||
@@ -288,52 +695,137 @@ Begin by exploring the project structure.`;
|
||||
|
||||
const prompt = this.buildRegenerationPrompt(projectDefinition);
|
||||
|
||||
currentPhase = "regeneration";
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_progress",
|
||||
content: "Starting spec regeneration...\n",
|
||||
content: `[Phase: ${currentPhase}] Starting spec regeneration...\n`,
|
||||
});
|
||||
console.log(`[SpecRegeneration] Phase: ${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;
|
||||
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)}...`);
|
||||
|
||||
// Special handling for UpdateFeatureStatus to show feature creation
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
} 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}...`);
|
||||
|
||||
// Special handling for UpdateFeatureStatus results
|
||||
if (toolName === "mcp__automaker-tools__UpdateFeatureStatus" || toolName === "UpdateFeatureStatus") {
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_progress",
|
||||
content: block.text,
|
||||
content: `[Feature Creation] ${result}\n`,
|
||||
});
|
||||
} else if (block.type === "tool_use") {
|
||||
} else {
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_tool",
|
||||
tool: block.name,
|
||||
input: block.input,
|
||||
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);
|
||||
currentPhase = "complete";
|
||||
console.log(`[SpecRegeneration] Phase: ${currentPhase} - Spec regeneration completed in ${elapsedTime}s`);
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_progress",
|
||||
content: `\n[Phase: ${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 +836,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;
|
||||
|
||||
@@ -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.title || feature.id}
|
||||
</CardTitle>
|
||||
{/* Show More/Less toggle - only show when description is likely truncated */}
|
||||
{feature.description.length > 100 && (
|
||||
{(feature.description || feature.summary || feature.title || "").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 || feature.title || ""}>
|
||||
{(() => {
|
||||
const displayText = feature.description || feature.summary || feature.title || "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,7 +14,7 @@ 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";
|
||||
@@ -36,6 +36,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 +82,244 @@ 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
|
||||
console.log("[SpecView] Spec generation is running - restoring state");
|
||||
|
||||
// Only restore state if we haven't already done so for this project
|
||||
// This prevents resetting state when switching tabs
|
||||
if (!stateRestoredRef.current) {
|
||||
setIsCreating(true);
|
||||
setIsRegenerating(true);
|
||||
stateRestoredRef.current = true;
|
||||
}
|
||||
|
||||
// Try to extract the current phase from existing logs
|
||||
let detectedPhase = "";
|
||||
if (logsRef.current) {
|
||||
// Look for the most recent phase in the logs
|
||||
const phaseMatches = logsRef.current.matchAll(/\[Phase:\s*([^\]]+)\]/g);
|
||||
const phases = Array.from(phaseMatches);
|
||||
if (phases.length > 0) {
|
||||
// Get the last phase mentioned in the logs
|
||||
detectedPhase = phases[phases.length - 1][1];
|
||||
console.log(`[SpecView] Detected phase from logs: ${detectedPhase}`);
|
||||
}
|
||||
|
||||
// Also check for feature generation indicators in logs
|
||||
const hasFeatureGeneration = logsRef.current.includes("Feature Generation") ||
|
||||
logsRef.current.includes("Feature Creation") ||
|
||||
logsRef.current.includes("Creating feature") ||
|
||||
logsRef.current.includes("feature_generation");
|
||||
|
||||
if (hasFeatureGeneration && !detectedPhase) {
|
||||
detectedPhase = "feature_generation";
|
||||
console.log("[SpecView] Detected feature generation from logs");
|
||||
}
|
||||
}
|
||||
|
||||
// Update phase from logs if we found one and don't have a specific phase set
|
||||
// This allows the phase to update as new events come in
|
||||
if (detectedPhase) {
|
||||
setCurrentPhase((prevPhase) => {
|
||||
// Only update if we don't have a phase or if the detected phase is more recent
|
||||
if (!prevPhase || prevPhase === "unknown" || prevPhase === "in progress") {
|
||||
return detectedPhase;
|
||||
}
|
||||
return prevPhase;
|
||||
});
|
||||
} else if (!currentPhase) {
|
||||
// Use a more descriptive default instead of "unknown"
|
||||
setCurrentPhase("in progress");
|
||||
}
|
||||
|
||||
// Don't clear logs - they may have been persisted
|
||||
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")) {
|
||||
// Add a resume message to existing logs only if not already present
|
||||
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) {
|
||||
// Check if we might still be in feature generation phase based on logs
|
||||
const mightBeGeneratingFeatures = logsRef.current && (
|
||||
logsRef.current.includes("Feature Generation") ||
|
||||
logsRef.current.includes("Feature Creation") ||
|
||||
logsRef.current.includes("Creating feature") ||
|
||||
logsRef.current.includes("feature_generation") ||
|
||||
currentPhase === "feature_generation"
|
||||
);
|
||||
|
||||
if (mightBeGeneratingFeatures && specExists) {
|
||||
// Spec exists and we might still be generating features - keep state active
|
||||
console.log("[SpecView] Detected potential feature generation - keeping state active");
|
||||
if (!isCreating && !isRegenerating) {
|
||||
setIsCreating(true);
|
||||
}
|
||||
if (currentPhase !== "feature_generation") {
|
||||
setCurrentPhase("feature_generation");
|
||||
}
|
||||
} else {
|
||||
// Not running - clear running 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
|
||||
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) {
|
||||
// Not running but we think we are - check if we're truly done
|
||||
// Look for recent activity in logs (within last 30 seconds worth of content)
|
||||
const recentLogs = logsRef.current.slice(-5000); // Last ~5000 chars
|
||||
const hasRecentFeatureActivity = recentLogs.includes("Feature Creation") ||
|
||||
recentLogs.includes("Creating feature") ||
|
||||
recentLogs.match(/\[Feature Creation\].*$/m);
|
||||
|
||||
// Check if we have a completion message or complete phase
|
||||
const hasCompletion = logsRef.current.includes("All tasks completed") ||
|
||||
logsRef.current.includes("[Complete] All tasks completed") ||
|
||||
logsRef.current.includes("[Phase: complete]");
|
||||
|
||||
if (hasCompletion || (!hasRecentFeatureActivity && currentPhase !== "feature_generation")) {
|
||||
// No recent activity and not running - we're done
|
||||
console.log("[SpecView] Visibility change: Generation appears complete - clearing state");
|
||||
setIsCreating(false);
|
||||
setIsRegenerating(false);
|
||||
setIsGeneratingFeatures(false);
|
||||
setCurrentPhase("");
|
||||
stateRestoredRef.current = false;
|
||||
loadSpec();
|
||||
} else if (currentPhase === "feature_generation" && !hasRecentFeatureActivity) {
|
||||
// We were in feature generation but no recent activity - might be done
|
||||
// Wait a moment and check again
|
||||
setTimeout(async () => {
|
||||
if (api.specRegeneration) {
|
||||
const recheckStatus = await api.specRegeneration.status();
|
||||
if (!recheckStatus.isRunning) {
|
||||
console.log("[SpecView] Re-check after visibility: Still not running - clearing state");
|
||||
setIsCreating(false);
|
||||
setIsRegenerating(false);
|
||||
setIsGeneratingFeatures(false);
|
||||
setCurrentPhase("");
|
||||
stateRestoredRef.current = false;
|
||||
loadSpec();
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
} 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, currentPhase, 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 not running but we think we are, verify we're truly done
|
||||
if (!status.isRunning && (isCreating || isRegenerating || isGeneratingFeatures)) {
|
||||
// Check logs for completion indicators
|
||||
const hasCompletion = logsRef.current.includes("All tasks completed") ||
|
||||
logsRef.current.includes("[Complete] All tasks completed") ||
|
||||
logsRef.current.includes("[Phase: complete]") ||
|
||||
currentPhase === "complete";
|
||||
|
||||
// Also check if we haven't seen feature activity recently
|
||||
const recentLogs = logsRef.current.slice(-3000); // Last 3000 chars (more context)
|
||||
const hasRecentFeatureActivity = recentLogs.includes("Feature Creation") ||
|
||||
recentLogs.includes("Creating feature") ||
|
||||
recentLogs.includes("UpdateFeatureStatus") ||
|
||||
recentLogs.includes("[Tool]") && recentLogs.includes("UpdateFeatureStatus");
|
||||
|
||||
// If we're in feature_generation phase and not running, we're likely done
|
||||
// (features are created via tool calls, so when stream ends, they're done)
|
||||
const isFeatureGenComplete = currentPhase === "feature_generation" &&
|
||||
!hasRecentFeatureActivity;
|
||||
|
||||
if (hasCompletion || isFeatureGenComplete) {
|
||||
console.log("[SpecView] Periodic check: Generation complete - clearing state", {
|
||||
hasCompletion,
|
||||
hasRecentFeatureActivity,
|
||||
currentPhase,
|
||||
isFeatureGenComplete
|
||||
});
|
||||
setIsCreating(false);
|
||||
setIsRegenerating(false);
|
||||
setIsGeneratingFeatures(false);
|
||||
setCurrentPhase("");
|
||||
stateRestoredRef.current = false;
|
||||
loadSpec();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[SpecView] Periodic status check error:", error);
|
||||
}
|
||||
}, 2000); // Check every 2 seconds (more frequent)
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [currentProject, isCreating, isRegenerating, isGeneratingFeatures, currentPhase, loadSpec]);
|
||||
|
||||
// Subscribe to spec regeneration events
|
||||
useEffect(() => {
|
||||
const api = getElectronAPI();
|
||||
@@ -77,18 +328,151 @@ 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();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 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 (before checking)
|
||||
const completionLog = logsRef.current + `\n[Complete] ${event.message}\n`;
|
||||
logsRef.current = completionLog;
|
||||
setLogs(completionLog);
|
||||
|
||||
// Check if this is the final completion
|
||||
const isFinalCompletion = event.message?.includes("All tasks completed") ||
|
||||
event.message === "All tasks completed!" ||
|
||||
event.message === "All tasks completed";
|
||||
|
||||
// Check if we've already seen a completion phase in logs (including the message we just added)
|
||||
const hasSeenCompletePhase = logsRef.current.includes("[Phase: complete]");
|
||||
|
||||
// Check recent logs for feature activity
|
||||
const recentLogs = logsRef.current.slice(-2000);
|
||||
const hasRecentFeatureActivity = recentLogs.includes("Feature Creation") ||
|
||||
recentLogs.includes("Creating feature") ||
|
||||
recentLogs.includes("UpdateFeatureStatus");
|
||||
|
||||
// Check if we're still generating features (only for intermediate completion)
|
||||
const isGeneratingFeatures = !isFinalCompletion &&
|
||||
!hasSeenCompletePhase &&
|
||||
(event.message?.includes("Features are being generated") ||
|
||||
event.message?.includes("features are being generated"));
|
||||
|
||||
// If we're in feature_generation but no recent activity and we see completion, we're done
|
||||
const shouldComplete = isFinalCompletion ||
|
||||
hasSeenCompletePhase ||
|
||||
(currentPhase === "feature_generation" && !hasRecentFeatureActivity && !isGeneratingFeatures);
|
||||
|
||||
if (shouldComplete) {
|
||||
// Fully complete - clear all states immediately
|
||||
console.log("[SpecView] Final completion detected - clearing state", {
|
||||
isFinalCompletion,
|
||||
hasSeenCompletePhase,
|
||||
shouldComplete,
|
||||
hasRecentFeatureActivity,
|
||||
currentPhase,
|
||||
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] Spec complete, continuing with feature generation", {
|
||||
isGeneratingFeatures,
|
||||
hasRecentFeatureActivity,
|
||||
currentPhase
|
||||
});
|
||||
}
|
||||
|
||||
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 +510,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 +529,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 +556,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 +576,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 +683,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 +720,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 +813,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 +822,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 +843,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 +888,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 +964,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 +1001,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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1843,6 +1843,23 @@ 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;
|
||||
if (mockSpecRegenerationTimeout) {
|
||||
@@ -2007,6 +2024,51 @@ async function simulateSpecRegeneration(
|
||||
mockSpecRegenerationTimeout = null;
|
||||
}
|
||||
|
||||
async function simulateFeatureGeneration(projectPath: string) {
|
||||
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;
|
||||
|
||||
emitSpecRegenerationEvent({
|
||||
type: "spec_regeneration_progress",
|
||||
content: "[Feature Creation] Creating features from roadmap...\n",
|
||||
});
|
||||
|
||||
await new Promise((resolve) => {
|
||||
mockSpecRegenerationTimeout = setTimeout(resolve, 1000);
|
||||
});
|
||||
if (!mockSpecRegenerationRunning) return;
|
||||
|
||||
emitSpecRegenerationEvent({
|
||||
type: "spec_regeneration_progress",
|
||||
content: "[Phase: complete] All tasks completed!\n",
|
||||
});
|
||||
|
||||
emitSpecRegenerationEvent({
|
||||
type: "spec_regeneration_complete",
|
||||
message: "All tasks completed!",
|
||||
});
|
||||
|
||||
mockSpecRegenerationRunning = false;
|
||||
mockSpecRegenerationTimeout = null;
|
||||
}
|
||||
|
||||
// Mock Features API implementation
|
||||
function createMockFeaturesAPI(): FeaturesAPI {
|
||||
// Store features in mock file system using features/{id}/feature.json pattern
|
||||
|
||||
@@ -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");
|
||||
|
||||
5
app/src/types/electron.d.ts
vendored
5
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;
|
||||
|
||||
Reference in New Issue
Block a user