mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
feat(feature-suggestions): implement feature suggestions and spec regeneration functionality
- Introduced a new `FeatureSuggestionsService` to analyze projects and generate feature suggestions based on the project structure and existing features. - Added IPC handlers for generating and stopping feature suggestions, as well as checking their status. - Implemented a `SpecRegenerationService` to create and regenerate application specifications based on user-defined project overviews and definitions. - Enhanced the UI with a `FeatureSuggestionsDialog` for displaying generated suggestions and allowing users to import them into their project. - Updated the sidebar and board view components to integrate feature suggestions and spec regeneration functionalities, improving project management capabilities. These changes significantly enhance the application's ability to assist users in feature planning and specification management.
This commit is contained in:
@@ -1,20 +1 @@
|
||||
[
|
||||
{
|
||||
"id": "feature-1765335919754-r010d1fw5",
|
||||
"category": "Uncategorized",
|
||||
"description": "what does the text in the button say?\n",
|
||||
"steps": [],
|
||||
"status": "waiting_approval",
|
||||
"startedAt": "2025-12-10T03:05:34.894Z",
|
||||
"imagePaths": [
|
||||
{
|
||||
"id": "img-1765335919132-0x3t37l1r",
|
||||
"path": "/Users/webdevcody/Library/Application Support/automaker/images/1765335919131-g4qvs053g_Screenshot_2025-12-09_at_10.05.17_PM.png",
|
||||
"filename": "Screenshot 2025-12-09 at 10.05.17 PM.png",
|
||||
"mimeType": "image/png"
|
||||
}
|
||||
],
|
||||
"skipTests": true,
|
||||
"summary": "Investigated button text in the app. Main buttons found in welcome-view.tsx: \"Create Project\" (primary action), \"Browse Folder\" (secondary action), \"Browse\" (directory selector), \"Cancel\", \"Get Started\". No code changes made - this was an investigative question."
|
||||
}
|
||||
]
|
||||
[]
|
||||
@@ -7,6 +7,8 @@ const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron");
|
||||
const fs = require("fs/promises");
|
||||
const agentService = require("./agent-service");
|
||||
const autoModeService = require("./auto-mode-service");
|
||||
const featureSuggestionsService = require("./services/feature-suggestions-service");
|
||||
const specRegenerationService = require("./services/spec-regeneration-service");
|
||||
|
||||
let mainWindow = null;
|
||||
|
||||
@@ -669,3 +671,224 @@ ipcMain.handle(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 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(),
|
||||
};
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// 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 };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -139,6 +139,58 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// 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
|
||||
|
||||
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,
|
||||
PanelLeftClose,
|
||||
ChevronDown,
|
||||
Redo2,
|
||||
Check,
|
||||
BookOpen,
|
||||
GripVertical,
|
||||
RotateCw,
|
||||
RotateCcw,
|
||||
Trash2,
|
||||
Undo2,
|
||||
} from "lucide-react";
|
||||
@@ -44,8 +47,15 @@ import {
|
||||
KeyboardShortcut,
|
||||
} from "@/hooks/use-keyboard-shortcuts";
|
||||
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 { Sparkles, Loader2 } from "lucide-react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import type { SpecRegenerationEvent } from "@/types/electron";
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
@@ -155,6 +165,7 @@ export function Sidebar() {
|
||||
currentProject,
|
||||
currentView,
|
||||
sidebarOpen,
|
||||
projectHistory,
|
||||
addProject,
|
||||
setCurrentProject,
|
||||
setCurrentView,
|
||||
@@ -163,6 +174,8 @@ export function Sidebar() {
|
||||
deleteTrashedProject,
|
||||
emptyTrash,
|
||||
reorderProjects,
|
||||
cyclePrevProject,
|
||||
cycleNextProject,
|
||||
} = useAppStore();
|
||||
|
||||
// State for project picker dropdown
|
||||
@@ -171,6 +184,17 @@ export function Sidebar() {
|
||||
const [activeTrashId, setActiveTrashId] = useState<string | null>(null);
|
||||
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
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
@@ -197,6 +221,93 @@ export function Sidebar() {
|
||||
[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.
|
||||
* Used by both the 'O' keyboard shortcut and the folder icon button.
|
||||
@@ -210,6 +321,9 @@ export function Sidebar() {
|
||||
const name = path.split("/").pop() || "Untitled Project";
|
||||
|
||||
try {
|
||||
// Check if this is a brand new project (no .automaker directory)
|
||||
const hadAutomakerDir = await hasAutomakerDir(path);
|
||||
|
||||
// Initialize the .automaker directory structure
|
||||
const initResult = await initializeProject(path);
|
||||
|
||||
@@ -230,7 +344,20 @@ export function Sidebar() {
|
||||
addProject(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(
|
||||
initResult.isNewProject ? "Project initialized" : "Project updated",
|
||||
{
|
||||
@@ -422,6 +549,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
|
||||
if (currentProject) {
|
||||
navSections.forEach((section) => {
|
||||
@@ -451,6 +592,9 @@ export function Sidebar() {
|
||||
toggleSidebar,
|
||||
projects.length,
|
||||
handleOpenFolder,
|
||||
projectHistory.length,
|
||||
cyclePrevProject,
|
||||
cycleNextProject,
|
||||
]);
|
||||
|
||||
// Register keyboard shortcuts
|
||||
@@ -464,7 +608,7 @@ export function Sidebar() {
|
||||
<aside
|
||||
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",
|
||||
sidebarOpen ? "w-16 lg:w-60" : "w-16"
|
||||
sidebarOpen ? "w-16 lg:w-72" : "w-16"
|
||||
)}
|
||||
data-testid="sidebar"
|
||||
>
|
||||
@@ -566,16 +710,16 @@ export function Sidebar() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project Selector */}
|
||||
{/* Project Selector with Cycle Buttons */}
|
||||
{sidebarOpen && projects.length > 0 && (
|
||||
<div className="px-2 mt-3">
|
||||
<div className="px-2 mt-3 flex items-center gap-1.5">
|
||||
<DropdownMenu
|
||||
open={isProjectPickerOpen}
|
||||
onOpenChange={setIsProjectPickerOpen}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<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"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
@@ -625,6 +769,34 @@ export function Sidebar() {
|
||||
</DndContext>
|
||||
</DropdownMenuContent>
|
||||
</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>
|
||||
)}
|
||||
|
||||
@@ -865,6 +1037,103 @@ export function Sidebar() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
import { KanbanColumn } from "./kanban-column";
|
||||
import { KanbanCard } from "./kanban-card";
|
||||
import { AgentOutputModal } from "./agent-output-modal";
|
||||
import { FeatureSuggestionsDialog } from "./feature-suggestions-dialog";
|
||||
import {
|
||||
Plus,
|
||||
RefreshCw,
|
||||
@@ -63,6 +64,7 @@ import {
|
||||
CheckCircle2,
|
||||
MessageSquare,
|
||||
GitCommit,
|
||||
Lightbulb,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
@@ -92,7 +94,6 @@ export function BoardView() {
|
||||
updateFeature,
|
||||
removeFeature,
|
||||
moveFeature,
|
||||
runningAutoTasks,
|
||||
maxConcurrency,
|
||||
setMaxConcurrency,
|
||||
defaultSkipTests,
|
||||
@@ -124,6 +125,8 @@ export function BoardView() {
|
||||
const [followUpImagePaths, setFollowUpImagePaths] = useState<
|
||||
DescriptionImagePath[]
|
||||
>([]);
|
||||
const [showSuggestionsDialog, setShowSuggestionsDialog] = useState(false);
|
||||
const [suggestionsCount, setSuggestionsCount] = useState(0);
|
||||
|
||||
// Make current project available globally for modal
|
||||
useEffect(() => {
|
||||
@@ -135,12 +138,30 @@ export function BoardView() {
|
||||
};
|
||||
}, [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
|
||||
const prevProjectPathRef = useRef<string | null>(null);
|
||||
const isSwitchingProjectRef = useRef<boolean>(false);
|
||||
|
||||
// Auto mode hook
|
||||
const autoMode = useAutoMode();
|
||||
// Get runningTasks from the hook (scoped to current project)
|
||||
const runningAutoTasks = autoMode.runningTasks;
|
||||
|
||||
// Get in-progress features for keyboard shortcuts (memoized for shortcuts)
|
||||
const inProgressFeaturesForShortcuts = useMemo(() => {
|
||||
@@ -340,11 +361,15 @@ export function BoardView() {
|
||||
// Listen for auto mode feature completion and errors to reload features
|
||||
useEffect(() => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) return;
|
||||
if (!api?.autoMode || !currentProject) return;
|
||||
|
||||
const { removeRunningTask } = useAppStore.getState();
|
||||
const projectId = currentProject.id;
|
||||
|
||||
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") {
|
||||
// Reload features when a feature is completed
|
||||
console.log("[Board] Feature completed, reloading features...");
|
||||
@@ -358,7 +383,7 @@ export function BoardView() {
|
||||
|
||||
// Remove from running tasks so it moves to the correct column
|
||||
if (event.featureId) {
|
||||
removeRunningTask(event.featureId);
|
||||
removeRunningTask(eventProjectId, event.featureId);
|
||||
}
|
||||
|
||||
loadFeatures();
|
||||
@@ -370,7 +395,7 @@ export function BoardView() {
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [loadFeatures]);
|
||||
}, [loadFeatures, currentProject]);
|
||||
|
||||
useEffect(() => {
|
||||
loadFeatures();
|
||||
@@ -383,6 +408,8 @@ export function BoardView() {
|
||||
|
||||
// Sync running tasks from electron backend on mount
|
||||
useEffect(() => {
|
||||
if (!currentProject) return;
|
||||
|
||||
const syncRunningTasks = async () => {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
@@ -395,13 +422,14 @@ export function BoardView() {
|
||||
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();
|
||||
clearRunningTasks();
|
||||
const projectId = currentProject.id;
|
||||
clearRunningTasks(projectId);
|
||||
|
||||
// Add each running feature to the store
|
||||
status.runningFeatures.forEach((featureId: string) => {
|
||||
addRunningTask(featureId);
|
||||
addRunningTask(projectId, featureId);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -410,7 +438,7 @@ export function BoardView() {
|
||||
};
|
||||
|
||||
syncRunningTasks();
|
||||
}, []);
|
||||
}, [currentProject]);
|
||||
|
||||
// Check which features have context files
|
||||
useEffect(() => {
|
||||
@@ -1272,21 +1300,42 @@ export function BoardView() {
|
||||
<Trash2 className="w-3 h-3 mr-1" />
|
||||
Delete All
|
||||
</Button>
|
||||
) : column.id === "backlog" &&
|
||||
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-white/10 border border-white/20">
|
||||
{ACTION_SHORTCUTS.startNext}
|
||||
</span>
|
||||
</Button>
|
||||
) : column.id === "backlog" ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-yellow-500 hover:text-yellow-400 hover:bg-yellow-500/10 relative"
|
||||
onClick={() => setShowSuggestionsDialog(true)}
|
||||
title="Feature Suggestions"
|
||||
data-testid="feature-suggestions-button"
|
||||
>
|
||||
<Lightbulb className="w-3.5 h-3.5" />
|
||||
{suggestionsCount > 0 && (
|
||||
<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"
|
||||
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-white/10 border border-white/20">
|
||||
{ACTION_SHORTCUTS.startNext}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
@@ -1754,6 +1803,16 @@ export function BoardView() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ export function ContextView() {
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add File
|
||||
<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"
|
||||
>
|
||||
{ACTION_SHORTCUTS.addContextFile}
|
||||
@@ -387,9 +387,9 @@ export function ContextView() {
|
||||
onDragOver={handleDragOver}
|
||||
>
|
||||
{/* Left Panel - File List */}
|
||||
<div className="w-64 border-r border-white/10 flex flex-col overflow-hidden">
|
||||
<div className="p-3 border-b border-white/10">
|
||||
<h2 className="text-sm font-semibold text-zinc-400">
|
||||
<div className="w-64 border-r border-border flex flex-col overflow-hidden">
|
||||
<div className="p-3 border-b border-border">
|
||||
<h2 className="text-sm font-semibold text-muted-foreground">
|
||||
Context Files ({contextFiles.length})
|
||||
</h2>
|
||||
</div>
|
||||
@@ -399,8 +399,8 @@ export function ContextView() {
|
||||
>
|
||||
{contextFiles.length === 0 ? (
|
||||
<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" />
|
||||
<p className="text-sm text-zinc-500">
|
||||
<Upload className="w-8 h-8 text-muted-foreground mb-2" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No context files yet.
|
||||
<br />
|
||||
Drop files here or click Add File.
|
||||
@@ -415,8 +415,8 @@ export function ContextView() {
|
||||
className={cn(
|
||||
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-left transition-colors",
|
||||
selectedFile?.path === file.path
|
||||
? "bg-brand-500/20 text-white border border-brand-500/30"
|
||||
: "text-zinc-400 hover:bg-white/5 hover:text-white"
|
||||
? "bg-primary/20 text-foreground border border-primary/30"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
)}
|
||||
data-testid={`context-file-${file.name}`}
|
||||
>
|
||||
@@ -438,12 +438,12 @@ export function ContextView() {
|
||||
{selectedFile ? (
|
||||
<>
|
||||
{/* 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">
|
||||
{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">
|
||||
{selectedFile.name}
|
||||
@@ -477,7 +477,7 @@ export function ContextView() {
|
||||
<div className="flex-1 overflow-hidden p-4">
|
||||
{selectedFile.type === "image" ? (
|
||||
<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"
|
||||
>
|
||||
<img
|
||||
@@ -503,9 +503,9 @@ export function ContextView() {
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<File className="w-12 h-12 text-zinc-600 mx-auto mb-3" />
|
||||
<p className="text-zinc-500">Select a file to view or edit</p>
|
||||
<p className="text-zinc-600 text-sm mt-1">
|
||||
<File className="w-12 h-12 text-muted-foreground mx-auto mb-3" />
|
||||
<p className="text-foreground-secondary">Select a file to view or edit</p>
|
||||
<p className="text-muted-foreground text-sm mt-1">
|
||||
Or drop files here to add them
|
||||
</p>
|
||||
</div>
|
||||
@@ -516,7 +516,10 @@ export function ContextView() {
|
||||
|
||||
{/* Add File Dialog */}
|
||||
<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>
|
||||
<DialogTitle>Add Context File</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -564,7 +567,7 @@ export function ContextView() {
|
||||
<div
|
||||
className={cn(
|
||||
"relative rounded-lg transition-colors",
|
||||
isDropHovering && "ring-2 ring-brand-500"
|
||||
isDropHovering && "ring-2 ring-primary"
|
||||
)}
|
||||
>
|
||||
<textarea
|
||||
@@ -576,15 +579,15 @@ export function ContextView() {
|
||||
onDragLeave={handleTextAreaDragLeave}
|
||||
placeholder="Enter context content here or drag & drop a .txt or .md file..."
|
||||
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",
|
||||
isDropHovering && "border-brand-500 bg-brand-500/10"
|
||||
"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-primary bg-primary/10"
|
||||
)}
|
||||
spellCheck={false}
|
||||
data-testid="new-file-content"
|
||||
/>
|
||||
{isDropHovering && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-brand-500/20 rounded-lg pointer-events-none">
|
||||
<div className="flex flex-col items-center text-brand-400">
|
||||
<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-primary">
|
||||
<Upload className="w-8 h-8 mb-2" />
|
||||
<span className="text-sm font-medium">
|
||||
Drop .txt or .md file here
|
||||
@@ -593,7 +596,7 @@ export function ContextView() {
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
@@ -602,7 +605,7 @@ export function ContextView() {
|
||||
{newFileType === "image" && (
|
||||
<div className="space-y-2">
|
||||
<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
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@@ -622,9 +625,9 @@ export function ContextView() {
|
||||
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
|
||||
? "Click to change"
|
||||
: "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 { Button } from "@/components/ui/button";
|
||||
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() {
|
||||
const { currentProject, appSpec, setAppSpec } = useAppStore();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = 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
|
||||
const loadSpec = useCallback(async () => {
|
||||
@@ -26,10 +48,16 @@ export function SpecView() {
|
||||
|
||||
if (result.success && result.content) {
|
||||
setAppSpec(result.content);
|
||||
setSpecExists(true);
|
||||
setHasChanges(false);
|
||||
} else {
|
||||
// File doesn't exist
|
||||
setAppSpec("");
|
||||
setSpecExists(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load spec:", error);
|
||||
setSpecExists(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -39,6 +67,35 @@ export function SpecView() {
|
||||
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
|
||||
const saveSpec = async () => {
|
||||
if (!currentProject) return;
|
||||
@@ -63,6 +120,62 @@ export function SpecView() {
|
||||
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) {
|
||||
return (
|
||||
<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 (
|
||||
<div
|
||||
className="flex-1 flex flex-col overflow-hidden content-bg"
|
||||
@@ -102,6 +330,20 @@ export function SpecView() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setShowRegenerateDialog(true)}
|
||||
disabled={isRegenerating}
|
||||
data-testid="regenerate-spec"
|
||||
>
|
||||
{isRegenerating ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{isRegenerating ? "Regenerating..." : "Regenerate"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={saveSpec}
|
||||
@@ -127,6 +369,65 @@ export function SpecView() {
|
||||
/>
|
||||
</Card>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { useEffect, useCallback, useMemo } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import type { AutoModeEvent } from "@/types/electron";
|
||||
import { getElectronAPI, type AutoModeEvent } from "@/lib/electron";
|
||||
|
||||
/**
|
||||
* Hook for managing auto mode
|
||||
* Hook for managing auto mode (scoped per project)
|
||||
*/
|
||||
export function useAutoMode() {
|
||||
const {
|
||||
isAutoModeRunning,
|
||||
autoModeByProject,
|
||||
setAutoModeRunning,
|
||||
runningAutoTasks,
|
||||
addRunningTask,
|
||||
removeRunningTask,
|
||||
clearRunningTasks,
|
||||
@@ -20,9 +18,8 @@ export function useAutoMode() {
|
||||
maxConcurrency,
|
||||
} = useAppStore(
|
||||
useShallow((state) => ({
|
||||
isAutoModeRunning: state.isAutoModeRunning,
|
||||
autoModeByProject: state.autoModeByProject,
|
||||
setAutoModeRunning: state.setAutoModeRunning,
|
||||
runningAutoTasks: state.runningAutoTasks,
|
||||
addRunningTask: state.addRunningTask,
|
||||
removeRunningTask: state.removeRunningTask,
|
||||
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
|
||||
const canStartNewTask = runningAutoTasks.length < maxConcurrency;
|
||||
|
||||
// Handle auto mode events
|
||||
useEffect(() => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) return;
|
||||
if (!api?.autoMode || !projectId) return;
|
||||
|
||||
const unsubscribe = api.autoMode.onEvent((event: AutoModeEvent) => {
|
||||
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) {
|
||||
case "auto_mode_feature_start":
|
||||
addRunningTask(event.featureId);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "start",
|
||||
message: `Started working on feature`,
|
||||
});
|
||||
if (event.featureId) {
|
||||
addRunningTask(eventProjectId, event.featureId);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "start",
|
||||
message: `Started working on feature`,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "auto_mode_feature_complete":
|
||||
// Feature completed - remove from running tasks and UI will reload features on its own
|
||||
console.log(
|
||||
"[AutoMode] Feature completed:",
|
||||
event.featureId,
|
||||
"passes:",
|
||||
event.passes
|
||||
);
|
||||
removeRunningTask(event.featureId);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "complete",
|
||||
message: event.passes
|
||||
? "Feature completed successfully"
|
||||
: "Feature completed with failures",
|
||||
passes: event.passes,
|
||||
});
|
||||
if (event.featureId) {
|
||||
console.log(
|
||||
"[AutoMode] Feature completed:",
|
||||
event.featureId,
|
||||
"passes:",
|
||||
event.passes
|
||||
);
|
||||
removeRunningTask(eventProjectId, event.featureId);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "complete",
|
||||
message: event.passes
|
||||
? "Feature completed successfully"
|
||||
: "Feature completed with failures",
|
||||
passes: event.passes,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "auto_mode_complete":
|
||||
// All features completed
|
||||
setAutoModeRunning(false);
|
||||
clearRunningTasks();
|
||||
// All features completed for this project
|
||||
setAutoModeRunning(eventProjectId, false);
|
||||
clearRunningTasks(eventProjectId);
|
||||
console.log("[AutoMode] All features completed!");
|
||||
break;
|
||||
|
||||
case "auto_mode_error":
|
||||
console.error("[AutoMode Error]", event.error);
|
||||
if (event.featureId) {
|
||||
if (event.featureId && event.error) {
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "error",
|
||||
@@ -92,7 +107,7 @@ export function useAutoMode() {
|
||||
|
||||
case "auto_mode_progress":
|
||||
// Log progress updates (throttle to avoid spam)
|
||||
if (event.content && event.content.length > 10) {
|
||||
if (event.featureId && event.content && event.content.length > 10) {
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "progress",
|
||||
@@ -103,31 +118,36 @@ export function useAutoMode() {
|
||||
|
||||
case "auto_mode_tool":
|
||||
// Log tool usage
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "tool",
|
||||
message: `Using tool: ${event.tool}`,
|
||||
tool: event.tool,
|
||||
});
|
||||
if (event.featureId && event.tool) {
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "tool",
|
||||
message: `Using tool: ${event.tool}`,
|
||||
tool: event.tool,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "auto_mode_phase":
|
||||
// Log phase transitions (Planning, Action, Verification)
|
||||
console.log(
|
||||
`[AutoMode] Phase: ${event.phase} for ${event.featureId}`
|
||||
);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: event.phase,
|
||||
message: event.message,
|
||||
phase: event.phase,
|
||||
});
|
||||
if (event.featureId && event.phase && event.message) {
|
||||
console.log(
|
||||
`[AutoMode] Phase: ${event.phase} for ${event.featureId}`
|
||||
);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: event.phase,
|
||||
message: event.message,
|
||||
phase: event.phase,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [
|
||||
projectId,
|
||||
addRunningTask,
|
||||
removeRunningTask,
|
||||
clearRunningTasks,
|
||||
@@ -151,7 +171,7 @@ export function useAutoMode() {
|
||||
const result = await api.autoMode.start(currentProject.path, maxConcurrency);
|
||||
|
||||
if (result.success) {
|
||||
setAutoModeRunning(true);
|
||||
setAutoModeRunning(currentProject.id, true);
|
||||
console.log(`[AutoMode] Started successfully with maxConcurrency: ${maxConcurrency}`);
|
||||
} else {
|
||||
console.error("[AutoMode] Failed to start:", result.error);
|
||||
@@ -159,13 +179,20 @@ export function useAutoMode() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AutoMode] Error starting:", error);
|
||||
setAutoModeRunning(false);
|
||||
if (currentProject) {
|
||||
setAutoModeRunning(currentProject.id, false);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}, [currentProject, setAutoModeRunning, maxConcurrency]);
|
||||
|
||||
// Stop auto mode
|
||||
const stop = useCallback(async () => {
|
||||
if (!currentProject) {
|
||||
console.error("No project selected");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) {
|
||||
@@ -175,8 +202,8 @@ export function useAutoMode() {
|
||||
const result = await api.autoMode.stop();
|
||||
|
||||
if (result.success) {
|
||||
setAutoModeRunning(false);
|
||||
clearRunningTasks();
|
||||
setAutoModeRunning(currentProject.id, false);
|
||||
clearRunningTasks(currentProject.id);
|
||||
console.log("[AutoMode] Stopped successfully");
|
||||
} else {
|
||||
console.error("[AutoMode] Failed to stop:", result.error);
|
||||
@@ -186,11 +213,16 @@ export function useAutoMode() {
|
||||
console.error("[AutoMode] Error stopping:", error);
|
||||
throw error;
|
||||
}
|
||||
}, [setAutoModeRunning, clearRunningTasks]);
|
||||
}, [currentProject, setAutoModeRunning, clearRunningTasks]);
|
||||
|
||||
// Stop a specific feature
|
||||
const stopFeature = useCallback(
|
||||
async (featureId: string) => {
|
||||
if (!currentProject) {
|
||||
console.error("No project selected");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.stopFeature) {
|
||||
@@ -200,7 +232,7 @@ export function useAutoMode() {
|
||||
const result = await api.autoMode.stopFeature(featureId);
|
||||
|
||||
if (result.success) {
|
||||
removeRunningTask(featureId);
|
||||
removeRunningTask(currentProject.id, featureId);
|
||||
console.log("[AutoMode] Feature stopped successfully:", featureId);
|
||||
addAutoModeActivity({
|
||||
featureId,
|
||||
@@ -217,7 +249,7 @@ export function useAutoMode() {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[removeRunningTask, addAutoModeActivity]
|
||||
[currentProject, removeRunningTask, addAutoModeActivity]
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -102,7 +102,7 @@ export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) {
|
||||
export const NAV_SHORTCUTS: Record<string, string> = {
|
||||
board: "K", // K for Kanban
|
||||
agent: "A", // A for Agent
|
||||
spec: "E", // E for Editor (Spec)
|
||||
spec: "D", // D for Document (Spec)
|
||||
context: "C", // C for Context
|
||||
tools: "T", // T for Tools
|
||||
settings: "S", // S for Settings
|
||||
@@ -121,8 +121,10 @@ export const UI_SHORTCUTS: Record<string, string> = {
|
||||
export const ACTION_SHORTCUTS: Record<string, string> = {
|
||||
addFeature: "N", // N for New feature
|
||||
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)
|
||||
openProject: "O", // O for Open project (navigate to welcome view)
|
||||
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)
|
||||
};
|
||||
|
||||
@@ -47,6 +47,7 @@ export type AutoModePhase = "planning" | "action" | "verification";
|
||||
export interface AutoModeEvent {
|
||||
type: "auto_mode_feature_start" | "auto_mode_progress" | "auto_mode_tool" | "auto_mode_feature_complete" | "auto_mode_error" | "auto_mode_complete" | "auto_mode_phase";
|
||||
featureId?: string;
|
||||
projectId?: string;
|
||||
feature?: object;
|
||||
content?: string;
|
||||
tool?: string;
|
||||
@@ -57,6 +58,47 @@ export interface AutoModeEvent {
|
||||
phase?: AutoModePhase;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
start: (projectPath: string, maxConcurrency?: number) => Promise<{ success: boolean; error?: string }>;
|
||||
stop: () => Promise<{ success: boolean; error?: string }>;
|
||||
@@ -93,12 +135,15 @@ export interface ElectronAPI {
|
||||
getPath: (name: string) => Promise<string>;
|
||||
saveImageToTemp?: (data: string, filename: string, mimeType: string) => Promise<SaveImageResult>;
|
||||
autoMode?: AutoModeAPI;
|
||||
suggestions?: SuggestionsAPI;
|
||||
specRegeneration?: SpecRegenerationAPI;
|
||||
}
|
||||
|
||||
// Augment global Window interface
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI?: ElectronAPI;
|
||||
isElectron?: boolean;
|
||||
electronAPI: ElectronAPI | undefined;
|
||||
isElectron: boolean | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +410,12 @@ export const getElectronAPI = (): ElectronAPI => {
|
||||
|
||||
// Mock Auto Mode API
|
||||
autoMode: createMockAutoModeAPI(),
|
||||
|
||||
// Mock Suggestions API
|
||||
suggestions: createMockSuggestionsAPI(),
|
||||
|
||||
// Mock Spec Regeneration API
|
||||
specRegeneration: createMockSpecRegenerationAPI(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -768,6 +819,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
|
||||
|
||||
export interface Project {
|
||||
|
||||
@@ -15,31 +15,6 @@ export interface ProjectInitResult {
|
||||
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
|
||||
*/
|
||||
@@ -47,15 +22,16 @@ const DEFAULT_FEATURE_LIST = JSON.stringify([], null, 2);
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
directories: [
|
||||
".automaker",
|
||||
".automaker/context",
|
||||
".automaker/agents-context",
|
||||
".automaker/images",
|
||||
],
|
||||
files: {
|
||||
".automaker/app_spec.txt": DEFAULT_APP_SPEC,
|
||||
".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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,8 @@ export interface AppState {
|
||||
projects: Project[];
|
||||
currentProject: Project | null;
|
||||
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
|
||||
currentView: ViewMode;
|
||||
@@ -119,9 +121,11 @@ export interface AppState {
|
||||
currentChatSession: ChatSession | null;
|
||||
chatHistoryOpen: boolean;
|
||||
|
||||
// Auto Mode
|
||||
isAutoModeRunning: boolean;
|
||||
runningAutoTasks: string[]; // Feature IDs being worked on (supports concurrent tasks)
|
||||
// Auto Mode (per-project state, keyed by project ID)
|
||||
autoModeByProject: Record<string, {
|
||||
isRunning: boolean;
|
||||
runningTasks: string[]; // Feature IDs being worked on
|
||||
}>;
|
||||
autoModeActivityLog: AutoModeActivity[];
|
||||
maxConcurrency: number; // Maximum number of concurrent agent tasks
|
||||
|
||||
@@ -162,6 +166,8 @@ export interface AppActions {
|
||||
emptyTrash: () => void;
|
||||
setCurrentProject: (project: Project | null) => 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
|
||||
setCurrentView: (view: ViewMode) => void;
|
||||
@@ -198,11 +204,12 @@ export interface AppActions {
|
||||
setChatHistoryOpen: (open: boolean) => void;
|
||||
toggleChatHistory: () => void;
|
||||
|
||||
// Auto Mode actions
|
||||
setAutoModeRunning: (running: boolean) => void;
|
||||
addRunningTask: (taskId: string) => void;
|
||||
removeRunningTask: (taskId: string) => void;
|
||||
clearRunningTasks: () => void;
|
||||
// Auto Mode actions (per-project)
|
||||
setAutoModeRunning: (projectId: string, running: boolean) => void;
|
||||
addRunningTask: (projectId: string, taskId: string) => void;
|
||||
removeRunningTask: (projectId: string, taskId: string) => void;
|
||||
clearRunningTasks: (projectId: string) => void;
|
||||
getAutoModeState: (projectId: string) => { isRunning: boolean; runningTasks: string[] };
|
||||
addAutoModeActivity: (
|
||||
activity: Omit<AutoModeActivity, "id" | "timestamp">
|
||||
) => void;
|
||||
@@ -223,6 +230,8 @@ const initialState: AppState = {
|
||||
projects: [],
|
||||
currentProject: null,
|
||||
trashedProjects: [],
|
||||
projectHistory: [],
|
||||
projectHistoryIndex: -1,
|
||||
currentView: "welcome",
|
||||
sidebarOpen: true,
|
||||
theme: "dark",
|
||||
@@ -236,8 +245,7 @@ const initialState: AppState = {
|
||||
chatSessions: [],
|
||||
currentChatSession: null,
|
||||
chatHistoryOpen: false,
|
||||
isAutoModeRunning: false,
|
||||
runningAutoTasks: [],
|
||||
autoModeByProject: {},
|
||||
autoModeActivityLog: [],
|
||||
maxConcurrency: 3, // Default to 3 concurrent agents
|
||||
kanbanCardDetailLevel: "standard", // Default to standard detail level
|
||||
@@ -363,11 +371,59 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
set({ currentProject: project });
|
||||
if (project) {
|
||||
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 {
|
||||
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
|
||||
setCurrentView: (view) => set({ currentView: view }),
|
||||
toggleSidebar: () => set({ sidebarOpen: !get().sidebarOpen }),
|
||||
@@ -522,25 +578,63 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
|
||||
toggleChatHistory: () => set({ chatHistoryOpen: !get().chatHistoryOpen }),
|
||||
|
||||
// Auto Mode actions
|
||||
setAutoModeRunning: (running) => set({ isAutoModeRunning: running }),
|
||||
|
||||
addRunningTask: (taskId) => {
|
||||
const current = get().runningAutoTasks;
|
||||
if (!current.includes(taskId)) {
|
||||
set({ runningAutoTasks: [...current, taskId] });
|
||||
}
|
||||
},
|
||||
|
||||
removeRunningTask: (taskId) => {
|
||||
// Auto Mode actions (per-project)
|
||||
setAutoModeRunning: (projectId, running) => {
|
||||
const current = get().autoModeByProject;
|
||||
const projectState = current[projectId] || { isRunning: false, runningTasks: [] };
|
||||
set({
|
||||
runningAutoTasks: get().runningAutoTasks.filter(
|
||||
(id) => id !== taskId
|
||||
),
|
||||
autoModeByProject: {
|
||||
...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) => {
|
||||
const id = `activity-${Date.now()}-${Math.random()
|
||||
@@ -579,6 +673,8 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
projects: state.projects,
|
||||
currentProject: state.currentProject,
|
||||
trashedProjects: state.trashedProjects,
|
||||
projectHistory: state.projectHistory,
|
||||
projectHistoryIndex: state.projectHistoryIndex,
|
||||
currentView: state.currentView,
|
||||
theme: state.theme,
|
||||
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";
|
||||
featureId: string;
|
||||
projectId?: string;
|
||||
feature: unknown;
|
||||
}
|
||||
| {
|
||||
type: "auto_mode_progress";
|
||||
featureId: string;
|
||||
projectId?: string;
|
||||
content: string;
|
||||
}
|
||||
| {
|
||||
type: "auto_mode_tool";
|
||||
featureId: string;
|
||||
projectId?: string;
|
||||
tool: string;
|
||||
input: unknown;
|
||||
}
|
||||
| {
|
||||
type: "auto_mode_feature_complete";
|
||||
featureId: string;
|
||||
projectId?: string;
|
||||
passes: boolean;
|
||||
message: string;
|
||||
}
|
||||
@@ -185,18 +189,72 @@ export type AutoModeEvent =
|
||||
type: "auto_mode_error";
|
||||
error: string;
|
||||
featureId?: string;
|
||||
projectId?: string;
|
||||
}
|
||||
| {
|
||||
type: "auto_mode_complete";
|
||||
message: string;
|
||||
projectId?: string;
|
||||
}
|
||||
| {
|
||||
type: "auto_mode_phase";
|
||||
featureId: string;
|
||||
projectId?: string;
|
||||
phase: "planning" | "action" | "verification";
|
||||
message: 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 {
|
||||
start: (projectPath: string) => Promise<{
|
||||
success: boolean;
|
||||
@@ -316,6 +374,9 @@ export interface ElectronAPI {
|
||||
|
||||
// Auto Mode APIs
|
||||
autoMode: AutoModeAPI;
|
||||
|
||||
// Spec Regeneration APIs
|
||||
specRegeneration: SpecRegenerationAPI;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
Reference in New Issue
Block a user