Compare commits

...

3 Commits

Author SHA1 Message Date
Web Dev Cody
41f14167a6 Merge pull request #20 from AutoMaker-Org/running-agents-list
feat: implement running agents view and enhance auto mode functionality
2025-12-10 22:04:48 -05:00
Cody Seibert
f17abc93c2 Merge branch 'main' into running-agents-list 2025-12-10 21:53:41 -05:00
Cody Seibert
d08f922631 feat: implement running agents view and enhance auto mode functionality
- Added a new `RunningAgentsView` component to display currently active agents working on features.
- Implemented auto-refresh functionality for the running agents list every 2 seconds.
- Enhanced the auto mode service to support project-specific operations, including starting and stopping auto mode for individual projects.
- Updated IPC handlers to manage auto mode status and running agents more effectively.
- Introduced audio settings to mute notifications when agents complete tasks.
- Refactored existing components to accommodate new features and improve overall user experience.
2025-12-10 21:51:00 -05:00
24 changed files with 1482 additions and 408 deletions

View File

@@ -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!"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -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 };
}
}

View File

@@ -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 };
}
});

View File

@@ -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

View File

@@ -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

View File

@@ -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}`;
}
}
/**

View File

@@ -251,7 +251,7 @@ class ClaudeProvider extends ModelProvider {
async detectInstallation() {
const claudeCliDetector = require('./claude-cli-detector');
return claudeCliDetector.getInstallationInfo();
return claudeCliDetector.getFullStatus();
}
getAvailableModels() {

View File

@@ -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 <ContextView />;
case "profiles":
return <ProfilesView />;
case "running-agents":
return <RunningAgentsView />;
default:
return <WelcomeView />;
}

View File

@@ -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() {
</span>
)}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-48" data-testid="project-theme-menu">
<DropdownMenuSubContent
className="w-48"
data-testid="project-theme-menu"
>
<DropdownMenuLabel className="text-xs text-muted-foreground">
Select theme for this project
</DropdownMenuLabel>
@@ -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() {
<DropdownMenuRadioItem
key={option.value}
value={option.value}
data-testid={`project-theme-${option.value || 'global'}`}
data-testid={`project-theme-${
option.value || "global"
}`}
>
<Icon className="w-4 h-4 mr-2" />
<span>{option.label}</span>
@@ -955,21 +973,30 @@ export function Sidebar() {
<DropdownMenuLabel className="text-xs text-muted-foreground">
Project History
</DropdownMenuLabel>
<DropdownMenuItem onClick={cyclePrevProject} data-testid="cycle-prev-project">
<DropdownMenuItem
onClick={cyclePrevProject}
data-testid="cycle-prev-project"
>
<Undo2 className="w-4 h-4 mr-2" />
<span className="flex-1">Previous</span>
<span className="text-[10px] font-mono text-muted-foreground ml-2">
{formatShortcut(shortcuts.cyclePrevProject, true)}
</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={cycleNextProject} data-testid="cycle-next-project">
<DropdownMenuItem
onClick={cycleNextProject}
data-testid="cycle-next-project"
>
<Redo2 className="w-4 h-4 mr-2" />
<span className="flex-1">Next</span>
<span className="text-[10px] font-mono text-muted-foreground ml-2">
{formatShortcut(shortcuts.cycleNextProject, true)}
</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={clearProjectHistory} data-testid="clear-project-history">
<DropdownMenuItem
onClick={clearProjectHistory}
data-testid="clear-project-history"
>
<RotateCcw className="w-4 h-4 mr-2" />
<span>Clear history</span>
</DropdownMenuItem>
@@ -1078,8 +1105,79 @@ export function Sidebar() {
</nav>
</div>
{/* Bottom Section - User / Settings */}
{/* Bottom Section - Running Agents / Bug Report / Settings */}
<div className="border-t border-sidebar-border bg-sidebar-accent/10 shrink-0">
{/* Running Agents Link */}
<div className="p-2 pb-0">
<button
onClick={() => setCurrentView("running-agents")}
className={cn(
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag",
isActiveRoute("running-agents")
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
sidebarOpen ? "justify-start" : "justify-center"
)}
title={!sidebarOpen ? "Running Agents" : undefined}
data-testid="running-agents-link"
>
{isActiveRoute("running-agents") && (
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div>
)}
<Activity
className={cn(
"w-4 h-4 shrink-0 transition-colors",
isActiveRoute("running-agents")
? "text-brand-500"
: "group-hover:text-brand-400"
)}
/>
<span
className={cn(
"ml-2.5 font-medium text-sm flex-1 text-left",
sidebarOpen ? "hidden lg:block" : "hidden"
)}
>
Running Agents
</span>
{!sidebarOpen && (
<span className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border">
Running Agents
</span>
)}
</button>
</div>
{/* Bug Report Link */}
<div className="p-2 pb-0 pt-0">
<button
onClick={() => {
const api = getElectronAPI();
api.openExternalLink("https://github.com/AutoMaker-Org/automaker/issues");
}}
className={cn(
"group flex items-center w-full px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all titlebar-no-drag",
"text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
sidebarOpen ? "justify-start" : "justify-center"
)}
title={!sidebarOpen ? "Report Bug / Feature Request" : undefined}
data-testid="bug-report-link"
>
<Bug className="w-4 h-4 shrink-0 transition-colors group-hover:text-brand-400" />
<span
className={cn(
"ml-2.5 font-medium text-sm flex-1 text-left",
sidebarOpen ? "hidden lg:block" : "hidden"
)}
>
Report Bug / Feature Request
</span>
{!sidebarOpen && (
<span className="absolute left-full ml-2 px-2 py-1 bg-popover text-popover-foreground text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-border">
Report Bug / Feature Request
</span>
)}
</button>
</div>
{/* Settings Link */}
<div className="p-2">
<button
@@ -1272,8 +1370,8 @@ export function Sidebar() {
Generate feature list
</label>
<p className="text-xs text-muted-foreground">
Automatically create features in the features folder
from the implementation roadmap after the spec is generated.
Automatically create features in the features folder from the
implementation roadmap after the spec is generated.
</p>
</div>
</div>

View File

@@ -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"
)}
>
<XIcon />

View File

@@ -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(
<span key="shift" className="leading-none flex items-center justify-center">
<span
key="shift"
className="leading-none flex items-center justify-center"
>
</span>
);
@@ -134,11 +140,7 @@ function getHotkeyDisplayLabel(config: HotkeyConfig, isMac: boolean): React.Reac
</span>
);
return (
<span className="inline-flex items-center gap-1.5">
{parts}
</span>
);
return <span className="inline-flex items-center gap-1.5">{parts}</span>;
}
/**
@@ -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 ? (
<span
className="ml-3 px-2 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/10 border border-primary-foreground/20 inline-flex items-center gap-1.5"
data-testid="hotkey-indicator"
>
{getHotkeyDisplayLabel(config, isMac)}
</span>
) : null;
const hotkeyIndicator =
config && showHotkeyIndicator ? (
<span
className="px-2 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/10 border border-primary-foreground/20 inline-flex items-center gap-1.5"
data-testid="hotkey-indicator"
>
{getHotkeyDisplayLabel(config, isMac)}
</span>
) : null;
return (
<Button

View File

@@ -592,7 +592,7 @@ export function AgentView() {
</div>
<Card
className={cn(
"max-w-[80%]",
"max-w-[80%] py-0",
message.role === "user"
? "bg-transparent border border-primary text-foreground"
: "border-l-4 border-primary bg-card"
@@ -628,7 +628,7 @@ export function AgentView() {
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
<Bot className="w-4 h-4 text-primary" />
</div>
<Card className="border-l-4 border-primary bg-card">
<Card className="border-l-4 border-primary bg-card py-0">
<CardContent className="p-3">
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin text-primary" />

View File

@@ -357,7 +357,10 @@ ${Object.entries(projectAnalysis.filesByExtension)
)
.sort((a: [string, number], b: [string, number]) => b[1] - a[1])
.slice(0, 5)
.map(([ext, count]: [string, number]) => ` <language ext=".${ext}" count="${count}" />`)
.map(
([ext, count]: [string, number]) =>
` <language ext=".${ext}" count="${count}" />`
)
.join("\n")}
</languages>
<frameworks>
@@ -756,6 +759,10 @@ ${Object.entries(projectAnalysis.filesByExtension)
}
// Create each feature using the features API
if (!api.features) {
throw new Error("Features API not available");
}
for (const feature of detectedFeatures) {
await api.features.create(currentProject.path, feature);
}
@@ -829,7 +836,9 @@ ${Object.entries(projectAnalysis.filesByExtension)
</div>
{node.isDirectory && isExpanded && node.children && (
<div>
{node.children.map((child: FileTreeNode) => renderNode(child, depth + 1))}
{node.children.map((child: FileTreeNode) =>
renderNode(child, depth + 1)
)}
</div>
)}
</div>
@@ -953,7 +962,10 @@ ${Object.entries(projectAnalysis.filesByExtension)
<CardContent>
<div className="space-y-2">
{Object.entries(projectAnalysis.filesByExtension)
.sort((a: [string, number], b: [string, number]) => b[1] - a[1])
.sort(
(a: [string, number], b: [string, number]) =>
b[1] - a[1]
)
.slice(0, 15)
.map(([ext, count]: [string, number]) => (
<div key={ext} className="flex justify-between text-sm">
@@ -1096,7 +1108,9 @@ ${Object.entries(projectAnalysis.filesByExtension)
data-testid="analysis-file-tree"
>
<div className="p-2">
{projectAnalysis.fileTree.map((node: FileTreeNode) => renderNode(node))}
{projectAnalysis.fileTree.map((node: FileTreeNode) =>
renderNode(node)
)}
</div>
</CardContent>
</Card>

View File

@@ -529,16 +529,22 @@ export function BoardView() {
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;
// Use event's projectPath or projectId if available, otherwise use current project
// Board view only reacts to events for the currently selected project
const eventProjectId = ('projectId' in event && event.projectId) || projectId;
if (event.type === "auto_mode_feature_complete") {
// Reload features when a feature is completed
console.log("[Board] Feature completed, reloading features...");
loadFeatures();
// Play ding sound when feature is done
const audio = new Audio("/sounds/ding.mp3");
audio.play().catch((err) => console.warn("Could not play ding sound:", err));
// Play ding sound when feature is done (unless muted)
const { muteDoneSound } = useAppStore.getState();
if (!muteDoneSound) {
const audio = new Audio("/sounds/ding.mp3");
audio
.play()
.catch((err) => console.warn("Could not play ding sound:", err));
}
} else if (event.type === "auto_mode_error") {
// Reload features when an error occurs (feature moved to waiting_approval)
console.log(
@@ -580,22 +586,36 @@ export function BoardView() {
const api = getElectronAPI();
if (!api?.autoMode?.status) return;
const status = await api.autoMode.status();
if (status.success && status.runningFeatures) {
console.log(
"[Board] Syncing running tasks from backend:",
status.runningFeatures
);
// Clear existing running tasks for this project and add the actual running ones
const { clearRunningTasks, addRunningTask } = useAppStore.getState();
const status = await api.autoMode.status(currentProject.path);
if (status.success) {
const projectId = currentProject.id;
clearRunningTasks(projectId);
const { clearRunningTasks, addRunningTask, setAutoModeRunning } =
useAppStore.getState();
// Add each running feature to the store
status.runningFeatures.forEach((featureId: string) => {
addRunningTask(projectId, featureId);
});
// Sync running features if available
if (status.runningFeatures) {
console.log(
"[Board] Syncing running tasks from backend:",
status.runningFeatures
);
// Clear existing running tasks for this project and add the actual running ones
clearRunningTasks(projectId);
// Add each running feature to the store
status.runningFeatures.forEach((featureId: string) => {
addRunningTask(projectId, featureId);
});
}
// Sync auto mode running state (backend returns autoLoopRunning, mock returns isRunning)
const isAutoModeRunning =
status.autoLoopRunning ?? status.isRunning ?? false;
console.log(
"[Board] Syncing auto mode running state:",
isAutoModeRunning
);
setAutoModeRunning(projectId, isAutoModeRunning);
}
} catch (error) {
console.error("[Board] Failed to sync running tasks:", error);
@@ -1899,7 +1919,7 @@ export function BoardView() {
data-testid="start-next-button"
>
<FastForward className="w-3 h-3 mr-1" />
Start Next
Pull Top
</HotkeyButton>
)}
</div>

View File

@@ -20,8 +20,11 @@ import {
StopCircle,
ChevronDown,
ChevronRight,
RefreshCw,
Shield,
Zap,
} from "lucide-react";
import { getElectronAPI, FeatureSuggestion, SuggestionsEvent } from "@/lib/electron";
import { getElectronAPI, FeatureSuggestion, SuggestionsEvent, SuggestionType } from "@/lib/electron";
import { useAppStore, Feature } from "@/store/app-store";
import { toast } from "sonner";
@@ -36,6 +39,39 @@ interface FeatureSuggestionsDialogProps {
setIsGenerating: (generating: boolean) => void;
}
// Configuration for each suggestion type
const suggestionTypeConfig: Record<SuggestionType, {
label: string;
icon: React.ComponentType<{ className?: string }>;
description: string;
color: string;
}> = {
features: {
label: "Feature Suggestions",
icon: Lightbulb,
description: "Discover missing features and improvements",
color: "text-yellow-500",
},
refactoring: {
label: "Refactoring Suggestions",
icon: RefreshCw,
description: "Find code smells and refactoring opportunities",
color: "text-blue-500",
},
security: {
label: "Security Suggestions",
icon: Shield,
description: "Identify security vulnerabilities and issues",
color: "text-red-500",
},
performance: {
label: "Performance Suggestions",
icon: Zap,
description: "Discover performance bottlenecks and optimizations",
color: "text-green-500",
},
};
export function FeatureSuggestionsDialog({
open,
onClose,
@@ -49,6 +85,7 @@ export function FeatureSuggestionsDialog({
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const [isImporting, setIsImporting] = useState(false);
const [currentSuggestionType, setCurrentSuggestionType] = useState<SuggestionType | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const autoScrollRef = useRef(true);
@@ -87,7 +124,8 @@ export function FeatureSuggestionsDialog({
setSuggestions(event.suggestions);
// Select all by default
setSelectedIds(new Set(event.suggestions.map((s) => s.id)));
toast.success(`Generated ${event.suggestions.length} feature suggestions!`);
const typeLabel = currentSuggestionType ? suggestionTypeConfig[currentSuggestionType].label.toLowerCase() : "suggestions";
toast.success(`Generated ${event.suggestions.length} ${typeLabel}!`);
} else {
toast.info("No suggestions generated. Try again.");
}
@@ -100,10 +138,10 @@ export function FeatureSuggestionsDialog({
return () => {
unsubscribe();
};
}, [open, setSuggestions, setIsGenerating]);
}, [open, setSuggestions, setIsGenerating, currentSuggestionType]);
// Start generating suggestions
const handleGenerate = useCallback(async () => {
// Start generating suggestions for a specific type
const handleGenerate = useCallback(async (suggestionType: SuggestionType) => {
const api = getElectronAPI();
if (!api?.suggestions) {
toast.error("Suggestions API not available");
@@ -114,9 +152,10 @@ export function FeatureSuggestionsDialog({
setProgress([]);
setSuggestions([]);
setSelectedIds(new Set());
setCurrentSuggestionType(suggestionType);
try {
const result = await api.suggestions.generate(projectPath);
const result = await api.suggestions.generate(projectPath, suggestionType);
if (!result.success) {
toast.error(result.error || "Failed to start generation");
setIsGenerating(false);
@@ -203,8 +242,10 @@ export function FeatureSuggestionsDialog({
}));
// Create each new feature using the features API
for (const feature of newFeatures) {
await api.features.create(projectPath, feature);
if (api.features) {
for (const feature of newFeatures) {
await api.features.create(projectPath, feature);
}
}
// Merge with existing features for store update
@@ -219,6 +260,7 @@ export function FeatureSuggestionsDialog({
setSuggestions([]);
setSelectedIds(new Set());
setProgress([]);
setCurrentSuggestionType(null);
onClose();
} catch (error) {
@@ -238,16 +280,17 @@ export function FeatureSuggestionsDialog({
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]);
// Go back to type selection
const handleBackToSelection = useCallback(() => {
setSuggestions([]);
setSelectedIds(new Set());
setProgress([]);
setCurrentSuggestionType(null);
}, [setSuggestions]);
const hasStarted = progress.length > 0 || suggestions.length > 0;
const hasSuggestions = suggestions.length > 0;
const currentConfig = currentSuggestionType ? suggestionTypeConfig[currentSuggestionType] : null;
return (
<Dialog open={open} onOpenChange={onClose}>
@@ -257,31 +300,56 @@ export function FeatureSuggestionsDialog({
>
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2">
<Lightbulb className="w-5 h-5 text-yellow-500" />
Feature Suggestions
{currentConfig ? (
<>
<currentConfig.icon className={`w-5 h-5 ${currentConfig.color}`} />
{currentConfig.label}
</>
) : (
<>
<Lightbulb className="w-5 h-5 text-yellow-500" />
AI Suggestions
</>
)}
</DialogTitle>
<DialogDescription>
Analyze your project to discover missing features and improvements.
The AI will scan your codebase and suggest features ordered by priority.
{currentConfig
? currentConfig.description
: "Analyze your project to discover improvements. Choose a suggestion type below."}
</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.
// Initial state - show suggestion type buttons
<div className="flex-1 flex flex-col items-center justify-center py-8">
<p className="text-muted-foreground text-center max-w-lg mb-8">
Our AI will analyze your project and generate actionable suggestions.
Choose what type of analysis you want to perform:
</p>
<Button onClick={handleGenerate} size="lg">
<Lightbulb className="w-4 h-4 mr-2" />
Generate Suggestions
</Button>
<div className="grid grid-cols-2 gap-4 w-full max-w-2xl">
{(Object.entries(suggestionTypeConfig) as [SuggestionType, typeof suggestionTypeConfig[SuggestionType]][]).map(
([type, config]) => {
const Icon = config.icon;
return (
<Button
key={type}
variant="outline"
className="h-auto py-6 px-6 flex flex-col items-center gap-3 hover:border-primary/50 transition-colors"
onClick={() => handleGenerate(type)}
data-testid={`generate-${type}-btn`}
>
<Icon className={`w-8 h-8 ${config.color}`} />
<div className="text-center">
<div className="font-semibold">{config.label.replace(" Suggestions", "")}</div>
<div className="text-xs text-muted-foreground mt-1">
{config.description}
</div>
</div>
</Button>
);
}
)}
</div>
</div>
) : isGenerating ? (
// Generating state - show progress
@@ -410,20 +478,34 @@ export function FeatureSuggestionsDialog({
<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 className="flex gap-2">
<Button variant="outline" onClick={handleBackToSelection}>
Back to Selection
</Button>
{currentSuggestionType && (
<Button onClick={() => handleGenerate(currentSuggestionType)}>
<Lightbulb className="w-4 h-4 mr-2" />
Try Again
</Button>
)}
</div>
</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="outline" onClick={handleBackToSelection}>
Back
</Button>
{currentSuggestionType && (
<Button variant="outline" onClick={() => handleGenerate(currentSuggestionType)}>
{currentConfig && <currentConfig.icon className="w-4 h-4 mr-2" />}
Regenerate
</Button>
)}
</div>
<div className="flex gap-2">
<Button variant="ghost" onClick={onClose}>
Cancel

View File

@@ -248,21 +248,12 @@ export const KanbanCard = memo(function KanbanCard({
{...attributes}
{...(isDraggable ? listeners : {})}
>
{/* Shortcut key badge for in-progress cards */}
{shortcutKey && (
<div
className="absolute top-2 left-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70 z-10"
data-testid={`shortcut-key-${feature.id}`}
>
{shortcutKey}
</div>
)}
{/* Skip Tests indicator badge */}
{feature.skipTests && !feature.error && (
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
shortcutKey ? "top-2 left-10" : "top-2 left-2",
"top-2 left-2",
"bg-orange-500/20 border border-orange-500/50 text-orange-400"
)}
data-testid={`skip-tests-badge-${feature.id}`}
@@ -277,7 +268,7 @@ export const KanbanCard = memo(function KanbanCard({
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
shortcutKey ? "top-2 left-10" : "top-2 left-2",
"top-2 left-2",
"bg-red-500/20 border border-red-500/50 text-red-400"
)}
data-testid={`error-badge-${feature.id}`}
@@ -299,9 +290,7 @@ export const KanbanCard = memo(function KanbanCard({
// Position below error badge if present, otherwise use normal position
feature.error || feature.skipTests
? "top-8 left-2"
: shortcutKey
? "top-2 left-10"
: "top-2 left-2"
: "top-2 left-2"
)}
data-testid={`branch-badge-${feature.id}`}
>
@@ -319,7 +308,7 @@ export const KanbanCard = memo(function KanbanCard({
className={cn(
"p-3 pb-2 block", // Reset grid layout to block for custom kanban card layout
// Add extra top padding when badges are present to prevent text overlap
(feature.skipTests || feature.error || shortcutKey) && "pt-10",
(feature.skipTests || feature.error) && "pt-10",
// Add even more top padding when both badges and branch are shown
hasWorktree && (feature.skipTests || feature.error) && "pt-14"
)}
@@ -613,6 +602,14 @@ export const KanbanCard = memo(function KanbanCard({
>
<FileText className="w-3 h-3 mr-1" />
Logs
{shortcutKey && (
<span
className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/10 border border-primary-foreground/20"
data-testid={`shortcut-key-${feature.id}`}
>
{shortcutKey}
</span>
)}
</Button>
)}
{onForceStop && (

View File

@@ -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<RunningAgent[]>([]);
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 (
<div className="flex-1 flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="flex-1 flex flex-col overflow-hidden p-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-brand-500/10">
<Activity className="h-6 w-6 text-brand-500" />
</div>
<div>
<h1 className="text-2xl font-bold">Running Agents</h1>
<p className="text-sm text-muted-foreground">
{runningAgents.length === 0
? "No agents currently running"
: `${runningAgents.length} agent${runningAgents.length === 1 ? "" : "s"} running across all projects`}
</p>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
disabled={refreshing}
>
<RefreshCw
className={cn("h-4 w-4 mr-2", refreshing && "animate-spin")}
/>
Refresh
</Button>
</div>
{/* Content */}
{runningAgents.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center text-center">
<div className="p-4 rounded-full bg-muted/50 mb-4">
<Bot className="h-12 w-12 text-muted-foreground" />
</div>
<h2 className="text-lg font-medium mb-2">No Running Agents</h2>
<p className="text-muted-foreground max-w-md">
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".
</p>
</div>
) : (
<div className="flex-1 overflow-auto">
<div className="space-y-3">
{runningAgents.map((agent) => (
<div
key={`${agent.projectPath}-${agent.featureId}`}
className="flex items-center justify-between p-4 rounded-lg border border-border bg-card hover:bg-accent/50 transition-colors"
>
<div className="flex items-center gap-4 min-w-0">
{/* Status indicator */}
<div className="relative">
<Bot className="h-8 w-8 text-brand-500" />
<span className="absolute -top-1 -right-1 flex h-3 w-3">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex rounded-full h-3 w-3 bg-green-500" />
</span>
</div>
{/* Agent info */}
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium truncate">
{agent.featureId}
</span>
{agent.isAutoMode && (
<span className="px-2 py-0.5 text-[10px] font-medium rounded-full bg-brand-500/10 text-brand-500 border border-brand-500/30">
AUTO
</span>
)}
</div>
<button
onClick={() => handleNavigateToProject(agent)}
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<Folder className="h-3.5 w-3.5" />
<span className="truncate">{agent.projectName}</span>
</button>
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2 flex-shrink-0">
<Button
variant="ghost"
size="sm"
onClick={() => handleNavigateToProject(agent)}
className="text-muted-foreground hover:text-foreground"
>
View Project
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => handleStopAgent(agent.featureId)}
>
<Square className="h-3.5 w-3.5 mr-1.5" />
Stop
</Button>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -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 */}
<div
id="audio"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Volume2 className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
Audio
</h2>
</div>
<p className="text-sm text-muted-foreground">
Configure audio and notification settings.
</p>
</div>
<div className="p-6 space-y-4">
{/* Mute Done Sound Setting */}
<div className="space-y-3">
<div className="flex items-start space-x-3">
<Checkbox
id="mute-done-sound"
checked={muteDoneSound}
onCheckedChange={(checked) =>
setMuteDoneSound(checked === true)
}
className="mt-0.5"
data-testid="mute-done-sound-checkbox"
/>
<div className="space-y-1">
<Label
htmlFor="mute-done-sound"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<VolumeX className="w-4 h-4 text-brand-500" />
Mute notification sound when agents complete
</Label>
<p className="text-xs text-muted-foreground">
When enabled, disables the &quot;ding&quot; sound that
plays when an agent completes a feature. The feature
will still move to the completed column, but without
audio notification.
</p>
</div>
</div>
</div>
</div>
</div>
{/* Feature Defaults Section */}
<FeatureDefaultsSection
showProfilesOnly={showProfilesOnly}

View File

@@ -1,7 +1,8 @@
import { useEffect, useCallback, useMemo } from "react";
import { useShallow } from "zustand/react/shallow";
import { useAppStore } from "@/store/app-store";
import { getElectronAPI, type AutoModeEvent } from "@/lib/electron";
import { getElectronAPI } from "@/lib/electron";
import type { AutoModeEvent } from "@/types/electron";
/**
* Hook for managing auto mode (scoped per project)
@@ -16,6 +17,7 @@ export function useAutoMode() {
currentProject,
addAutoModeActivity,
maxConcurrency,
projects,
} = useAppStore(
useShallow((state) => ({
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);

View File

@@ -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<RunningAgentsResult>;
}
// 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<string>;
openExternalLink: (url: string) => Promise<{ success: boolean; error?: string }>;
openDirectory: () => Promise<DialogResult>;
openFile: (options?: object) => Promise<DialogResult>;
readFile: (filePath: string) => Promise<FileResult>;
@@ -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<SuggestionType, string> = {
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 {

View File

@@ -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<KeyboardShortcuts>) => void;
resetKeyboardShortcuts: () => void;
// Audio Settings actions
setMuteDoneSound: (muted: boolean) => void;
// AI Profile actions
addAIProfile: (profile: Omit<AIProfile, "id">) => void;
updateAIProfile: (id: string, updates: Partial<AIProfile>) => 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<AppState & AppActions>()(
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<AppState & AppActions>()(
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,
}),

View File

@@ -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;
}>;