mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
Merge remote-tracking branch 'origin/main' into feat/extend-models-support
This commit is contained in:
@@ -331,4 +331,4 @@
|
|||||||
"model": "sonnet",
|
"model": "sonnet",
|
||||||
"thinkingLevel": "medium"
|
"thinkingLevel": "medium"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ const fs = require("fs/promises");
|
|||||||
const agentService = require("./agent-service");
|
const agentService = require("./agent-service");
|
||||||
const autoModeService = require("./auto-mode-service");
|
const autoModeService = require("./auto-mode-service");
|
||||||
const worktreeManager = require("./services/worktree-manager");
|
const worktreeManager = require("./services/worktree-manager");
|
||||||
|
const featureSuggestionsService = require("./services/feature-suggestions-service");
|
||||||
|
const specRegenerationService = require("./services/spec-regeneration-service");
|
||||||
|
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
|
|
||||||
@@ -769,6 +771,91 @@ ipcMain.handle("mcp:update-feature-status", async (_, { featureId, status, proje
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Feature Suggestions IPC Handlers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Track running suggestions analysis
|
||||||
|
let suggestionsExecution = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate feature suggestions by analyzing the project
|
||||||
|
*/
|
||||||
|
ipcMain.handle(
|
||||||
|
"suggestions:generate",
|
||||||
|
async (_, { projectPath }) => {
|
||||||
|
console.log("[IPC] suggestions:generate called with:", { projectPath });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if already running
|
||||||
|
if (suggestionsExecution && suggestionsExecution.isActive()) {
|
||||||
|
return { success: false, error: "Suggestions generation is already running" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create execution context
|
||||||
|
suggestionsExecution = {
|
||||||
|
abortController: null,
|
||||||
|
query: null,
|
||||||
|
isActive: () => suggestionsExecution !== null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendToRenderer = (data) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send("suggestions:event", data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start generating suggestions (runs in background)
|
||||||
|
featureSuggestionsService
|
||||||
|
.generateSuggestions(projectPath, sendToRenderer, suggestionsExecution)
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("[IPC] suggestions:generate background error:", error);
|
||||||
|
sendToRenderer({
|
||||||
|
type: "suggestions_error",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
suggestionsExecution = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return immediately
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[IPC] suggestions:generate error:", error);
|
||||||
|
suggestionsExecution = null;
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the current suggestions generation
|
||||||
|
*/
|
||||||
|
ipcMain.handle("suggestions:stop", async () => {
|
||||||
|
console.log("[IPC] suggestions:stop called");
|
||||||
|
try {
|
||||||
|
if (suggestionsExecution && suggestionsExecution.abortController) {
|
||||||
|
suggestionsExecution.abortController.abort();
|
||||||
|
}
|
||||||
|
suggestionsExecution = null;
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[IPC] suggestions:stop error:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get suggestions generation status
|
||||||
|
*/
|
||||||
|
ipcMain.handle("suggestions:status", () => {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
isRunning: suggestionsExecution !== null && suggestionsExecution.isActive(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// OpenAI API Handlers
|
// OpenAI API Handlers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -840,6 +927,142 @@ ipcMain.handle(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Spec Regeneration IPC Handlers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Track running spec regeneration
|
||||||
|
let specRegenerationExecution = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate the app spec based on project definition
|
||||||
|
*/
|
||||||
|
ipcMain.handle(
|
||||||
|
"spec-regeneration:generate",
|
||||||
|
async (_, { projectPath, projectDefinition }) => {
|
||||||
|
console.log("[IPC] spec-regeneration:generate called with:", { projectPath });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if already running
|
||||||
|
if (specRegenerationExecution && specRegenerationExecution.isActive()) {
|
||||||
|
return { success: false, error: "Spec regeneration is already running" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create execution context
|
||||||
|
specRegenerationExecution = {
|
||||||
|
abortController: null,
|
||||||
|
query: null,
|
||||||
|
isActive: () => specRegenerationExecution !== null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendToRenderer = (data) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send("spec-regeneration:event", data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start regenerating spec (runs in background)
|
||||||
|
specRegenerationService
|
||||||
|
.regenerateSpec(projectPath, projectDefinition, sendToRenderer, specRegenerationExecution)
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("[IPC] spec-regeneration:generate background error:", error);
|
||||||
|
sendToRenderer({
|
||||||
|
type: "spec_regeneration_error",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
specRegenerationExecution = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return immediately
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[IPC] spec-regeneration:generate error:", error);
|
||||||
|
specRegenerationExecution = null;
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the current spec regeneration
|
||||||
|
*/
|
||||||
|
ipcMain.handle("spec-regeneration:stop", async () => {
|
||||||
|
console.log("[IPC] spec-regeneration:stop called");
|
||||||
|
try {
|
||||||
|
if (specRegenerationExecution && specRegenerationExecution.abortController) {
|
||||||
|
specRegenerationExecution.abortController.abort();
|
||||||
|
}
|
||||||
|
specRegenerationExecution = null;
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[IPC] spec-regeneration:stop error:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get spec regeneration status
|
||||||
|
*/
|
||||||
|
ipcMain.handle("spec-regeneration:status", () => {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
isRunning: specRegenerationExecution !== null && specRegenerationExecution.isActive(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create initial app spec for a new project
|
||||||
|
*/
|
||||||
|
ipcMain.handle(
|
||||||
|
"spec-regeneration:create",
|
||||||
|
async (_, { projectPath, projectOverview, generateFeatures = true }) => {
|
||||||
|
console.log("[IPC] spec-regeneration:create called with:", { projectPath, generateFeatures });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if already running
|
||||||
|
if (specRegenerationExecution && specRegenerationExecution.isActive()) {
|
||||||
|
return { success: false, error: "Spec creation is already running" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create execution context
|
||||||
|
specRegenerationExecution = {
|
||||||
|
abortController: null,
|
||||||
|
query: null,
|
||||||
|
isActive: () => specRegenerationExecution !== null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendToRenderer = (data) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send("spec-regeneration:event", data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start creating spec (runs in background)
|
||||||
|
specRegenerationService
|
||||||
|
.createInitialSpec(projectPath, projectOverview, sendToRenderer, specRegenerationExecution, generateFeatures)
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("[IPC] spec-regeneration:create background error:", error);
|
||||||
|
sendToRenderer({
|
||||||
|
type: "spec_regeneration_error",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
specRegenerationExecution = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return immediately
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[IPC] spec-regeneration:create error:", error);
|
||||||
|
specRegenerationExecution = null;
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge feature worktree changes back to main branch
|
* Merge feature worktree changes back to main branch
|
||||||
*/
|
*/
|
||||||
@@ -870,7 +1093,6 @@ ipcMain.handle(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get worktree info for a feature
|
* Get worktree info for a feature
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -200,6 +200,58 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
getFileDiff: (projectPath, filePath) =>
|
getFileDiff: (projectPath, filePath) =>
|
||||||
ipcRenderer.invoke("git:get-file-diff", { projectPath, filePath }),
|
ipcRenderer.invoke("git:get-file-diff", { projectPath, filePath }),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Feature Suggestions API
|
||||||
|
suggestions: {
|
||||||
|
// Generate feature suggestions
|
||||||
|
generate: (projectPath) =>
|
||||||
|
ipcRenderer.invoke("suggestions:generate", { projectPath }),
|
||||||
|
|
||||||
|
// Stop generating suggestions
|
||||||
|
stop: () => ipcRenderer.invoke("suggestions:stop"),
|
||||||
|
|
||||||
|
// Get suggestions status
|
||||||
|
status: () => ipcRenderer.invoke("suggestions:status"),
|
||||||
|
|
||||||
|
// Listen for suggestions events
|
||||||
|
onEvent: (callback) => {
|
||||||
|
const subscription = (_, data) => callback(data);
|
||||||
|
ipcRenderer.on("suggestions:event", subscription);
|
||||||
|
|
||||||
|
// Return unsubscribe function
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener("suggestions:event", subscription);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Spec Regeneration API
|
||||||
|
specRegeneration: {
|
||||||
|
// Create initial app spec for a new project
|
||||||
|
create: (projectPath, projectOverview, generateFeatures = true) =>
|
||||||
|
ipcRenderer.invoke("spec-regeneration:create", { projectPath, projectOverview, generateFeatures }),
|
||||||
|
|
||||||
|
// Regenerate the app spec
|
||||||
|
generate: (projectPath, projectDefinition) =>
|
||||||
|
ipcRenderer.invoke("spec-regeneration:generate", { projectPath, projectDefinition }),
|
||||||
|
|
||||||
|
// Stop regenerating spec
|
||||||
|
stop: () => ipcRenderer.invoke("spec-regeneration:stop"),
|
||||||
|
|
||||||
|
// Get regeneration status
|
||||||
|
status: () => ipcRenderer.invoke("spec-regeneration:status"),
|
||||||
|
|
||||||
|
// Listen for regeneration events
|
||||||
|
onEvent: (callback) => {
|
||||||
|
const subscription = (_, data) => callback(data);
|
||||||
|
ipcRenderer.on("spec-regeneration:event", subscription);
|
||||||
|
|
||||||
|
// Return unsubscribe function
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener("spec-regeneration:event", subscription);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also expose a flag to detect if we're in Electron
|
// Also expose a flag to detect if we're in Electron
|
||||||
|
|||||||
269
app/electron/services/feature-suggestions-service.js
Normal file
269
app/electron/services/feature-suggestions-service.js
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
|
||||||
|
const promptBuilder = require("./prompt-builder");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature Suggestions Service - Analyzes project and generates feature suggestions
|
||||||
|
*/
|
||||||
|
class FeatureSuggestionsService {
|
||||||
|
constructor() {
|
||||||
|
this.runningAnalysis = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate feature suggestions by analyzing the project
|
||||||
|
*/
|
||||||
|
async generateSuggestions(projectPath, sendToRenderer, execution) {
|
||||||
|
console.log(
|
||||||
|
`[FeatureSuggestions] Generating suggestions for: ${projectPath}`
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
execution.abortController = abortController;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
model: "claude-sonnet-4-20250514",
|
||||||
|
systemPrompt: this.getSystemPrompt(),
|
||||||
|
maxTurns: 50,
|
||||||
|
cwd: projectPath,
|
||||||
|
allowedTools: ["Read", "Glob", "Grep", "Bash"],
|
||||||
|
permissionMode: "acceptEdits",
|
||||||
|
sandbox: {
|
||||||
|
enabled: true,
|
||||||
|
autoAllowBashIfSandboxed: true,
|
||||||
|
},
|
||||||
|
abortController: abortController,
|
||||||
|
};
|
||||||
|
|
||||||
|
const prompt = this.buildAnalysisPrompt();
|
||||||
|
|
||||||
|
sendToRenderer({
|
||||||
|
type: "suggestions_progress",
|
||||||
|
content: "Starting project analysis...\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentQuery = query({ prompt, options });
|
||||||
|
execution.query = currentQuery;
|
||||||
|
|
||||||
|
let fullResponse = "";
|
||||||
|
for await (const msg of currentQuery) {
|
||||||
|
if (!execution.isActive()) break;
|
||||||
|
|
||||||
|
if (msg.type === "assistant" && msg.message?.content) {
|
||||||
|
for (const block of msg.message.content) {
|
||||||
|
if (block.type === "text") {
|
||||||
|
fullResponse += block.text;
|
||||||
|
sendToRenderer({
|
||||||
|
type: "suggestions_progress",
|
||||||
|
content: block.text,
|
||||||
|
});
|
||||||
|
} else if (block.type === "tool_use") {
|
||||||
|
sendToRenderer({
|
||||||
|
type: "suggestions_tool",
|
||||||
|
tool: block.name,
|
||||||
|
input: block.input,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execution.query = null;
|
||||||
|
execution.abortController = null;
|
||||||
|
|
||||||
|
// Parse the suggestions from the response
|
||||||
|
const suggestions = this.parseSuggestions(fullResponse);
|
||||||
|
|
||||||
|
sendToRenderer({
|
||||||
|
type: "suggestions_complete",
|
||||||
|
suggestions: suggestions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
suggestions: suggestions,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AbortError || error?.name === "AbortError") {
|
||||||
|
console.log("[FeatureSuggestions] Analysis aborted");
|
||||||
|
if (execution) {
|
||||||
|
execution.abortController = null;
|
||||||
|
execution.query = null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Analysis aborted",
|
||||||
|
suggestions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
"[FeatureSuggestions] Error generating suggestions:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
if (execution) {
|
||||||
|
execution.abortController = null;
|
||||||
|
execution.query = null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse suggestions from the LLM response
|
||||||
|
* Looks for JSON array in the response
|
||||||
|
*/
|
||||||
|
parseSuggestions(response) {
|
||||||
|
try {
|
||||||
|
// Try to find JSON array in the response
|
||||||
|
// Look for ```json ... ``` blocks first
|
||||||
|
const jsonBlockMatch = response.match(/```json\s*([\s\S]*?)```/);
|
||||||
|
if (jsonBlockMatch) {
|
||||||
|
const parsed = JSON.parse(jsonBlockMatch[1].trim());
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return this.validateSuggestions(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find a raw JSON array
|
||||||
|
const jsonArrayMatch = response.match(/\[\s*\{[\s\S]*\}\s*\]/);
|
||||||
|
if (jsonArrayMatch) {
|
||||||
|
const parsed = JSON.parse(jsonArrayMatch[0]);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return this.validateSuggestions(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
"[FeatureSuggestions] Could not parse suggestions from response"
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[FeatureSuggestions] Error parsing suggestions:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and normalize suggestions
|
||||||
|
*/
|
||||||
|
validateSuggestions(suggestions) {
|
||||||
|
return suggestions
|
||||||
|
.filter((s) => s && typeof s === "object")
|
||||||
|
.map((s, index) => ({
|
||||||
|
id: `suggestion-${Date.now()}-${index}`,
|
||||||
|
category: s.category || "Uncategorized",
|
||||||
|
description: s.description || s.title || "No description",
|
||||||
|
steps: Array.isArray(s.steps) ? s.steps : [],
|
||||||
|
priority: typeof s.priority === "number" ? s.priority : index + 1,
|
||||||
|
reasoning: s.reasoning || "",
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.priority - b.priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the system prompt for feature suggestion analysis
|
||||||
|
*/
|
||||||
|
getSystemPrompt() {
|
||||||
|
return `You are an expert software architect and product manager. Your job is to analyze a codebase and suggest missing features that would improve the application.
|
||||||
|
|
||||||
|
You should:
|
||||||
|
1. Thoroughly analyze the project structure, code, and any existing documentation
|
||||||
|
2. Identify what the application does and what features it currently has (look at the .automaker/app_spec.txt file as well if it exists)
|
||||||
|
3. Generate a comprehensive list of missing features that would be valuable to users
|
||||||
|
4. Prioritize features by impact and complexity
|
||||||
|
5. Provide clear, actionable descriptions and implementation steps
|
||||||
|
|
||||||
|
When analyzing, look at:
|
||||||
|
- README files and documentation
|
||||||
|
- Package.json, cargo.toml, or similar config files for tech stack
|
||||||
|
- Source code structure and organization
|
||||||
|
- Existing features and their implementation patterns
|
||||||
|
- Common patterns in similar applications
|
||||||
|
- User experience improvements
|
||||||
|
- Developer experience improvements
|
||||||
|
- Performance optimizations
|
||||||
|
- Security enhancements
|
||||||
|
|
||||||
|
You have access to file reading and search tools. Use them to understand the codebase.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the prompt for analyzing the project
|
||||||
|
*/
|
||||||
|
buildAnalysisPrompt() {
|
||||||
|
return `Analyze this project and generate a list of suggested features that are missing or would improve the application.
|
||||||
|
|
||||||
|
**Your Task:**
|
||||||
|
|
||||||
|
1. First, explore the project structure:
|
||||||
|
- Read README.md, package.json, or similar config files
|
||||||
|
- Scan the source code directory structure
|
||||||
|
- Identify the tech stack and frameworks used
|
||||||
|
- Look at existing features and how they're implemented
|
||||||
|
|
||||||
|
2. Identify what the application does:
|
||||||
|
- What is the main purpose?
|
||||||
|
- What features are already implemented?
|
||||||
|
- What patterns and conventions are used?
|
||||||
|
|
||||||
|
3. Generate feature suggestions:
|
||||||
|
- Think about what's missing compared to similar applications
|
||||||
|
- Consider user experience improvements
|
||||||
|
- Consider developer experience improvements
|
||||||
|
- Think about performance, security, and reliability
|
||||||
|
- Consider testing and documentation improvements
|
||||||
|
|
||||||
|
4. **CRITICAL: Output your suggestions as a JSON array** at the end of your response, formatted like this:
|
||||||
|
|
||||||
|
\`\`\`json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"category": "User Experience",
|
||||||
|
"description": "Add dark mode support with system preference detection",
|
||||||
|
"steps": [
|
||||||
|
"Create a ThemeProvider context to manage theme state",
|
||||||
|
"Add a toggle component in the settings or header",
|
||||||
|
"Implement CSS variables for theme colors",
|
||||||
|
"Add localStorage persistence for user preference"
|
||||||
|
],
|
||||||
|
"priority": 1,
|
||||||
|
"reasoning": "Dark mode is a standard feature that improves accessibility and user comfort"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Performance",
|
||||||
|
"description": "Implement lazy loading for heavy components",
|
||||||
|
"steps": [
|
||||||
|
"Identify components that are heavy or rarely used",
|
||||||
|
"Use React.lazy() and Suspense for code splitting",
|
||||||
|
"Add loading states for lazy-loaded components"
|
||||||
|
],
|
||||||
|
"priority": 2,
|
||||||
|
"reasoning": "Improves initial load time and reduces bundle size"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Important Guidelines:**
|
||||||
|
- Generate at least 10-20 feature suggestions
|
||||||
|
- Order them by priority (1 = highest priority)
|
||||||
|
- Each feature should have clear, actionable steps
|
||||||
|
- Categories should be meaningful (e.g., "User Experience", "Performance", "Security", "Testing", "Documentation", "Developer Experience", "Accessibility", etc.)
|
||||||
|
- Be specific about what files might need to be created or modified
|
||||||
|
- Consider the existing tech stack and patterns when suggesting implementation steps
|
||||||
|
|
||||||
|
Begin by exploring the project structure.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the current analysis
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
if (this.runningAnalysis && this.runningAnalysis.abortController) {
|
||||||
|
this.runningAnalysis.abortController.abort();
|
||||||
|
}
|
||||||
|
this.runningAnalysis = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new FeatureSuggestionsService();
|
||||||
519
app/electron/services/spec-regeneration-service.js
Normal file
519
app/electron/services/spec-regeneration-service.js
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
|
||||||
|
const fs = require("fs/promises");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XML template for app_spec.txt
|
||||||
|
*/
|
||||||
|
const APP_SPEC_XML_TEMPLATE = `<project_specification>
|
||||||
|
<project_name></project_name>
|
||||||
|
|
||||||
|
<overview>
|
||||||
|
</overview>
|
||||||
|
|
||||||
|
<technology_stack>
|
||||||
|
<frontend>
|
||||||
|
<framework></framework>
|
||||||
|
<ui_library></ui_library>
|
||||||
|
<styling></styling>
|
||||||
|
<state_management></state_management>
|
||||||
|
<drag_drop></drag_drop>
|
||||||
|
<icons></icons>
|
||||||
|
</frontend>
|
||||||
|
<desktop_shell>
|
||||||
|
<framework></framework>
|
||||||
|
<language></language>
|
||||||
|
<inter_process_communication></inter_process_communication>
|
||||||
|
<file_system></file_system>
|
||||||
|
</desktop_shell>
|
||||||
|
<ai_engine>
|
||||||
|
<logic_model></logic_model>
|
||||||
|
<design_model></design_model>
|
||||||
|
<orchestration></orchestration>
|
||||||
|
</ai_engine>
|
||||||
|
<testing>
|
||||||
|
<framework></framework>
|
||||||
|
<unit></unit>
|
||||||
|
</testing>
|
||||||
|
</technology_stack>
|
||||||
|
|
||||||
|
<core_capabilities>
|
||||||
|
<project_management>
|
||||||
|
</project_management>
|
||||||
|
|
||||||
|
<intelligent_analysis>
|
||||||
|
</intelligent_analysis>
|
||||||
|
|
||||||
|
<kanban_workflow>
|
||||||
|
</kanban_workflow>
|
||||||
|
|
||||||
|
<autonomous_agent_engine>
|
||||||
|
</autonomous_agent_engine>
|
||||||
|
|
||||||
|
<extensibility>
|
||||||
|
</extensibility>
|
||||||
|
</core_capabilities>
|
||||||
|
|
||||||
|
<ui_layout>
|
||||||
|
<window_structure>
|
||||||
|
</window_structure>
|
||||||
|
<theme>
|
||||||
|
</theme>
|
||||||
|
</ui_layout>
|
||||||
|
|
||||||
|
<development_workflow>
|
||||||
|
<local_testing>
|
||||||
|
</local_testing>
|
||||||
|
</development_workflow>
|
||||||
|
|
||||||
|
<implementation_roadmap>
|
||||||
|
<phase_1_foundation>
|
||||||
|
</phase_1_foundation>
|
||||||
|
<phase_2_core_logic>
|
||||||
|
</phase_2_core_logic>
|
||||||
|
<phase_3_kanban_and_interaction>
|
||||||
|
</phase_3_kanban_and_interaction>
|
||||||
|
<phase_4_polish>
|
||||||
|
</phase_4_polish>
|
||||||
|
</implementation_roadmap>
|
||||||
|
</project_specification>`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spec Regeneration Service - Regenerates app spec based on project description and tech stack
|
||||||
|
*/
|
||||||
|
class SpecRegenerationService {
|
||||||
|
constructor() {
|
||||||
|
this.runningRegeneration = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create initial app spec for a new project
|
||||||
|
* @param {string} projectPath - Path to the project
|
||||||
|
* @param {string} projectOverview - User's project description
|
||||||
|
* @param {Function} sendToRenderer - Function to send events to renderer
|
||||||
|
* @param {Object} execution - Execution context with abort controller
|
||||||
|
* @param {boolean} generateFeatures - Whether to generate feature_list.json entries
|
||||||
|
*/
|
||||||
|
async createInitialSpec(projectPath, projectOverview, sendToRenderer, execution, generateFeatures = true) {
|
||||||
|
console.log(`[SpecRegeneration] Creating initial spec for: ${projectPath}, generateFeatures: ${generateFeatures}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
execution.abortController = abortController;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
model: "claude-sonnet-4-20250514",
|
||||||
|
systemPrompt: this.getInitialCreationSystemPrompt(generateFeatures),
|
||||||
|
maxTurns: 50,
|
||||||
|
cwd: projectPath,
|
||||||
|
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
|
||||||
|
permissionMode: "acceptEdits",
|
||||||
|
sandbox: {
|
||||||
|
enabled: true,
|
||||||
|
autoAllowBashIfSandboxed: true,
|
||||||
|
},
|
||||||
|
abortController: abortController,
|
||||||
|
};
|
||||||
|
|
||||||
|
const prompt = this.buildInitialCreationPrompt(projectOverview, generateFeatures);
|
||||||
|
|
||||||
|
sendToRenderer({
|
||||||
|
type: "spec_regeneration_progress",
|
||||||
|
content: "Starting project analysis and spec creation...\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentQuery = query({ prompt, options });
|
||||||
|
execution.query = currentQuery;
|
||||||
|
|
||||||
|
let fullResponse = "";
|
||||||
|
for await (const msg of currentQuery) {
|
||||||
|
if (!execution.isActive()) break;
|
||||||
|
|
||||||
|
if (msg.type === "assistant" && msg.message?.content) {
|
||||||
|
for (const block of msg.message.content) {
|
||||||
|
if (block.type === "text") {
|
||||||
|
fullResponse += block.text;
|
||||||
|
sendToRenderer({
|
||||||
|
type: "spec_regeneration_progress",
|
||||||
|
content: block.text,
|
||||||
|
});
|
||||||
|
} else if (block.type === "tool_use") {
|
||||||
|
sendToRenderer({
|
||||||
|
type: "spec_regeneration_tool",
|
||||||
|
tool: block.name,
|
||||||
|
input: block.input,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execution.query = null;
|
||||||
|
execution.abortController = null;
|
||||||
|
|
||||||
|
sendToRenderer({
|
||||||
|
type: "spec_regeneration_complete",
|
||||||
|
message: "Initial spec creation complete!",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Initial spec creation complete",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AbortError || error?.name === "AbortError") {
|
||||||
|
console.log("[SpecRegeneration] Creation aborted");
|
||||||
|
if (execution) {
|
||||||
|
execution.abortController = null;
|
||||||
|
execution.query = null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Creation aborted",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("[SpecRegeneration] Error creating initial spec:", error);
|
||||||
|
if (execution) {
|
||||||
|
execution.abortController = null;
|
||||||
|
execution.query = null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the system prompt for initial spec creation
|
||||||
|
* @param {boolean} generateFeatures - Whether features should be generated
|
||||||
|
*/
|
||||||
|
getInitialCreationSystemPrompt(generateFeatures = true) {
|
||||||
|
const featureListInstructions = generateFeatures
|
||||||
|
? `
|
||||||
|
**FEATURE LIST GENERATION**
|
||||||
|
|
||||||
|
After creating the app_spec.txt, you MUST also update the .automaker/feature_list.json file with all features from the implementation_roadmap section.
|
||||||
|
|
||||||
|
For EACH feature in each phase of the implementation_roadmap:
|
||||||
|
1. Read the app_spec.txt you just created
|
||||||
|
2. Extract every single feature from each phase (phase_1, phase_2, phase_3, phase_4, etc.)
|
||||||
|
3. Write ALL features to .automaker/feature_list.json in order
|
||||||
|
|
||||||
|
The feature_list.json format should be:
|
||||||
|
\`\`\`json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "feature-<timestamp>-<index>",
|
||||||
|
"category": "<phase name, e.g., 'Phase 1: Foundation'>",
|
||||||
|
"description": "<feature description>",
|
||||||
|
"status": "backlog",
|
||||||
|
"steps": ["Step 1", "Step 2", "..."],
|
||||||
|
"skipTests": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
IMPORTANT: Include EVERY feature from the implementation_roadmap. Do not skip any.`
|
||||||
|
: `
|
||||||
|
**CRITICAL FILE PROTECTION**
|
||||||
|
|
||||||
|
THE FOLLOWING FILE IS ABSOLUTELY FORBIDDEN FROM DIRECT MODIFICATION:
|
||||||
|
- .automaker/feature_list.json
|
||||||
|
|
||||||
|
**YOU MUST NEVER:**
|
||||||
|
- Use the Write tool on .automaker/feature_list.json
|
||||||
|
- Use the Edit tool on .automaker/feature_list.json
|
||||||
|
- Use any Bash command that writes to .automaker/feature_list.json`;
|
||||||
|
|
||||||
|
return `You are an expert software architect and product manager. Your job is to analyze an existing codebase and generate a comprehensive application specification based on a user's project overview.
|
||||||
|
|
||||||
|
You should:
|
||||||
|
1. First, thoroughly analyze the project structure to understand the existing tech stack
|
||||||
|
2. Read key configuration files (package.json, tsconfig.json, Cargo.toml, requirements.txt, etc.) to understand dependencies and frameworks
|
||||||
|
3. Understand the current architecture and patterns used
|
||||||
|
4. Based on the user's project overview, create a comprehensive app specification
|
||||||
|
5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application
|
||||||
|
6. Use the XML template format provided
|
||||||
|
7. Write the specification to .automaker/app_spec.txt
|
||||||
|
|
||||||
|
When analyzing, look at:
|
||||||
|
- package.json, cargo.toml, requirements.txt or similar config files for tech stack
|
||||||
|
- Source code structure and organization
|
||||||
|
- Framework-specific patterns (Next.js, React, Django, etc.)
|
||||||
|
- Database configurations and schemas
|
||||||
|
- API structures and patterns
|
||||||
|
${featureListInstructions}
|
||||||
|
|
||||||
|
You CAN and SHOULD modify:
|
||||||
|
- .automaker/app_spec.txt (this is your primary target)${generateFeatures ? '\n- .automaker/feature_list.json (to populate features from implementation_roadmap)' : ''}
|
||||||
|
|
||||||
|
You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the prompt for initial spec creation
|
||||||
|
* @param {string} projectOverview - User's project description
|
||||||
|
* @param {boolean} generateFeatures - Whether to generate feature_list.json entries
|
||||||
|
*/
|
||||||
|
buildInitialCreationPrompt(projectOverview, generateFeatures = true) {
|
||||||
|
const featureGenerationStep = generateFeatures
|
||||||
|
? `
|
||||||
|
5. **IMPORTANT - GENERATE FEATURE LIST**: After writing the app_spec.txt:
|
||||||
|
- Read back the app_spec.txt file you just created
|
||||||
|
- Look at the implementation_roadmap section
|
||||||
|
- For EVERY feature listed in each phase (phase_1, phase_2, phase_3, phase_4, etc.), create an entry
|
||||||
|
- Write ALL these features to \`.automaker/feature_list.json\` in the order they appear
|
||||||
|
- Each feature should have: id (feature-timestamp-index), category (phase name), description, status: "backlog", steps array, and skipTests: true
|
||||||
|
- Do NOT skip any features - include every single one from the roadmap`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return `I need you to create an initial application specification for my project. I haven't set up an app_spec.txt yet, so this will be the first one.
|
||||||
|
|
||||||
|
**My Project Overview:**
|
||||||
|
${projectOverview}
|
||||||
|
|
||||||
|
**Your Task:**
|
||||||
|
|
||||||
|
1. First, explore the project to understand the existing tech stack:
|
||||||
|
- Read package.json, Cargo.toml, requirements.txt, or similar config files
|
||||||
|
- Identify all frameworks and libraries being used
|
||||||
|
- Understand the current project structure and architecture
|
||||||
|
- Note any database, authentication, or other infrastructure in use
|
||||||
|
|
||||||
|
2. Based on my project overview and the existing tech stack, create a comprehensive app specification using this XML template:
|
||||||
|
|
||||||
|
\`\`\`xml
|
||||||
|
${APP_SPEC_XML_TEMPLATE}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
3. Fill out the template with:
|
||||||
|
- **project_name**: Extract from the project or derive from overview
|
||||||
|
- **overview**: A clear description based on my project overview
|
||||||
|
- **technology_stack**: All technologies you discover in the project (fill out the relevant sections, remove irrelevant ones)
|
||||||
|
- **core_capabilities**: List all the major capabilities the app should have based on my overview
|
||||||
|
- **ui_layout**: Describe the UI structure if relevant
|
||||||
|
- **development_workflow**: Note any testing or development patterns
|
||||||
|
- **implementation_roadmap**: Break down the features into phases - be VERY detailed here, listing every feature that needs to be built
|
||||||
|
|
||||||
|
4. **IMPORTANT**: Write the complete specification to the file \`.automaker/app_spec.txt\`
|
||||||
|
${featureGenerationStep}
|
||||||
|
|
||||||
|
**Guidelines:**
|
||||||
|
- Be comprehensive! Include ALL features needed for a complete application
|
||||||
|
- Only include technology_stack sections that are relevant (e.g., skip desktop_shell if it's a web-only app)
|
||||||
|
- Add new sections to core_capabilities as needed for the specific project
|
||||||
|
- The implementation_roadmap should reflect logical phases for building out the app - list EVERY feature individually
|
||||||
|
- Consider user flows, error states, and edge cases when defining features
|
||||||
|
- Each phase should have multiple specific, actionable features
|
||||||
|
|
||||||
|
Begin by exploring the project structure.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate the app spec based on user's project definition
|
||||||
|
*/
|
||||||
|
async regenerateSpec(projectPath, projectDefinition, sendToRenderer, execution) {
|
||||||
|
console.log(`[SpecRegeneration] Regenerating spec for: ${projectPath}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
execution.abortController = abortController;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
model: "claude-sonnet-4-20250514",
|
||||||
|
systemPrompt: this.getSystemPrompt(),
|
||||||
|
maxTurns: 50,
|
||||||
|
cwd: projectPath,
|
||||||
|
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
|
||||||
|
permissionMode: "acceptEdits",
|
||||||
|
sandbox: {
|
||||||
|
enabled: true,
|
||||||
|
autoAllowBashIfSandboxed: true,
|
||||||
|
},
|
||||||
|
abortController: abortController,
|
||||||
|
};
|
||||||
|
|
||||||
|
const prompt = this.buildRegenerationPrompt(projectDefinition);
|
||||||
|
|
||||||
|
sendToRenderer({
|
||||||
|
type: "spec_regeneration_progress",
|
||||||
|
content: "Starting spec regeneration...\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentQuery = query({ prompt, options });
|
||||||
|
execution.query = currentQuery;
|
||||||
|
|
||||||
|
let fullResponse = "";
|
||||||
|
for await (const msg of currentQuery) {
|
||||||
|
if (!execution.isActive()) break;
|
||||||
|
|
||||||
|
if (msg.type === "assistant" && msg.message?.content) {
|
||||||
|
for (const block of msg.message.content) {
|
||||||
|
if (block.type === "text") {
|
||||||
|
fullResponse += block.text;
|
||||||
|
sendToRenderer({
|
||||||
|
type: "spec_regeneration_progress",
|
||||||
|
content: block.text,
|
||||||
|
});
|
||||||
|
} else if (block.type === "tool_use") {
|
||||||
|
sendToRenderer({
|
||||||
|
type: "spec_regeneration_tool",
|
||||||
|
tool: block.name,
|
||||||
|
input: block.input,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execution.query = null;
|
||||||
|
execution.abortController = null;
|
||||||
|
|
||||||
|
sendToRenderer({
|
||||||
|
type: "spec_regeneration_complete",
|
||||||
|
message: "Spec regeneration complete!",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Spec regeneration complete",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AbortError || error?.name === "AbortError") {
|
||||||
|
console.log("[SpecRegeneration] Regeneration aborted");
|
||||||
|
if (execution) {
|
||||||
|
execution.abortController = null;
|
||||||
|
execution.query = null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Regeneration aborted",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("[SpecRegeneration] Error regenerating spec:", error);
|
||||||
|
if (execution) {
|
||||||
|
execution.abortController = null;
|
||||||
|
execution.query = null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the system prompt for spec regeneration
|
||||||
|
*/
|
||||||
|
getSystemPrompt() {
|
||||||
|
return `You are an expert software architect and product manager. Your job is to analyze an existing codebase and generate a comprehensive application specification based on a user's project definition.
|
||||||
|
|
||||||
|
You should:
|
||||||
|
1. First, thoroughly analyze the project structure to understand the existing tech stack
|
||||||
|
2. Read key configuration files (package.json, tsconfig.json, etc.) to understand dependencies and frameworks
|
||||||
|
3. Understand the current architecture and patterns used
|
||||||
|
4. Based on the user's project definition, create a comprehensive app specification that includes ALL features needed to realize their vision
|
||||||
|
5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application
|
||||||
|
6. Write the specification to .automaker/app_spec.txt
|
||||||
|
|
||||||
|
When analyzing, look at:
|
||||||
|
- package.json, cargo.toml, or similar config files for tech stack
|
||||||
|
- Source code structure and organization
|
||||||
|
- Framework-specific patterns (Next.js, React, etc.)
|
||||||
|
- Database configurations and schemas
|
||||||
|
- API structures and patterns
|
||||||
|
|
||||||
|
**CRITICAL FILE PROTECTION**
|
||||||
|
|
||||||
|
THE FOLLOWING FILE IS ABSOLUTELY FORBIDDEN FROM DIRECT MODIFICATION:
|
||||||
|
- .automaker/feature_list.json
|
||||||
|
|
||||||
|
**YOU MUST NEVER:**
|
||||||
|
- Use the Write tool on .automaker/feature_list.json
|
||||||
|
- Use the Edit tool on .automaker/feature_list.json
|
||||||
|
- Use any Bash command that writes to .automaker/feature_list.json
|
||||||
|
|
||||||
|
You CAN and SHOULD modify:
|
||||||
|
- .automaker/app_spec.txt (this is your primary target)
|
||||||
|
|
||||||
|
You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the prompt for regenerating the spec
|
||||||
|
*/
|
||||||
|
buildRegenerationPrompt(projectDefinition) {
|
||||||
|
return `I need you to regenerate my application specification based on the following project definition. Be very comprehensive and liberal when defining features - I want a complete, polished application.
|
||||||
|
|
||||||
|
**My Project Definition:**
|
||||||
|
${projectDefinition}
|
||||||
|
|
||||||
|
**Your Task:**
|
||||||
|
|
||||||
|
1. First, explore the project to understand the existing tech stack:
|
||||||
|
- Read package.json or similar config files
|
||||||
|
- Identify all frameworks and libraries being used
|
||||||
|
- Understand the current project structure and architecture
|
||||||
|
- Note any database, authentication, or other infrastructure in use
|
||||||
|
|
||||||
|
2. Based on my project definition and the existing tech stack, create a comprehensive app specification that includes:
|
||||||
|
- Product Overview: A clear description of what the app does
|
||||||
|
- Tech Stack: All technologies currently in use
|
||||||
|
- Features: A COMPREHENSIVE list of all features needed to realize my vision
|
||||||
|
- Be liberal! Include all features that would make this a complete, production-ready application
|
||||||
|
- Include core features, supporting features, and nice-to-have features
|
||||||
|
- Think about user experience, error handling, edge cases, etc.
|
||||||
|
- Architecture Notes: Any important architectural decisions or patterns
|
||||||
|
|
||||||
|
3. **IMPORTANT**: Write the complete specification to the file \`.automaker/app_spec.txt\`
|
||||||
|
|
||||||
|
**Format Guidelines for the Spec:**
|
||||||
|
|
||||||
|
Use this general structure:
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
# [App Name] - Application Specification
|
||||||
|
|
||||||
|
## Product Overview
|
||||||
|
[Description of what the app does and its purpose]
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- Frontend: [frameworks, libraries]
|
||||||
|
- Backend: [frameworks, APIs]
|
||||||
|
- Database: [if applicable]
|
||||||
|
- Other: [other relevant tech]
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### [Category 1]
|
||||||
|
- **[Feature Name]**: [Detailed description of the feature]
|
||||||
|
- **[Feature Name]**: [Detailed description]
|
||||||
|
...
|
||||||
|
|
||||||
|
### [Category 2]
|
||||||
|
- **[Feature Name]**: [Detailed description]
|
||||||
|
...
|
||||||
|
|
||||||
|
## Architecture Notes
|
||||||
|
[Any important architectural notes, patterns, or conventions]
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Remember:**
|
||||||
|
- Be comprehensive! Include ALL features needed for a complete application
|
||||||
|
- Consider user flows, error states, loading states, etc.
|
||||||
|
- Include authentication, authorization if relevant
|
||||||
|
- Think about what would make this a polished, production-ready app
|
||||||
|
- The more detailed and complete the spec, the better
|
||||||
|
|
||||||
|
Begin by exploring the project structure.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the current regeneration
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
if (this.runningRegeneration && this.runningRegeneration.abortController) {
|
||||||
|
this.runningRegeneration.abortController.abort();
|
||||||
|
}
|
||||||
|
this.runningRegeneration = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new SpecRegenerationService();
|
||||||
@@ -16,9 +16,12 @@ import {
|
|||||||
PanelLeft,
|
PanelLeft,
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
Redo2,
|
||||||
Check,
|
Check,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
GripVertical,
|
GripVertical,
|
||||||
|
RotateCw,
|
||||||
|
RotateCcw,
|
||||||
Trash2,
|
Trash2,
|
||||||
Undo2,
|
Undo2,
|
||||||
UserCircle,
|
UserCircle,
|
||||||
@@ -45,8 +48,15 @@ import {
|
|||||||
KeyboardShortcut,
|
KeyboardShortcut,
|
||||||
} from "@/hooks/use-keyboard-shortcuts";
|
} from "@/hooks/use-keyboard-shortcuts";
|
||||||
import { getElectronAPI, Project, TrashedProject } from "@/lib/electron";
|
import { getElectronAPI, Project, TrashedProject } from "@/lib/electron";
|
||||||
import { initializeProject } from "@/lib/project-init";
|
import {
|
||||||
|
initializeProject,
|
||||||
|
hasAppSpec,
|
||||||
|
hasAutomakerDir,
|
||||||
|
} from "@/lib/project-init";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { Sparkles, Loader2 } from "lucide-react";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import type { SpecRegenerationEvent } from "@/types/electron";
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
@@ -156,6 +166,7 @@ export function Sidebar() {
|
|||||||
currentProject,
|
currentProject,
|
||||||
currentView,
|
currentView,
|
||||||
sidebarOpen,
|
sidebarOpen,
|
||||||
|
projectHistory,
|
||||||
addProject,
|
addProject,
|
||||||
setCurrentProject,
|
setCurrentProject,
|
||||||
setCurrentView,
|
setCurrentView,
|
||||||
@@ -164,6 +175,8 @@ export function Sidebar() {
|
|||||||
deleteTrashedProject,
|
deleteTrashedProject,
|
||||||
emptyTrash,
|
emptyTrash,
|
||||||
reorderProjects,
|
reorderProjects,
|
||||||
|
cyclePrevProject,
|
||||||
|
cycleNextProject,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
|
|
||||||
// State for project picker dropdown
|
// State for project picker dropdown
|
||||||
@@ -172,6 +185,17 @@ export function Sidebar() {
|
|||||||
const [activeTrashId, setActiveTrashId] = useState<string | null>(null);
|
const [activeTrashId, setActiveTrashId] = useState<string | null>(null);
|
||||||
const [isEmptyingTrash, setIsEmptyingTrash] = useState(false);
|
const [isEmptyingTrash, setIsEmptyingTrash] = useState(false);
|
||||||
|
|
||||||
|
// State for new project setup dialog
|
||||||
|
const [showSetupDialog, setShowSetupDialog] = useState(false);
|
||||||
|
const [setupProjectPath, setSetupProjectPath] = useState("");
|
||||||
|
const [projectOverview, setProjectOverview] = useState("");
|
||||||
|
const [isCreatingSpec, setIsCreatingSpec] = useState(false);
|
||||||
|
const [creatingSpecProjectPath, setCreatingSpecProjectPath] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [generateFeatures, setGenerateFeatures] = useState(true);
|
||||||
|
const [showSpecIndicator, setShowSpecIndicator] = useState(true);
|
||||||
|
|
||||||
// Sensors for drag-and-drop
|
// Sensors for drag-and-drop
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
@@ -198,6 +222,93 @@ export function Sidebar() {
|
|||||||
[projects, reorderProjects]
|
[projects, reorderProjects]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Subscribe to spec regeneration events
|
||||||
|
useEffect(() => {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.specRegeneration) return;
|
||||||
|
|
||||||
|
const unsubscribe = api.specRegeneration.onEvent(
|
||||||
|
(event: SpecRegenerationEvent) => {
|
||||||
|
console.log("[Sidebar] Spec regeneration event:", event.type);
|
||||||
|
|
||||||
|
if (event.type === "spec_regeneration_complete") {
|
||||||
|
setIsCreatingSpec(false);
|
||||||
|
setCreatingSpecProjectPath(null);
|
||||||
|
setShowSetupDialog(false);
|
||||||
|
setProjectOverview("");
|
||||||
|
setSetupProjectPath("");
|
||||||
|
toast.success("App specification created", {
|
||||||
|
description: "Your project is now set up and ready to go!",
|
||||||
|
});
|
||||||
|
// Navigate to spec view to show the new spec
|
||||||
|
setCurrentView("spec");
|
||||||
|
} else if (event.type === "spec_regeneration_error") {
|
||||||
|
setIsCreatingSpec(false);
|
||||||
|
setCreatingSpecProjectPath(null);
|
||||||
|
toast.error("Failed to create specification", {
|
||||||
|
description: event.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [setCurrentView]);
|
||||||
|
|
||||||
|
// Handle creating initial spec for new project
|
||||||
|
const handleCreateInitialSpec = useCallback(async () => {
|
||||||
|
if (!setupProjectPath || !projectOverview.trim()) return;
|
||||||
|
|
||||||
|
setIsCreatingSpec(true);
|
||||||
|
setCreatingSpecProjectPath(setupProjectPath);
|
||||||
|
setShowSpecIndicator(true);
|
||||||
|
setShowSetupDialog(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.specRegeneration) {
|
||||||
|
toast.error("Spec regeneration not available");
|
||||||
|
setIsCreatingSpec(false);
|
||||||
|
setCreatingSpecProjectPath(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await api.specRegeneration.create(
|
||||||
|
setupProjectPath,
|
||||||
|
projectOverview.trim(),
|
||||||
|
generateFeatures
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error("[Sidebar] Failed to start spec creation:", result.error);
|
||||||
|
setIsCreatingSpec(false);
|
||||||
|
setCreatingSpecProjectPath(null);
|
||||||
|
toast.error("Failed to create specification", {
|
||||||
|
description: result.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// If successful, we'll wait for the events to update the state
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Sidebar] Failed to create spec:", error);
|
||||||
|
setIsCreatingSpec(false);
|
||||||
|
setCreatingSpecProjectPath(null);
|
||||||
|
toast.error("Failed to create specification", {
|
||||||
|
description: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [setupProjectPath, projectOverview]);
|
||||||
|
|
||||||
|
// Handle skipping setup
|
||||||
|
const handleSkipSetup = useCallback(() => {
|
||||||
|
setShowSetupDialog(false);
|
||||||
|
setProjectOverview("");
|
||||||
|
setSetupProjectPath("");
|
||||||
|
toast.info("Setup skipped", {
|
||||||
|
description: "You can set up your app_spec.txt later from the Spec view.",
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the system folder selection dialog and initializes the selected project.
|
* Opens the system folder selection dialog and initializes the selected project.
|
||||||
* Used by both the 'O' keyboard shortcut and the folder icon button.
|
* Used by both the 'O' keyboard shortcut and the folder icon button.
|
||||||
@@ -211,6 +322,9 @@ export function Sidebar() {
|
|||||||
const name = path.split("/").pop() || "Untitled Project";
|
const name = path.split("/").pop() || "Untitled Project";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check if this is a brand new project (no .automaker directory)
|
||||||
|
const hadAutomakerDir = await hasAutomakerDir(path);
|
||||||
|
|
||||||
// Initialize the .automaker directory structure
|
// Initialize the .automaker directory structure
|
||||||
const initResult = await initializeProject(path);
|
const initResult = await initializeProject(path);
|
||||||
|
|
||||||
@@ -231,7 +345,20 @@ export function Sidebar() {
|
|||||||
addProject(project);
|
addProject(project);
|
||||||
setCurrentProject(project);
|
setCurrentProject(project);
|
||||||
|
|
||||||
if (initResult.createdFiles && initResult.createdFiles.length > 0) {
|
// Check if app_spec.txt exists
|
||||||
|
const specExists = await hasAppSpec(path);
|
||||||
|
|
||||||
|
if (!hadAutomakerDir && !specExists) {
|
||||||
|
// This is a brand new project - show setup dialog
|
||||||
|
setSetupProjectPath(path);
|
||||||
|
setShowSetupDialog(true);
|
||||||
|
toast.success("Project opened", {
|
||||||
|
description: `Opened ${name}. Let's set up your app specification!`,
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
initResult.createdFiles &&
|
||||||
|
initResult.createdFiles.length > 0
|
||||||
|
) {
|
||||||
toast.success(
|
toast.success(
|
||||||
initResult.isNewProject ? "Project initialized" : "Project updated",
|
initResult.isNewProject ? "Project initialized" : "Project updated",
|
||||||
{
|
{
|
||||||
@@ -428,6 +555,20 @@ export function Sidebar() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Project cycling shortcuts - only when we have project history
|
||||||
|
if (projectHistory.length > 1) {
|
||||||
|
shortcuts.push({
|
||||||
|
key: ACTION_SHORTCUTS.cyclePrevProject,
|
||||||
|
action: () => cyclePrevProject(),
|
||||||
|
description: "Cycle to previous project (MRU)",
|
||||||
|
});
|
||||||
|
shortcuts.push({
|
||||||
|
key: ACTION_SHORTCUTS.cycleNextProject,
|
||||||
|
action: () => cycleNextProject(),
|
||||||
|
description: "Cycle to next project (LRU)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Only enable nav shortcuts if there's a current project
|
// Only enable nav shortcuts if there's a current project
|
||||||
if (currentProject) {
|
if (currentProject) {
|
||||||
navSections.forEach((section) => {
|
navSections.forEach((section) => {
|
||||||
@@ -457,6 +598,9 @@ export function Sidebar() {
|
|||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
projects.length,
|
projects.length,
|
||||||
handleOpenFolder,
|
handleOpenFolder,
|
||||||
|
projectHistory.length,
|
||||||
|
cyclePrevProject,
|
||||||
|
cycleNextProject,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Register keyboard shortcuts
|
// Register keyboard shortcuts
|
||||||
@@ -470,7 +614,7 @@ export function Sidebar() {
|
|||||||
<aside
|
<aside
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-shrink-0 border-r border-sidebar-border bg-sidebar backdrop-blur-md flex flex-col z-30 transition-all duration-300 relative",
|
"flex-shrink-0 border-r border-sidebar-border bg-sidebar backdrop-blur-md flex flex-col z-30 transition-all duration-300 relative",
|
||||||
sidebarOpen ? "w-16 lg:w-60" : "w-16"
|
sidebarOpen ? "w-16 lg:w-72" : "w-16"
|
||||||
)}
|
)}
|
||||||
data-testid="sidebar"
|
data-testid="sidebar"
|
||||||
>
|
>
|
||||||
@@ -572,16 +716,16 @@ export function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Project Selector */}
|
{/* Project Selector with Cycle Buttons */}
|
||||||
{sidebarOpen && projects.length > 0 && (
|
{sidebarOpen && projects.length > 0 && (
|
||||||
<div className="px-2 mt-3">
|
<div className="px-2 mt-3 flex items-center gap-1.5">
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
open={isProjectPickerOpen}
|
open={isProjectPickerOpen}
|
||||||
onOpenChange={setIsProjectPickerOpen}
|
onOpenChange={setIsProjectPickerOpen}
|
||||||
>
|
>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button
|
<button
|
||||||
className="w-full flex items-center justify-between px-3 py-2.5 rounded-lg bg-sidebar-accent/10 border border-sidebar-border hover:bg-sidebar-accent/20 transition-all text-foreground titlebar-no-drag"
|
className="flex-1 flex items-center justify-between px-3 py-2.5 rounded-lg bg-sidebar-accent/10 border border-sidebar-border hover:bg-sidebar-accent/20 transition-all text-foreground titlebar-no-drag min-w-0"
|
||||||
data-testid="project-selector"
|
data-testid="project-selector"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
@@ -631,6 +775,34 @@ export function Sidebar() {
|
|||||||
</DndContext>
|
</DndContext>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
|
{/* Project Cycle Buttons - only show when there's history */}
|
||||||
|
{projectHistory.length > 1 && (
|
||||||
|
<div className="hidden lg:flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={cyclePrevProject}
|
||||||
|
className="flex items-center justify-center w-8 h-8 rounded-lg text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border transition-all titlebar-no-drag group relative"
|
||||||
|
title={`Previous project (${ACTION_SHORTCUTS.cyclePrevProject})`}
|
||||||
|
data-testid="cycle-prev-project"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
<span className="absolute -bottom-5 px-1 py-0.5 text-[9px] font-mono rounded bg-sidebar-accent/20 border border-sidebar-border text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity z-10">
|
||||||
|
{ACTION_SHORTCUTS.cyclePrevProject}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={cycleNextProject}
|
||||||
|
className="flex items-center justify-center w-8 h-8 rounded-lg text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border transition-all titlebar-no-drag group relative"
|
||||||
|
title={`Next project (${ACTION_SHORTCUTS.cycleNextProject})`}
|
||||||
|
data-testid="cycle-next-project"
|
||||||
|
>
|
||||||
|
<RotateCw className="w-4 h-4" />
|
||||||
|
<span className="absolute -bottom-5 px-1 py-0.5 text-[9px] font-mono rounded bg-sidebar-accent/20 border border-sidebar-border text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity z-10">
|
||||||
|
{ACTION_SHORTCUTS.cycleNextProject}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -871,6 +1043,103 @@ export function Sidebar() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* New Project Setup Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={showSetupDialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open && !isCreatingSpec) {
|
||||||
|
handleSkipSetup();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Set Up Your Project</DialogTitle>
|
||||||
|
<DialogDescription className="text-muted-foreground">
|
||||||
|
We didn't find an app_spec.txt file. Let us help you generate
|
||||||
|
your app_spec.txt to help describe your project for our system.
|
||||||
|
We'll analyze your project's tech stack and create a
|
||||||
|
comprehensive specification.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Project Overview</label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Describe what your project does and what features you want to
|
||||||
|
build. Be as detailed as you want - this will help us create a
|
||||||
|
better specification.
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
className="w-full h-48 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
value={projectOverview}
|
||||||
|
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
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-3 pt-2">
|
||||||
|
<Checkbox
|
||||||
|
id="sidebar-generate-features"
|
||||||
|
checked={generateFeatures}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setGenerateFeatures(checked === true)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label
|
||||||
|
htmlFor="sidebar-generate-features"
|
||||||
|
className="text-sm font-medium cursor-pointer"
|
||||||
|
>
|
||||||
|
Generate feature list
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Automatically populate feature_list.json with all features
|
||||||
|
from the implementation roadmap after the spec is generated.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="ghost" onClick={handleSkipSetup}>
|
||||||
|
Skip for now
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateInitialSpec}
|
||||||
|
disabled={!projectOverview.trim()}
|
||||||
|
>
|
||||||
|
<Sparkles className="w-4 h-4 mr-2" />
|
||||||
|
Generate Spec
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Spec Creation Indicator - Bottom Right Toast */}
|
||||||
|
{isCreatingSpec &&
|
||||||
|
showSpecIndicator &&
|
||||||
|
currentProject?.path === creatingSpecProjectPath && (
|
||||||
|
<div className="fixed bottom-4 right-4 z-50 flex items-center gap-3 bg-card border border-border rounded-lg shadow-lg p-4 max-w-sm">
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin text-primary flex-shrink-0" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium">Creating App Specification</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
|
Working on your project...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSpecIndicator(false)}
|
||||||
|
className="p-1 hover:bg-muted rounded-md transition-colors flex-shrink-0"
|
||||||
|
aria-label="Dismiss notification"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
|||||||
import { KanbanColumn } from "./kanban-column";
|
import { KanbanColumn } from "./kanban-column";
|
||||||
import { KanbanCard } from "./kanban-card";
|
import { KanbanCard } from "./kanban-card";
|
||||||
import { AgentOutputModal } from "./agent-output-modal";
|
import { AgentOutputModal } from "./agent-output-modal";
|
||||||
|
import { FeatureSuggestionsDialog } from "./feature-suggestions-dialog";
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -77,6 +78,7 @@ import {
|
|||||||
Rocket,
|
Rocket,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
UserCircle,
|
UserCircle,
|
||||||
|
Lightbulb,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
@@ -180,7 +182,6 @@ export function BoardView() {
|
|||||||
updateFeature,
|
updateFeature,
|
||||||
removeFeature,
|
removeFeature,
|
||||||
moveFeature,
|
moveFeature,
|
||||||
runningAutoTasks,
|
|
||||||
maxConcurrency,
|
maxConcurrency,
|
||||||
setMaxConcurrency,
|
setMaxConcurrency,
|
||||||
defaultSkipTests,
|
defaultSkipTests,
|
||||||
@@ -227,6 +228,8 @@ export function BoardView() {
|
|||||||
// Local state to temporarily show advanced options when profiles-only mode is enabled
|
// Local state to temporarily show advanced options when profiles-only mode is enabled
|
||||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||||
const [showEditAdvancedOptions, setShowEditAdvancedOptions] = useState(false);
|
const [showEditAdvancedOptions, setShowEditAdvancedOptions] = useState(false);
|
||||||
|
const [showSuggestionsDialog, setShowSuggestionsDialog] = useState(false);
|
||||||
|
const [suggestionsCount, setSuggestionsCount] = useState(0);
|
||||||
|
|
||||||
// Make current project available globally for modal
|
// Make current project available globally for modal
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -238,12 +241,30 @@ export function BoardView() {
|
|||||||
};
|
};
|
||||||
}, [currentProject]);
|
}, [currentProject]);
|
||||||
|
|
||||||
|
// Listen for suggestions events to update count
|
||||||
|
useEffect(() => {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api?.suggestions) return;
|
||||||
|
|
||||||
|
const unsubscribe = api.suggestions.onEvent((event) => {
|
||||||
|
if (event.type === "suggestions_complete" && event.suggestions) {
|
||||||
|
setSuggestionsCount(event.suggestions.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Track previous project to detect switches
|
// Track previous project to detect switches
|
||||||
const prevProjectPathRef = useRef<string | null>(null);
|
const prevProjectPathRef = useRef<string | null>(null);
|
||||||
const isSwitchingProjectRef = useRef<boolean>(false);
|
const isSwitchingProjectRef = useRef<boolean>(false);
|
||||||
|
|
||||||
// Auto mode hook
|
// Auto mode hook
|
||||||
const autoMode = useAutoMode();
|
const autoMode = useAutoMode();
|
||||||
|
// Get runningTasks from the hook (scoped to current project)
|
||||||
|
const runningAutoTasks = autoMode.runningTasks;
|
||||||
|
|
||||||
// Window state hook for compact dialog mode
|
// Window state hook for compact dialog mode
|
||||||
const { isMaximized } = useWindowState();
|
const { isMaximized } = useWindowState();
|
||||||
@@ -449,11 +470,15 @@ export function BoardView() {
|
|||||||
// Listen for auto mode feature completion and errors to reload features
|
// Listen for auto mode feature completion and errors to reload features
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api?.autoMode) return;
|
if (!api?.autoMode || !currentProject) return;
|
||||||
|
|
||||||
const { removeRunningTask } = useAppStore.getState();
|
const { removeRunningTask } = useAppStore.getState();
|
||||||
|
const projectId = currentProject.id;
|
||||||
|
|
||||||
const unsubscribe = api.autoMode.onEvent((event) => {
|
const unsubscribe = api.autoMode.onEvent((event) => {
|
||||||
|
// Use event's projectId if available, otherwise use current project
|
||||||
|
const eventProjectId = event.projectId || projectId;
|
||||||
|
|
||||||
if (event.type === "auto_mode_feature_complete") {
|
if (event.type === "auto_mode_feature_complete") {
|
||||||
// Reload features when a feature is completed
|
// Reload features when a feature is completed
|
||||||
console.log("[Board] Feature completed, reloading features...");
|
console.log("[Board] Feature completed, reloading features...");
|
||||||
@@ -467,7 +492,7 @@ export function BoardView() {
|
|||||||
|
|
||||||
// Remove from running tasks so it moves to the correct column
|
// Remove from running tasks so it moves to the correct column
|
||||||
if (event.featureId) {
|
if (event.featureId) {
|
||||||
removeRunningTask(event.featureId);
|
removeRunningTask(eventProjectId, event.featureId);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFeatures();
|
loadFeatures();
|
||||||
@@ -479,7 +504,7 @@ export function BoardView() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, [loadFeatures]);
|
}, [loadFeatures, currentProject]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFeatures();
|
loadFeatures();
|
||||||
@@ -492,6 +517,8 @@ export function BoardView() {
|
|||||||
|
|
||||||
// Sync running tasks from electron backend on mount
|
// Sync running tasks from electron backend on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!currentProject) return;
|
||||||
|
|
||||||
const syncRunningTasks = async () => {
|
const syncRunningTasks = async () => {
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
@@ -504,13 +531,14 @@ export function BoardView() {
|
|||||||
status.runningFeatures
|
status.runningFeatures
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clear existing running tasks and add the actual running ones
|
// Clear existing running tasks for this project and add the actual running ones
|
||||||
const { clearRunningTasks, addRunningTask } = useAppStore.getState();
|
const { clearRunningTasks, addRunningTask } = useAppStore.getState();
|
||||||
clearRunningTasks();
|
const projectId = currentProject.id;
|
||||||
|
clearRunningTasks(projectId);
|
||||||
|
|
||||||
// Add each running feature to the store
|
// Add each running feature to the store
|
||||||
status.runningFeatures.forEach((featureId: string) => {
|
status.runningFeatures.forEach((featureId: string) => {
|
||||||
addRunningTask(featureId);
|
addRunningTask(projectId, featureId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -519,7 +547,7 @@ export function BoardView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
syncRunningTasks();
|
syncRunningTasks();
|
||||||
}, []);
|
}, [currentProject]);
|
||||||
|
|
||||||
// Check which features have context files
|
// Check which features have context files
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1542,21 +1570,42 @@ export function BoardView() {
|
|||||||
<Trash2 className="w-3 h-3 mr-1" />
|
<Trash2 className="w-3 h-3 mr-1" />
|
||||||
Delete All
|
Delete All
|
||||||
</Button>
|
</Button>
|
||||||
) : column.id === "backlog" &&
|
) : column.id === "backlog" ? (
|
||||||
columnFeatures.length > 0 ? (
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-6 px-2 text-xs text-primary hover:text-primary hover:bg-primary/10"
|
className="h-6 w-6 p-0 text-yellow-500 hover:text-yellow-400 hover:bg-yellow-500/10 relative"
|
||||||
onClick={handleStartNextFeatures}
|
onClick={() => setShowSuggestionsDialog(true)}
|
||||||
data-testid="start-next-button"
|
title="Feature Suggestions"
|
||||||
>
|
data-testid="feature-suggestions-button"
|
||||||
<FastForward className="w-3 h-3 mr-1" />
|
>
|
||||||
Start Next
|
<Lightbulb className="w-3.5 h-3.5" />
|
||||||
<span className="ml-1 px-1 py-0.5 text-[9px] font-mono rounded bg-accent border border-border-glass">
|
{suggestionsCount > 0 && (
|
||||||
{ACTION_SHORTCUTS.startNext}
|
<span
|
||||||
</span>
|
className="absolute -top-1 -right-1 w-4 h-4 text-[9px] font-mono rounded-full bg-yellow-500 text-black flex items-center justify-center"
|
||||||
</Button>
|
data-testid="suggestions-count"
|
||||||
|
>
|
||||||
|
{suggestionsCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{columnFeatures.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 px-2 text-xs text-primary hover:text-primary hover:bg-primary/10"
|
||||||
|
onClick={handleStartNextFeatures}
|
||||||
|
data-testid="start-next-button"
|
||||||
|
>
|
||||||
|
<FastForward className="w-3 h-3 mr-1" />
|
||||||
|
Start Next
|
||||||
|
<span className="ml-1 px-1 py-0.5 text-[9px] font-mono rounded bg-accent border border-border-glass">
|
||||||
|
{ACTION_SHORTCUTS.startNext}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -2512,6 +2561,16 @@ export function BoardView() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Feature Suggestions Dialog */}
|
||||||
|
<FeatureSuggestionsDialog
|
||||||
|
open={showSuggestionsDialog}
|
||||||
|
onClose={() => {
|
||||||
|
setShowSuggestionsDialog(false);
|
||||||
|
// Clear the count when dialog is closed (suggestions were either imported or dismissed)
|
||||||
|
}}
|
||||||
|
projectPath={currentProject.path}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -371,7 +371,7 @@ export function ContextView() {
|
|||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
Add File
|
Add File
|
||||||
<span
|
<span
|
||||||
className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-white/10 border border-white/20"
|
className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-secondary border border-border"
|
||||||
data-testid="shortcut-add-context-file"
|
data-testid="shortcut-add-context-file"
|
||||||
>
|
>
|
||||||
{ACTION_SHORTCUTS.addContextFile}
|
{ACTION_SHORTCUTS.addContextFile}
|
||||||
@@ -387,9 +387,9 @@ export function ContextView() {
|
|||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
>
|
>
|
||||||
{/* Left Panel - File List */}
|
{/* Left Panel - File List */}
|
||||||
<div className="w-64 border-r border-white/10 flex flex-col overflow-hidden">
|
<div className="w-64 border-r border-border flex flex-col overflow-hidden">
|
||||||
<div className="p-3 border-b border-white/10">
|
<div className="p-3 border-b border-border">
|
||||||
<h2 className="text-sm font-semibold text-zinc-400">
|
<h2 className="text-sm font-semibold text-muted-foreground">
|
||||||
Context Files ({contextFiles.length})
|
Context Files ({contextFiles.length})
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -399,8 +399,8 @@ export function ContextView() {
|
|||||||
>
|
>
|
||||||
{contextFiles.length === 0 ? (
|
{contextFiles.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-center p-4">
|
<div className="flex flex-col items-center justify-center h-full text-center p-4">
|
||||||
<Upload className="w-8 h-8 text-zinc-500 mb-2" />
|
<Upload className="w-8 h-8 text-muted-foreground mb-2" />
|
||||||
<p className="text-sm text-zinc-500">
|
<p className="text-sm text-muted-foreground">
|
||||||
No context files yet.
|
No context files yet.
|
||||||
<br />
|
<br />
|
||||||
Drop files here or click Add File.
|
Drop files here or click Add File.
|
||||||
@@ -415,8 +415,8 @@ export function ContextView() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-left transition-colors",
|
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-left transition-colors",
|
||||||
selectedFile?.path === file.path
|
selectedFile?.path === file.path
|
||||||
? "bg-brand-500/20 text-white border border-brand-500/30"
|
? "bg-primary/20 text-foreground border border-primary/30"
|
||||||
: "text-zinc-400 hover:bg-white/5 hover:text-white"
|
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
)}
|
)}
|
||||||
data-testid={`context-file-${file.name}`}
|
data-testid={`context-file-${file.name}`}
|
||||||
>
|
>
|
||||||
@@ -438,12 +438,12 @@ export function ContextView() {
|
|||||||
{selectedFile ? (
|
{selectedFile ? (
|
||||||
<>
|
<>
|
||||||
{/* File toolbar */}
|
{/* File toolbar */}
|
||||||
<div className="flex items-center justify-between p-3 border-b border-white/10 bg-zinc-900/50">
|
<div className="flex items-center justify-between p-3 border-b border-border bg-card">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{selectedFile.type === "image" ? (
|
{selectedFile.type === "image" ? (
|
||||||
<ImageIcon className="w-4 h-4 text-zinc-400" />
|
<ImageIcon className="w-4 h-4 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<FileText className="w-4 h-4 text-zinc-400" />
|
<FileText className="w-4 h-4 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{selectedFile.name}
|
{selectedFile.name}
|
||||||
@@ -477,7 +477,7 @@ export function ContextView() {
|
|||||||
<div className="flex-1 overflow-hidden p-4">
|
<div className="flex-1 overflow-hidden p-4">
|
||||||
{selectedFile.type === "image" ? (
|
{selectedFile.type === "image" ? (
|
||||||
<div
|
<div
|
||||||
className="h-full flex items-center justify-center bg-zinc-900/50 rounded-lg"
|
className="h-full flex items-center justify-center bg-card rounded-lg"
|
||||||
data-testid="image-preview"
|
data-testid="image-preview"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -503,9 +503,9 @@ export function ContextView() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex items-center justify-center">
|
<div className="flex-1 flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<File className="w-12 h-12 text-zinc-600 mx-auto mb-3" />
|
<File className="w-12 h-12 text-muted-foreground mx-auto mb-3" />
|
||||||
<p className="text-zinc-500">Select a file to view or edit</p>
|
<p className="text-foreground-secondary">Select a file to view or edit</p>
|
||||||
<p className="text-zinc-600 text-sm mt-1">
|
<p className="text-muted-foreground text-sm mt-1">
|
||||||
Or drop files here to add them
|
Or drop files here to add them
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -516,7 +516,10 @@ export function ContextView() {
|
|||||||
|
|
||||||
{/* Add File Dialog */}
|
{/* Add File Dialog */}
|
||||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||||
<DialogContent data-testid="add-context-dialog">
|
<DialogContent
|
||||||
|
data-testid="add-context-dialog"
|
||||||
|
className="w-[60vw] max-w-[60vw] max-h-[80vh] flex flex-col"
|
||||||
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add Context File</DialogTitle>
|
<DialogTitle>Add Context File</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -564,7 +567,7 @@ export function ContextView() {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative rounded-lg transition-colors",
|
"relative rounded-lg transition-colors",
|
||||||
isDropHovering && "ring-2 ring-brand-500"
|
isDropHovering && "ring-2 ring-primary"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -576,15 +579,15 @@ export function ContextView() {
|
|||||||
onDragLeave={handleTextAreaDragLeave}
|
onDragLeave={handleTextAreaDragLeave}
|
||||||
placeholder="Enter context content here or drag & drop a .txt or .md file..."
|
placeholder="Enter context content here or drag & drop a .txt or .md file..."
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full h-40 p-3 font-mono text-sm bg-zinc-900 border border-zinc-700 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent",
|
"w-full h-40 p-3 font-mono text-sm bg-background border border-border rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent",
|
||||||
isDropHovering && "border-brand-500 bg-brand-500/10"
|
isDropHovering && "border-primary bg-primary/10"
|
||||||
)}
|
)}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
data-testid="new-file-content"
|
data-testid="new-file-content"
|
||||||
/>
|
/>
|
||||||
{isDropHovering && (
|
{isDropHovering && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-brand-500/20 rounded-lg pointer-events-none">
|
<div className="absolute inset-0 flex items-center justify-center bg-primary/20 rounded-lg pointer-events-none">
|
||||||
<div className="flex flex-col items-center text-brand-400">
|
<div className="flex flex-col items-center text-primary">
|
||||||
<Upload className="w-8 h-8 mb-2" />
|
<Upload className="w-8 h-8 mb-2" />
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
Drop .txt or .md file here
|
Drop .txt or .md file here
|
||||||
@@ -593,7 +596,7 @@ export function ContextView() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-zinc-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
Drag & drop .txt or .md files to import their content
|
Drag & drop .txt or .md files to import their content
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -602,7 +605,7 @@ export function ContextView() {
|
|||||||
{newFileType === "image" && (
|
{newFileType === "image" && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Upload Image</Label>
|
<Label>Upload Image</Label>
|
||||||
<div className="border-2 border-dashed border-zinc-700 rounded-lg p-4 text-center">
|
<div className="border-2 border-dashed border-border rounded-lg p-4 text-center">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -622,9 +625,9 @@ export function ContextView() {
|
|||||||
className="max-w-32 max-h-32 object-contain mb-2"
|
className="max-w-32 max-h-32 object-contain mb-2"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Upload className="w-8 h-8 text-zinc-500 mb-2" />
|
<Upload className="w-8 h-8 text-muted-foreground mb-2" />
|
||||||
)}
|
)}
|
||||||
<span className="text-sm text-zinc-400">
|
<span className="text-sm text-muted-foreground">
|
||||||
{uploadedImageData
|
{uploadedImageData
|
||||||
? "Click to change"
|
? "Click to change"
|
||||||
: "Click to upload"}
|
: "Click to upload"}
|
||||||
|
|||||||
433
app/src/components/views/feature-suggestions-dialog.tsx
Normal file
433
app/src/components/views/feature-suggestions-dialog.tsx
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Loader2,
|
||||||
|
Lightbulb,
|
||||||
|
Download,
|
||||||
|
StopCircle,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { getElectronAPI, FeatureSuggestion, SuggestionsEvent } from "@/lib/electron";
|
||||||
|
import { useAppStore, Feature } from "@/store/app-store";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface FeatureSuggestionsDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
projectPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeatureSuggestionsDialog({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
projectPath,
|
||||||
|
}: FeatureSuggestionsDialogProps) {
|
||||||
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
|
const [progress, setProgress] = useState<string[]>([]);
|
||||||
|
const [suggestions, setSuggestions] = useState<FeatureSuggestion[]>([]);
|
||||||
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
|
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||||
|
const [isImporting, setIsImporting] = useState(false);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const autoScrollRef = useRef(true);
|
||||||
|
|
||||||
|
const { features, setFeatures } = useAppStore();
|
||||||
|
|
||||||
|
// Auto-scroll progress when new content arrives
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoScrollRef.current && scrollRef.current && isGenerating) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [progress, isGenerating]);
|
||||||
|
|
||||||
|
// Listen for suggestion events
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api?.suggestions) return;
|
||||||
|
|
||||||
|
const unsubscribe = api.suggestions.onEvent((event: SuggestionsEvent) => {
|
||||||
|
if (event.type === "suggestions_progress") {
|
||||||
|
setProgress((prev) => [...prev, event.content || ""]);
|
||||||
|
} else if (event.type === "suggestions_tool") {
|
||||||
|
const toolName = event.tool || "Unknown Tool";
|
||||||
|
setProgress((prev) => [...prev, `Using tool: ${toolName}\n`]);
|
||||||
|
} else if (event.type === "suggestions_complete") {
|
||||||
|
setIsGenerating(false);
|
||||||
|
if (event.suggestions && event.suggestions.length > 0) {
|
||||||
|
setSuggestions(event.suggestions);
|
||||||
|
// Select all by default
|
||||||
|
setSelectedIds(new Set(event.suggestions.map((s) => s.id)));
|
||||||
|
toast.success(`Generated ${event.suggestions.length} feature suggestions!`);
|
||||||
|
} else {
|
||||||
|
toast.info("No suggestions generated. Try again.");
|
||||||
|
}
|
||||||
|
} else if (event.type === "suggestions_error") {
|
||||||
|
setIsGenerating(false);
|
||||||
|
toast.error(`Error: ${event.error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
// Start generating suggestions
|
||||||
|
const handleGenerate = useCallback(async () => {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api?.suggestions) {
|
||||||
|
toast.error("Suggestions API not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsGenerating(true);
|
||||||
|
setProgress([]);
|
||||||
|
setSuggestions([]);
|
||||||
|
setSelectedIds(new Set());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await api.suggestions.generate(projectPath);
|
||||||
|
if (!result.success) {
|
||||||
|
toast.error(result.error || "Failed to start generation");
|
||||||
|
setIsGenerating(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to generate suggestions:", error);
|
||||||
|
toast.error("Failed to start generation");
|
||||||
|
setIsGenerating(false);
|
||||||
|
}
|
||||||
|
}, [projectPath]);
|
||||||
|
|
||||||
|
// Stop generating
|
||||||
|
const handleStop = useCallback(async () => {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api?.suggestions) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.suggestions.stop();
|
||||||
|
setIsGenerating(false);
|
||||||
|
toast.info("Generation stopped");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to stop generation:", error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Toggle suggestion selection
|
||||||
|
const toggleSelection = useCallback((id: string) => {
|
||||||
|
setSelectedIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) {
|
||||||
|
next.delete(id);
|
||||||
|
} else {
|
||||||
|
next.add(id);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Toggle expand/collapse for a suggestion
|
||||||
|
const toggleExpanded = useCallback((id: string) => {
|
||||||
|
setExpandedIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) {
|
||||||
|
next.delete(id);
|
||||||
|
} else {
|
||||||
|
next.add(id);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Select/deselect all
|
||||||
|
const toggleSelectAll = useCallback(() => {
|
||||||
|
if (selectedIds.size === suggestions.length) {
|
||||||
|
setSelectedIds(new Set());
|
||||||
|
} else {
|
||||||
|
setSelectedIds(new Set(suggestions.map((s) => s.id)));
|
||||||
|
}
|
||||||
|
}, [selectedIds.size, suggestions]);
|
||||||
|
|
||||||
|
// Import selected suggestions as features
|
||||||
|
const handleImport = useCallback(async () => {
|
||||||
|
if (selectedIds.size === 0) {
|
||||||
|
toast.warning("No suggestions selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsImporting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
const selectedSuggestions = suggestions.filter((s) =>
|
||||||
|
selectedIds.has(s.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create new features from selected suggestions
|
||||||
|
const newFeatures: Feature[] = selectedSuggestions.map((s) => ({
|
||||||
|
id: `feature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
category: s.category,
|
||||||
|
description: s.description,
|
||||||
|
steps: s.steps,
|
||||||
|
status: "backlog" as const,
|
||||||
|
skipTests: true, // As specified, testing mode true
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Merge with existing features
|
||||||
|
const updatedFeatures = [...features, ...newFeatures];
|
||||||
|
|
||||||
|
// Save to file
|
||||||
|
const featureListPath = `${projectPath}/.automaker/feature_list.json`;
|
||||||
|
await api.writeFile(featureListPath, JSON.stringify(updatedFeatures, null, 2));
|
||||||
|
|
||||||
|
// Update store
|
||||||
|
setFeatures(updatedFeatures);
|
||||||
|
|
||||||
|
toast.success(`Imported ${newFeatures.length} features to backlog!`);
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to import features:", error);
|
||||||
|
toast.error("Failed to import features");
|
||||||
|
} finally {
|
||||||
|
setIsImporting(false);
|
||||||
|
}
|
||||||
|
}, [selectedIds, suggestions, features, setFeatures, projectPath, onClose]);
|
||||||
|
|
||||||
|
// Handle scroll to detect if user scrolled up
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!scrollRef.current) return;
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||||
|
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
|
||||||
|
autoScrollRef.current = isAtBottom;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset state when dialog closes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
// Don't reset immediately - allow re-open to see results
|
||||||
|
// Only reset if explicitly closed without importing
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const hasStarted = progress.length > 0 || suggestions.length > 0;
|
||||||
|
const hasSuggestions = suggestions.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
|
<DialogContent
|
||||||
|
className="w-[70vw] max-w-[70vw] max-h-[85vh] flex flex-col"
|
||||||
|
data-testid="feature-suggestions-dialog"
|
||||||
|
>
|
||||||
|
<DialogHeader className="flex-shrink-0">
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Lightbulb className="w-5 h-5 text-yellow-500" />
|
||||||
|
Feature Suggestions
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Analyze your project to discover missing features and improvements.
|
||||||
|
The AI will scan your codebase and suggest features ordered by priority.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{!hasStarted ? (
|
||||||
|
// Initial state - show explanation and generate button
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center py-8 text-center">
|
||||||
|
<Lightbulb className="w-16 h-16 text-yellow-500/50 mb-4" />
|
||||||
|
<h3 className="text-lg font-semibold mb-2">
|
||||||
|
Discover Missing Features
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground max-w-md mb-6">
|
||||||
|
Our AI will analyze your project structure, code patterns, and
|
||||||
|
existing features to generate a prioritized list of suggestions
|
||||||
|
for new features you could add.
|
||||||
|
</p>
|
||||||
|
<Button onClick={handleGenerate} size="lg">
|
||||||
|
<Lightbulb className="w-4 h-4 mr-2" />
|
||||||
|
Generate Suggestions
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : isGenerating ? (
|
||||||
|
// Generating state - show progress
|
||||||
|
<div className="flex-1 flex flex-col min-h-0">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Analyzing project...
|
||||||
|
</div>
|
||||||
|
<Button variant="destructive" size="sm" onClick={handleStop}>
|
||||||
|
<StopCircle className="w-4 h-4 mr-2" />
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
className="flex-1 overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-xs min-h-[200px] max-h-[400px]"
|
||||||
|
>
|
||||||
|
<div className="whitespace-pre-wrap break-words text-zinc-300">
|
||||||
|
{progress.join("")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : hasSuggestions ? (
|
||||||
|
// Results state - show suggestions list
|
||||||
|
<div className="flex-1 flex flex-col min-h-0">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{suggestions.length} suggestions generated
|
||||||
|
</span>
|
||||||
|
<Button variant="ghost" size="sm" onClick={toggleSelectAll}>
|
||||||
|
{selectedIds.size === suggestions.length
|
||||||
|
? "Deselect All"
|
||||||
|
: "Select All"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{selectedIds.size} selected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className="flex-1 overflow-y-auto space-y-2 min-h-[200px] max-h-[400px] pr-2"
|
||||||
|
>
|
||||||
|
{suggestions.map((suggestion) => {
|
||||||
|
const isSelected = selectedIds.has(suggestion.id);
|
||||||
|
const isExpanded = expandedIds.has(suggestion.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={suggestion.id}
|
||||||
|
className={`border rounded-lg p-3 transition-colors ${
|
||||||
|
isSelected
|
||||||
|
? "border-primary bg-primary/5"
|
||||||
|
: "border-border hover:border-primary/50"
|
||||||
|
}`}
|
||||||
|
data-testid={`suggestion-${suggestion.id}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Checkbox
|
||||||
|
id={suggestion.id}
|
||||||
|
checked={isSelected}
|
||||||
|
onCheckedChange={() => toggleSelection(suggestion.id)}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleExpanded(suggestion.id)}
|
||||||
|
className="flex items-center gap-1 text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<span className="text-xs px-2 py-0.5 rounded-full bg-primary/20 text-primary font-medium">
|
||||||
|
#{suggestion.priority}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs px-2 py-0.5 rounded-full bg-secondary text-secondary-foreground">
|
||||||
|
{suggestion.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Label
|
||||||
|
htmlFor={suggestion.id}
|
||||||
|
className="text-sm font-medium cursor-pointer"
|
||||||
|
>
|
||||||
|
{suggestion.description}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="mt-3 space-y-2 text-sm">
|
||||||
|
{suggestion.reasoning && (
|
||||||
|
<p className="text-muted-foreground italic">
|
||||||
|
{suggestion.reasoning}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{suggestion.steps.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-muted-foreground mb-1">
|
||||||
|
Implementation Steps:
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc list-inside text-xs text-muted-foreground space-y-0.5">
|
||||||
|
{suggestion.steps.map((step, i) => (
|
||||||
|
<li key={i}>{step}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// No results state
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center py-8 text-center">
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
No suggestions were generated. Try running the analysis again.
|
||||||
|
</p>
|
||||||
|
<Button onClick={handleGenerate}>
|
||||||
|
<Lightbulb className="w-4 h-4 mr-2" />
|
||||||
|
Try Again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter className="flex-shrink-0">
|
||||||
|
{hasSuggestions && (
|
||||||
|
<div className="flex gap-2 w-full justify-between">
|
||||||
|
<Button variant="outline" onClick={handleGenerate}>
|
||||||
|
<Lightbulb className="w-4 h-4 mr-2" />
|
||||||
|
Regenerate
|
||||||
|
</Button>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="ghost" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleImport}
|
||||||
|
disabled={selectedIds.size === 0 || isImporting}
|
||||||
|
>
|
||||||
|
{isImporting ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
Import {selectedIds.size} Feature
|
||||||
|
{selectedIds.size !== 1 ? "s" : ""}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!hasSuggestions && !isGenerating && hasStarted && (
|
||||||
|
<Button variant="ghost" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,13 +5,35 @@ import { useAppStore } from "@/store/app-store";
|
|||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Save, RefreshCw, FileText } from "lucide-react";
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2 } from "lucide-react";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import type { SpecRegenerationEvent } from "@/types/electron";
|
||||||
|
|
||||||
export function SpecView() {
|
export function SpecView() {
|
||||||
const { currentProject, appSpec, setAppSpec } = useAppStore();
|
const { currentProject, appSpec, setAppSpec } = useAppStore();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [specExists, setSpecExists] = useState(true);
|
||||||
|
|
||||||
|
// Regeneration state
|
||||||
|
const [showRegenerateDialog, setShowRegenerateDialog] = useState(false);
|
||||||
|
const [projectDefinition, setProjectDefinition] = useState("");
|
||||||
|
const [isRegenerating, setIsRegenerating] = useState(false);
|
||||||
|
|
||||||
|
// Create spec state
|
||||||
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||||
|
const [projectOverview, setProjectOverview] = useState("");
|
||||||
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
const [generateFeatures, setGenerateFeatures] = useState(true);
|
||||||
|
|
||||||
// Load spec from file
|
// Load spec from file
|
||||||
const loadSpec = useCallback(async () => {
|
const loadSpec = useCallback(async () => {
|
||||||
@@ -26,10 +48,16 @@ export function SpecView() {
|
|||||||
|
|
||||||
if (result.success && result.content) {
|
if (result.success && result.content) {
|
||||||
setAppSpec(result.content);
|
setAppSpec(result.content);
|
||||||
|
setSpecExists(true);
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
|
} else {
|
||||||
|
// File doesn't exist
|
||||||
|
setAppSpec("");
|
||||||
|
setSpecExists(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load spec:", error);
|
console.error("Failed to load spec:", error);
|
||||||
|
setSpecExists(false);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -39,6 +67,35 @@ export function SpecView() {
|
|||||||
loadSpec();
|
loadSpec();
|
||||||
}, [loadSpec]);
|
}, [loadSpec]);
|
||||||
|
|
||||||
|
// Subscribe to spec regeneration events
|
||||||
|
useEffect(() => {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.specRegeneration) return;
|
||||||
|
|
||||||
|
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();
|
||||||
|
} else if (event.type === "spec_regeneration_error") {
|
||||||
|
setIsRegenerating(false);
|
||||||
|
setIsCreating(false);
|
||||||
|
console.error("[SpecView] Regeneration error:", event.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [loadSpec]);
|
||||||
|
|
||||||
// Save spec to file
|
// Save spec to file
|
||||||
const saveSpec = async () => {
|
const saveSpec = async () => {
|
||||||
if (!currentProject) return;
|
if (!currentProject) return;
|
||||||
@@ -63,6 +120,62 @@ export function SpecView() {
|
|||||||
setHasChanges(true);
|
setHasChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRegenerate = async () => {
|
||||||
|
if (!currentProject || !projectDefinition.trim()) return;
|
||||||
|
|
||||||
|
setIsRegenerating(true);
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.specRegeneration) {
|
||||||
|
console.error("[SpecView] Spec regeneration not available");
|
||||||
|
setIsRegenerating(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await api.specRegeneration.generate(
|
||||||
|
currentProject.path,
|
||||||
|
projectDefinition.trim()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error("[SpecView] Failed to start regeneration:", result.error);
|
||||||
|
setIsRegenerating(false);
|
||||||
|
}
|
||||||
|
// If successful, we'll wait for the events to update the state
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[SpecView] Failed to regenerate spec:", error);
|
||||||
|
setIsRegenerating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateSpec = async () => {
|
||||||
|
if (!currentProject || !projectOverview.trim()) return;
|
||||||
|
|
||||||
|
setIsCreating(true);
|
||||||
|
setShowCreateDialog(false);
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.specRegeneration) {
|
||||||
|
console.error("[SpecView] Spec regeneration not available");
|
||||||
|
setIsCreating(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await api.specRegeneration.create(
|
||||||
|
currentProject.path,
|
||||||
|
projectOverview.trim(),
|
||||||
|
generateFeatures
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error("[SpecView] Failed to start spec creation:", result.error);
|
||||||
|
setIsCreating(false);
|
||||||
|
}
|
||||||
|
// If successful, we'll wait for the events to update the state
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[SpecView] Failed to create spec:", error);
|
||||||
|
setIsCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!currentProject) {
|
if (!currentProject) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -85,6 +198,121 @@ export function SpecView() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show empty state when no spec exists (isCreating is handled by bottom-right indicator in sidebar)
|
||||||
|
if (!specExists) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex-1 flex flex-col overflow-hidden content-bg"
|
||||||
|
data-testid="spec-view-empty"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<FileText className="w-5 h-5 text-muted-foreground" />
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-bold">App Specification</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{currentProject.path}/.automaker/app_spec.txt
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Empty State */}
|
||||||
|
<div className="flex-1 flex items-center justify-center p-8">
|
||||||
|
<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" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold mb-3">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.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
onClick={() => setShowCreateDialog(true)}
|
||||||
|
>
|
||||||
|
<FilePlus2 className="w-5 h-5 mr-2" />
|
||||||
|
Create app_spec
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Create Dialog */}
|
||||||
|
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create App Specification</DialogTitle>
|
||||||
|
<DialogDescription className="text-muted-foreground">
|
||||||
|
We didn't find an app_spec.txt file. Let us help you generate your app_spec.txt
|
||||||
|
to help describe your project for our system. We'll analyze your project's
|
||||||
|
tech stack and create a comprehensive specification.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
Project Overview
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Describe what your project does and what features you want to build.
|
||||||
|
Be as detailed as you want - this will help us create a better specification.
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
className="w-full h-48 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
value={projectOverview}
|
||||||
|
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
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-3 pt-2">
|
||||||
|
<Checkbox
|
||||||
|
id="generate-features"
|
||||||
|
checked={generateFeatures}
|
||||||
|
onCheckedChange={(checked) => setGenerateFeatures(checked === true)}
|
||||||
|
/>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label
|
||||||
|
htmlFor="generate-features"
|
||||||
|
className="text-sm font-medium cursor-pointer"
|
||||||
|
>
|
||||||
|
Generate feature list
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Automatically populate feature_list.json with all features from the
|
||||||
|
implementation roadmap after the spec is generated.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setShowCreateDialog(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateSpec}
|
||||||
|
disabled={!projectOverview.trim()}
|
||||||
|
>
|
||||||
|
<Sparkles className="w-4 h-4 mr-2" />
|
||||||
|
Generate Spec
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex-1 flex flex-col overflow-hidden content-bg"
|
className="flex-1 flex flex-col overflow-hidden content-bg"
|
||||||
@@ -102,6 +330,20 @@ export function SpecView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<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
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={saveSpec}
|
onClick={saveSpec}
|
||||||
@@ -127,6 +369,65 @@ export function SpecView() {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Regenerate Dialog */}
|
||||||
|
<Dialog open={showRegenerateDialog} onOpenChange={setShowRegenerateDialog}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Regenerate App Specification</DialogTitle>
|
||||||
|
<DialogDescription className="text-muted-foreground">
|
||||||
|
We will regenerate your app spec based on a short project definition and the
|
||||||
|
current tech stack found in your project. The agent will analyze your codebase
|
||||||
|
to understand your existing technologies and create a comprehensive specification.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
Describe your project
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Provide a clear description of what your app should do. Be as detailed as you
|
||||||
|
want - the more context you provide, the more comprehensive the spec will be.
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
className="w-full h-40 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
value={projectDefinition}
|
||||||
|
onChange={(e) => setProjectDefinition(e.target.value)}
|
||||||
|
placeholder="e.g., A task management app where users can create projects, add tasks with due dates, assign tasks to team members, track progress with a kanban board, and receive notifications for upcoming deadlines..."
|
||||||
|
disabled={isRegenerating}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setShowRegenerateDialog(false)}
|
||||||
|
disabled={isRegenerating}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleRegenerate}
|
||||||
|
disabled={!projectDefinition.trim() || isRegenerating}
|
||||||
|
>
|
||||||
|
{isRegenerating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Regenerating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles className="w-4 h-4 mr-2" />
|
||||||
|
Regenerate Spec
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { useEffect, useCallback } from "react";
|
import { useEffect, useCallback, useMemo } from "react";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useAppStore } from "@/store/app-store";
|
import { useAppStore } from "@/store/app-store";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI, type AutoModeEvent } from "@/lib/electron";
|
||||||
import type { AutoModeEvent } from "@/types/electron";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for managing auto mode
|
* Hook for managing auto mode (scoped per project)
|
||||||
*/
|
*/
|
||||||
export function useAutoMode() {
|
export function useAutoMode() {
|
||||||
const {
|
const {
|
||||||
isAutoModeRunning,
|
autoModeByProject,
|
||||||
setAutoModeRunning,
|
setAutoModeRunning,
|
||||||
runningAutoTasks,
|
|
||||||
addRunningTask,
|
addRunningTask,
|
||||||
removeRunningTask,
|
removeRunningTask,
|
||||||
clearRunningTasks,
|
clearRunningTasks,
|
||||||
@@ -20,9 +18,8 @@ export function useAutoMode() {
|
|||||||
maxConcurrency,
|
maxConcurrency,
|
||||||
} = useAppStore(
|
} = useAppStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
isAutoModeRunning: state.isAutoModeRunning,
|
autoModeByProject: state.autoModeByProject,
|
||||||
setAutoModeRunning: state.setAutoModeRunning,
|
setAutoModeRunning: state.setAutoModeRunning,
|
||||||
runningAutoTasks: state.runningAutoTasks,
|
|
||||||
addRunningTask: state.addRunningTask,
|
addRunningTask: state.addRunningTask,
|
||||||
removeRunningTask: state.removeRunningTask,
|
removeRunningTask: state.removeRunningTask,
|
||||||
clearRunningTasks: state.clearRunningTasks,
|
clearRunningTasks: state.clearRunningTasks,
|
||||||
@@ -32,56 +29,74 @@ export function useAutoMode() {
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get project-specific auto mode state
|
||||||
|
const projectId = currentProject?.id;
|
||||||
|
const projectAutoModeState = useMemo(() => {
|
||||||
|
if (!projectId) return { isRunning: false, runningTasks: [] };
|
||||||
|
return autoModeByProject[projectId] || { isRunning: false, runningTasks: [] };
|
||||||
|
}, [autoModeByProject, projectId]);
|
||||||
|
|
||||||
|
const isAutoModeRunning = projectAutoModeState.isRunning;
|
||||||
|
const runningAutoTasks = projectAutoModeState.runningTasks;
|
||||||
|
|
||||||
// Check if we can start a new task based on concurrency limit
|
// Check if we can start a new task based on concurrency limit
|
||||||
const canStartNewTask = runningAutoTasks.length < maxConcurrency;
|
const canStartNewTask = runningAutoTasks.length < maxConcurrency;
|
||||||
|
|
||||||
// Handle auto mode events
|
// Handle auto mode events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api?.autoMode) return;
|
if (!api?.autoMode || !projectId) return;
|
||||||
|
|
||||||
const unsubscribe = api.autoMode.onEvent((event: AutoModeEvent) => {
|
const unsubscribe = api.autoMode.onEvent((event: AutoModeEvent) => {
|
||||||
console.log("[AutoMode Event]", event);
|
console.log("[AutoMode Event]", event);
|
||||||
|
|
||||||
|
// Events include projectId from backend, use it to scope updates
|
||||||
|
// Fall back to current projectId if not provided in event
|
||||||
|
const eventProjectId = event.projectId ?? projectId;
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "auto_mode_feature_start":
|
case "auto_mode_feature_start":
|
||||||
addRunningTask(event.featureId);
|
if (event.featureId) {
|
||||||
addAutoModeActivity({
|
addRunningTask(eventProjectId, event.featureId);
|
||||||
featureId: event.featureId,
|
addAutoModeActivity({
|
||||||
type: "start",
|
featureId: event.featureId,
|
||||||
message: `Started working on feature`,
|
type: "start",
|
||||||
});
|
message: `Started working on feature`,
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "auto_mode_feature_complete":
|
case "auto_mode_feature_complete":
|
||||||
// Feature completed - remove from running tasks and UI will reload features on its own
|
// Feature completed - remove from running tasks and UI will reload features on its own
|
||||||
console.log(
|
if (event.featureId) {
|
||||||
"[AutoMode] Feature completed:",
|
console.log(
|
||||||
event.featureId,
|
"[AutoMode] Feature completed:",
|
||||||
"passes:",
|
event.featureId,
|
||||||
event.passes
|
"passes:",
|
||||||
);
|
event.passes
|
||||||
removeRunningTask(event.featureId);
|
);
|
||||||
addAutoModeActivity({
|
removeRunningTask(eventProjectId, event.featureId);
|
||||||
featureId: event.featureId,
|
addAutoModeActivity({
|
||||||
type: "complete",
|
featureId: event.featureId,
|
||||||
message: event.passes
|
type: "complete",
|
||||||
? "Feature completed successfully"
|
message: event.passes
|
||||||
: "Feature completed with failures",
|
? "Feature completed successfully"
|
||||||
passes: event.passes,
|
: "Feature completed with failures",
|
||||||
});
|
passes: event.passes,
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "auto_mode_complete":
|
case "auto_mode_complete":
|
||||||
// All features completed
|
// All features completed for this project
|
||||||
setAutoModeRunning(false);
|
setAutoModeRunning(eventProjectId, false);
|
||||||
clearRunningTasks();
|
clearRunningTasks(eventProjectId);
|
||||||
console.log("[AutoMode] All features completed!");
|
console.log("[AutoMode] All features completed!");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "auto_mode_error":
|
case "auto_mode_error":
|
||||||
console.error("[AutoMode Error]", event.error);
|
console.error("[AutoMode Error]", event.error);
|
||||||
if (event.featureId) {
|
if (event.featureId && event.error) {
|
||||||
addAutoModeActivity({
|
addAutoModeActivity({
|
||||||
featureId: event.featureId,
|
featureId: event.featureId,
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -92,7 +107,7 @@ export function useAutoMode() {
|
|||||||
|
|
||||||
case "auto_mode_progress":
|
case "auto_mode_progress":
|
||||||
// Log progress updates (throttle to avoid spam)
|
// Log progress updates (throttle to avoid spam)
|
||||||
if (event.content && event.content.length > 10) {
|
if (event.featureId && event.content && event.content.length > 10) {
|
||||||
addAutoModeActivity({
|
addAutoModeActivity({
|
||||||
featureId: event.featureId,
|
featureId: event.featureId,
|
||||||
type: "progress",
|
type: "progress",
|
||||||
@@ -103,31 +118,36 @@ export function useAutoMode() {
|
|||||||
|
|
||||||
case "auto_mode_tool":
|
case "auto_mode_tool":
|
||||||
// Log tool usage
|
// Log tool usage
|
||||||
addAutoModeActivity({
|
if (event.featureId && event.tool) {
|
||||||
featureId: event.featureId,
|
addAutoModeActivity({
|
||||||
type: "tool",
|
featureId: event.featureId,
|
||||||
message: `Using tool: ${event.tool}`,
|
type: "tool",
|
||||||
tool: event.tool,
|
message: `Using tool: ${event.tool}`,
|
||||||
});
|
tool: event.tool,
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "auto_mode_phase":
|
case "auto_mode_phase":
|
||||||
// Log phase transitions (Planning, Action, Verification)
|
// Log phase transitions (Planning, Action, Verification)
|
||||||
console.log(
|
if (event.featureId && event.phase && event.message) {
|
||||||
`[AutoMode] Phase: ${event.phase} for ${event.featureId}`
|
console.log(
|
||||||
);
|
`[AutoMode] Phase: ${event.phase} for ${event.featureId}`
|
||||||
addAutoModeActivity({
|
);
|
||||||
featureId: event.featureId,
|
addAutoModeActivity({
|
||||||
type: event.phase,
|
featureId: event.featureId,
|
||||||
message: event.message,
|
type: event.phase,
|
||||||
phase: event.phase,
|
message: event.message,
|
||||||
});
|
phase: event.phase,
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, [
|
}, [
|
||||||
|
projectId,
|
||||||
addRunningTask,
|
addRunningTask,
|
||||||
removeRunningTask,
|
removeRunningTask,
|
||||||
clearRunningTasks,
|
clearRunningTasks,
|
||||||
@@ -151,7 +171,7 @@ export function useAutoMode() {
|
|||||||
const result = await api.autoMode.start(currentProject.path, maxConcurrency);
|
const result = await api.autoMode.start(currentProject.path, maxConcurrency);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setAutoModeRunning(true);
|
setAutoModeRunning(currentProject.id, true);
|
||||||
console.log(`[AutoMode] Started successfully with maxConcurrency: ${maxConcurrency}`);
|
console.log(`[AutoMode] Started successfully with maxConcurrency: ${maxConcurrency}`);
|
||||||
} else {
|
} else {
|
||||||
console.error("[AutoMode] Failed to start:", result.error);
|
console.error("[AutoMode] Failed to start:", result.error);
|
||||||
@@ -159,13 +179,20 @@ export function useAutoMode() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[AutoMode] Error starting:", error);
|
console.error("[AutoMode] Error starting:", error);
|
||||||
setAutoModeRunning(false);
|
if (currentProject) {
|
||||||
|
setAutoModeRunning(currentProject.id, false);
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, [currentProject, setAutoModeRunning, maxConcurrency]);
|
}, [currentProject, setAutoModeRunning, maxConcurrency]);
|
||||||
|
|
||||||
// Stop auto mode
|
// Stop auto mode
|
||||||
const stop = useCallback(async () => {
|
const stop = useCallback(async () => {
|
||||||
|
if (!currentProject) {
|
||||||
|
console.error("No project selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api?.autoMode) {
|
if (!api?.autoMode) {
|
||||||
@@ -175,8 +202,8 @@ export function useAutoMode() {
|
|||||||
const result = await api.autoMode.stop();
|
const result = await api.autoMode.stop();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setAutoModeRunning(false);
|
setAutoModeRunning(currentProject.id, false);
|
||||||
clearRunningTasks();
|
clearRunningTasks(currentProject.id);
|
||||||
console.log("[AutoMode] Stopped successfully");
|
console.log("[AutoMode] Stopped successfully");
|
||||||
} else {
|
} else {
|
||||||
console.error("[AutoMode] Failed to stop:", result.error);
|
console.error("[AutoMode] Failed to stop:", result.error);
|
||||||
@@ -186,11 +213,16 @@ export function useAutoMode() {
|
|||||||
console.error("[AutoMode] Error stopping:", error);
|
console.error("[AutoMode] Error stopping:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, [setAutoModeRunning, clearRunningTasks]);
|
}, [currentProject, setAutoModeRunning, clearRunningTasks]);
|
||||||
|
|
||||||
// Stop a specific feature
|
// Stop a specific feature
|
||||||
const stopFeature = useCallback(
|
const stopFeature = useCallback(
|
||||||
async (featureId: string) => {
|
async (featureId: string) => {
|
||||||
|
if (!currentProject) {
|
||||||
|
console.error("No project selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api?.autoMode?.stopFeature) {
|
if (!api?.autoMode?.stopFeature) {
|
||||||
@@ -200,7 +232,7 @@ export function useAutoMode() {
|
|||||||
const result = await api.autoMode.stopFeature(featureId);
|
const result = await api.autoMode.stopFeature(featureId);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
removeRunningTask(featureId);
|
removeRunningTask(currentProject.id, featureId);
|
||||||
console.log("[AutoMode] Feature stopped successfully:", featureId);
|
console.log("[AutoMode] Feature stopped successfully:", featureId);
|
||||||
addAutoModeActivity({
|
addAutoModeActivity({
|
||||||
featureId,
|
featureId,
|
||||||
@@ -217,7 +249,7 @@ export function useAutoMode() {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[removeRunningTask, addAutoModeActivity]
|
[currentProject, removeRunningTask, addAutoModeActivity]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) {
|
|||||||
export const NAV_SHORTCUTS: Record<string, string> = {
|
export const NAV_SHORTCUTS: Record<string, string> = {
|
||||||
board: "K", // K for Kanban
|
board: "K", // K for Kanban
|
||||||
agent: "A", // A for Agent
|
agent: "A", // A for Agent
|
||||||
spec: "E", // E for Editor (Spec)
|
spec: "D", // D for Document (Spec)
|
||||||
context: "C", // C for Context
|
context: "C", // C for Context
|
||||||
tools: "T", // T for Tools
|
tools: "T", // T for Tools
|
||||||
settings: "S", // S for Settings
|
settings: "S", // S for Settings
|
||||||
@@ -121,8 +121,10 @@ export const UI_SHORTCUTS: Record<string, string> = {
|
|||||||
export const ACTION_SHORTCUTS: Record<string, string> = {
|
export const ACTION_SHORTCUTS: Record<string, string> = {
|
||||||
addFeature: "N", // N for New feature
|
addFeature: "N", // N for New feature
|
||||||
addContextFile: "F", // F for File (add context file)
|
addContextFile: "F", // F for File (add context file)
|
||||||
startNext: "Q", // Q for Queue (start next features from backlog)
|
startNext: "G", // G for Grab (start next features from backlog)
|
||||||
newSession: "W", // W for new session (in agent view)
|
newSession: "W", // W for new session (in agent view)
|
||||||
openProject: "O", // O for Open project (navigate to welcome view)
|
openProject: "O", // O for Open project (navigate to welcome view)
|
||||||
projectPicker: "P", // P for Project picker
|
projectPicker: "P", // P for Project picker
|
||||||
|
cyclePrevProject: "Q", // Q for previous project (cycle back through MRU)
|
||||||
|
cycleNextProject: "E", // E for next project (cycle forward through MRU)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,6 +44,47 @@ export interface StatResult {
|
|||||||
// Auto Mode types - Import from electron.d.ts to avoid duplication
|
// Auto Mode types - Import from electron.d.ts to avoid duplication
|
||||||
import type { AutoModeEvent, ModelDefinition, ProviderStatus, WorktreeAPI, GitAPI, WorktreeInfo, WorktreeStatus, FileDiffsResult, FileDiffResult, FileStatus } from "@/types/electron";
|
import type { AutoModeEvent, ModelDefinition, ProviderStatus, WorktreeAPI, GitAPI, WorktreeInfo, WorktreeStatus, FileDiffsResult, FileDiffResult, FileStatus } from "@/types/electron";
|
||||||
|
|
||||||
|
// Feature Suggestions types
|
||||||
|
export interface FeatureSuggestion {
|
||||||
|
id: string;
|
||||||
|
category: string;
|
||||||
|
description: string;
|
||||||
|
steps: string[];
|
||||||
|
priority: number;
|
||||||
|
reasoning: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuggestionsEvent {
|
||||||
|
type: "suggestions_progress" | "suggestions_tool" | "suggestions_complete" | "suggestions_error";
|
||||||
|
content?: string;
|
||||||
|
tool?: string;
|
||||||
|
input?: unknown;
|
||||||
|
suggestions?: FeatureSuggestion[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuggestionsAPI {
|
||||||
|
generate: (projectPath: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
stop: () => Promise<{ success: boolean; error?: string }>;
|
||||||
|
status: () => Promise<{ success: boolean; isRunning?: boolean; error?: string }>;
|
||||||
|
onEvent: (callback: (event: SuggestionsEvent) => void) => () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spec Regeneration types
|
||||||
|
export type SpecRegenerationEvent =
|
||||||
|
| { type: "spec_regeneration_progress"; content: string }
|
||||||
|
| { type: "spec_regeneration_tool"; tool: string; input: unknown }
|
||||||
|
| { type: "spec_regeneration_complete"; message: string }
|
||||||
|
| { type: "spec_regeneration_error"; error: string };
|
||||||
|
|
||||||
|
export interface SpecRegenerationAPI {
|
||||||
|
create: (projectPath: string, projectOverview: string, generateFeatures?: boolean) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
generate: (projectPath: string, projectDefinition: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
stop: () => Promise<{ success: boolean; error?: string }>;
|
||||||
|
status: () => Promise<{ success: boolean; isRunning?: boolean; error?: string }>;
|
||||||
|
onEvent: (callback: (event: SpecRegenerationEvent) => void) => () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AutoModeAPI {
|
export interface AutoModeAPI {
|
||||||
start: (projectPath: string, maxConcurrency?: number) => Promise<{ success: boolean; error?: string }>;
|
start: (projectPath: string, maxConcurrency?: number) => Promise<{ success: boolean; error?: string }>;
|
||||||
stop: () => Promise<{ success: boolean; error?: string }>;
|
stop: () => Promise<{ success: boolean; error?: string }>;
|
||||||
@@ -79,7 +120,6 @@ export interface ElectronAPI {
|
|||||||
trashItem?: (filePath: string) => Promise<WriteResult>;
|
trashItem?: (filePath: string) => Promise<WriteResult>;
|
||||||
getPath: (name: string) => Promise<string>;
|
getPath: (name: string) => Promise<string>;
|
||||||
saveImageToTemp?: (data: string, filename: string, mimeType: string, projectPath?: string) => Promise<SaveImageResult>;
|
saveImageToTemp?: (data: string, filename: string, mimeType: string, projectPath?: string) => Promise<SaveImageResult>;
|
||||||
autoMode?: AutoModeAPI;
|
|
||||||
checkClaudeCli?: () => Promise<{
|
checkClaudeCli?: () => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
status?: string;
|
status?: string;
|
||||||
@@ -130,6 +170,8 @@ export interface ElectronAPI {
|
|||||||
}>;
|
}>;
|
||||||
worktree?: WorktreeAPI;
|
worktree?: WorktreeAPI;
|
||||||
git?: GitAPI;
|
git?: GitAPI;
|
||||||
|
suggestions?: SuggestionsAPI;
|
||||||
|
specRegeneration?: SpecRegenerationAPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Window interface is declared in @/types/electron.d.ts
|
// Note: Window interface is declared in @/types/electron.d.ts
|
||||||
@@ -427,6 +469,12 @@ export const getElectronAPI = (): ElectronAPI => {
|
|||||||
|
|
||||||
// Mock Git API (for non-worktree operations)
|
// Mock Git API (for non-worktree operations)
|
||||||
git: createMockGitAPI(),
|
git: createMockGitAPI(),
|
||||||
|
|
||||||
|
// Mock Suggestions API
|
||||||
|
suggestions: createMockSuggestionsAPI(),
|
||||||
|
|
||||||
|
// Mock Spec Regeneration API
|
||||||
|
specRegeneration: createMockSpecRegenerationAPI(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -924,6 +972,381 @@ function delay(ms: number, featureId: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mock Suggestions state and implementation
|
||||||
|
let mockSuggestionsRunning = false;
|
||||||
|
let mockSuggestionsCallbacks: ((event: SuggestionsEvent) => void)[] = [];
|
||||||
|
let mockSuggestionsTimeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
function createMockSuggestionsAPI(): SuggestionsAPI {
|
||||||
|
return {
|
||||||
|
generate: async (projectPath: string) => {
|
||||||
|
if (mockSuggestionsRunning) {
|
||||||
|
return { success: false, error: "Suggestions generation is already running" };
|
||||||
|
}
|
||||||
|
|
||||||
|
mockSuggestionsRunning = true;
|
||||||
|
console.log(`[Mock] Generating suggestions for: ${projectPath}`);
|
||||||
|
|
||||||
|
// Simulate async suggestion generation
|
||||||
|
simulateSuggestionsGeneration();
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
stop: async () => {
|
||||||
|
mockSuggestionsRunning = false;
|
||||||
|
if (mockSuggestionsTimeout) {
|
||||||
|
clearTimeout(mockSuggestionsTimeout);
|
||||||
|
mockSuggestionsTimeout = null;
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
status: async () => {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
isRunning: mockSuggestionsRunning,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onEvent: (callback: (event: SuggestionsEvent) => void) => {
|
||||||
|
mockSuggestionsCallbacks.push(callback);
|
||||||
|
return () => {
|
||||||
|
mockSuggestionsCallbacks = mockSuggestionsCallbacks.filter(cb => cb !== callback);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitSuggestionsEvent(event: SuggestionsEvent) {
|
||||||
|
mockSuggestionsCallbacks.forEach(cb => cb(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function simulateSuggestionsGeneration() {
|
||||||
|
// Emit progress events
|
||||||
|
emitSuggestionsEvent({
|
||||||
|
type: "suggestions_progress",
|
||||||
|
content: "Starting project analysis...\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise(resolve => {
|
||||||
|
mockSuggestionsTimeout = setTimeout(resolve, 500);
|
||||||
|
});
|
||||||
|
if (!mockSuggestionsRunning) return;
|
||||||
|
|
||||||
|
emitSuggestionsEvent({
|
||||||
|
type: "suggestions_tool",
|
||||||
|
tool: "Glob",
|
||||||
|
input: { pattern: "**/*.{ts,tsx,js,jsx}" },
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise(resolve => {
|
||||||
|
mockSuggestionsTimeout = setTimeout(resolve, 500);
|
||||||
|
});
|
||||||
|
if (!mockSuggestionsRunning) return;
|
||||||
|
|
||||||
|
emitSuggestionsEvent({
|
||||||
|
type: "suggestions_progress",
|
||||||
|
content: "Analyzing codebase structure...\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise(resolve => {
|
||||||
|
mockSuggestionsTimeout = setTimeout(resolve, 500);
|
||||||
|
});
|
||||||
|
if (!mockSuggestionsRunning) return;
|
||||||
|
|
||||||
|
emitSuggestionsEvent({
|
||||||
|
type: "suggestions_progress",
|
||||||
|
content: "Identifying missing features...\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise(resolve => {
|
||||||
|
mockSuggestionsTimeout = setTimeout(resolve, 500);
|
||||||
|
});
|
||||||
|
if (!mockSuggestionsRunning) return;
|
||||||
|
|
||||||
|
// Generate mock suggestions
|
||||||
|
const mockSuggestions: FeatureSuggestion[] = [
|
||||||
|
{
|
||||||
|
id: `suggestion-${Date.now()}-0`,
|
||||||
|
category: "User Experience",
|
||||||
|
description: "Add dark mode toggle with system preference detection",
|
||||||
|
steps: [
|
||||||
|
"Create a ThemeProvider context to manage theme state",
|
||||||
|
"Add a toggle component in the settings or header",
|
||||||
|
"Implement CSS variables for theme colors",
|
||||||
|
"Add localStorage persistence for user preference"
|
||||||
|
],
|
||||||
|
priority: 1,
|
||||||
|
reasoning: "Dark mode is a standard feature that improves accessibility and user comfort"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `suggestion-${Date.now()}-1`,
|
||||||
|
category: "Performance",
|
||||||
|
description: "Implement lazy loading for heavy components",
|
||||||
|
steps: [
|
||||||
|
"Identify components that are heavy or rarely used",
|
||||||
|
"Use React.lazy() and Suspense for code splitting",
|
||||||
|
"Add loading states for lazy-loaded components"
|
||||||
|
],
|
||||||
|
priority: 2,
|
||||||
|
reasoning: "Improves initial load time and reduces bundle size"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `suggestion-${Date.now()}-2`,
|
||||||
|
category: "Accessibility",
|
||||||
|
description: "Add keyboard navigation support throughout the app",
|
||||||
|
steps: [
|
||||||
|
"Implement focus management for modals and dialogs",
|
||||||
|
"Add keyboard shortcuts for common actions",
|
||||||
|
"Ensure all interactive elements are focusable",
|
||||||
|
"Add ARIA labels and roles where needed"
|
||||||
|
],
|
||||||
|
priority: 3,
|
||||||
|
reasoning: "Improves accessibility for users who rely on keyboard navigation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `suggestion-${Date.now()}-3`,
|
||||||
|
category: "Testing",
|
||||||
|
description: "Add comprehensive unit test coverage",
|
||||||
|
steps: [
|
||||||
|
"Set up Jest and React Testing Library",
|
||||||
|
"Create tests for all utility functions",
|
||||||
|
"Add component tests for critical UI elements",
|
||||||
|
"Set up CI pipeline for automated testing"
|
||||||
|
],
|
||||||
|
priority: 4,
|
||||||
|
reasoning: "Ensures code quality and prevents regressions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `suggestion-${Date.now()}-4`,
|
||||||
|
category: "Developer Experience",
|
||||||
|
description: "Add Storybook for component documentation",
|
||||||
|
steps: [
|
||||||
|
"Install and configure Storybook",
|
||||||
|
"Create stories for all UI components",
|
||||||
|
"Add interaction tests using play functions",
|
||||||
|
"Set up Chromatic for visual regression testing"
|
||||||
|
],
|
||||||
|
priority: 5,
|
||||||
|
reasoning: "Improves component development workflow and documentation"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
emitSuggestionsEvent({
|
||||||
|
type: "suggestions_complete",
|
||||||
|
suggestions: mockSuggestions,
|
||||||
|
});
|
||||||
|
|
||||||
|
mockSuggestionsRunning = false;
|
||||||
|
mockSuggestionsTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock Spec Regeneration state and implementation
|
||||||
|
let mockSpecRegenerationRunning = false;
|
||||||
|
let mockSpecRegenerationCallbacks: ((event: SpecRegenerationEvent) => void)[] = [];
|
||||||
|
let mockSpecRegenerationTimeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
|
||||||
|
return {
|
||||||
|
create: async (projectPath: string, projectOverview: string, generateFeatures = true) => {
|
||||||
|
if (mockSpecRegenerationRunning) {
|
||||||
|
return { success: false, error: "Spec creation is already running" };
|
||||||
|
}
|
||||||
|
|
||||||
|
mockSpecRegenerationRunning = true;
|
||||||
|
console.log(`[Mock] Creating initial spec for: ${projectPath}, generateFeatures: ${generateFeatures}`);
|
||||||
|
|
||||||
|
// Simulate async spec creation
|
||||||
|
simulateSpecCreation(projectPath, projectOverview, generateFeatures);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
generate: async (projectPath: string, projectDefinition: string) => {
|
||||||
|
if (mockSpecRegenerationRunning) {
|
||||||
|
return { success: false, error: "Spec regeneration is already running" };
|
||||||
|
}
|
||||||
|
|
||||||
|
mockSpecRegenerationRunning = true;
|
||||||
|
console.log(`[Mock] Regenerating spec for: ${projectPath}`);
|
||||||
|
|
||||||
|
// Simulate async spec regeneration
|
||||||
|
simulateSpecRegeneration(projectPath, projectDefinition);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
stop: async () => {
|
||||||
|
mockSpecRegenerationRunning = false;
|
||||||
|
if (mockSpecRegenerationTimeout) {
|
||||||
|
clearTimeout(mockSpecRegenerationTimeout);
|
||||||
|
mockSpecRegenerationTimeout = null;
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
status: async () => {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
isRunning: mockSpecRegenerationRunning,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onEvent: (callback: (event: SpecRegenerationEvent) => void) => {
|
||||||
|
mockSpecRegenerationCallbacks.push(callback);
|
||||||
|
return () => {
|
||||||
|
mockSpecRegenerationCallbacks = mockSpecRegenerationCallbacks.filter(cb => cb !== callback);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitSpecRegenerationEvent(event: SpecRegenerationEvent) {
|
||||||
|
mockSpecRegenerationCallbacks.forEach(cb => cb(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function simulateSpecCreation(projectPath: string, projectOverview: string, generateFeatures = true) {
|
||||||
|
emitSpecRegenerationEvent({
|
||||||
|
type: "spec_regeneration_progress",
|
||||||
|
content: "Starting project analysis...\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise(resolve => {
|
||||||
|
mockSpecRegenerationTimeout = setTimeout(resolve, 500);
|
||||||
|
});
|
||||||
|
if (!mockSpecRegenerationRunning) return;
|
||||||
|
|
||||||
|
emitSpecRegenerationEvent({
|
||||||
|
type: "spec_regeneration_tool",
|
||||||
|
tool: "Glob",
|
||||||
|
input: { pattern: "**/*.{json,ts,tsx}" },
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise(resolve => {
|
||||||
|
mockSpecRegenerationTimeout = setTimeout(resolve, 500);
|
||||||
|
});
|
||||||
|
if (!mockSpecRegenerationRunning) return;
|
||||||
|
|
||||||
|
emitSpecRegenerationEvent({
|
||||||
|
type: "spec_regeneration_progress",
|
||||||
|
content: "Detecting tech stack...\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise(resolve => {
|
||||||
|
mockSpecRegenerationTimeout = setTimeout(resolve, 500);
|
||||||
|
});
|
||||||
|
if (!mockSpecRegenerationRunning) return;
|
||||||
|
|
||||||
|
// Write mock app_spec.txt
|
||||||
|
mockFileSystem[`${projectPath}/.automaker/app_spec.txt`] = `<project_specification>
|
||||||
|
<project_name>Demo Project</project_name>
|
||||||
|
|
||||||
|
<overview>
|
||||||
|
${projectOverview}
|
||||||
|
</overview>
|
||||||
|
|
||||||
|
<technology_stack>
|
||||||
|
<frontend>
|
||||||
|
<framework>Next.js</framework>
|
||||||
|
<ui_library>React</ui_library>
|
||||||
|
<styling>Tailwind CSS</styling>
|
||||||
|
</frontend>
|
||||||
|
</technology_stack>
|
||||||
|
|
||||||
|
<core_capabilities>
|
||||||
|
<feature_1>Core functionality based on overview</feature_1>
|
||||||
|
</core_capabilities>
|
||||||
|
|
||||||
|
<implementation_roadmap>
|
||||||
|
<phase_1_foundation>Setup and basic structure</phase_1_foundation>
|
||||||
|
<phase_2_core_logic>Core features implementation</phase_2_core_logic>
|
||||||
|
</implementation_roadmap>
|
||||||
|
</project_specification>`;
|
||||||
|
|
||||||
|
// If generateFeatures is true, also write feature_list.json
|
||||||
|
if (generateFeatures) {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
mockFileSystem[`${projectPath}/.automaker/feature_list.json`] = JSON.stringify([
|
||||||
|
{
|
||||||
|
id: `feature-${timestamp}-0`,
|
||||||
|
category: "Phase 1: Foundation",
|
||||||
|
description: "Setup and basic structure",
|
||||||
|
status: "backlog",
|
||||||
|
steps: ["Initialize project", "Configure dependencies"],
|
||||||
|
skipTests: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `feature-${timestamp}-1`,
|
||||||
|
category: "Phase 2: Core Logic",
|
||||||
|
description: "Core features implementation",
|
||||||
|
status: "backlog",
|
||||||
|
steps: ["Implement core functionality", "Add business logic"],
|
||||||
|
skipTests: true,
|
||||||
|
},
|
||||||
|
], null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitSpecRegenerationEvent({
|
||||||
|
type: "spec_regeneration_complete",
|
||||||
|
message: "Initial spec creation complete!",
|
||||||
|
});
|
||||||
|
|
||||||
|
mockSpecRegenerationRunning = false;
|
||||||
|
mockSpecRegenerationTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function simulateSpecRegeneration(projectPath: string, projectDefinition: string) {
|
||||||
|
emitSpecRegenerationEvent({
|
||||||
|
type: "spec_regeneration_progress",
|
||||||
|
content: "Starting spec regeneration...\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise(resolve => {
|
||||||
|
mockSpecRegenerationTimeout = setTimeout(resolve, 500);
|
||||||
|
});
|
||||||
|
if (!mockSpecRegenerationRunning) return;
|
||||||
|
|
||||||
|
emitSpecRegenerationEvent({
|
||||||
|
type: "spec_regeneration_progress",
|
||||||
|
content: "Analyzing codebase...\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise(resolve => {
|
||||||
|
mockSpecRegenerationTimeout = setTimeout(resolve, 500);
|
||||||
|
});
|
||||||
|
if (!mockSpecRegenerationRunning) return;
|
||||||
|
|
||||||
|
// Write regenerated spec
|
||||||
|
mockFileSystem[`${projectPath}/.automaker/app_spec.txt`] = `<project_specification>
|
||||||
|
<project_name>Regenerated Project</project_name>
|
||||||
|
|
||||||
|
<overview>
|
||||||
|
${projectDefinition}
|
||||||
|
</overview>
|
||||||
|
|
||||||
|
<technology_stack>
|
||||||
|
<frontend>
|
||||||
|
<framework>Next.js</framework>
|
||||||
|
<ui_library>React</ui_library>
|
||||||
|
<styling>Tailwind CSS</styling>
|
||||||
|
</frontend>
|
||||||
|
</technology_stack>
|
||||||
|
|
||||||
|
<core_capabilities>
|
||||||
|
<feature_1>Regenerated features based on definition</feature_1>
|
||||||
|
</core_capabilities>
|
||||||
|
</project_specification>`;
|
||||||
|
|
||||||
|
emitSpecRegenerationEvent({
|
||||||
|
type: "spec_regeneration_complete",
|
||||||
|
message: "Spec regeneration complete!",
|
||||||
|
});
|
||||||
|
|
||||||
|
mockSpecRegenerationRunning = false;
|
||||||
|
mockSpecRegenerationTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Utility functions for project management
|
// Utility functions for project management
|
||||||
|
|
||||||
export interface Project {
|
export interface Project {
|
||||||
|
|||||||
@@ -15,31 +15,6 @@ export interface ProjectInitResult {
|
|||||||
existingFiles?: string[];
|
existingFiles?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Default app_spec.txt template for new projects
|
|
||||||
*/
|
|
||||||
const DEFAULT_APP_SPEC = `<project_specification>
|
|
||||||
<project_name>Untitled Project</project_name>
|
|
||||||
|
|
||||||
<overview>
|
|
||||||
Describe your project here. This file will be analyzed by an AI agent
|
|
||||||
to understand your project structure and tech stack.
|
|
||||||
</overview>
|
|
||||||
|
|
||||||
<technology_stack>
|
|
||||||
<!-- The AI agent will fill this in after analyzing your project -->
|
|
||||||
</technology_stack>
|
|
||||||
|
|
||||||
<core_capabilities>
|
|
||||||
<!-- List core features and capabilities -->
|
|
||||||
</core_capabilities>
|
|
||||||
|
|
||||||
<implemented_features>
|
|
||||||
<!-- The AI agent will populate this based on code analysis -->
|
|
||||||
</implemented_features>
|
|
||||||
</project_specification>
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default feature_list.json template for new projects
|
* Default feature_list.json template for new projects
|
||||||
*/
|
*/
|
||||||
@@ -47,15 +22,16 @@ const DEFAULT_FEATURE_LIST = JSON.stringify([], null, 2);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Required files and directories in the .automaker directory
|
* Required files and directories in the .automaker directory
|
||||||
|
* Note: app_spec.txt is NOT created automatically - user must set it up via the spec editor
|
||||||
*/
|
*/
|
||||||
const REQUIRED_STRUCTURE = {
|
const REQUIRED_STRUCTURE = {
|
||||||
directories: [
|
directories: [
|
||||||
".automaker",
|
".automaker",
|
||||||
".automaker/context",
|
".automaker/context",
|
||||||
".automaker/agents-context",
|
".automaker/agents-context",
|
||||||
|
".automaker/images",
|
||||||
],
|
],
|
||||||
files: {
|
files: {
|
||||||
".automaker/app_spec.txt": DEFAULT_APP_SPEC,
|
|
||||||
".automaker/feature_list.json": DEFAULT_FEATURE_LIST,
|
".automaker/feature_list.json": DEFAULT_FEATURE_LIST,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -186,3 +162,37 @@ export async function getProjectInitStatus(projectPath: string): Promise<{
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the app_spec.txt file exists for a project
|
||||||
|
*
|
||||||
|
* @param projectPath - The root path of the project
|
||||||
|
* @returns true if app_spec.txt exists
|
||||||
|
*/
|
||||||
|
export async function hasAppSpec(projectPath: string): Promise<boolean> {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
try {
|
||||||
|
const fullPath = `${projectPath}/.automaker/app_spec.txt`;
|
||||||
|
return await api.exists(fullPath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[project-init] Error checking app_spec.txt:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the .automaker directory exists for a project
|
||||||
|
*
|
||||||
|
* @param projectPath - The root path of the project
|
||||||
|
* @returns true if .automaker directory exists
|
||||||
|
*/
|
||||||
|
export async function hasAutomakerDir(projectPath: string): Promise<boolean> {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
try {
|
||||||
|
const fullPath = `${projectPath}/.automaker`;
|
||||||
|
return await api.exists(fullPath);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[project-init] Error checking .automaker dir:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ export interface AppState {
|
|||||||
projects: Project[];
|
projects: Project[];
|
||||||
currentProject: Project | null;
|
currentProject: Project | null;
|
||||||
trashedProjects: TrashedProject[];
|
trashedProjects: TrashedProject[];
|
||||||
|
projectHistory: string[]; // Array of project IDs in MRU order (most recent first)
|
||||||
|
projectHistoryIndex: number; // Current position in project history for cycling
|
||||||
|
|
||||||
// View state
|
// View state
|
||||||
currentView: ViewMode;
|
currentView: ViewMode;
|
||||||
@@ -174,9 +176,11 @@ export interface AppState {
|
|||||||
currentChatSession: ChatSession | null;
|
currentChatSession: ChatSession | null;
|
||||||
chatHistoryOpen: boolean;
|
chatHistoryOpen: boolean;
|
||||||
|
|
||||||
// Auto Mode
|
// Auto Mode (per-project state, keyed by project ID)
|
||||||
isAutoModeRunning: boolean;
|
autoModeByProject: Record<string, {
|
||||||
runningAutoTasks: string[]; // Feature IDs being worked on (supports concurrent tasks)
|
isRunning: boolean;
|
||||||
|
runningTasks: string[]; // Feature IDs being worked on
|
||||||
|
}>;
|
||||||
autoModeActivityLog: AutoModeActivity[];
|
autoModeActivityLog: AutoModeActivity[];
|
||||||
maxConcurrency: number; // Maximum number of concurrent agent tasks
|
maxConcurrency: number; // Maximum number of concurrent agent tasks
|
||||||
|
|
||||||
@@ -230,6 +234,8 @@ export interface AppActions {
|
|||||||
emptyTrash: () => void;
|
emptyTrash: () => void;
|
||||||
setCurrentProject: (project: Project | null) => void;
|
setCurrentProject: (project: Project | null) => void;
|
||||||
reorderProjects: (oldIndex: number, newIndex: number) => void;
|
reorderProjects: (oldIndex: number, newIndex: number) => void;
|
||||||
|
cyclePrevProject: () => void; // Cycle back through project history (Q)
|
||||||
|
cycleNextProject: () => void; // Cycle forward through project history (E)
|
||||||
|
|
||||||
// View actions
|
// View actions
|
||||||
setCurrentView: (view: ViewMode) => void;
|
setCurrentView: (view: ViewMode) => void;
|
||||||
@@ -266,11 +272,12 @@ export interface AppActions {
|
|||||||
setChatHistoryOpen: (open: boolean) => void;
|
setChatHistoryOpen: (open: boolean) => void;
|
||||||
toggleChatHistory: () => void;
|
toggleChatHistory: () => void;
|
||||||
|
|
||||||
// Auto Mode actions
|
// Auto Mode actions (per-project)
|
||||||
setAutoModeRunning: (running: boolean) => void;
|
setAutoModeRunning: (projectId: string, running: boolean) => void;
|
||||||
addRunningTask: (taskId: string) => void;
|
addRunningTask: (projectId: string, taskId: string) => void;
|
||||||
removeRunningTask: (taskId: string) => void;
|
removeRunningTask: (projectId: string, taskId: string) => void;
|
||||||
clearRunningTasks: () => void;
|
clearRunningTasks: (projectId: string) => void;
|
||||||
|
getAutoModeState: (projectId: string) => { isRunning: boolean; runningTasks: string[] };
|
||||||
addAutoModeActivity: (
|
addAutoModeActivity: (
|
||||||
activity: Omit<AutoModeActivity, "id" | "timestamp">
|
activity: Omit<AutoModeActivity, "id" | "timestamp">
|
||||||
) => void;
|
) => void;
|
||||||
@@ -362,6 +369,8 @@ const initialState: AppState = {
|
|||||||
projects: [],
|
projects: [],
|
||||||
currentProject: null,
|
currentProject: null,
|
||||||
trashedProjects: [],
|
trashedProjects: [],
|
||||||
|
projectHistory: [],
|
||||||
|
projectHistoryIndex: -1,
|
||||||
currentView: "welcome",
|
currentView: "welcome",
|
||||||
sidebarOpen: true,
|
sidebarOpen: true,
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
@@ -376,8 +385,7 @@ const initialState: AppState = {
|
|||||||
chatSessions: [],
|
chatSessions: [],
|
||||||
currentChatSession: null,
|
currentChatSession: null,
|
||||||
chatHistoryOpen: false,
|
chatHistoryOpen: false,
|
||||||
isAutoModeRunning: false,
|
autoModeByProject: {},
|
||||||
runningAutoTasks: [],
|
|
||||||
autoModeActivityLog: [],
|
autoModeActivityLog: [],
|
||||||
maxConcurrency: 3, // Default to 3 concurrent agents
|
maxConcurrency: 3, // Default to 3 concurrent agents
|
||||||
kanbanCardDetailLevel: "standard", // Default to standard detail level
|
kanbanCardDetailLevel: "standard", // Default to standard detail level
|
||||||
@@ -508,11 +516,59 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
set({ currentProject: project });
|
set({ currentProject: project });
|
||||||
if (project) {
|
if (project) {
|
||||||
set({ currentView: "board" });
|
set({ currentView: "board" });
|
||||||
|
// Add to project history (MRU order)
|
||||||
|
const currentHistory = get().projectHistory;
|
||||||
|
// Remove this project if it's already in history
|
||||||
|
const filteredHistory = currentHistory.filter((id) => id !== project.id);
|
||||||
|
// Add to the front (most recent)
|
||||||
|
const newHistory = [project.id, ...filteredHistory];
|
||||||
|
// Reset history index to 0 (current project)
|
||||||
|
set({ projectHistory: newHistory, projectHistoryIndex: 0 });
|
||||||
} else {
|
} else {
|
||||||
set({ currentView: "welcome" });
|
set({ currentView: "welcome" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cyclePrevProject: () => {
|
||||||
|
const { projectHistory, projectHistoryIndex, projects } = get();
|
||||||
|
if (projectHistory.length <= 1) return; // Need at least 2 projects to cycle
|
||||||
|
|
||||||
|
// Move to the next index (going back in history = higher index)
|
||||||
|
const newIndex = (projectHistoryIndex + 1) % projectHistory.length;
|
||||||
|
const targetProjectId = projectHistory[newIndex];
|
||||||
|
const targetProject = projects.find((p) => p.id === targetProjectId);
|
||||||
|
|
||||||
|
if (targetProject) {
|
||||||
|
// Update the index but don't modify history order when cycling
|
||||||
|
set({
|
||||||
|
currentProject: targetProject,
|
||||||
|
projectHistoryIndex: newIndex,
|
||||||
|
currentView: "board"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cycleNextProject: () => {
|
||||||
|
const { projectHistory, projectHistoryIndex, projects } = get();
|
||||||
|
if (projectHistory.length <= 1) return; // Need at least 2 projects to cycle
|
||||||
|
|
||||||
|
// Move to the previous index (going forward = lower index, wrapping around)
|
||||||
|
const newIndex = projectHistoryIndex <= 0
|
||||||
|
? projectHistory.length - 1
|
||||||
|
: projectHistoryIndex - 1;
|
||||||
|
const targetProjectId = projectHistory[newIndex];
|
||||||
|
const targetProject = projects.find((p) => p.id === targetProjectId);
|
||||||
|
|
||||||
|
if (targetProject) {
|
||||||
|
// Update the index but don't modify history order when cycling
|
||||||
|
set({
|
||||||
|
currentProject: targetProject,
|
||||||
|
projectHistoryIndex: newIndex,
|
||||||
|
currentView: "board"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// View actions
|
// View actions
|
||||||
setCurrentView: (view) => set({ currentView: view }),
|
setCurrentView: (view) => set({ currentView: view }),
|
||||||
toggleSidebar: () => set({ sidebarOpen: !get().sidebarOpen }),
|
toggleSidebar: () => set({ sidebarOpen: !get().sidebarOpen }),
|
||||||
@@ -667,25 +723,63 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
|
|
||||||
toggleChatHistory: () => set({ chatHistoryOpen: !get().chatHistoryOpen }),
|
toggleChatHistory: () => set({ chatHistoryOpen: !get().chatHistoryOpen }),
|
||||||
|
|
||||||
// Auto Mode actions
|
// Auto Mode actions (per-project)
|
||||||
setAutoModeRunning: (running) => set({ isAutoModeRunning: running }),
|
setAutoModeRunning: (projectId, running) => {
|
||||||
|
const current = get().autoModeByProject;
|
||||||
addRunningTask: (taskId) => {
|
const projectState = current[projectId] || { isRunning: false, runningTasks: [] };
|
||||||
const current = get().runningAutoTasks;
|
|
||||||
if (!current.includes(taskId)) {
|
|
||||||
set({ runningAutoTasks: [...current, taskId] });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeRunningTask: (taskId) => {
|
|
||||||
set({
|
set({
|
||||||
runningAutoTasks: get().runningAutoTasks.filter(
|
autoModeByProject: {
|
||||||
(id) => id !== taskId
|
...current,
|
||||||
),
|
[projectId]: { ...projectState, isRunning: running },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
clearRunningTasks: () => set({ runningAutoTasks: [] }),
|
addRunningTask: (projectId, taskId) => {
|
||||||
|
const current = get().autoModeByProject;
|
||||||
|
const projectState = current[projectId] || { isRunning: false, runningTasks: [] };
|
||||||
|
if (!projectState.runningTasks.includes(taskId)) {
|
||||||
|
set({
|
||||||
|
autoModeByProject: {
|
||||||
|
...current,
|
||||||
|
[projectId]: {
|
||||||
|
...projectState,
|
||||||
|
runningTasks: [...projectState.runningTasks, taskId],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeRunningTask: (projectId, taskId) => {
|
||||||
|
const current = get().autoModeByProject;
|
||||||
|
const projectState = current[projectId] || { isRunning: false, runningTasks: [] };
|
||||||
|
set({
|
||||||
|
autoModeByProject: {
|
||||||
|
...current,
|
||||||
|
[projectId]: {
|
||||||
|
...projectState,
|
||||||
|
runningTasks: projectState.runningTasks.filter((id) => id !== taskId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
clearRunningTasks: (projectId) => {
|
||||||
|
const current = get().autoModeByProject;
|
||||||
|
const projectState = current[projectId] || { isRunning: false, runningTasks: [] };
|
||||||
|
set({
|
||||||
|
autoModeByProject: {
|
||||||
|
...current,
|
||||||
|
[projectId]: { ...projectState, runningTasks: [] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getAutoModeState: (projectId) => {
|
||||||
|
const projectState = get().autoModeByProject[projectId];
|
||||||
|
return projectState || { isRunning: false, runningTasks: [] };
|
||||||
|
},
|
||||||
|
|
||||||
addAutoModeActivity: (activity) => {
|
addAutoModeActivity: (activity) => {
|
||||||
const id = `activity-${Date.now()}-${Math.random()
|
const id = `activity-${Date.now()}-${Math.random()
|
||||||
@@ -766,6 +860,8 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
projects: state.projects,
|
projects: state.projects,
|
||||||
currentProject: state.currentProject,
|
currentProject: state.currentProject,
|
||||||
trashedProjects: state.trashedProjects,
|
trashedProjects: state.trashedProjects,
|
||||||
|
projectHistory: state.projectHistory,
|
||||||
|
projectHistoryIndex: state.projectHistoryIndex,
|
||||||
currentView: state.currentView,
|
currentView: state.currentView,
|
||||||
theme: state.theme,
|
theme: state.theme,
|
||||||
sidebarOpen: state.sidebarOpen,
|
sidebarOpen: state.sidebarOpen,
|
||||||
|
|||||||
61
app/src/types/electron.d.ts
vendored
61
app/src/types/electron.d.ts
vendored
@@ -162,22 +162,26 @@ export type AutoModeEvent =
|
|||||||
| {
|
| {
|
||||||
type: "auto_mode_feature_start";
|
type: "auto_mode_feature_start";
|
||||||
featureId: string;
|
featureId: string;
|
||||||
|
projectId?: string;
|
||||||
feature: unknown;
|
feature: unknown;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "auto_mode_progress";
|
type: "auto_mode_progress";
|
||||||
featureId: string;
|
featureId: string;
|
||||||
|
projectId?: string;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "auto_mode_tool";
|
type: "auto_mode_tool";
|
||||||
featureId: string;
|
featureId: string;
|
||||||
|
projectId?: string;
|
||||||
tool: string;
|
tool: string;
|
||||||
input: unknown;
|
input: unknown;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "auto_mode_feature_complete";
|
type: "auto_mode_feature_complete";
|
||||||
featureId: string;
|
featureId: string;
|
||||||
|
projectId?: string;
|
||||||
passes: boolean;
|
passes: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
@@ -185,14 +189,17 @@ export type AutoModeEvent =
|
|||||||
type: "auto_mode_error";
|
type: "auto_mode_error";
|
||||||
error: string;
|
error: string;
|
||||||
featureId?: string;
|
featureId?: string;
|
||||||
|
projectId?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "auto_mode_complete";
|
type: "auto_mode_complete";
|
||||||
message: string;
|
message: string;
|
||||||
|
projectId?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "auto_mode_phase";
|
type: "auto_mode_phase";
|
||||||
featureId: string;
|
featureId: string;
|
||||||
|
projectId?: string;
|
||||||
phase: "planning" | "action" | "verification";
|
phase: "planning" | "action" | "verification";
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
@@ -205,6 +212,57 @@ export type AutoModeEvent =
|
|||||||
estimatedTime?: string;
|
estimatedTime?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SpecRegenerationEvent =
|
||||||
|
| {
|
||||||
|
type: "spec_regeneration_progress";
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "spec_regeneration_tool";
|
||||||
|
tool: string;
|
||||||
|
input: unknown;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "spec_regeneration_complete";
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "spec_regeneration_error";
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SpecRegenerationAPI {
|
||||||
|
create: (
|
||||||
|
projectPath: string,
|
||||||
|
projectOverview: string,
|
||||||
|
generateFeatures?: boolean
|
||||||
|
) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
generate: (
|
||||||
|
projectPath: string,
|
||||||
|
projectDefinition: string
|
||||||
|
) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
stop: () => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
status: () => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
isRunning?: boolean;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
onEvent: (callback: (event: SpecRegenerationEvent) => void) => () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AutoModeAPI {
|
export interface AutoModeAPI {
|
||||||
start: (projectPath: string) => Promise<{
|
start: (projectPath: string) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -411,6 +469,9 @@ export interface ElectronAPI {
|
|||||||
|
|
||||||
// Git Operations APIs (for non-worktree operations)
|
// Git Operations APIs (for non-worktree operations)
|
||||||
git: GitAPI;
|
git: GitAPI;
|
||||||
|
|
||||||
|
// Spec Regeneration APIs
|
||||||
|
specRegeneration: SpecRegenerationAPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorktreeInfo {
|
export interface WorktreeInfo {
|
||||||
|
|||||||
Reference in New Issue
Block a user