diff --git a/.automaker/features/feature-1765414180387-4zcc7wpdv/feature.json b/.automaker/features/feature-1765414180387-4zcc7wpdv/feature.json deleted file mode 100644 index 44e88b55..00000000 --- a/.automaker/features/feature-1765414180387-4zcc7wpdv/feature.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "category": "Core", - "description": "do nothing, code nothing, print yolo", - "steps": [], - "status": "waiting_approval", - "images": [], - "imagePaths": [], - "skipTests": true, - "model": "opus", - "thinkingLevel": "none", - "id": "feature-1765414180387-4zcc7wpdv", - "startedAt": "2025-12-11T00:49:41.713Z", - "summary": "No code changes required. Feature requested 'do nothing, code nothing, print yolo' - completed as specified. YOLO!" -} \ No newline at end of file diff --git a/.automaker/images/1765405989164-sxpyqufh4_Screenshot_2025-12-10_at_5.33.08_PM.png b/.automaker/images/1765405989164-sxpyqufh4_Screenshot_2025-12-10_at_5.33.08_PM.png deleted file mode 100644 index c383cdfe..00000000 Binary files a/.automaker/images/1765405989164-sxpyqufh4_Screenshot_2025-12-10_at_5.33.08_PM.png and /dev/null differ diff --git a/.automaker/images/1765408161734-damvl2960_Screenshot_2025-12-10_at_6.09.19_PM.png b/.automaker/images/1765408161734-damvl2960_Screenshot_2025-12-10_at_6.09.19_PM.png deleted file mode 100644 index 77e03afa..00000000 Binary files a/.automaker/images/1765408161734-damvl2960_Screenshot_2025-12-10_at_6.09.19_PM.png and /dev/null differ diff --git a/app/electron/auto-mode-service.js b/app/electron/auto-mode-service.js index fbc2f232..20c75246 100644 --- a/app/electron/auto-mode-service.js +++ b/app/electron/auto-mode-service.js @@ -20,11 +20,64 @@ class AutoModeService { constructor() { // Track multiple concurrent feature executions this.runningFeatures = new Map(); // featureId -> { abortController, query, projectPath, sendToRenderer } - this.autoLoopRunning = false; // Separate flag for the auto loop - this.autoLoopAbortController = null; - this.autoLoopInterval = null; // Timer for periodic checking + + // Per-project auto loop state (keyed by projectPath) + this.projectLoops = new Map(); // projectPath -> { isRunning, interval, abortController, sendToRenderer, maxConcurrency } + this.checkIntervalMs = 5000; // Check every 5 seconds - this.maxConcurrency = 3; // Default max concurrency + this.maxConcurrency = 3; // Default max concurrency (global default) + } + + /** + * Get or create project loop state + */ + getProjectLoopState(projectPath) { + if (!this.projectLoops.has(projectPath)) { + this.projectLoops.set(projectPath, { + isRunning: false, + interval: null, + abortController: null, + sendToRenderer: null, + maxConcurrency: this.maxConcurrency, + }); + } + return this.projectLoops.get(projectPath); + } + + /** + * Check if any project has auto mode running + */ + hasAnyAutoLoopRunning() { + for (const [, state] of this.projectLoops) { + if (state.isRunning) return true; + } + return false; + } + + /** + * Get running features for a specific project + */ + getRunningFeaturesForProject(projectPath) { + const features = []; + for (const [featureId, execution] of this.runningFeatures) { + if (execution.projectPath === projectPath) { + features.push(featureId); + } + } + return features; + } + + /** + * Count running features for a specific project + */ + getRunningCountForProject(projectPath) { + let count = 0; + for (const [, execution] of this.runningFeatures) { + if (execution.projectPath === projectPath) { + count++; + } + } + return count; } /** @@ -43,6 +96,18 @@ class AutoModeService { return context; } + /** + * Helper to emit event with projectPath included + */ + emitEvent(projectPath, sendToRenderer, event) { + if (sendToRenderer) { + sendToRenderer({ + ...event, + projectPath, + }); + } + } + /** * Setup worktree for a feature * Creates an isolated git worktree where the agent can work @@ -65,7 +130,7 @@ class AutoModeService { return { useWorktree: false, workPath: projectPath }; } - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_progress", featureId: feature.id, content: "Creating isolated worktree for feature...\n", @@ -75,7 +140,7 @@ class AutoModeService { if (!result.success) { console.warn(`[AutoMode] Failed to create worktree: ${result.error}. Falling back to main project.`); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_progress", featureId: feature.id, content: `Warning: Could not create worktree (${result.error}). Working directly on main project.\n`, @@ -84,7 +149,7 @@ class AutoModeService { } console.log(`[AutoMode] Created worktree at: ${result.worktreePath}, branch: ${result.branchName}`); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_progress", featureId: feature.id, content: `Working in isolated branch: ${result.branchName}\n`, @@ -107,46 +172,56 @@ class AutoModeService { } /** - * Start auto mode - continuously implement features + * Start auto mode for a specific project - continuously implement features + * Each project can have its own independent auto mode loop */ async start({ projectPath, sendToRenderer, maxConcurrency }) { - if (this.autoLoopRunning) { - throw new Error("Auto mode loop is already running"); + const projectState = this.getProjectLoopState(projectPath); + + if (projectState.isRunning) { + throw new Error(`Auto mode loop is already running for project: ${projectPath}`); } - this.autoLoopRunning = true; - this.maxConcurrency = maxConcurrency || 3; + projectState.isRunning = true; + projectState.maxConcurrency = maxConcurrency || 3; + projectState.sendToRenderer = sendToRenderer; console.log( - `[AutoMode] Starting auto mode for project: ${projectPath} with max concurrency: ${this.maxConcurrency}` + `[AutoMode] Starting auto mode for project: ${projectPath} with max concurrency: ${projectState.maxConcurrency}` ); - // Start the periodic checking loop - this.runPeriodicLoop(projectPath, sendToRenderer); + // Start the periodic checking loop for this project + this.runPeriodicLoopForProject(projectPath); return { success: true }; } /** - * Stop auto mode - stops the auto loop but lets running features complete + * Stop auto mode for a specific project - stops the auto loop but lets running features complete * This only turns off the auto toggle to prevent picking up new features. * Running tasks will continue until they complete naturally. */ - async stop() { - console.log("[AutoMode] Stopping auto mode (letting running features complete)"); + async stop({ projectPath }) { + console.log(`[AutoMode] Stopping auto mode for project: ${projectPath} (letting running features complete)`); - this.autoLoopRunning = false; + const projectState = this.projectLoops.get(projectPath); + if (!projectState) { + console.log(`[AutoMode] No auto mode state found for project: ${projectPath}`); + return { success: true, runningFeatures: 0 }; + } - // Clear the interval timer - if (this.autoLoopInterval) { - clearInterval(this.autoLoopInterval); - this.autoLoopInterval = null; + projectState.isRunning = false; + + // Clear the interval timer for this project + if (projectState.interval) { + clearInterval(projectState.interval); + projectState.interval = null; } // Abort auto loop if running - if (this.autoLoopAbortController) { - this.autoLoopAbortController.abort(); - this.autoLoopAbortController = null; + if (projectState.abortController) { + projectState.abortController.abort(); + projectState.abortController = null; } // NOTE: We intentionally do NOT abort running features here. @@ -154,23 +229,58 @@ class AutoModeService { // from being picked up. Running features will complete naturally. // Use stopFeature() to cancel a specific running feature if needed. - const runningCount = this.runningFeatures.size; - console.log(`[AutoMode] Auto loop stopped. ${runningCount} feature(s) still running and will complete.`); + const runningCount = this.getRunningCountForProject(projectPath); + console.log(`[AutoMode] Auto loop stopped for ${projectPath}. ${runningCount} feature(s) still running and will complete.`); return { success: true, runningFeatures: runningCount }; } /** - * Get status of auto mode + * Get status of auto mode (global and per-project) */ - getStatus() { + getStatus({ projectPath } = {}) { + // If projectPath is specified, return status for that project + if (projectPath) { + const projectState = this.projectLoops.get(projectPath); + return { + autoLoopRunning: projectState?.isRunning || false, + runningFeatures: this.getRunningFeaturesForProject(projectPath), + runningCount: this.getRunningCountForProject(projectPath), + }; + } + + // Otherwise return global status + const allRunningProjects = []; + for (const [path, state] of this.projectLoops) { + if (state.isRunning) { + allRunningProjects.push(path); + } + } + return { - autoLoopRunning: this.autoLoopRunning, + autoLoopRunning: this.hasAnyAutoLoopRunning(), + runningProjects: allRunningProjects, runningFeatures: Array.from(this.runningFeatures.keys()), runningCount: this.runningFeatures.size, }; } + /** + * Get status for all projects with auto mode + */ + getAllProjectStatuses() { + const statuses = {}; + for (const [projectPath, state] of this.projectLoops) { + statuses[projectPath] = { + isRunning: state.isRunning, + runningFeatures: this.getRunningFeaturesForProject(projectPath), + runningCount: this.getRunningCountForProject(projectPath), + maxConcurrency: state.maxConcurrency, + }; + } + return statuses; + } + /** * Run a specific feature by ID * @param {string} projectPath - Path to the project @@ -218,7 +328,7 @@ class AutoModeService { projectPath ); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_start", featureId: feature.id, feature: { ...feature, worktreePath: worktreeSetup.workPath, branchName: worktreeSetup.branchName }, @@ -253,7 +363,7 @@ class AutoModeService { // Keep context file for viewing output later (deleted only when card is removed) - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_complete", featureId: feature.id, passes: result.passes, @@ -288,7 +398,7 @@ class AutoModeService { console.error("[AutoMode] Failed to update feature status after error:", statusError); } - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_error", error: error.message, featureId: featureId, @@ -333,7 +443,7 @@ class AutoModeService { console.log(`[AutoMode] Verifying feature: ${feature.description}`); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_start", featureId: feature.id, feature: feature, @@ -357,7 +467,7 @@ class AutoModeService { // Keep context file for viewing output later (deleted only when card is removed) - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_complete", featureId: feature.id, passes: result.passes, @@ -392,7 +502,7 @@ class AutoModeService { console.error("[AutoMode] Failed to update feature status after error:", statusError); } - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_error", error: error.message, featureId: featureId, @@ -437,7 +547,7 @@ class AutoModeService { console.log(`[AutoMode] Resuming feature: ${feature.description}`); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_start", featureId: feature.id, feature: feature, @@ -481,7 +591,7 @@ class AutoModeService { `\n\nšŸ”„ Auto-retry #${attempts} - Continuing implementation...\n\n` ); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_progress", featureId: feature.id, content: `\nšŸ”„ Auto-retry #${attempts} - Agent ended early, continuing...\n`, @@ -524,7 +634,7 @@ class AutoModeService { // Keep context file for viewing output later (deleted only when card is removed) - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_complete", featureId: feature.id, passes: finalResult.passes, @@ -559,7 +669,7 @@ class AutoModeService { console.error("[AutoMode] Failed to update feature status after error:", statusError); } - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_error", error: error.message, featureId: featureId, @@ -572,42 +682,52 @@ class AutoModeService { } /** - * New periodic loop - checks available slots and starts features up to max concurrency + * New periodic loop for a specific project - checks available slots and starts features up to max concurrency * This loop continues running even if there are no backlog items */ - runPeriodicLoop(projectPath, sendToRenderer) { + runPeriodicLoopForProject(projectPath) { + const projectState = this.getProjectLoopState(projectPath); + console.log( - `[AutoMode] Starting periodic loop with interval: ${this.checkIntervalMs}ms` + `[AutoMode] Starting periodic loop for ${projectPath} with interval: ${this.checkIntervalMs}ms` ); // Initial check immediately - this.checkAndStartFeatures(projectPath, sendToRenderer); + this.checkAndStartFeaturesForProject(projectPath); // Then check periodically - this.autoLoopInterval = setInterval(() => { - if (this.autoLoopRunning) { - this.checkAndStartFeatures(projectPath, sendToRenderer); + projectState.interval = setInterval(() => { + if (projectState.isRunning) { + this.checkAndStartFeaturesForProject(projectPath); } }, this.checkIntervalMs); } /** - * Check how many features are running and start new ones if under max concurrency + * Check how many features are running for a specific project and start new ones if under max concurrency */ - async checkAndStartFeatures(projectPath, sendToRenderer) { + async checkAndStartFeaturesForProject(projectPath) { + const projectState = this.projectLoops.get(projectPath); + if (!projectState || !projectState.isRunning) { + return; + } + + const sendToRenderer = projectState.sendToRenderer; + const maxConcurrency = projectState.maxConcurrency; + try { - // Check how many are currently running - const currentRunningCount = this.runningFeatures.size; + // Check how many are currently running FOR THIS PROJECT + const currentRunningCount = this.getRunningCountForProject(projectPath); console.log( - `[AutoMode] Checking features - Running: ${currentRunningCount}/${this.maxConcurrency}` + `[AutoMode] [${projectPath}] Checking features - Running: ${currentRunningCount}/${maxConcurrency}` ); - // Calculate available slots - const availableSlots = this.maxConcurrency - currentRunningCount; + // Calculate available slots for this project + const availableSlots = maxConcurrency - currentRunningCount; if (availableSlots <= 0) { - console.log("[AutoMode] At max concurrency, waiting..."); + console.log(`[AutoMode] [${projectPath}] At max concurrency, waiting...`); return; } @@ -616,7 +736,7 @@ class AutoModeService { const backlogFeatures = features.filter((f) => f.status === "backlog"); if (backlogFeatures.length === 0) { - console.log("[AutoMode] No backlog features available, waiting..."); + console.log(`[AutoMode] [${projectPath}] No backlog features available, waiting...`); return; } @@ -624,7 +744,7 @@ class AutoModeService { const featuresToStart = backlogFeatures.slice(0, availableSlots); console.log( - `[AutoMode] Starting ${featuresToStart.length} feature(s) from backlog` + `[AutoMode] [${projectPath}] Starting ${featuresToStart.length} feature(s) from backlog` ); // Start each feature (don't await - run in parallel like drag operations) @@ -632,7 +752,7 @@ class AutoModeService { this.startFeatureAsync(feature, projectPath, sendToRenderer); } } catch (error) { - console.error("[AutoMode] Error checking/starting features:", error); + console.error(`[AutoMode] [${projectPath}] Error checking/starting features:`, error); } } @@ -678,7 +798,7 @@ class AutoModeService { projectPath ); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_start", featureId: feature.id, feature: { ...feature, worktreePath: worktreeSetup.workPath, branchName: worktreeSetup.branchName }, @@ -713,7 +833,7 @@ class AutoModeService { // Keep context file for viewing output later (deleted only when card is removed) - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_complete", featureId: feature.id, passes: result.passes, @@ -746,7 +866,7 @@ class AutoModeService { console.error("[AutoMode] Failed to update feature status after error:", statusError); } - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_error", error: error.message, featureId: featureId, @@ -778,7 +898,7 @@ class AutoModeService { this.runningFeatures.set(analysisId, execution); try { - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_start", featureId: analysisId, feature: { @@ -796,7 +916,7 @@ class AutoModeService { execution ); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_complete", featureId: analysisId, passes: result.success, @@ -806,7 +926,7 @@ class AutoModeService { return { success: true, message: result.message }; } catch (error) { console.error("[AutoMode] Error analyzing project:", error); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_error", error: error.message, featureId: analysisId, @@ -911,7 +1031,7 @@ class AutoModeService { projectPath ); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_start", featureId: feature.id, feature: feature, @@ -956,7 +1076,7 @@ class AutoModeService { // Keep context file for viewing output later (deleted only when card is removed) - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_complete", featureId: feature.id, passes: result.passes, @@ -989,7 +1109,7 @@ class AutoModeService { console.error("[AutoMode] Failed to update feature status after error:", statusError); } - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_error", error: error.message, featureId: featureId, @@ -1021,13 +1141,13 @@ class AutoModeService { throw new Error(`Feature ${featureId} not found`); } - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_start", featureId: feature.id, feature: { ...feature, description: "Committing changes..." }, }); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_phase", featureId, phase: "action", @@ -1051,7 +1171,7 @@ class AutoModeService { // Keep context file for viewing output later (deleted only when card is removed) - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_feature_complete", featureId: feature.id, passes: true, @@ -1061,7 +1181,7 @@ class AutoModeService { return { success: true }; } catch (error) { console.error("[AutoMode] Error committing feature:", error); - sendToRenderer({ + this.emitEvent(projectPath, sendToRenderer, { type: "auto_mode_error", error: error.message, featureId: featureId, @@ -1108,26 +1228,22 @@ class AutoModeService { // Delete context file await contextManager.deleteContextFile(projectPath, featureId); - if (sendToRenderer) { - sendToRenderer({ - type: "auto_mode_feature_complete", - featureId: featureId, - passes: false, - message: "Feature reverted - all changes discarded", - }); - } + this.emitEvent(projectPath, sendToRenderer, { + type: "auto_mode_feature_complete", + featureId: featureId, + passes: false, + message: "Feature reverted - all changes discarded", + }); console.log(`[AutoMode] Feature ${featureId} reverted successfully`); return { success: true, removedPath: result.removedPath }; } catch (error) { console.error("[AutoMode] Error reverting feature:", error); - if (sendToRenderer) { - sendToRenderer({ - type: "auto_mode_error", - error: error.message, - featureId: featureId, - }); - } + this.emitEvent(projectPath, sendToRenderer, { + type: "auto_mode_error", + error: error.message, + featureId: featureId, + }); return { success: false, error: error.message }; } } @@ -1147,13 +1263,11 @@ class AutoModeService { throw new Error(`Feature ${featureId} not found`); } - if (sendToRenderer) { - sendToRenderer({ - type: "auto_mode_progress", - featureId: featureId, - content: "Merging feature branch into main...\n", - }); - } + this.emitEvent(projectPath, sendToRenderer, { + type: "auto_mode_progress", + featureId: featureId, + content: "Merging feature branch into main...\n", + }); // Merge the worktree const result = await worktreeManager.mergeWorktree(projectPath, featureId, { @@ -1171,26 +1285,22 @@ class AutoModeService { // Update feature status to verified await featureLoader.updateFeatureStatus(featureId, "verified", projectPath); - if (sendToRenderer) { - sendToRenderer({ - type: "auto_mode_feature_complete", - featureId: featureId, - passes: true, - message: `Feature merged into ${result.intoBranch}`, - }); - } + this.emitEvent(projectPath, sendToRenderer, { + type: "auto_mode_feature_complete", + featureId: featureId, + passes: true, + message: `Feature merged into ${result.intoBranch}`, + }); console.log(`[AutoMode] Feature ${featureId} merged successfully`); return { success: true, mergedBranch: result.mergedBranch }; } catch (error) { console.error("[AutoMode] Error merging feature:", error); - if (sendToRenderer) { - sendToRenderer({ - type: "auto_mode_error", - error: error.message, - featureId: featureId, - }); - } + this.emitEvent(projectPath, sendToRenderer, { + type: "auto_mode_error", + error: error.message, + featureId: featureId, + }); return { success: false, error: error.message }; } } diff --git a/app/electron/main.js b/app/electron/main.js index 7f5c9c63..270f5386 100644 --- a/app/electron/main.js +++ b/app/electron/main.js @@ -355,6 +355,17 @@ ipcMain.handle("ping", () => { return "pong"; }); +// Open external link in default browser +ipcMain.handle("shell:openExternal", async (_, url) => { + try { + await shell.openExternal(url); + return { success: true }; + } catch (error) { + console.error("[IPC] shell:openExternal error:", error); + return { success: false, error: error.message }; + } +}); + // ============================================================================ // Agent IPC Handlers // ============================================================================ @@ -574,11 +585,11 @@ ipcMain.handle( ); /** - * Stop auto mode + * Stop auto mode for a specific project */ -ipcMain.handle("auto-mode:stop", async () => { +ipcMain.handle("auto-mode:stop", async (_, { projectPath }) => { try { - return await autoModeService.stop(); + return await autoModeService.stop({ projectPath }); } catch (error) { console.error("[IPC] auto-mode:stop error:", error); return { success: false, error: error.message }; @@ -586,11 +597,11 @@ ipcMain.handle("auto-mode:stop", async () => { }); /** - * Get auto mode status + * Get auto mode status (optionally for a specific project) */ -ipcMain.handle("auto-mode:status", () => { +ipcMain.handle("auto-mode:status", (_, { projectPath } = {}) => { try { - return { success: true, ...autoModeService.getStatus() }; + return { success: true, ...autoModeService.getStatus({ projectPath }) }; } catch (error) { console.error("[IPC] auto-mode:status error:", error); return { success: false, error: error.message }; @@ -942,9 +953,11 @@ let suggestionsExecution = null; /** * Generate feature suggestions by analyzing the project + * @param {string} projectPath - The path to the project + * @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance" */ -ipcMain.handle("suggestions:generate", async (_, { projectPath }) => { - console.log("[IPC] suggestions:generate called with:", { projectPath }); +ipcMain.handle("suggestions:generate", async (_, { projectPath, suggestionType = "features" }) => { + console.log("[IPC] suggestions:generate called with:", { projectPath, suggestionType }); try { // Check if already running @@ -970,7 +983,7 @@ ipcMain.handle("suggestions:generate", async (_, { projectPath }) => { // Start generating suggestions (runs in background) featureSuggestionsService - .generateSuggestions(projectPath, sendToRenderer, suggestionsExecution) + .generateSuggestions(projectPath, sendToRenderer, suggestionsExecution, suggestionType) .catch((error) => { console.error("[IPC] suggestions:generate background error:", error); sendToRenderer({ @@ -1776,3 +1789,41 @@ ipcMain.handle( } } ); + +// ============================================================================ +// Running Agents IPC Handlers +// ============================================================================ + +/** + * Get all currently running agents across all projects + */ +ipcMain.handle("running-agents:getAll", () => { + try { + const status = autoModeService.getStatus(); + const allStatuses = autoModeService.getAllProjectStatuses(); + + // Build a list of running agents with their details + const runningAgents = []; + + for (const [projectPath, projectStatus] of Object.entries(allStatuses)) { + for (const featureId of projectStatus.runningFeatures) { + runningAgents.push({ + featureId, + projectPath, + projectName: projectPath.split(/[/\\]/).pop() || projectPath, + isAutoMode: projectStatus.isRunning, + }); + } + } + + return { + success: true, + runningAgents, + totalCount: status.runningCount, + autoLoopRunning: status.autoLoopRunning, + }; + } catch (error) { + console.error("[IPC] running-agents:getAll error:", error); + return { success: false, error: error.message }; + } +}); diff --git a/app/electron/preload.js b/app/electron/preload.js index 37230315..aa3d68c1 100644 --- a/app/electron/preload.js +++ b/app/electron/preload.js @@ -6,6 +6,9 @@ contextBridge.exposeInMainWorld("electronAPI", { // IPC test ping: () => ipcRenderer.invoke("ping"), + // Shell APIs + openExternalLink: (url) => ipcRenderer.invoke("shell:openExternal", url), + // Dialog APIs openDirectory: () => ipcRenderer.invoke("dialog:openDirectory"), openFile: (options) => ipcRenderer.invoke("dialog:openFile", options), @@ -97,15 +100,15 @@ contextBridge.exposeInMainWorld("electronAPI", { // Auto Mode API autoMode: { - // Start auto mode + // Start auto mode for a specific project start: (projectPath, maxConcurrency) => ipcRenderer.invoke("auto-mode:start", { projectPath, maxConcurrency }), - // Stop auto mode - stop: () => ipcRenderer.invoke("auto-mode:stop"), + // Stop auto mode for a specific project + stop: (projectPath) => ipcRenderer.invoke("auto-mode:stop", { projectPath }), - // Get auto mode status - status: () => ipcRenderer.invoke("auto-mode:status"), + // Get auto mode status (optionally for a specific project) + status: (projectPath) => ipcRenderer.invoke("auto-mode:status", { projectPath }), // Run a specific feature runFeature: (projectPath, featureId, useWorktrees) => @@ -243,8 +246,9 @@ contextBridge.exposeInMainWorld("electronAPI", { // Feature Suggestions API suggestions: { // Generate feature suggestions - generate: (projectPath) => - ipcRenderer.invoke("suggestions:generate", { projectPath }), + // suggestionType can be: "features", "refactoring", "security", "performance" + generate: (projectPath, suggestionType = "features") => + ipcRenderer.invoke("suggestions:generate", { projectPath, suggestionType }), // Stop generating suggestions stop: () => ipcRenderer.invoke("suggestions:stop"), @@ -382,6 +386,12 @@ contextBridge.exposeInMainWorld("electronAPI", { getAgentOutput: (projectPath, featureId) => ipcRenderer.invoke("features:getAgentOutput", { projectPath, featureId }), }, + + // Running Agents API + runningAgents: { + // Get all running agents across all projects + getAll: () => ipcRenderer.invoke("running-agents:getAll"), + }, }); // Also expose a flag to detect if we're in Electron diff --git a/app/electron/services/claude-cli-detector.js b/app/electron/services/claude-cli-detector.js index f18f9d0e..715754db 100644 --- a/app/electron/services/claude-cli-detector.js +++ b/app/electron/services/claude-cli-detector.js @@ -371,6 +371,30 @@ class ClaudeCliDetector { }; } + /** + * Get installation info and recommendations + * @returns {Object} Installation status and recommendations + */ + static getInstallationInfo() { + const detection = this.detectClaudeInstallation(); + + if (detection.installed) { + return { + status: 'installed', + method: detection.method, + version: detection.version, + path: detection.path, + recommendation: 'Claude Code CLI is ready for ultrathink' + }; + } + + return { + status: 'not_installed', + recommendation: 'Install Claude Code CLI for optimal ultrathink performance', + installCommands: this.getInstallCommands() + }; + } + /** * Get installation commands for different platforms * @returns {Object} Installation commands diff --git a/app/electron/services/feature-suggestions-service.js b/app/electron/services/feature-suggestions-service.js index 28063e66..2241e9b3 100644 --- a/app/electron/services/feature-suggestions-service.js +++ b/app/electron/services/feature-suggestions-service.js @@ -11,10 +11,14 @@ class FeatureSuggestionsService { /** * Generate feature suggestions by analyzing the project + * @param {string} projectPath - Path to the project + * @param {Function} sendToRenderer - Function to send events to renderer + * @param {Object} execution - Execution context with abort controller + * @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance" */ - async generateSuggestions(projectPath, sendToRenderer, execution) { + async generateSuggestions(projectPath, sendToRenderer, execution, suggestionType = "features") { console.log( - `[FeatureSuggestions] Generating suggestions for: ${projectPath}` + `[FeatureSuggestions] Generating ${suggestionType} suggestions for: ${projectPath}` ); try { @@ -23,7 +27,7 @@ class FeatureSuggestionsService { const options = { model: "claude-sonnet-4-20250514", - systemPrompt: this.getSystemPrompt(), + systemPrompt: this.getSystemPrompt(suggestionType), maxTurns: 50, cwd: projectPath, allowedTools: ["Read", "Glob", "Grep", "Bash"], @@ -35,7 +39,7 @@ class FeatureSuggestionsService { abortController: abortController, }; - const prompt = this.buildAnalysisPrompt(); + const prompt = this.buildAnalysisPrompt(suggestionType); sendToRenderer({ type: "suggestions_progress", @@ -163,36 +167,102 @@ class FeatureSuggestionsService { /** * Get the system prompt for feature suggestion analysis + * @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance" */ - 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. + getSystemPrompt(suggestionType = "features") { + const basePrompt = `You are an expert software architect. Your job is to analyze a codebase and provide actionable suggestions. -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 +You have access to file reading and search tools. Use them to understand the codebase. 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 +- Existing code patterns and implementation styles`; -You have access to file reading and search tools. Use them to understand the codebase.`; + switch (suggestionType) { + case "refactoring": + return `${basePrompt} + +Your specific focus is on **refactoring suggestions**. You should: +1. Identify code smells and areas that need cleanup +2. Find duplicated code that could be consolidated +3. Spot overly complex functions or classes that should be broken down +4. Look for inconsistent naming conventions or coding patterns +5. Find opportunities to improve code organization and modularity +6. Identify violations of SOLID principles or common design patterns +7. Look for dead code or unused dependencies + +Prioritize suggestions by: +- Impact on maintainability +- Risk level (lower risk refactorings first) +- Complexity of the refactoring`; + + case "security": + return `${basePrompt} + +Your specific focus is on **security vulnerabilities and improvements**. You should: +1. Identify potential security vulnerabilities (OWASP Top 10) +2. Look for hardcoded secrets, API keys, or credentials +3. Check for proper input validation and sanitization +4. Identify SQL injection, XSS, or command injection risks +5. Review authentication and authorization patterns +6. Check for secure communication (HTTPS, encryption) +7. Look for insecure dependencies or outdated packages +8. Review error handling that might leak sensitive information +9. Check for proper session management +10. Identify insecure file handling or path traversal risks + +Prioritize by severity: +- Critical: Exploitable vulnerabilities with high impact +- High: Security issues that could lead to data exposure +- Medium: Best practice violations that weaken security +- Low: Minor improvements to security posture`; + + case "performance": + return `${basePrompt} + +Your specific focus is on **performance issues and optimizations**. You should: +1. Identify N+1 query problems or inefficient database access +2. Look for unnecessary re-renders in React/frontend code +3. Find opportunities for caching or memoization +4. Identify large bundle sizes or unoptimized imports +5. Look for blocking operations that could be async +6. Find memory leaks or inefficient memory usage +7. Identify slow algorithms or data structure choices +8. Look for missing indexes in database schemas +9. Find opportunities for lazy loading or code splitting +10. Identify unnecessary network requests or API calls + +Prioritize by: +- Impact on user experience +- Frequency of the slow path +- Ease of implementation`; + + default: // "features" + return `${basePrompt} + +Your specific focus is on **missing features and improvements**. You should: +1. Identify what the application does and what features it currently has +2. Look at the .automaker/app_spec.txt file if it exists +3. Generate a comprehensive list of missing features that would be valuable to users +4. Consider user experience improvements +5. Consider developer experience improvements +6. Look at common patterns in similar applications + +Prioritize features by: +- Impact on users +- Alignment with project goals +- Complexity of implementation`; + } } /** * Build the prompt for analyzing the project + * @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance" */ - buildAnalysisPrompt() { - return `Analyze this project and generate a list of suggested features that are missing or would improve the application. + buildAnalysisPrompt(suggestionType = "features") { + const commonIntro = `Analyze this project and generate a list of actionable suggestions. **Your Task:** @@ -200,13 +270,89 @@ You have access to file reading and search tools. Use them to understand the cod - 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 + - Look at existing code and how it's implemented 2. Identify what the application does: - What is the main purpose? - - What features are already implemented? - What patterns and conventions are used? +`; + const commonOutput = ` +**CRITICAL: Output your suggestions as a JSON array** at the end of your response, formatted like this: + +\`\`\`json +[ + { + "category": "Category Name", + "description": "Clear description of the suggestion", + "steps": [ + "Step 1 to implement", + "Step 2 to implement", + "Step 3 to implement" + ], + "priority": 1, + "reasoning": "Why this is important" + } +] +\`\`\` + +**Important Guidelines:** +- Generate at least 10-15 suggestions +- Order them by priority (1 = highest priority) +- Each suggestion should have clear, actionable steps +- Be specific about what files might need to be modified +- Consider the existing tech stack and patterns + +Begin by exploring the project structure.`; + + switch (suggestionType) { + case "refactoring": + return `${commonIntro} +3. Look for refactoring opportunities: + - Find code duplication across the codebase + - Identify functions or classes that are too long or complex + - Look for inconsistent patterns or naming conventions + - Find tightly coupled code that should be decoupled + - Identify opportunities to extract reusable utilities + - Look for dead code or unused exports + - Check for proper separation of concerns + +Categories to use: "Code Smell", "Duplication", "Complexity", "Architecture", "Naming", "Dead Code", "Coupling", "Testing" +${commonOutput}`; + + case "security": + return `${commonIntro} +3. Look for security issues: + - Check for hardcoded secrets or API keys + - Look for potential injection vulnerabilities (SQL, XSS, command) + - Review authentication and authorization code + - Check input validation and sanitization + - Look for insecure dependencies + - Review error handling for information leakage + - Check for proper HTTPS/TLS usage + - Look for insecure file operations + +Categories to use: "Critical", "High", "Medium", "Low" (based on severity) +${commonOutput}`; + + case "performance": + return `${commonIntro} +3. Look for performance issues: + - Find N+1 queries or inefficient database access patterns + - Look for unnecessary re-renders in React components + - Identify missing memoization opportunities + - Check bundle size and import patterns + - Look for synchronous operations that could be async + - Find potential memory leaks + - Identify slow algorithms or data structures + - Look for missing caching opportunities + - Check for unnecessary network requests + +Categories to use: "Database", "Rendering", "Memory", "Bundle Size", "Caching", "Algorithm", "Network" +${commonOutput}`; + + default: // "features" + return `${commonIntro} 3. Generate feature suggestions: - Think about what's missing compared to similar applications - Consider user experience improvements @@ -214,45 +360,9 @@ You have access to file reading and search tools. Use them to understand the cod - 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.`; +Categories to use: "User Experience", "Performance", "Security", "Testing", "Documentation", "Developer Experience", "Accessibility", etc. +${commonOutput}`; + } } /** diff --git a/app/electron/services/model-provider.js b/app/electron/services/model-provider.js index b07588c9..e6212fdf 100644 --- a/app/electron/services/model-provider.js +++ b/app/electron/services/model-provider.js @@ -251,7 +251,7 @@ class ClaudeProvider extends ModelProvider { async detectInstallation() { const claudeCliDetector = require('./claude-cli-detector'); - return claudeCliDetector.getInstallationInfo(); + return claudeCliDetector.getFullStatus(); } getAvailableModels() { diff --git a/app/src/app/page.tsx b/app/src/app/page.tsx index e320c2fd..0f50a5b0 100644 --- a/app/src/app/page.tsx +++ b/app/src/app/page.tsx @@ -12,6 +12,7 @@ import { InterviewView } from "@/components/views/interview-view"; import { ContextView } from "@/components/views/context-view"; import { ProfilesView } from "@/components/views/profiles-view"; import { SetupView } from "@/components/views/setup-view"; +import { RunningAgentsView } from "@/components/views/running-agents-view"; import { useAppStore } from "@/store/app-store"; import { useSetupStore } from "@/store/setup-store"; import { getElectronAPI, isElectron } from "@/lib/electron"; @@ -178,6 +179,8 @@ export default function Home() { return ; case "profiles": return ; + case "running-agents": + return ; default: return ; } diff --git a/app/src/components/layout/sidebar.tsx b/app/src/components/layout/sidebar.tsx index 418dd5e4..e5abe185 100644 --- a/app/src/components/layout/sidebar.tsx +++ b/app/src/components/layout/sidebar.tsx @@ -40,6 +40,8 @@ import { Radio, Monitor, Search, + Bug, + Activity, } from "lucide-react"; import { DropdownMenu, @@ -394,7 +396,8 @@ export function Sidebar() { if (!result.canceled && result.filePaths[0]) { const path = result.filePaths[0]; // Extract folder name from path (works on both Windows and Mac/Linux) - const name = path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project"; + const name = + path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project"; try { // Check if this is a brand new project (no .automaker directory) @@ -572,7 +575,10 @@ export function Sidebar() { // Handle selecting the currently highlighted project const selectHighlightedProject = useCallback(() => { - if (filteredProjects.length > 0 && selectedProjectIndex < filteredProjects.length) { + if ( + filteredProjects.length > 0 && + selectedProjectIndex < filteredProjects.length + ) { setCurrentProject(filteredProjects[selectedProjectIndex]); setIsProjectPickerOpen(false); } @@ -596,7 +602,11 @@ export function Sidebar() { } else if (event.key === "ArrowUp") { event.preventDefault(); setSelectedProjectIndex((prev) => (prev > 0 ? prev - 1 : prev)); - } else if (event.key.toLowerCase() === "p" && !event.metaKey && !event.ctrlKey) { + } else if ( + event.key.toLowerCase() === "p" && + !event.metaKey && + !event.ctrlKey + ) { // Toggle off when P is pressed (not with modifiers) while dropdown is open // Only if not typing in the search input if (document.activeElement !== projectSearchInputRef.current) { @@ -913,7 +923,10 @@ export function Sidebar() { )} - + Select theme for this project @@ -922,7 +935,10 @@ export function Sidebar() { value={currentProject.theme || ""} onValueChange={(value) => { if (currentProject) { - setProjectTheme(currentProject.id, value === "" ? null : value as any); + setProjectTheme( + currentProject.id, + value === "" ? null : (value as any) + ); } }} > @@ -932,7 +948,9 @@ export function Sidebar() { {option.label} @@ -955,21 +973,30 @@ export function Sidebar() { Project History - + Previous {formatShortcut(shortcuts.cyclePrevProject, true)} - + Next {formatShortcut(shortcuts.cycleNextProject, true)} - + Clear history @@ -1078,8 +1105,79 @@ export function Sidebar() { - {/* Bottom Section - User / Settings */} + {/* Bottom Section - Running Agents / Bug Report / Settings */}
+ {/* Running Agents Link */} +
+ +
+ {/* Bug Report Link */} +
+ +
{/* Settings Link */}
diff --git a/app/src/components/ui/dialog.tsx b/app/src/components/ui/dialog.tsx index 385fd354..e8b4828f 100644 --- a/app/src/components/ui/dialog.tsx +++ b/app/src/components/ui/dialog.tsx @@ -82,7 +82,7 @@ function DialogContent({ data-slot="dialog-close" className={cn( "ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute rounded-xs opacity-70 transition-opacity cursor-pointer hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - compact ? "top-2 right-2" : "top-4 right-4" + compact ? "top-2 right-3" : "top-3 right-5" )} > diff --git a/app/src/components/ui/hotkey-button.tsx b/app/src/components/ui/hotkey-button.tsx index 0b4f10d6..f4383a85 100644 --- a/app/src/components/ui/hotkey-button.tsx +++ b/app/src/components/ui/hotkey-button.tsx @@ -56,7 +56,10 @@ function parseHotkeyConfig(hotkey: string | HotkeyConfig): HotkeyConfig { /** * Generate the display label for the hotkey */ -function getHotkeyDisplayLabel(config: HotkeyConfig, isMac: boolean): React.ReactNode { +function getHotkeyDisplayLabel( + config: HotkeyConfig, + isMac: boolean +): React.ReactNode { if (config.label) { return config.label; } @@ -73,7 +76,10 @@ function getHotkeyDisplayLabel(config: HotkeyConfig, isMac: boolean): React.Reac if (config.shift) { parts.push( - + ⇧ ); @@ -134,11 +140,7 @@ function getHotkeyDisplayLabel(config: HotkeyConfig, isMac: boolean): React.Reac ); - return ( - - {parts} - - ); + return {parts}; } /** @@ -205,7 +207,11 @@ export function HotkeyButton({ // Don't trigger when typing in inputs (unless explicitly scoped or using cmdCtrl modifier) // cmdCtrl shortcuts like Cmd+Enter should work even in inputs as they're intentional submit actions - if (!scopeRef && !config.cmdCtrl && isInputElement(document.activeElement)) { + if ( + !scopeRef && + !config.cmdCtrl && + isInputElement(document.activeElement) + ) { return; } @@ -228,7 +234,8 @@ export function HotkeyButton({ // If scoped, check that the scope element is visible if (scopeRef && scopeRef.current) { const scopeEl = scopeRef.current; - const isVisible = scopeEl.offsetParent !== null || + const isVisible = + scopeEl.offsetParent !== null || getComputedStyle(scopeEl).display !== "none"; if (!isVisible) return; } @@ -259,14 +266,15 @@ export function HotkeyButton({ }, [config, hotkeyActive, handleKeyDown]); // Render the hotkey indicator - const hotkeyIndicator = config && showHotkeyIndicator ? ( - - {getHotkeyDisplayLabel(config, isMac)} - - ) : null; + const hotkeyIndicator = + config && showHotkeyIndicator ? ( + + {getHotkeyDisplayLabel(config, isMac)} + + ) : null; return ( +
+ {(Object.entries(suggestionTypeConfig) as [SuggestionType, typeof suggestionTypeConfig[SuggestionType]][]).map( + ([type, config]) => { + const Icon = config.icon; + return ( + + ); + } + )} +
) : isGenerating ? ( // Generating state - show progress @@ -410,20 +478,34 @@ export function FeatureSuggestionsDialog({

No suggestions were generated. Try running the analysis again.

- +
+ + {currentSuggestionType && ( + + )} +
)} {hasSuggestions && (
- +
+ + {currentSuggestionType && ( + + )} +
)} {onForceStop && ( diff --git a/app/src/components/views/running-agents-view.tsx b/app/src/components/views/running-agents-view.tsx new file mode 100644 index 00000000..baad1eaf --- /dev/null +++ b/app/src/components/views/running-agents-view.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { Bot, Folder, Loader2, RefreshCw, Square, Activity } from "lucide-react"; +import { getElectronAPI, RunningAgent } from "@/lib/electron"; +import { useAppStore } from "@/store/app-store"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +export function RunningAgentsView() { + const [runningAgents, setRunningAgents] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const { setCurrentProject, projects, setCurrentView } = useAppStore(); + + const fetchRunningAgents = useCallback(async () => { + try { + const api = getElectronAPI(); + if (api.runningAgents) { + const result = await api.runningAgents.getAll(); + if (result.success && result.runningAgents) { + setRunningAgents(result.runningAgents); + } + } + } catch (error) { + console.error("[RunningAgentsView] Error fetching running agents:", error); + } finally { + setLoading(false); + setRefreshing(false); + } + }, []); + + // Initial fetch + useEffect(() => { + fetchRunningAgents(); + }, [fetchRunningAgents]); + + // Auto-refresh every 2 seconds + useEffect(() => { + const interval = setInterval(() => { + fetchRunningAgents(); + }, 2000); + + return () => clearInterval(interval); + }, [fetchRunningAgents]); + + // Subscribe to auto-mode events to update in real-time + useEffect(() => { + const api = getElectronAPI(); + if (!api.autoMode) return; + + const unsubscribe = api.autoMode.onEvent((event) => { + // When a feature completes or errors, refresh the list + if ( + event.type === "auto_mode_feature_complete" || + event.type === "auto_mode_error" + ) { + fetchRunningAgents(); + } + }); + + return () => { + unsubscribe(); + }; + }, [fetchRunningAgents]); + + const handleRefresh = useCallback(() => { + setRefreshing(true); + fetchRunningAgents(); + }, [fetchRunningAgents]); + + const handleStopAgent = useCallback(async (featureId: string) => { + try { + const api = getElectronAPI(); + if (api.autoMode) { + await api.autoMode.stopFeature(featureId); + // Refresh list after stopping + fetchRunningAgents(); + } + } catch (error) { + console.error("[RunningAgentsView] Error stopping agent:", error); + } + }, [fetchRunningAgents]); + + const handleNavigateToProject = useCallback((agent: RunningAgent) => { + // Find the project by path + const project = projects.find((p) => p.path === agent.projectPath); + if (project) { + setCurrentProject(project); + setCurrentView("board"); + } + }, [projects, setCurrentProject, setCurrentView]); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Running Agents

+

+ {runningAgents.length === 0 + ? "No agents currently running" + : `${runningAgents.length} agent${runningAgents.length === 1 ? "" : "s"} running across all projects`} +

+
+
+ +
+ + {/* Content */} + {runningAgents.length === 0 ? ( +
+
+ +
+

No Running Agents

+

+ Agents will appear here when they are actively working on features. + Start an agent from the Kanban board by dragging a feature to "In Progress". +

+
+ ) : ( +
+
+ {runningAgents.map((agent) => ( +
+
+ {/* Status indicator */} +
+ + + + + +
+ + {/* Agent info */} +
+
+ + {agent.featureId} + + {agent.isAutoMode && ( + + AUTO + + )} +
+ +
+
+ + {/* Actions */} +
+ + +
+
+ ))} +
+
+ )} +
+ ); +} diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index a9dd2ed9..2936505f 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -2,9 +2,23 @@ import { useState } from "react"; import { useAppStore } from "@/store/app-store"; +import { Label } from "@/components/ui/label"; +import { + Key, + Palette, + Terminal, + Atom, + LayoutGrid, + FlaskConical, + Trash2, + Settings2, + Volume2, + VolumeX, +} from "lucide-react"; +import { Checkbox } from "@/components/ui/checkbox"; + import { useCliStatus } from "./settings-view/hooks/use-cli-status"; import { useScrollTracking } from "@/hooks/use-scroll-tracking"; -import { NAV_ITEMS } from "./settings-view/config/navigation"; import { SettingsHeader } from "./settings-view/components/settings-header"; import { KeyboardMapDialog } from "./settings-view/components/keyboard-map-dialog"; import { DeleteProjectDialog } from "./settings-view/components/delete-project-dialog"; @@ -17,9 +31,25 @@ import { KanbanDisplaySection } from "./settings-view/kanban-display/kanban-disp import { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts/keyboard-shortcuts-section"; import { FeatureDefaultsSection } from "./settings-view/feature-defaults/feature-defaults-section"; import { DangerZoneSection } from "./settings-view/danger-zone/danger-zone-section"; -import type { Project as SettingsProject, Theme } from "./settings-view/shared/types"; +import type { + Project as SettingsProject, + Theme, +} from "./settings-view/shared/types"; import type { Project as ElectronProject } from "@/lib/electron"; +// Navigation items for the side panel +const NAV_ITEMS = [ + { id: "api-keys", label: "API Keys", icon: Key }, + { id: "claude", label: "Claude", icon: Terminal }, + { id: "codex", label: "Codex", icon: Atom }, + { id: "appearance", label: "Appearance", icon: Palette }, + { id: "kanban", label: "Kanban Display", icon: LayoutGrid }, + { id: "audio", label: "Audio", icon: Volume2 }, + { id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 }, + { id: "defaults", label: "Feature Defaults", icon: FlaskConical }, + { id: "danger", label: "Danger Zone", icon: Trash2 }, +]; + export function SettingsView() { const { theme, @@ -33,12 +63,16 @@ export function SettingsView() { setUseWorktrees, showProfilesOnly, setShowProfilesOnly, + muteDoneSound, + setMuteDoneSound, currentProject, moveProjectToTrash, } = useAppStore(); // Convert electron Project to settings-view Project type - const convertProject = (project: ElectronProject | null): SettingsProject | null => { + const convertProject = ( + project: ElectronProject | null + ): SettingsProject | null => { if (!project) return null; return { id: project.id, @@ -143,6 +177,55 @@ export function SettingsView() { onOpenKeyboardMap={() => setShowKeyboardMapDialog(true)} /> + {/* Audio Section */} +
+
+
+ +

+ Audio +

+
+

+ Configure audio and notification settings. +

+
+
+ {/* Mute Done Sound Setting */} +
+
+ + setMuteDoneSound(checked === true) + } + className="mt-0.5" + data-testid="mute-done-sound-checkbox" + /> +
+ +

+ When enabled, disables the "ding" sound that + plays when an agent completes a feature. The feature + will still move to the completed column, but without + audio notification. +

+
+
+
+
+
+ {/* Feature Defaults Section */} ({ autoModeByProject: state.autoModeByProject, @@ -26,9 +28,16 @@ export function useAutoMode() { currentProject: state.currentProject, addAutoModeActivity: state.addAutoModeActivity, maxConcurrency: state.maxConcurrency, + projects: state.projects, })) ); + // Helper to look up project ID from path + const getProjectIdFromPath = useCallback((path: string): string | undefined => { + const project = projects.find(p => p.path === path); + return project?.id; + }, [projects]); + // Get project-specific auto mode state const projectId = currentProject?.id; const projectAutoModeState = useMemo(() => { @@ -42,17 +51,32 @@ export function useAutoMode() { // Check if we can start a new task based on concurrency limit const canStartNewTask = runningAutoTasks.length < maxConcurrency; - // Handle auto mode events + // Handle auto mode events - listen globally for all projects useEffect(() => { const api = getElectronAPI(); - if (!api?.autoMode || !projectId) return; + if (!api?.autoMode) return; const unsubscribe = api.autoMode.onEvent((event: AutoModeEvent) => { console.log("[AutoMode Event]", event); - // Events include projectId from backend, use it to scope updates + // Events include projectPath from backend - use it to look up project ID // Fall back to current projectId if not provided in event - const eventProjectId = event.projectId ?? projectId; + let eventProjectId: string | undefined; + if ('projectPath' in event && event.projectPath) { + eventProjectId = getProjectIdFromPath(event.projectPath); + } + if (!eventProjectId && 'projectId' in event && event.projectId) { + eventProjectId = event.projectId; + } + if (!eventProjectId) { + eventProjectId = projectId; + } + + // Skip event if we couldn't determine the project + if (!eventProjectId) { + console.warn("[AutoMode] Could not determine project for event:", event); + return; + } switch (event.type) { case "auto_mode_feature_start": @@ -153,8 +177,47 @@ export function useAutoMode() { clearRunningTasks, setAutoModeRunning, addAutoModeActivity, + getProjectIdFromPath, ]); + // Restore auto mode for all projects that were running when app was closed + // This runs once on mount to restart auto loops for persisted running states + useEffect(() => { + const api = getElectronAPI(); + if (!api?.autoMode) return; + + // Find all projects that have auto mode marked as running + const projectsToRestart: Array<{ projectId: string; projectPath: string }> = []; + for (const [projectId, state] of Object.entries(autoModeByProject)) { + if (state.isRunning) { + // Find the project path for this project ID + const project = projects.find(p => p.id === projectId); + if (project) { + projectsToRestart.push({ projectId, projectPath: project.path }); + } + } + } + + // Restart auto mode for each project + for (const { projectId, projectPath } of projectsToRestart) { + console.log(`[AutoMode] Restoring auto mode for project: ${projectPath}`); + api.autoMode.start(projectPath, maxConcurrency).then(result => { + if (!result.success) { + console.error(`[AutoMode] Failed to restore auto mode for ${projectPath}:`, result.error); + // Mark as not running if we couldn't restart + setAutoModeRunning(projectId, false); + } else { + console.log(`[AutoMode] Restored auto mode for ${projectPath}`); + } + }).catch(error => { + console.error(`[AutoMode] Error restoring auto mode for ${projectPath}:`, error); + setAutoModeRunning(projectId, false); + }); + } + // Only run once on mount - intentionally empty dependency array + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + // Start auto mode const start = useCallback(async () => { if (!currentProject) { @@ -199,7 +262,7 @@ export function useAutoMode() { throw new Error("Auto mode API not available"); } - const result = await api.autoMode.stop(); + const result = await api.autoMode.stop(currentProject.path); if (result.success) { setAutoModeRunning(currentProject.id, false); diff --git a/app/src/lib/electron.ts b/app/src/lib/electron.ts index 52428167..43ae6e9a 100644 --- a/app/src/lib/electron.ts +++ b/app/src/lib/electron.ts @@ -58,6 +58,26 @@ import type { // Feature type - Import from app-store import type { Feature } from "@/store/app-store"; +// Running Agent type +export interface RunningAgent { + featureId: string; + projectPath: string; + projectName: string; + isAutoMode: boolean; +} + +export interface RunningAgentsResult { + success: boolean; + runningAgents?: RunningAgent[]; + totalCount?: number; + autoLoopRunning?: boolean; + error?: string; +} + +export interface RunningAgentsAPI { + getAll: () => Promise; +} + // Feature Suggestions types export interface FeatureSuggestion { id: string; @@ -81,9 +101,12 @@ export interface SuggestionsEvent { error?: string; } +export type SuggestionType = "features" | "refactoring" | "security" | "performance"; + export interface SuggestionsAPI { generate: ( - projectPath: string + projectPath: string, + suggestionType?: SuggestionType ) => Promise<{ success: boolean; error?: string }>; stop: () => Promise<{ success: boolean; error?: string }>; status: () => Promise<{ @@ -153,15 +176,18 @@ export interface AutoModeAPI { projectPath: string, maxConcurrency?: number ) => Promise<{ success: boolean; error?: string }>; - stop: () => Promise<{ success: boolean; error?: string }>; + stop: (projectPath: string) => Promise<{ success: boolean; error?: string; runningFeatures?: number }>; stopFeature: ( featureId: string ) => Promise<{ success: boolean; error?: string }>; - status: () => Promise<{ + status: (projectPath?: string) => Promise<{ success: boolean; isRunning?: boolean; + autoLoopRunning?: boolean; // Backend uses this name instead of isRunning currentFeatureId?: string | null; runningFeatures?: string[]; + runningProjects?: string[]; + runningCount?: number; error?: string; }>; runFeature: ( @@ -205,6 +231,7 @@ export interface SaveImageResult { export interface ElectronAPI { ping: () => Promise; + openExternalLink: (url: string) => Promise<{ success: boolean; error?: string }>; openDirectory: () => Promise; openFile: (options?: object) => Promise; readFile: (filePath: string) => Promise; @@ -276,6 +303,7 @@ export interface ElectronAPI { specRegeneration?: SpecRegenerationAPI; autoMode?: AutoModeAPI; features?: FeaturesAPI; + runningAgents?: RunningAgentsAPI; setup?: { getClaudeStatus: () => Promise<{ success: boolean; @@ -397,6 +425,12 @@ export const getElectronAPI = (): ElectronAPI => { return { ping: async () => "pong (mock)", + openExternalLink: async (url: string) => { + // In web mode, open in a new tab + window.open(url, "_blank", "noopener,noreferrer"); + return { success: true }; + }, + openDirectory: async () => { // In web mode, we'll use a prompt to simulate directory selection const path = prompt( @@ -675,6 +709,9 @@ export const getElectronAPI = (): ElectronAPI => { // Mock Features API features: createMockFeaturesAPI(), + + // Mock Running Agents API + runningAgents: createMockRunningAgentsAPI(), }; }; @@ -1020,13 +1057,14 @@ function createMockAutoModeAPI(): AutoModeAPI { return { success: true }; }, - stop: async () => { + stop: async (_projectPath: string) => { mockAutoModeRunning = false; + const runningCount = mockRunningFeatures.size; mockRunningFeatures.clear(); // Clear all timeouts mockAutoModeTimeouts.forEach((timeout) => clearTimeout(timeout)); mockAutoModeTimeouts.clear(); - return { success: true }; + return { success: true, runningFeatures: runningCount }; }, stopFeature: async (featureId: string) => { @@ -1055,12 +1093,14 @@ function createMockAutoModeAPI(): AutoModeAPI { return { success: true }; }, - status: async () => { + status: async (_projectPath?: string) => { return { success: true, isRunning: mockAutoModeRunning, + autoLoopRunning: mockAutoModeRunning, currentFeatureId: mockAutoModeRunning ? "feature-0" : null, runningFeatures: Array.from(mockRunningFeatures), + runningCount: mockRunningFeatures.size, }; }, @@ -1441,7 +1481,7 @@ let mockSuggestionsTimeout: NodeJS.Timeout | null = null; function createMockSuggestionsAPI(): SuggestionsAPI { return { - generate: async (projectPath: string) => { + generate: async (projectPath: string, suggestionType: SuggestionType = "features") => { if (mockSuggestionsRunning) { return { success: false, @@ -1450,10 +1490,10 @@ function createMockSuggestionsAPI(): SuggestionsAPI { } mockSuggestionsRunning = true; - console.log(`[Mock] Generating suggestions for: ${projectPath}`); + console.log(`[Mock] Generating ${suggestionType} suggestions for: ${projectPath}`); // Simulate async suggestion generation - simulateSuggestionsGeneration(); + simulateSuggestionsGeneration(suggestionType); return { success: true }; }, @@ -1489,11 +1529,18 @@ function emitSuggestionsEvent(event: SuggestionsEvent) { mockSuggestionsCallbacks.forEach((cb) => cb(event)); } -async function simulateSuggestionsGeneration() { +async function simulateSuggestionsGeneration(suggestionType: SuggestionType = "features") { + const typeLabels: Record = { + features: "feature suggestions", + refactoring: "refactoring opportunities", + security: "security vulnerabilities", + performance: "performance issues", + }; + // Emit progress events emitSuggestionsEvent({ type: "suggestions_progress", - content: "Starting project analysis...\n", + content: `Starting project analysis for ${typeLabels[suggestionType]}...\n`, }); await new Promise((resolve) => { @@ -1524,7 +1571,7 @@ async function simulateSuggestionsGeneration() { emitSuggestionsEvent({ type: "suggestions_progress", - content: "Identifying missing features...\n", + content: `Identifying ${typeLabels[suggestionType]}...\n`, }); await new Promise((resolve) => { @@ -1532,75 +1579,184 @@ async function simulateSuggestionsGeneration() { }); 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", - }, - ]; + // Generate mock suggestions based on type + let mockSuggestions: FeatureSuggestion[]; + + switch (suggestionType) { + case "refactoring": + mockSuggestions = [ + { + id: `suggestion-${Date.now()}-0`, + category: "Code Smell", + description: "Extract duplicate validation logic into reusable utility", + steps: [ + "Identify all files with similar validation patterns", + "Create a validation utilities module", + "Replace duplicate code with utility calls", + "Add unit tests for the new utilities", + ], + priority: 1, + reasoning: "Reduces code duplication and improves maintainability", + }, + { + id: `suggestion-${Date.now()}-1`, + category: "Complexity", + description: "Break down large handleSubmit function into smaller functions", + steps: [ + "Identify the handleSubmit function in form components", + "Extract validation logic into separate function", + "Extract API call logic into separate function", + "Extract success/error handling into separate functions", + ], + priority: 2, + reasoning: "Function is too long and handles multiple responsibilities", + }, + { + id: `suggestion-${Date.now()}-2`, + category: "Architecture", + description: "Move business logic out of React components into hooks", + steps: [ + "Identify business logic in component files", + "Create custom hooks for reusable logic", + "Update components to use the new hooks", + "Add tests for the extracted hooks", + ], + priority: 3, + reasoning: "Improves separation of concerns and testability", + }, + ]; + break; + + case "security": + mockSuggestions = [ + { + id: `suggestion-${Date.now()}-0`, + category: "High", + description: "Sanitize user input before rendering to prevent XSS", + steps: [ + "Audit all places where user input is rendered", + "Implement input sanitization using DOMPurify", + "Add Content-Security-Policy headers", + "Test with common XSS payloads", + ], + priority: 1, + reasoning: "User input is rendered without proper sanitization", + }, + { + id: `suggestion-${Date.now()}-1`, + category: "Medium", + description: "Add rate limiting to authentication endpoints", + steps: [ + "Implement rate limiting middleware", + "Configure limits for login attempts", + "Add account lockout after failed attempts", + "Log suspicious activity", + ], + priority: 2, + reasoning: "Prevents brute force attacks on authentication", + }, + { + id: `suggestion-${Date.now()}-2`, + category: "Low", + description: "Remove sensitive information from error messages", + steps: [ + "Audit error handling in API routes", + "Create generic error messages for production", + "Log detailed errors server-side only", + "Implement proper error boundaries", + ], + priority: 3, + reasoning: "Error messages may leak implementation details", + }, + ]; + break; + + case "performance": + mockSuggestions = [ + { + id: `suggestion-${Date.now()}-0`, + category: "Rendering", + description: "Add React.memo to prevent unnecessary re-renders", + steps: [ + "Profile component renders with React DevTools", + "Identify components that re-render unnecessarily", + "Wrap pure components with React.memo", + "Use useCallback for event handlers passed as props", + ], + priority: 1, + reasoning: "Components re-render even when props haven't changed", + }, + { + id: `suggestion-${Date.now()}-1`, + category: "Bundle Size", + description: "Implement code splitting for route components", + steps: [ + "Use React.lazy for route components", + "Add Suspense boundaries with loading states", + "Analyze bundle with webpack-bundle-analyzer", + "Consider dynamic imports for heavy libraries", + ], + priority: 2, + reasoning: "Initial bundle is larger than necessary", + }, + { + id: `suggestion-${Date.now()}-2`, + category: "Caching", + description: "Add memoization for expensive computations", + steps: [ + "Identify expensive calculations in render", + "Use useMemo for derived data", + "Consider using react-query for server state", + "Add caching headers for static assets", + ], + priority: 3, + reasoning: "Expensive computations run on every render", + }, + ]; + break; + + default: // "features" + mockSuggestions = [ + { + 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", + }, + ]; + } emitSuggestionsEvent({ type: "suggestions_complete", @@ -1921,6 +2077,30 @@ function createMockFeaturesAPI(): FeaturesAPI { }; } +// Mock Running Agents API implementation +function createMockRunningAgentsAPI(): RunningAgentsAPI { + return { + getAll: async () => { + console.log("[Mock] Getting all running agents"); + // Return running agents from mock auto mode state + const runningAgents: RunningAgent[] = Array.from(mockRunningFeatures).map( + (featureId) => ({ + featureId, + projectPath: "/mock/project", + projectName: "Mock Project", + isAutoMode: mockAutoModeRunning, + }) + ); + return { + success: true, + runningAgents, + totalCount: runningAgents.length, + autoLoopRunning: mockAutoModeRunning, + }; + }, + }; +} + // Utility functions for project management export interface Project { diff --git a/app/src/store/app-store.ts b/app/src/store/app-store.ts index 21d88563..d5a1b9c9 100644 --- a/app/src/store/app-store.ts +++ b/app/src/store/app-store.ts @@ -12,7 +12,8 @@ export type ViewMode = | "tools" | "interview" | "context" - | "profiles"; + | "profiles" + | "running-agents"; export type ThemeMode = | "light" @@ -328,6 +329,9 @@ export interface AppState { // Keyboard Shortcuts keyboardShortcuts: KeyboardShortcuts; // User-defined keyboard shortcuts + // Audio Settings + muteDoneSound: boolean; // When true, mute the notification sound when agents complete (default: false) + // Project Analysis projectAnalysis: ProjectAnalysis | null; isAnalyzing: boolean; @@ -435,6 +439,9 @@ export interface AppActions { setKeyboardShortcuts: (shortcuts: Partial) => void; resetKeyboardShortcuts: () => void; + // Audio Settings actions + setMuteDoneSound: (muted: boolean) => void; + // AI Profile actions addAIProfile: (profile: Omit) => void; updateAIProfile: (id: string, updates: Partial) => void; @@ -537,6 +544,7 @@ const initialState: AppState = { useWorktrees: false, // Default to disabled (worktree feature is experimental) showProfilesOnly: false, // Default to showing all options (not profiles only) keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts + muteDoneSound: false, // Default to sound enabled (not muted) aiProfiles: DEFAULT_AI_PROFILES, projectAnalysis: null, isAnalyzing: false, @@ -1065,6 +1073,9 @@ export const useAppStore = create()( set({ keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS }); }, + // Audio Settings actions + setMuteDoneSound: (muted) => set({ muteDoneSound: muted }), + // AI Profile actions addAIProfile: (profile) => { const id = `profile-${Date.now()}-${Math.random() @@ -1139,11 +1150,13 @@ export const useAppStore = create()( chatSessions: state.chatSessions, chatHistoryOpen: state.chatHistoryOpen, maxConcurrency: state.maxConcurrency, + autoModeByProject: state.autoModeByProject, kanbanCardDetailLevel: state.kanbanCardDetailLevel, defaultSkipTests: state.defaultSkipTests, useWorktrees: state.useWorktrees, showProfilesOnly: state.showProfilesOnly, keyboardShortcuts: state.keyboardShortcuts, + muteDoneSound: state.muteDoneSound, aiProfiles: state.aiProfiles, lastSelectedSessionByProject: state.lastSelectedSessionByProject, }), diff --git a/app/src/types/electron.d.ts b/app/src/types/electron.d.ts index 073487aa..46ce7e2e 100644 --- a/app/src/types/electron.d.ts +++ b/app/src/types/electron.d.ts @@ -163,18 +163,21 @@ export type AutoModeEvent = type: "auto_mode_feature_start"; featureId: string; projectId?: string; + projectPath?: string; feature: unknown; } | { type: "auto_mode_progress"; featureId: string; projectId?: string; + projectPath?: string; content: string; } | { type: "auto_mode_tool"; featureId: string; projectId?: string; + projectPath?: string; tool: string; input: unknown; } @@ -182,6 +185,7 @@ export type AutoModeEvent = type: "auto_mode_feature_complete"; featureId: string; projectId?: string; + projectPath?: string; passes: boolean; message: string; } @@ -190,22 +194,26 @@ export type AutoModeEvent = error: string; featureId?: string; projectId?: string; + projectPath?: string; } | { type: "auto_mode_complete"; message: string; projectId?: string; + projectPath?: string; } | { type: "auto_mode_phase"; featureId: string; projectId?: string; + projectPath?: string; phase: "planning" | "action" | "verification"; message: string; } | { type: "auto_mode_ultrathink_preparation"; featureId: string; + projectPath?: string; warnings: string[]; recommendations: string[]; estimatedCost?: number; @@ -264,14 +272,15 @@ export interface SpecRegenerationAPI { } export interface AutoModeAPI { - start: (projectPath: string) => Promise<{ + start: (projectPath: string, maxConcurrency?: number) => Promise<{ success: boolean; error?: string; }>; - stop: () => Promise<{ + stop: (projectPath: string) => Promise<{ success: boolean; error?: string; + runningFeatures?: number; }>; stopFeature: (featureId: string) => Promise<{ @@ -279,11 +288,14 @@ export interface AutoModeAPI { error?: string; }>; - status: () => Promise<{ + status: (projectPath?: string) => Promise<{ success: boolean; + autoLoopRunning?: boolean; isRunning?: boolean; currentFeatureId?: string | null; runningFeatures?: string[]; + runningProjects?: string[]; + runningCount?: number; error?: string; }>;