mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
Merge remote-tracking branch 'origin/main' into feat/extend-models-support
This commit is contained in:
@@ -331,4 +331,4 @@
|
||||
"model": "sonnet",
|
||||
"thinkingLevel": "medium"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -8,6 +8,8 @@ const fs = require("fs/promises");
|
||||
const agentService = require("./agent-service");
|
||||
const autoModeService = require("./auto-mode-service");
|
||||
const worktreeManager = require("./services/worktree-manager");
|
||||
const featureSuggestionsService = require("./services/feature-suggestions-service");
|
||||
const specRegenerationService = require("./services/spec-regeneration-service");
|
||||
|
||||
let mainWindow = null;
|
||||
|
||||
@@ -769,6 +771,91 @@ ipcMain.handle("mcp:update-feature-status", async (_, { featureId, status, proje
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Feature Suggestions IPC Handlers
|
||||
// ============================================================================
|
||||
|
||||
// Track running suggestions analysis
|
||||
let suggestionsExecution = null;
|
||||
|
||||
/**
|
||||
* Generate feature suggestions by analyzing the project
|
||||
*/
|
||||
ipcMain.handle(
|
||||
"suggestions:generate",
|
||||
async (_, { projectPath }) => {
|
||||
console.log("[IPC] suggestions:generate called with:", { projectPath });
|
||||
|
||||
try {
|
||||
// Check if already running
|
||||
if (suggestionsExecution && suggestionsExecution.isActive()) {
|
||||
return { success: false, error: "Suggestions generation is already running" };
|
||||
}
|
||||
|
||||
// Create execution context
|
||||
suggestionsExecution = {
|
||||
abortController: null,
|
||||
query: null,
|
||||
isActive: () => suggestionsExecution !== null,
|
||||
};
|
||||
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("suggestions:event", data);
|
||||
}
|
||||
};
|
||||
|
||||
// Start generating suggestions (runs in background)
|
||||
featureSuggestionsService
|
||||
.generateSuggestions(projectPath, sendToRenderer, suggestionsExecution)
|
||||
.catch((error) => {
|
||||
console.error("[IPC] suggestions:generate background error:", error);
|
||||
sendToRenderer({
|
||||
type: "suggestions_error",
|
||||
error: error.message,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
suggestionsExecution = null;
|
||||
});
|
||||
|
||||
// Return immediately
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("[IPC] suggestions:generate error:", error);
|
||||
suggestionsExecution = null;
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Stop the current suggestions generation
|
||||
*/
|
||||
ipcMain.handle("suggestions:stop", async () => {
|
||||
console.log("[IPC] suggestions:stop called");
|
||||
try {
|
||||
if (suggestionsExecution && suggestionsExecution.abortController) {
|
||||
suggestionsExecution.abortController.abort();
|
||||
}
|
||||
suggestionsExecution = null;
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("[IPC] suggestions:stop error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get suggestions generation status
|
||||
*/
|
||||
ipcMain.handle("suggestions:status", () => {
|
||||
return {
|
||||
success: true,
|
||||
isRunning: suggestionsExecution !== null && suggestionsExecution.isActive(),
|
||||
};
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// OpenAI API Handlers
|
||||
// ============================================================================
|
||||
@@ -840,6 +927,142 @@ ipcMain.handle(
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Spec Regeneration IPC Handlers
|
||||
// ============================================================================
|
||||
|
||||
// Track running spec regeneration
|
||||
let specRegenerationExecution = null;
|
||||
|
||||
/**
|
||||
* Regenerate the app spec based on project definition
|
||||
*/
|
||||
ipcMain.handle(
|
||||
"spec-regeneration:generate",
|
||||
async (_, { projectPath, projectDefinition }) => {
|
||||
console.log("[IPC] spec-regeneration:generate called with:", { projectPath });
|
||||
|
||||
try {
|
||||
// Check if already running
|
||||
if (specRegenerationExecution && specRegenerationExecution.isActive()) {
|
||||
return { success: false, error: "Spec regeneration is already running" };
|
||||
}
|
||||
|
||||
// Create execution context
|
||||
specRegenerationExecution = {
|
||||
abortController: null,
|
||||
query: null,
|
||||
isActive: () => specRegenerationExecution !== null,
|
||||
};
|
||||
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("spec-regeneration:event", data);
|
||||
}
|
||||
};
|
||||
|
||||
// Start regenerating spec (runs in background)
|
||||
specRegenerationService
|
||||
.regenerateSpec(projectPath, projectDefinition, sendToRenderer, specRegenerationExecution)
|
||||
.catch((error) => {
|
||||
console.error("[IPC] spec-regeneration:generate background error:", error);
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_error",
|
||||
error: error.message,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
specRegenerationExecution = null;
|
||||
});
|
||||
|
||||
// Return immediately
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("[IPC] spec-regeneration:generate error:", error);
|
||||
specRegenerationExecution = null;
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Stop the current spec regeneration
|
||||
*/
|
||||
ipcMain.handle("spec-regeneration:stop", async () => {
|
||||
console.log("[IPC] spec-regeneration:stop called");
|
||||
try {
|
||||
if (specRegenerationExecution && specRegenerationExecution.abortController) {
|
||||
specRegenerationExecution.abortController.abort();
|
||||
}
|
||||
specRegenerationExecution = null;
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("[IPC] spec-regeneration:stop error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get spec regeneration status
|
||||
*/
|
||||
ipcMain.handle("spec-regeneration:status", () => {
|
||||
return {
|
||||
success: true,
|
||||
isRunning: specRegenerationExecution !== null && specRegenerationExecution.isActive(),
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Create initial app spec for a new project
|
||||
*/
|
||||
ipcMain.handle(
|
||||
"spec-regeneration:create",
|
||||
async (_, { projectPath, projectOverview, generateFeatures = true }) => {
|
||||
console.log("[IPC] spec-regeneration:create called with:", { projectPath, generateFeatures });
|
||||
|
||||
try {
|
||||
// Check if already running
|
||||
if (specRegenerationExecution && specRegenerationExecution.isActive()) {
|
||||
return { success: false, error: "Spec creation is already running" };
|
||||
}
|
||||
|
||||
// Create execution context
|
||||
specRegenerationExecution = {
|
||||
abortController: null,
|
||||
query: null,
|
||||
isActive: () => specRegenerationExecution !== null,
|
||||
};
|
||||
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("spec-regeneration:event", data);
|
||||
}
|
||||
};
|
||||
|
||||
// Start creating spec (runs in background)
|
||||
specRegenerationService
|
||||
.createInitialSpec(projectPath, projectOverview, sendToRenderer, specRegenerationExecution, generateFeatures)
|
||||
.catch((error) => {
|
||||
console.error("[IPC] spec-regeneration:create background error:", error);
|
||||
sendToRenderer({
|
||||
type: "spec_regeneration_error",
|
||||
error: error.message,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
specRegenerationExecution = null;
|
||||
});
|
||||
|
||||
// Return immediately
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("[IPC] spec-regeneration:create error:", error);
|
||||
specRegenerationExecution = null;
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Merge feature worktree changes back to main branch
|
||||
*/
|
||||
@@ -870,7 +1093,6 @@ ipcMain.handle(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Get worktree info for a feature
|
||||
*/
|
||||
|
||||
@@ -200,6 +200,58 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
getFileDiff: (projectPath, filePath) =>
|
||||
ipcRenderer.invoke("git:get-file-diff", { projectPath, filePath }),
|
||||
},
|
||||
|
||||
// Feature Suggestions API
|
||||
suggestions: {
|
||||
// Generate feature suggestions
|
||||
generate: (projectPath) =>
|
||||
ipcRenderer.invoke("suggestions:generate", { projectPath }),
|
||||
|
||||
// Stop generating suggestions
|
||||
stop: () => ipcRenderer.invoke("suggestions:stop"),
|
||||
|
||||
// Get suggestions status
|
||||
status: () => ipcRenderer.invoke("suggestions:status"),
|
||||
|
||||
// Listen for suggestions events
|
||||
onEvent: (callback) => {
|
||||
const subscription = (_, data) => callback(data);
|
||||
ipcRenderer.on("suggestions:event", subscription);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
ipcRenderer.removeListener("suggestions:event", subscription);
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Spec Regeneration API
|
||||
specRegeneration: {
|
||||
// Create initial app spec for a new project
|
||||
create: (projectPath, projectOverview, generateFeatures = true) =>
|
||||
ipcRenderer.invoke("spec-regeneration:create", { projectPath, projectOverview, generateFeatures }),
|
||||
|
||||
// Regenerate the app spec
|
||||
generate: (projectPath, projectDefinition) =>
|
||||
ipcRenderer.invoke("spec-regeneration:generate", { projectPath, projectDefinition }),
|
||||
|
||||
// Stop regenerating spec
|
||||
stop: () => ipcRenderer.invoke("spec-regeneration:stop"),
|
||||
|
||||
// Get regeneration status
|
||||
status: () => ipcRenderer.invoke("spec-regeneration:status"),
|
||||
|
||||
// Listen for regeneration events
|
||||
onEvent: (callback) => {
|
||||
const subscription = (_, data) => callback(data);
|
||||
ipcRenderer.on("spec-regeneration:event", subscription);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
ipcRenderer.removeListener("spec-regeneration:event", subscription);
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Also expose a flag to detect if we're in Electron
|
||||
|
||||
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,
|
||||
UserCircle,
|
||||
@@ -45,8 +48,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,
|
||||
@@ -156,6 +166,7 @@ export function Sidebar() {
|
||||
currentProject,
|
||||
currentView,
|
||||
sidebarOpen,
|
||||
projectHistory,
|
||||
addProject,
|
||||
setCurrentProject,
|
||||
setCurrentView,
|
||||
@@ -164,6 +175,8 @@ export function Sidebar() {
|
||||
deleteTrashedProject,
|
||||
emptyTrash,
|
||||
reorderProjects,
|
||||
cyclePrevProject,
|
||||
cycleNextProject,
|
||||
} = useAppStore();
|
||||
|
||||
// State for project picker dropdown
|
||||
@@ -172,6 +185,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, {
|
||||
@@ -198,6 +222,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.
|
||||
@@ -211,6 +322,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);
|
||||
|
||||
@@ -231,7 +345,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",
|
||||
{
|
||||
@@ -428,6 +555,20 @@ export function Sidebar() {
|
||||
});
|
||||
}
|
||||
|
||||
// Project cycling shortcuts - only when we have project history
|
||||
if (projectHistory.length > 1) {
|
||||
shortcuts.push({
|
||||
key: ACTION_SHORTCUTS.cyclePrevProject,
|
||||
action: () => cyclePrevProject(),
|
||||
description: "Cycle to previous project (MRU)",
|
||||
});
|
||||
shortcuts.push({
|
||||
key: ACTION_SHORTCUTS.cycleNextProject,
|
||||
action: () => cycleNextProject(),
|
||||
description: "Cycle to next project (LRU)",
|
||||
});
|
||||
}
|
||||
|
||||
// Only enable nav shortcuts if there's a current project
|
||||
if (currentProject) {
|
||||
navSections.forEach((section) => {
|
||||
@@ -457,6 +598,9 @@ export function Sidebar() {
|
||||
toggleSidebar,
|
||||
projects.length,
|
||||
handleOpenFolder,
|
||||
projectHistory.length,
|
||||
cyclePrevProject,
|
||||
cycleNextProject,
|
||||
]);
|
||||
|
||||
// Register keyboard shortcuts
|
||||
@@ -470,7 +614,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"
|
||||
>
|
||||
@@ -572,16 +716,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">
|
||||
@@ -631,6 +775,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>
|
||||
)}
|
||||
|
||||
@@ -871,6 +1043,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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
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,
|
||||
@@ -77,6 +78,7 @@ import {
|
||||
Rocket,
|
||||
Sparkles,
|
||||
UserCircle,
|
||||
Lightbulb,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
@@ -180,7 +182,6 @@ export function BoardView() {
|
||||
updateFeature,
|
||||
removeFeature,
|
||||
moveFeature,
|
||||
runningAutoTasks,
|
||||
maxConcurrency,
|
||||
setMaxConcurrency,
|
||||
defaultSkipTests,
|
||||
@@ -227,6 +228,8 @@ export function BoardView() {
|
||||
// Local state to temporarily show advanced options when profiles-only mode is enabled
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
const [showEditAdvancedOptions, setShowEditAdvancedOptions] = useState(false);
|
||||
const [showSuggestionsDialog, setShowSuggestionsDialog] = useState(false);
|
||||
const [suggestionsCount, setSuggestionsCount] = useState(0);
|
||||
|
||||
// Make current project available globally for modal
|
||||
useEffect(() => {
|
||||
@@ -238,12 +241,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;
|
||||
|
||||
// Window state hook for compact dialog mode
|
||||
const { isMaximized } = useWindowState();
|
||||
@@ -449,11 +470,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...");
|
||||
@@ -467,7 +492,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();
|
||||
@@ -479,7 +504,7 @@ export function BoardView() {
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [loadFeatures]);
|
||||
}, [loadFeatures, currentProject]);
|
||||
|
||||
useEffect(() => {
|
||||
loadFeatures();
|
||||
@@ -492,6 +517,8 @@ export function BoardView() {
|
||||
|
||||
// Sync running tasks from electron backend on mount
|
||||
useEffect(() => {
|
||||
if (!currentProject) return;
|
||||
|
||||
const syncRunningTasks = async () => {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
@@ -504,13 +531,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) {
|
||||
@@ -519,7 +547,7 @@ export function BoardView() {
|
||||
};
|
||||
|
||||
syncRunningTasks();
|
||||
}, []);
|
||||
}, [currentProject]);
|
||||
|
||||
// Check which features have context files
|
||||
useEffect(() => {
|
||||
@@ -1542,21 +1570,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-accent border border-border-glass">
|
||||
{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-accent border border-border-glass">
|
||||
{ACTION_SHORTCUTS.startNext}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
@@ -2512,6 +2561,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)
|
||||
};
|
||||
|
||||
@@ -44,6 +44,47 @@ export interface StatResult {
|
||||
// Auto Mode types - Import from electron.d.ts to avoid duplication
|
||||
import type { AutoModeEvent, ModelDefinition, ProviderStatus, WorktreeAPI, GitAPI, WorktreeInfo, WorktreeStatus, FileDiffsResult, FileDiffResult, FileStatus } from "@/types/electron";
|
||||
|
||||
// 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 }>;
|
||||
@@ -79,7 +120,6 @@ export interface ElectronAPI {
|
||||
trashItem?: (filePath: string) => Promise<WriteResult>;
|
||||
getPath: (name: string) => Promise<string>;
|
||||
saveImageToTemp?: (data: string, filename: string, mimeType: string, projectPath?: string) => Promise<SaveImageResult>;
|
||||
autoMode?: AutoModeAPI;
|
||||
checkClaudeCli?: () => Promise<{
|
||||
success: boolean;
|
||||
status?: string;
|
||||
@@ -130,6 +170,8 @@ export interface ElectronAPI {
|
||||
}>;
|
||||
worktree?: WorktreeAPI;
|
||||
git?: GitAPI;
|
||||
suggestions?: SuggestionsAPI;
|
||||
specRegeneration?: SpecRegenerationAPI;
|
||||
}
|
||||
|
||||
// Note: Window interface is declared in @/types/electron.d.ts
|
||||
@@ -427,6 +469,12 @@ export const getElectronAPI = (): ElectronAPI => {
|
||||
|
||||
// Mock Git API (for non-worktree operations)
|
||||
git: createMockGitAPI(),
|
||||
|
||||
// Mock Suggestions API
|
||||
suggestions: createMockSuggestionsAPI(),
|
||||
|
||||
// Mock Spec Regeneration API
|
||||
specRegeneration: createMockSpecRegenerationAPI(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -924,6 +972,381 @@ function delay(ms: number, featureId: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
// Mock Suggestions state and implementation
|
||||
let mockSuggestionsRunning = false;
|
||||
let mockSuggestionsCallbacks: ((event: SuggestionsEvent) => void)[] = [];
|
||||
let mockSuggestionsTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
function createMockSuggestionsAPI(): SuggestionsAPI {
|
||||
return {
|
||||
generate: async (projectPath: string) => {
|
||||
if (mockSuggestionsRunning) {
|
||||
return { success: false, error: "Suggestions generation is already running" };
|
||||
}
|
||||
|
||||
mockSuggestionsRunning = true;
|
||||
console.log(`[Mock] Generating suggestions for: ${projectPath}`);
|
||||
|
||||
// Simulate async suggestion generation
|
||||
simulateSuggestionsGeneration();
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
stop: async () => {
|
||||
mockSuggestionsRunning = false;
|
||||
if (mockSuggestionsTimeout) {
|
||||
clearTimeout(mockSuggestionsTimeout);
|
||||
mockSuggestionsTimeout = null;
|
||||
}
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
status: async () => {
|
||||
return {
|
||||
success: true,
|
||||
isRunning: mockSuggestionsRunning,
|
||||
};
|
||||
},
|
||||
|
||||
onEvent: (callback: (event: SuggestionsEvent) => void) => {
|
||||
mockSuggestionsCallbacks.push(callback);
|
||||
return () => {
|
||||
mockSuggestionsCallbacks = mockSuggestionsCallbacks.filter(cb => cb !== callback);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function emitSuggestionsEvent(event: SuggestionsEvent) {
|
||||
mockSuggestionsCallbacks.forEach(cb => cb(event));
|
||||
}
|
||||
|
||||
async function simulateSuggestionsGeneration() {
|
||||
// Emit progress events
|
||||
emitSuggestionsEvent({
|
||||
type: "suggestions_progress",
|
||||
content: "Starting project analysis...\n",
|
||||
});
|
||||
|
||||
await new Promise(resolve => {
|
||||
mockSuggestionsTimeout = setTimeout(resolve, 500);
|
||||
});
|
||||
if (!mockSuggestionsRunning) return;
|
||||
|
||||
emitSuggestionsEvent({
|
||||
type: "suggestions_tool",
|
||||
tool: "Glob",
|
||||
input: { pattern: "**/*.{ts,tsx,js,jsx}" },
|
||||
});
|
||||
|
||||
await new Promise(resolve => {
|
||||
mockSuggestionsTimeout = setTimeout(resolve, 500);
|
||||
});
|
||||
if (!mockSuggestionsRunning) return;
|
||||
|
||||
emitSuggestionsEvent({
|
||||
type: "suggestions_progress",
|
||||
content: "Analyzing codebase structure...\n",
|
||||
});
|
||||
|
||||
await new Promise(resolve => {
|
||||
mockSuggestionsTimeout = setTimeout(resolve, 500);
|
||||
});
|
||||
if (!mockSuggestionsRunning) return;
|
||||
|
||||
emitSuggestionsEvent({
|
||||
type: "suggestions_progress",
|
||||
content: "Identifying missing features...\n",
|
||||
});
|
||||
|
||||
await new Promise(resolve => {
|
||||
mockSuggestionsTimeout = setTimeout(resolve, 500);
|
||||
});
|
||||
if (!mockSuggestionsRunning) return;
|
||||
|
||||
// Generate mock suggestions
|
||||
const mockSuggestions: FeatureSuggestion[] = [
|
||||
{
|
||||
id: `suggestion-${Date.now()}-0`,
|
||||
category: "User Experience",
|
||||
description: "Add dark mode toggle with system preference detection",
|
||||
steps: [
|
||||
"Create a ThemeProvider context to manage theme state",
|
||||
"Add a toggle component in the settings or header",
|
||||
"Implement CSS variables for theme colors",
|
||||
"Add localStorage persistence for user preference"
|
||||
],
|
||||
priority: 1,
|
||||
reasoning: "Dark mode is a standard feature that improves accessibility and user comfort"
|
||||
},
|
||||
{
|
||||
id: `suggestion-${Date.now()}-1`,
|
||||
category: "Performance",
|
||||
description: "Implement lazy loading for heavy components",
|
||||
steps: [
|
||||
"Identify components that are heavy or rarely used",
|
||||
"Use React.lazy() and Suspense for code splitting",
|
||||
"Add loading states for lazy-loaded components"
|
||||
],
|
||||
priority: 2,
|
||||
reasoning: "Improves initial load time and reduces bundle size"
|
||||
},
|
||||
{
|
||||
id: `suggestion-${Date.now()}-2`,
|
||||
category: "Accessibility",
|
||||
description: "Add keyboard navigation support throughout the app",
|
||||
steps: [
|
||||
"Implement focus management for modals and dialogs",
|
||||
"Add keyboard shortcuts for common actions",
|
||||
"Ensure all interactive elements are focusable",
|
||||
"Add ARIA labels and roles where needed"
|
||||
],
|
||||
priority: 3,
|
||||
reasoning: "Improves accessibility for users who rely on keyboard navigation"
|
||||
},
|
||||
{
|
||||
id: `suggestion-${Date.now()}-3`,
|
||||
category: "Testing",
|
||||
description: "Add comprehensive unit test coverage",
|
||||
steps: [
|
||||
"Set up Jest and React Testing Library",
|
||||
"Create tests for all utility functions",
|
||||
"Add component tests for critical UI elements",
|
||||
"Set up CI pipeline for automated testing"
|
||||
],
|
||||
priority: 4,
|
||||
reasoning: "Ensures code quality and prevents regressions"
|
||||
},
|
||||
{
|
||||
id: `suggestion-${Date.now()}-4`,
|
||||
category: "Developer Experience",
|
||||
description: "Add Storybook for component documentation",
|
||||
steps: [
|
||||
"Install and configure Storybook",
|
||||
"Create stories for all UI components",
|
||||
"Add interaction tests using play functions",
|
||||
"Set up Chromatic for visual regression testing"
|
||||
],
|
||||
priority: 5,
|
||||
reasoning: "Improves component development workflow and documentation"
|
||||
}
|
||||
];
|
||||
|
||||
emitSuggestionsEvent({
|
||||
type: "suggestions_complete",
|
||||
suggestions: mockSuggestions,
|
||||
});
|
||||
|
||||
mockSuggestionsRunning = false;
|
||||
mockSuggestionsTimeout = null;
|
||||
}
|
||||
|
||||
// Mock Spec Regeneration state and implementation
|
||||
let mockSpecRegenerationRunning = false;
|
||||
let mockSpecRegenerationCallbacks: ((event: SpecRegenerationEvent) => void)[] = [];
|
||||
let mockSpecRegenerationTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
function createMockSpecRegenerationAPI(): SpecRegenerationAPI {
|
||||
return {
|
||||
create: async (projectPath: string, projectOverview: string, generateFeatures = true) => {
|
||||
if (mockSpecRegenerationRunning) {
|
||||
return { success: false, error: "Spec creation is already running" };
|
||||
}
|
||||
|
||||
mockSpecRegenerationRunning = true;
|
||||
console.log(`[Mock] Creating initial spec for: ${projectPath}, generateFeatures: ${generateFeatures}`);
|
||||
|
||||
// Simulate async spec creation
|
||||
simulateSpecCreation(projectPath, projectOverview, generateFeatures);
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
generate: async (projectPath: string, projectDefinition: string) => {
|
||||
if (mockSpecRegenerationRunning) {
|
||||
return { success: false, error: "Spec regeneration is already running" };
|
||||
}
|
||||
|
||||
mockSpecRegenerationRunning = true;
|
||||
console.log(`[Mock] Regenerating spec for: ${projectPath}`);
|
||||
|
||||
// Simulate async spec regeneration
|
||||
simulateSpecRegeneration(projectPath, projectDefinition);
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
stop: async () => {
|
||||
mockSpecRegenerationRunning = false;
|
||||
if (mockSpecRegenerationTimeout) {
|
||||
clearTimeout(mockSpecRegenerationTimeout);
|
||||
mockSpecRegenerationTimeout = null;
|
||||
}
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
status: async () => {
|
||||
return {
|
||||
success: true,
|
||||
isRunning: mockSpecRegenerationRunning,
|
||||
};
|
||||
},
|
||||
|
||||
onEvent: (callback: (event: SpecRegenerationEvent) => void) => {
|
||||
mockSpecRegenerationCallbacks.push(callback);
|
||||
return () => {
|
||||
mockSpecRegenerationCallbacks = mockSpecRegenerationCallbacks.filter(cb => cb !== callback);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function emitSpecRegenerationEvent(event: SpecRegenerationEvent) {
|
||||
mockSpecRegenerationCallbacks.forEach(cb => cb(event));
|
||||
}
|
||||
|
||||
async function simulateSpecCreation(projectPath: string, projectOverview: string, generateFeatures = true) {
|
||||
emitSpecRegenerationEvent({
|
||||
type: "spec_regeneration_progress",
|
||||
content: "Starting project analysis...\n",
|
||||
});
|
||||
|
||||
await new Promise(resolve => {
|
||||
mockSpecRegenerationTimeout = setTimeout(resolve, 500);
|
||||
});
|
||||
if (!mockSpecRegenerationRunning) return;
|
||||
|
||||
emitSpecRegenerationEvent({
|
||||
type: "spec_regeneration_tool",
|
||||
tool: "Glob",
|
||||
input: { pattern: "**/*.{json,ts,tsx}" },
|
||||
});
|
||||
|
||||
await new Promise(resolve => {
|
||||
mockSpecRegenerationTimeout = setTimeout(resolve, 500);
|
||||
});
|
||||
if (!mockSpecRegenerationRunning) return;
|
||||
|
||||
emitSpecRegenerationEvent({
|
||||
type: "spec_regeneration_progress",
|
||||
content: "Detecting tech stack...\n",
|
||||
});
|
||||
|
||||
await new Promise(resolve => {
|
||||
mockSpecRegenerationTimeout = setTimeout(resolve, 500);
|
||||
});
|
||||
if (!mockSpecRegenerationRunning) return;
|
||||
|
||||
// Write mock app_spec.txt
|
||||
mockFileSystem[`${projectPath}/.automaker/app_spec.txt`] = `<project_specification>
|
||||
<project_name>Demo Project</project_name>
|
||||
|
||||
<overview>
|
||||
${projectOverview}
|
||||
</overview>
|
||||
|
||||
<technology_stack>
|
||||
<frontend>
|
||||
<framework>Next.js</framework>
|
||||
<ui_library>React</ui_library>
|
||||
<styling>Tailwind CSS</styling>
|
||||
</frontend>
|
||||
</technology_stack>
|
||||
|
||||
<core_capabilities>
|
||||
<feature_1>Core functionality based on overview</feature_1>
|
||||
</core_capabilities>
|
||||
|
||||
<implementation_roadmap>
|
||||
<phase_1_foundation>Setup and basic structure</phase_1_foundation>
|
||||
<phase_2_core_logic>Core features implementation</phase_2_core_logic>
|
||||
</implementation_roadmap>
|
||||
</project_specification>`;
|
||||
|
||||
// If generateFeatures is true, also write feature_list.json
|
||||
if (generateFeatures) {
|
||||
const timestamp = Date.now();
|
||||
mockFileSystem[`${projectPath}/.automaker/feature_list.json`] = JSON.stringify([
|
||||
{
|
||||
id: `feature-${timestamp}-0`,
|
||||
category: "Phase 1: Foundation",
|
||||
description: "Setup and basic structure",
|
||||
status: "backlog",
|
||||
steps: ["Initialize project", "Configure dependencies"],
|
||||
skipTests: true,
|
||||
},
|
||||
{
|
||||
id: `feature-${timestamp}-1`,
|
||||
category: "Phase 2: Core Logic",
|
||||
description: "Core features implementation",
|
||||
status: "backlog",
|
||||
steps: ["Implement core functionality", "Add business logic"],
|
||||
skipTests: true,
|
||||
},
|
||||
], null, 2);
|
||||
}
|
||||
|
||||
emitSpecRegenerationEvent({
|
||||
type: "spec_regeneration_complete",
|
||||
message: "Initial spec creation complete!",
|
||||
});
|
||||
|
||||
mockSpecRegenerationRunning = false;
|
||||
mockSpecRegenerationTimeout = null;
|
||||
}
|
||||
|
||||
async function simulateSpecRegeneration(projectPath: string, projectDefinition: string) {
|
||||
emitSpecRegenerationEvent({
|
||||
type: "spec_regeneration_progress",
|
||||
content: "Starting spec regeneration...\n",
|
||||
});
|
||||
|
||||
await new Promise(resolve => {
|
||||
mockSpecRegenerationTimeout = setTimeout(resolve, 500);
|
||||
});
|
||||
if (!mockSpecRegenerationRunning) return;
|
||||
|
||||
emitSpecRegenerationEvent({
|
||||
type: "spec_regeneration_progress",
|
||||
content: "Analyzing codebase...\n",
|
||||
});
|
||||
|
||||
await new Promise(resolve => {
|
||||
mockSpecRegenerationTimeout = setTimeout(resolve, 500);
|
||||
});
|
||||
if (!mockSpecRegenerationRunning) return;
|
||||
|
||||
// Write regenerated spec
|
||||
mockFileSystem[`${projectPath}/.automaker/app_spec.txt`] = `<project_specification>
|
||||
<project_name>Regenerated Project</project_name>
|
||||
|
||||
<overview>
|
||||
${projectDefinition}
|
||||
</overview>
|
||||
|
||||
<technology_stack>
|
||||
<frontend>
|
||||
<framework>Next.js</framework>
|
||||
<ui_library>React</ui_library>
|
||||
<styling>Tailwind CSS</styling>
|
||||
</frontend>
|
||||
</technology_stack>
|
||||
|
||||
<core_capabilities>
|
||||
<feature_1>Regenerated features based on definition</feature_1>
|
||||
</core_capabilities>
|
||||
</project_specification>`;
|
||||
|
||||
emitSpecRegenerationEvent({
|
||||
type: "spec_regeneration_complete",
|
||||
message: "Spec regeneration complete!",
|
||||
});
|
||||
|
||||
mockSpecRegenerationRunning = false;
|
||||
mockSpecRegenerationTimeout = null;
|
||||
}
|
||||
|
||||
// Utility functions for project management
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,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;
|
||||
@@ -174,9 +176,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
|
||||
|
||||
@@ -230,6 +234,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;
|
||||
@@ -266,11 +272,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;
|
||||
@@ -362,6 +369,8 @@ const initialState: AppState = {
|
||||
projects: [],
|
||||
currentProject: null,
|
||||
trashedProjects: [],
|
||||
projectHistory: [],
|
||||
projectHistoryIndex: -1,
|
||||
currentView: "welcome",
|
||||
sidebarOpen: true,
|
||||
theme: "dark",
|
||||
@@ -376,8 +385,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
|
||||
@@ -508,11 +516,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 }),
|
||||
@@ -667,25 +723,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()
|
||||
@@ -766,6 +860,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,14 +189,17 @@ 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;
|
||||
}
|
||||
@@ -205,6 +212,57 @@ export type AutoModeEvent =
|
||||
estimatedTime?: string;
|
||||
};
|
||||
|
||||
export type SpecRegenerationEvent =
|
||||
| {
|
||||
type: "spec_regeneration_progress";
|
||||
content: string;
|
||||
}
|
||||
| {
|
||||
type: "spec_regeneration_tool";
|
||||
tool: string;
|
||||
input: unknown;
|
||||
}
|
||||
| {
|
||||
type: "spec_regeneration_complete";
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
type: "spec_regeneration_error";
|
||||
error: string;
|
||||
};
|
||||
|
||||
export interface SpecRegenerationAPI {
|
||||
create: (
|
||||
projectPath: string,
|
||||
projectOverview: string,
|
||||
generateFeatures?: boolean
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
generate: (
|
||||
projectPath: string,
|
||||
projectDefinition: string
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
stop: () => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
status: () => Promise<{
|
||||
success: boolean;
|
||||
isRunning?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
onEvent: (callback: (event: SpecRegenerationEvent) => void) => () => void;
|
||||
}
|
||||
|
||||
export interface AutoModeAPI {
|
||||
start: (projectPath: string) => Promise<{
|
||||
success: boolean;
|
||||
@@ -411,6 +469,9 @@ export interface ElectronAPI {
|
||||
|
||||
// Git Operations APIs (for non-worktree operations)
|
||||
git: GitAPI;
|
||||
|
||||
// Spec Regeneration APIs
|
||||
specRegeneration: SpecRegenerationAPI;
|
||||
}
|
||||
|
||||
export interface WorktreeInfo {
|
||||
|
||||
Reference in New Issue
Block a user