mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41f14167a6 | ||
|
|
f17abc93c2 | ||
|
|
d08f922631 |
@@ -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 |
@@ -20,11 +20,64 @@ class AutoModeService {
|
|||||||
constructor() {
|
constructor() {
|
||||||
// Track multiple concurrent feature executions
|
// Track multiple concurrent feature executions
|
||||||
this.runningFeatures = new Map(); // featureId -> { abortController, query, projectPath, sendToRenderer }
|
this.runningFeatures = new Map(); // featureId -> { abortController, query, projectPath, sendToRenderer }
|
||||||
this.autoLoopRunning = false; // Separate flag for the auto loop
|
|
||||||
this.autoLoopAbortController = null;
|
// Per-project auto loop state (keyed by projectPath)
|
||||||
this.autoLoopInterval = null; // Timer for periodic checking
|
this.projectLoops = new Map(); // projectPath -> { isRunning, interval, abortController, sendToRenderer, maxConcurrency }
|
||||||
|
|
||||||
this.checkIntervalMs = 5000; // Check every 5 seconds
|
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;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to emit event with projectPath included
|
||||||
|
*/
|
||||||
|
emitEvent(projectPath, sendToRenderer, event) {
|
||||||
|
if (sendToRenderer) {
|
||||||
|
sendToRenderer({
|
||||||
|
...event,
|
||||||
|
projectPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup worktree for a feature
|
* Setup worktree for a feature
|
||||||
* Creates an isolated git worktree where the agent can work
|
* Creates an isolated git worktree where the agent can work
|
||||||
@@ -65,7 +130,7 @@ class AutoModeService {
|
|||||||
return { useWorktree: false, workPath: projectPath };
|
return { useWorktree: false, workPath: projectPath };
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_progress",
|
type: "auto_mode_progress",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
content: "Creating isolated worktree for feature...\n",
|
content: "Creating isolated worktree for feature...\n",
|
||||||
@@ -75,7 +140,7 @@ class AutoModeService {
|
|||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.warn(`[AutoMode] Failed to create worktree: ${result.error}. Falling back to main project.`);
|
console.warn(`[AutoMode] Failed to create worktree: ${result.error}. Falling back to main project.`);
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_progress",
|
type: "auto_mode_progress",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
content: `Warning: Could not create worktree (${result.error}). Working directly on main project.\n`,
|
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}`);
|
console.log(`[AutoMode] Created worktree at: ${result.worktreePath}, branch: ${result.branchName}`);
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_progress",
|
type: "auto_mode_progress",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
content: `Working in isolated branch: ${result.branchName}\n`,
|
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 }) {
|
async start({ projectPath, sendToRenderer, maxConcurrency }) {
|
||||||
if (this.autoLoopRunning) {
|
const projectState = this.getProjectLoopState(projectPath);
|
||||||
throw new Error("Auto mode loop is already running");
|
|
||||||
|
if (projectState.isRunning) {
|
||||||
|
throw new Error(`Auto mode loop is already running for project: ${projectPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.autoLoopRunning = true;
|
projectState.isRunning = true;
|
||||||
this.maxConcurrency = maxConcurrency || 3;
|
projectState.maxConcurrency = maxConcurrency || 3;
|
||||||
|
projectState.sendToRenderer = sendToRenderer;
|
||||||
|
|
||||||
console.log(
|
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
|
// Start the periodic checking loop for this project
|
||||||
this.runPeriodicLoop(projectPath, sendToRenderer);
|
this.runPeriodicLoopForProject(projectPath);
|
||||||
|
|
||||||
return { success: true };
|
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.
|
* This only turns off the auto toggle to prevent picking up new features.
|
||||||
* Running tasks will continue until they complete naturally.
|
* Running tasks will continue until they complete naturally.
|
||||||
*/
|
*/
|
||||||
async stop() {
|
async stop({ projectPath }) {
|
||||||
console.log("[AutoMode] Stopping auto mode (letting running features complete)");
|
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
|
projectState.isRunning = false;
|
||||||
if (this.autoLoopInterval) {
|
|
||||||
clearInterval(this.autoLoopInterval);
|
// Clear the interval timer for this project
|
||||||
this.autoLoopInterval = null;
|
if (projectState.interval) {
|
||||||
|
clearInterval(projectState.interval);
|
||||||
|
projectState.interval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abort auto loop if running
|
// Abort auto loop if running
|
||||||
if (this.autoLoopAbortController) {
|
if (projectState.abortController) {
|
||||||
this.autoLoopAbortController.abort();
|
projectState.abortController.abort();
|
||||||
this.autoLoopAbortController = null;
|
projectState.abortController = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: We intentionally do NOT abort running features here.
|
// NOTE: We intentionally do NOT abort running features here.
|
||||||
@@ -154,23 +229,58 @@ class AutoModeService {
|
|||||||
// from being picked up. Running features will complete naturally.
|
// from being picked up. Running features will complete naturally.
|
||||||
// Use stopFeature() to cancel a specific running feature if needed.
|
// Use stopFeature() to cancel a specific running feature if needed.
|
||||||
|
|
||||||
const runningCount = this.runningFeatures.size;
|
const runningCount = this.getRunningCountForProject(projectPath);
|
||||||
console.log(`[AutoMode] Auto loop stopped. ${runningCount} feature(s) still running and will complete.`);
|
console.log(`[AutoMode] Auto loop stopped for ${projectPath}. ${runningCount} feature(s) still running and will complete.`);
|
||||||
|
|
||||||
return { success: true, runningFeatures: runningCount };
|
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 {
|
return {
|
||||||
autoLoopRunning: this.autoLoopRunning,
|
autoLoopRunning: this.hasAnyAutoLoopRunning(),
|
||||||
|
runningProjects: allRunningProjects,
|
||||||
runningFeatures: Array.from(this.runningFeatures.keys()),
|
runningFeatures: Array.from(this.runningFeatures.keys()),
|
||||||
runningCount: this.runningFeatures.size,
|
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
|
* Run a specific feature by ID
|
||||||
* @param {string} projectPath - Path to the project
|
* @param {string} projectPath - Path to the project
|
||||||
@@ -218,7 +328,7 @@ class AutoModeService {
|
|||||||
projectPath
|
projectPath
|
||||||
);
|
);
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_feature_start",
|
type: "auto_mode_feature_start",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
feature: { ...feature, worktreePath: worktreeSetup.workPath, branchName: worktreeSetup.branchName },
|
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)
|
// Keep context file for viewing output later (deleted only when card is removed)
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_feature_complete",
|
type: "auto_mode_feature_complete",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
passes: result.passes,
|
passes: result.passes,
|
||||||
@@ -288,7 +398,7 @@ class AutoModeService {
|
|||||||
console.error("[AutoMode] Failed to update feature status after error:", statusError);
|
console.error("[AutoMode] Failed to update feature status after error:", statusError);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_error",
|
type: "auto_mode_error",
|
||||||
error: error.message,
|
error: error.message,
|
||||||
featureId: featureId,
|
featureId: featureId,
|
||||||
@@ -333,7 +443,7 @@ class AutoModeService {
|
|||||||
|
|
||||||
console.log(`[AutoMode] Verifying feature: ${feature.description}`);
|
console.log(`[AutoMode] Verifying feature: ${feature.description}`);
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_feature_start",
|
type: "auto_mode_feature_start",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
feature: feature,
|
feature: feature,
|
||||||
@@ -357,7 +467,7 @@ class AutoModeService {
|
|||||||
|
|
||||||
// Keep context file for viewing output later (deleted only when card is removed)
|
// Keep context file for viewing output later (deleted only when card is removed)
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_feature_complete",
|
type: "auto_mode_feature_complete",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
passes: result.passes,
|
passes: result.passes,
|
||||||
@@ -392,7 +502,7 @@ class AutoModeService {
|
|||||||
console.error("[AutoMode] Failed to update feature status after error:", statusError);
|
console.error("[AutoMode] Failed to update feature status after error:", statusError);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_error",
|
type: "auto_mode_error",
|
||||||
error: error.message,
|
error: error.message,
|
||||||
featureId: featureId,
|
featureId: featureId,
|
||||||
@@ -437,7 +547,7 @@ class AutoModeService {
|
|||||||
|
|
||||||
console.log(`[AutoMode] Resuming feature: ${feature.description}`);
|
console.log(`[AutoMode] Resuming feature: ${feature.description}`);
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_feature_start",
|
type: "auto_mode_feature_start",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
feature: feature,
|
feature: feature,
|
||||||
@@ -481,7 +591,7 @@ class AutoModeService {
|
|||||||
`\n\n🔄 Auto-retry #${attempts} - Continuing implementation...\n\n`
|
`\n\n🔄 Auto-retry #${attempts} - Continuing implementation...\n\n`
|
||||||
);
|
);
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_progress",
|
type: "auto_mode_progress",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
content: `\n🔄 Auto-retry #${attempts} - Agent ended early, continuing...\n`,
|
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)
|
// Keep context file for viewing output later (deleted only when card is removed)
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_feature_complete",
|
type: "auto_mode_feature_complete",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
passes: finalResult.passes,
|
passes: finalResult.passes,
|
||||||
@@ -559,7 +669,7 @@ class AutoModeService {
|
|||||||
console.error("[AutoMode] Failed to update feature status after error:", statusError);
|
console.error("[AutoMode] Failed to update feature status after error:", statusError);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_error",
|
type: "auto_mode_error",
|
||||||
error: error.message,
|
error: error.message,
|
||||||
featureId: featureId,
|
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
|
* This loop continues running even if there are no backlog items
|
||||||
*/
|
*/
|
||||||
runPeriodicLoop(projectPath, sendToRenderer) {
|
runPeriodicLoopForProject(projectPath) {
|
||||||
|
const projectState = this.getProjectLoopState(projectPath);
|
||||||
|
|
||||||
console.log(
|
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
|
// Initial check immediately
|
||||||
this.checkAndStartFeatures(projectPath, sendToRenderer);
|
this.checkAndStartFeaturesForProject(projectPath);
|
||||||
|
|
||||||
// Then check periodically
|
// Then check periodically
|
||||||
this.autoLoopInterval = setInterval(() => {
|
projectState.interval = setInterval(() => {
|
||||||
if (this.autoLoopRunning) {
|
if (projectState.isRunning) {
|
||||||
this.checkAndStartFeatures(projectPath, sendToRenderer);
|
this.checkAndStartFeaturesForProject(projectPath);
|
||||||
}
|
}
|
||||||
}, this.checkIntervalMs);
|
}, 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 {
|
try {
|
||||||
// Check how many are currently running
|
// Check how many are currently running FOR THIS PROJECT
|
||||||
const currentRunningCount = this.runningFeatures.size;
|
const currentRunningCount = this.getRunningCountForProject(projectPath);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[AutoMode] Checking features - Running: ${currentRunningCount}/${this.maxConcurrency}`
|
`[AutoMode] [${projectPath}] Checking features - Running: ${currentRunningCount}/${maxConcurrency}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate available slots
|
// Calculate available slots for this project
|
||||||
const availableSlots = this.maxConcurrency - currentRunningCount;
|
const availableSlots = maxConcurrency - currentRunningCount;
|
||||||
|
|
||||||
if (availableSlots <= 0) {
|
if (availableSlots <= 0) {
|
||||||
console.log("[AutoMode] At max concurrency, waiting...");
|
console.log(`[AutoMode] [${projectPath}] At max concurrency, waiting...`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +736,7 @@ class AutoModeService {
|
|||||||
const backlogFeatures = features.filter((f) => f.status === "backlog");
|
const backlogFeatures = features.filter((f) => f.status === "backlog");
|
||||||
|
|
||||||
if (backlogFeatures.length === 0) {
|
if (backlogFeatures.length === 0) {
|
||||||
console.log("[AutoMode] No backlog features available, waiting...");
|
console.log(`[AutoMode] [${projectPath}] No backlog features available, waiting...`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,7 +744,7 @@ class AutoModeService {
|
|||||||
const featuresToStart = backlogFeatures.slice(0, availableSlots);
|
const featuresToStart = backlogFeatures.slice(0, availableSlots);
|
||||||
|
|
||||||
console.log(
|
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)
|
// Start each feature (don't await - run in parallel like drag operations)
|
||||||
@@ -632,7 +752,7 @@ class AutoModeService {
|
|||||||
this.startFeatureAsync(feature, projectPath, sendToRenderer);
|
this.startFeatureAsync(feature, projectPath, sendToRenderer);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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
|
projectPath
|
||||||
);
|
);
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_feature_start",
|
type: "auto_mode_feature_start",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
feature: { ...feature, worktreePath: worktreeSetup.workPath, branchName: worktreeSetup.branchName },
|
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)
|
// Keep context file for viewing output later (deleted only when card is removed)
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_feature_complete",
|
type: "auto_mode_feature_complete",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
passes: result.passes,
|
passes: result.passes,
|
||||||
@@ -746,7 +866,7 @@ class AutoModeService {
|
|||||||
console.error("[AutoMode] Failed to update feature status after error:", statusError);
|
console.error("[AutoMode] Failed to update feature status after error:", statusError);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_error",
|
type: "auto_mode_error",
|
||||||
error: error.message,
|
error: error.message,
|
||||||
featureId: featureId,
|
featureId: featureId,
|
||||||
@@ -778,7 +898,7 @@ class AutoModeService {
|
|||||||
this.runningFeatures.set(analysisId, execution);
|
this.runningFeatures.set(analysisId, execution);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_feature_start",
|
type: "auto_mode_feature_start",
|
||||||
featureId: analysisId,
|
featureId: analysisId,
|
||||||
feature: {
|
feature: {
|
||||||
@@ -796,7 +916,7 @@ class AutoModeService {
|
|||||||
execution
|
execution
|
||||||
);
|
);
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_feature_complete",
|
type: "auto_mode_feature_complete",
|
||||||
featureId: analysisId,
|
featureId: analysisId,
|
||||||
passes: result.success,
|
passes: result.success,
|
||||||
@@ -806,7 +926,7 @@ class AutoModeService {
|
|||||||
return { success: true, message: result.message };
|
return { success: true, message: result.message };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[AutoMode] Error analyzing project:", error);
|
console.error("[AutoMode] Error analyzing project:", error);
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_error",
|
type: "auto_mode_error",
|
||||||
error: error.message,
|
error: error.message,
|
||||||
featureId: analysisId,
|
featureId: analysisId,
|
||||||
@@ -911,7 +1031,7 @@ class AutoModeService {
|
|||||||
projectPath
|
projectPath
|
||||||
);
|
);
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_feature_start",
|
type: "auto_mode_feature_start",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
feature: feature,
|
feature: feature,
|
||||||
@@ -956,7 +1076,7 @@ class AutoModeService {
|
|||||||
|
|
||||||
// Keep context file for viewing output later (deleted only when card is removed)
|
// Keep context file for viewing output later (deleted only when card is removed)
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_feature_complete",
|
type: "auto_mode_feature_complete",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
passes: result.passes,
|
passes: result.passes,
|
||||||
@@ -989,7 +1109,7 @@ class AutoModeService {
|
|||||||
console.error("[AutoMode] Failed to update feature status after error:", statusError);
|
console.error("[AutoMode] Failed to update feature status after error:", statusError);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_error",
|
type: "auto_mode_error",
|
||||||
error: error.message,
|
error: error.message,
|
||||||
featureId: featureId,
|
featureId: featureId,
|
||||||
@@ -1021,13 +1141,13 @@ class AutoModeService {
|
|||||||
throw new Error(`Feature ${featureId} not found`);
|
throw new Error(`Feature ${featureId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_feature_start",
|
type: "auto_mode_feature_start",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
feature: { ...feature, description: "Committing changes..." },
|
feature: { ...feature, description: "Committing changes..." },
|
||||||
});
|
});
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_phase",
|
type: "auto_mode_phase",
|
||||||
featureId,
|
featureId,
|
||||||
phase: "action",
|
phase: "action",
|
||||||
@@ -1051,7 +1171,7 @@ class AutoModeService {
|
|||||||
|
|
||||||
// Keep context file for viewing output later (deleted only when card is removed)
|
// Keep context file for viewing output later (deleted only when card is removed)
|
||||||
|
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_feature_complete",
|
type: "auto_mode_feature_complete",
|
||||||
featureId: feature.id,
|
featureId: feature.id,
|
||||||
passes: true,
|
passes: true,
|
||||||
@@ -1061,7 +1181,7 @@ class AutoModeService {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[AutoMode] Error committing feature:", error);
|
console.error("[AutoMode] Error committing feature:", error);
|
||||||
sendToRenderer({
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
type: "auto_mode_error",
|
type: "auto_mode_error",
|
||||||
error: error.message,
|
error: error.message,
|
||||||
featureId: featureId,
|
featureId: featureId,
|
||||||
@@ -1108,26 +1228,22 @@ class AutoModeService {
|
|||||||
// Delete context file
|
// Delete context file
|
||||||
await contextManager.deleteContextFile(projectPath, featureId);
|
await contextManager.deleteContextFile(projectPath, featureId);
|
||||||
|
|
||||||
if (sendToRenderer) {
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
sendToRenderer({
|
type: "auto_mode_feature_complete",
|
||||||
type: "auto_mode_feature_complete",
|
featureId: featureId,
|
||||||
featureId: featureId,
|
passes: false,
|
||||||
passes: false,
|
message: "Feature reverted - all changes discarded",
|
||||||
message: "Feature reverted - all changes discarded",
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[AutoMode] Feature ${featureId} reverted successfully`);
|
console.log(`[AutoMode] Feature ${featureId} reverted successfully`);
|
||||||
return { success: true, removedPath: result.removedPath };
|
return { success: true, removedPath: result.removedPath };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[AutoMode] Error reverting feature:", error);
|
console.error("[AutoMode] Error reverting feature:", error);
|
||||||
if (sendToRenderer) {
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
sendToRenderer({
|
type: "auto_mode_error",
|
||||||
type: "auto_mode_error",
|
error: error.message,
|
||||||
error: error.message,
|
featureId: featureId,
|
||||||
featureId: featureId,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1147,13 +1263,11 @@ class AutoModeService {
|
|||||||
throw new Error(`Feature ${featureId} not found`);
|
throw new Error(`Feature ${featureId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sendToRenderer) {
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
sendToRenderer({
|
type: "auto_mode_progress",
|
||||||
type: "auto_mode_progress",
|
featureId: featureId,
|
||||||
featureId: featureId,
|
content: "Merging feature branch into main...\n",
|
||||||
content: "Merging feature branch into main...\n",
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge the worktree
|
// Merge the worktree
|
||||||
const result = await worktreeManager.mergeWorktree(projectPath, featureId, {
|
const result = await worktreeManager.mergeWorktree(projectPath, featureId, {
|
||||||
@@ -1171,26 +1285,22 @@ class AutoModeService {
|
|||||||
// Update feature status to verified
|
// Update feature status to verified
|
||||||
await featureLoader.updateFeatureStatus(featureId, "verified", projectPath);
|
await featureLoader.updateFeatureStatus(featureId, "verified", projectPath);
|
||||||
|
|
||||||
if (sendToRenderer) {
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
sendToRenderer({
|
type: "auto_mode_feature_complete",
|
||||||
type: "auto_mode_feature_complete",
|
featureId: featureId,
|
||||||
featureId: featureId,
|
passes: true,
|
||||||
passes: true,
|
message: `Feature merged into ${result.intoBranch}`,
|
||||||
message: `Feature merged into ${result.intoBranch}`,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[AutoMode] Feature ${featureId} merged successfully`);
|
console.log(`[AutoMode] Feature ${featureId} merged successfully`);
|
||||||
return { success: true, mergedBranch: result.mergedBranch };
|
return { success: true, mergedBranch: result.mergedBranch };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[AutoMode] Error merging feature:", error);
|
console.error("[AutoMode] Error merging feature:", error);
|
||||||
if (sendToRenderer) {
|
this.emitEvent(projectPath, sendToRenderer, {
|
||||||
sendToRenderer({
|
type: "auto_mode_error",
|
||||||
type: "auto_mode_error",
|
error: error.message,
|
||||||
error: error.message,
|
featureId: featureId,
|
||||||
featureId: featureId,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -355,6 +355,17 @@ ipcMain.handle("ping", () => {
|
|||||||
return "pong";
|
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
|
// 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 {
|
try {
|
||||||
return await autoModeService.stop();
|
return await autoModeService.stop({ projectPath });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[IPC] auto-mode:stop error:", error);
|
console.error("[IPC] auto-mode:stop error:", error);
|
||||||
return { success: false, error: error.message };
|
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 {
|
try {
|
||||||
return { success: true, ...autoModeService.getStatus() };
|
return { success: true, ...autoModeService.getStatus({ projectPath }) };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[IPC] auto-mode:status error:", error);
|
console.error("[IPC] auto-mode:status error:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
@@ -942,9 +953,11 @@ let suggestionsExecution = null;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate feature suggestions by analyzing the project
|
* 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 }) => {
|
ipcMain.handle("suggestions:generate", async (_, { projectPath, suggestionType = "features" }) => {
|
||||||
console.log("[IPC] suggestions:generate called with:", { projectPath });
|
console.log("[IPC] suggestions:generate called with:", { projectPath, suggestionType });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if already running
|
// Check if already running
|
||||||
@@ -970,7 +983,7 @@ ipcMain.handle("suggestions:generate", async (_, { projectPath }) => {
|
|||||||
|
|
||||||
// Start generating suggestions (runs in background)
|
// Start generating suggestions (runs in background)
|
||||||
featureSuggestionsService
|
featureSuggestionsService
|
||||||
.generateSuggestions(projectPath, sendToRenderer, suggestionsExecution)
|
.generateSuggestions(projectPath, sendToRenderer, suggestionsExecution, suggestionType)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("[IPC] suggestions:generate background error:", error);
|
console.error("[IPC] suggestions:generate background error:", error);
|
||||||
sendToRenderer({
|
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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
// IPC test
|
// IPC test
|
||||||
ping: () => ipcRenderer.invoke("ping"),
|
ping: () => ipcRenderer.invoke("ping"),
|
||||||
|
|
||||||
|
// Shell APIs
|
||||||
|
openExternalLink: (url) => ipcRenderer.invoke("shell:openExternal", url),
|
||||||
|
|
||||||
// Dialog APIs
|
// Dialog APIs
|
||||||
openDirectory: () => ipcRenderer.invoke("dialog:openDirectory"),
|
openDirectory: () => ipcRenderer.invoke("dialog:openDirectory"),
|
||||||
openFile: (options) => ipcRenderer.invoke("dialog:openFile", options),
|
openFile: (options) => ipcRenderer.invoke("dialog:openFile", options),
|
||||||
@@ -97,15 +100,15 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
|
|
||||||
// Auto Mode API
|
// Auto Mode API
|
||||||
autoMode: {
|
autoMode: {
|
||||||
// Start auto mode
|
// Start auto mode for a specific project
|
||||||
start: (projectPath, maxConcurrency) =>
|
start: (projectPath, maxConcurrency) =>
|
||||||
ipcRenderer.invoke("auto-mode:start", { projectPath, maxConcurrency }),
|
ipcRenderer.invoke("auto-mode:start", { projectPath, maxConcurrency }),
|
||||||
|
|
||||||
// Stop auto mode
|
// Stop auto mode for a specific project
|
||||||
stop: () => ipcRenderer.invoke("auto-mode:stop"),
|
stop: (projectPath) => ipcRenderer.invoke("auto-mode:stop", { projectPath }),
|
||||||
|
|
||||||
// Get auto mode status
|
// Get auto mode status (optionally for a specific project)
|
||||||
status: () => ipcRenderer.invoke("auto-mode:status"),
|
status: (projectPath) => ipcRenderer.invoke("auto-mode:status", { projectPath }),
|
||||||
|
|
||||||
// Run a specific feature
|
// Run a specific feature
|
||||||
runFeature: (projectPath, featureId, useWorktrees) =>
|
runFeature: (projectPath, featureId, useWorktrees) =>
|
||||||
@@ -243,8 +246,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
// Feature Suggestions API
|
// Feature Suggestions API
|
||||||
suggestions: {
|
suggestions: {
|
||||||
// Generate feature suggestions
|
// Generate feature suggestions
|
||||||
generate: (projectPath) =>
|
// suggestionType can be: "features", "refactoring", "security", "performance"
|
||||||
ipcRenderer.invoke("suggestions:generate", { projectPath }),
|
generate: (projectPath, suggestionType = "features") =>
|
||||||
|
ipcRenderer.invoke("suggestions:generate", { projectPath, suggestionType }),
|
||||||
|
|
||||||
// Stop generating suggestions
|
// Stop generating suggestions
|
||||||
stop: () => ipcRenderer.invoke("suggestions:stop"),
|
stop: () => ipcRenderer.invoke("suggestions:stop"),
|
||||||
@@ -382,6 +386,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
getAgentOutput: (projectPath, featureId) =>
|
getAgentOutput: (projectPath, featureId) =>
|
||||||
ipcRenderer.invoke("features: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
|
// Also expose a flag to detect if we're in Electron
|
||||||
|
|||||||
@@ -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
|
* Get installation commands for different platforms
|
||||||
* @returns {Object} Installation commands
|
* @returns {Object} Installation commands
|
||||||
|
|||||||
@@ -11,10 +11,14 @@ class FeatureSuggestionsService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate feature suggestions by analyzing the project
|
* 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(
|
console.log(
|
||||||
`[FeatureSuggestions] Generating suggestions for: ${projectPath}`
|
`[FeatureSuggestions] Generating ${suggestionType} suggestions for: ${projectPath}`
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -23,7 +27,7 @@ class FeatureSuggestionsService {
|
|||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
model: "claude-sonnet-4-20250514",
|
model: "claude-sonnet-4-20250514",
|
||||||
systemPrompt: this.getSystemPrompt(),
|
systemPrompt: this.getSystemPrompt(suggestionType),
|
||||||
maxTurns: 50,
|
maxTurns: 50,
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
allowedTools: ["Read", "Glob", "Grep", "Bash"],
|
allowedTools: ["Read", "Glob", "Grep", "Bash"],
|
||||||
@@ -35,7 +39,7 @@ class FeatureSuggestionsService {
|
|||||||
abortController: abortController,
|
abortController: abortController,
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = this.buildAnalysisPrompt();
|
const prompt = this.buildAnalysisPrompt(suggestionType);
|
||||||
|
|
||||||
sendToRenderer({
|
sendToRenderer({
|
||||||
type: "suggestions_progress",
|
type: "suggestions_progress",
|
||||||
@@ -163,36 +167,102 @@ class FeatureSuggestionsService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the system prompt for feature suggestion analysis
|
* Get the system prompt for feature suggestion analysis
|
||||||
|
* @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance"
|
||||||
*/
|
*/
|
||||||
getSystemPrompt() {
|
getSystemPrompt(suggestionType = "features") {
|
||||||
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.
|
const basePrompt = `You are an expert software architect. Your job is to analyze a codebase and provide actionable suggestions.
|
||||||
|
|
||||||
You should:
|
You have access to file reading and search tools. Use them to understand the codebase.
|
||||||
1. Thoroughly analyze the project structure, code, and any existing documentation
|
|
||||||
2. Identify what the application does and what features it currently has (look at the .automaker/app_spec.txt file as well if it exists)
|
|
||||||
3. Generate a comprehensive list of missing features that would be valuable to users
|
|
||||||
4. Prioritize features by impact and complexity
|
|
||||||
5. Provide clear, actionable descriptions and implementation steps
|
|
||||||
|
|
||||||
When analyzing, look at:
|
When analyzing, look at:
|
||||||
- README files and documentation
|
- README files and documentation
|
||||||
- Package.json, cargo.toml, or similar config files for tech stack
|
- Package.json, cargo.toml, or similar config files for tech stack
|
||||||
- Source code structure and organization
|
- Source code structure and organization
|
||||||
- Existing features and their implementation patterns
|
- Existing code patterns and implementation styles`;
|
||||||
- Common patterns in similar applications
|
|
||||||
- User experience improvements
|
|
||||||
- Developer experience improvements
|
|
||||||
- Performance optimizations
|
|
||||||
- Security enhancements
|
|
||||||
|
|
||||||
You have access to file reading and search tools. Use them to understand the codebase.`;
|
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
|
* Build the prompt for analyzing the project
|
||||||
|
* @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance"
|
||||||
*/
|
*/
|
||||||
buildAnalysisPrompt() {
|
buildAnalysisPrompt(suggestionType = "features") {
|
||||||
return `Analyze this project and generate a list of suggested features that are missing or would improve the application.
|
const commonIntro = `Analyze this project and generate a list of actionable suggestions.
|
||||||
|
|
||||||
**Your Task:**
|
**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
|
- Read README.md, package.json, or similar config files
|
||||||
- Scan the source code directory structure
|
- Scan the source code directory structure
|
||||||
- Identify the tech stack and frameworks used
|
- 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:
|
2. Identify what the application does:
|
||||||
- What is the main purpose?
|
- What is the main purpose?
|
||||||
- What features are already implemented?
|
|
||||||
- What patterns and conventions are used?
|
- 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:
|
3. Generate feature suggestions:
|
||||||
- Think about what's missing compared to similar applications
|
- Think about what's missing compared to similar applications
|
||||||
- Consider user experience improvements
|
- 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
|
- Think about performance, security, and reliability
|
||||||
- Consider testing and documentation improvements
|
- Consider testing and documentation improvements
|
||||||
|
|
||||||
4. **CRITICAL: Output your suggestions as a JSON array** at the end of your response, formatted like this:
|
Categories to use: "User Experience", "Performance", "Security", "Testing", "Documentation", "Developer Experience", "Accessibility", etc.
|
||||||
|
${commonOutput}`;
|
||||||
\`\`\`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.`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ class ClaudeProvider extends ModelProvider {
|
|||||||
|
|
||||||
async detectInstallation() {
|
async detectInstallation() {
|
||||||
const claudeCliDetector = require('./claude-cli-detector');
|
const claudeCliDetector = require('./claude-cli-detector');
|
||||||
return claudeCliDetector.getInstallationInfo();
|
return claudeCliDetector.getFullStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
getAvailableModels() {
|
getAvailableModels() {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { InterviewView } from "@/components/views/interview-view";
|
|||||||
import { ContextView } from "@/components/views/context-view";
|
import { ContextView } from "@/components/views/context-view";
|
||||||
import { ProfilesView } from "@/components/views/profiles-view";
|
import { ProfilesView } from "@/components/views/profiles-view";
|
||||||
import { SetupView } from "@/components/views/setup-view";
|
import { SetupView } from "@/components/views/setup-view";
|
||||||
|
import { RunningAgentsView } from "@/components/views/running-agents-view";
|
||||||
import { useAppStore } from "@/store/app-store";
|
import { useAppStore } from "@/store/app-store";
|
||||||
import { useSetupStore } from "@/store/setup-store";
|
import { useSetupStore } from "@/store/setup-store";
|
||||||
import { getElectronAPI, isElectron } from "@/lib/electron";
|
import { getElectronAPI, isElectron } from "@/lib/electron";
|
||||||
@@ -178,6 +179,8 @@ export default function Home() {
|
|||||||
return <ContextView />;
|
return <ContextView />;
|
||||||
case "profiles":
|
case "profiles":
|
||||||
return <ProfilesView />;
|
return <ProfilesView />;
|
||||||
|
case "running-agents":
|
||||||
|
return <RunningAgentsView />;
|
||||||
default:
|
default:
|
||||||
return <WelcomeView />;
|
return <WelcomeView />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ import {
|
|||||||
Radio,
|
Radio,
|
||||||
Monitor,
|
Monitor,
|
||||||
Search,
|
Search,
|
||||||
|
Bug,
|
||||||
|
Activity,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -394,7 +396,8 @@ export function Sidebar() {
|
|||||||
if (!result.canceled && result.filePaths[0]) {
|
if (!result.canceled && result.filePaths[0]) {
|
||||||
const path = result.filePaths[0];
|
const path = result.filePaths[0];
|
||||||
// Extract folder name from path (works on both Windows and Mac/Linux)
|
// 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 {
|
try {
|
||||||
// Check if this is a brand new project (no .automaker directory)
|
// Check if this is a brand new project (no .automaker directory)
|
||||||
@@ -572,7 +575,10 @@ export function Sidebar() {
|
|||||||
|
|
||||||
// Handle selecting the currently highlighted project
|
// Handle selecting the currently highlighted project
|
||||||
const selectHighlightedProject = useCallback(() => {
|
const selectHighlightedProject = useCallback(() => {
|
||||||
if (filteredProjects.length > 0 && selectedProjectIndex < filteredProjects.length) {
|
if (
|
||||||
|
filteredProjects.length > 0 &&
|
||||||
|
selectedProjectIndex < filteredProjects.length
|
||||||
|
) {
|
||||||
setCurrentProject(filteredProjects[selectedProjectIndex]);
|
setCurrentProject(filteredProjects[selectedProjectIndex]);
|
||||||
setIsProjectPickerOpen(false);
|
setIsProjectPickerOpen(false);
|
||||||
}
|
}
|
||||||
@@ -596,7 +602,11 @@ export function Sidebar() {
|
|||||||
} else if (event.key === "ArrowUp") {
|
} else if (event.key === "ArrowUp") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setSelectedProjectIndex((prev) => (prev > 0 ? prev - 1 : prev));
|
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
|
// Toggle off when P is pressed (not with modifiers) while dropdown is open
|
||||||
// Only if not typing in the search input
|
// Only if not typing in the search input
|
||||||
if (document.activeElement !== projectSearchInputRef.current) {
|
if (document.activeElement !== projectSearchInputRef.current) {
|
||||||
@@ -913,7 +923,10 @@ export function Sidebar() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuSubTrigger>
|
</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">
|
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||||
Select theme for this project
|
Select theme for this project
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
@@ -922,7 +935,10 @@ export function Sidebar() {
|
|||||||
value={currentProject.theme || ""}
|
value={currentProject.theme || ""}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
if (currentProject) {
|
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
|
<DropdownMenuRadioItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={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" />
|
<Icon className="w-4 h-4 mr-2" />
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
@@ -955,21 +973,30 @@ export function Sidebar() {
|
|||||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||||
Project History
|
Project History
|
||||||
</DropdownMenuLabel>
|
</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" />
|
<Undo2 className="w-4 h-4 mr-2" />
|
||||||
<span className="flex-1">Previous</span>
|
<span className="flex-1">Previous</span>
|
||||||
<span className="text-[10px] font-mono text-muted-foreground ml-2">
|
<span className="text-[10px] font-mono text-muted-foreground ml-2">
|
||||||
{formatShortcut(shortcuts.cyclePrevProject, true)}
|
{formatShortcut(shortcuts.cyclePrevProject, true)}
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenuItem>
|
</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" />
|
<Redo2 className="w-4 h-4 mr-2" />
|
||||||
<span className="flex-1">Next</span>
|
<span className="flex-1">Next</span>
|
||||||
<span className="text-[10px] font-mono text-muted-foreground ml-2">
|
<span className="text-[10px] font-mono text-muted-foreground ml-2">
|
||||||
{formatShortcut(shortcuts.cycleNextProject, true)}
|
{formatShortcut(shortcuts.cycleNextProject, true)}
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenuItem>
|
</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" />
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
<span>Clear history</span>
|
<span>Clear history</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -1078,8 +1105,79 @@ export function Sidebar() {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</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">
|
<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 */}
|
{/* Settings Link */}
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<button
|
<button
|
||||||
@@ -1272,8 +1370,8 @@ export function Sidebar() {
|
|||||||
Generate feature list
|
Generate feature list
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Automatically create features in the features folder
|
Automatically create features in the features folder from the
|
||||||
from the implementation roadmap after the spec is generated.
|
implementation roadmap after the spec is generated.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ function DialogContent({
|
|||||||
data-slot="dialog-close"
|
data-slot="dialog-close"
|
||||||
className={cn(
|
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",
|
"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 />
|
<XIcon />
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ function parseHotkeyConfig(hotkey: string | HotkeyConfig): HotkeyConfig {
|
|||||||
/**
|
/**
|
||||||
* Generate the display label for the hotkey
|
* 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) {
|
if (config.label) {
|
||||||
return config.label;
|
return config.label;
|
||||||
}
|
}
|
||||||
@@ -73,7 +76,10 @@ function getHotkeyDisplayLabel(config: HotkeyConfig, isMac: boolean): React.Reac
|
|||||||
|
|
||||||
if (config.shift) {
|
if (config.shift) {
|
||||||
parts.push(
|
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>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -134,11 +140,7 @@ function getHotkeyDisplayLabel(config: HotkeyConfig, isMac: boolean): React.Reac
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return <span className="inline-flex items-center gap-1.5">{parts}</span>;
|
||||||
<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)
|
// 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
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +234,8 @@ export function HotkeyButton({
|
|||||||
// If scoped, check that the scope element is visible
|
// If scoped, check that the scope element is visible
|
||||||
if (scopeRef && scopeRef.current) {
|
if (scopeRef && scopeRef.current) {
|
||||||
const scopeEl = scopeRef.current;
|
const scopeEl = scopeRef.current;
|
||||||
const isVisible = scopeEl.offsetParent !== null ||
|
const isVisible =
|
||||||
|
scopeEl.offsetParent !== null ||
|
||||||
getComputedStyle(scopeEl).display !== "none";
|
getComputedStyle(scopeEl).display !== "none";
|
||||||
if (!isVisible) return;
|
if (!isVisible) return;
|
||||||
}
|
}
|
||||||
@@ -259,14 +266,15 @@ export function HotkeyButton({
|
|||||||
}, [config, hotkeyActive, handleKeyDown]);
|
}, [config, hotkeyActive, handleKeyDown]);
|
||||||
|
|
||||||
// Render the hotkey indicator
|
// Render the hotkey indicator
|
||||||
const hotkeyIndicator = config && showHotkeyIndicator ? (
|
const hotkeyIndicator =
|
||||||
<span
|
config && showHotkeyIndicator ? (
|
||||||
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"
|
<span
|
||||||
data-testid="hotkey-indicator"
|
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>
|
{getHotkeyDisplayLabel(config, isMac)}
|
||||||
) : null;
|
</span>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -592,7 +592,7 @@ export function AgentView() {
|
|||||||
</div>
|
</div>
|
||||||
<Card
|
<Card
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-w-[80%]",
|
"max-w-[80%] py-0",
|
||||||
message.role === "user"
|
message.role === "user"
|
||||||
? "bg-transparent border border-primary text-foreground"
|
? "bg-transparent border border-primary text-foreground"
|
||||||
: "border-l-4 border-primary bg-card"
|
: "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">
|
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||||
<Bot className="w-4 h-4 text-primary" />
|
<Bot className="w-4 h-4 text-primary" />
|
||||||
</div>
|
</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">
|
<CardContent className="p-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Loader2 className="w-4 h-4 animate-spin text-primary" />
|
<Loader2 className="w-4 h-4 animate-spin text-primary" />
|
||||||
|
|||||||
@@ -357,7 +357,10 @@ ${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, 5)
|
.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")}
|
.join("\n")}
|
||||||
</languages>
|
</languages>
|
||||||
<frameworks>
|
<frameworks>
|
||||||
@@ -756,6 +759,10 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create each feature using the features API
|
// Create each feature using the features API
|
||||||
|
if (!api.features) {
|
||||||
|
throw new Error("Features API not available");
|
||||||
|
}
|
||||||
|
|
||||||
for (const feature of detectedFeatures) {
|
for (const feature of detectedFeatures) {
|
||||||
await api.features.create(currentProject.path, feature);
|
await api.features.create(currentProject.path, feature);
|
||||||
}
|
}
|
||||||
@@ -829,7 +836,9 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
</div>
|
</div>
|
||||||
{node.isDirectory && isExpanded && node.children && (
|
{node.isDirectory && isExpanded && node.children && (
|
||||||
<div>
|
<div>
|
||||||
{node.children.map((child: FileTreeNode) => renderNode(child, depth + 1))}
|
{node.children.map((child: FileTreeNode) =>
|
||||||
|
renderNode(child, depth + 1)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -953,7 +962,10 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{Object.entries(projectAnalysis.filesByExtension)
|
{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)
|
.slice(0, 15)
|
||||||
.map(([ext, count]: [string, number]) => (
|
.map(([ext, count]: [string, number]) => (
|
||||||
<div key={ext} className="flex justify-between text-sm">
|
<div key={ext} className="flex justify-between text-sm">
|
||||||
@@ -1096,7 +1108,9 @@ ${Object.entries(projectAnalysis.filesByExtension)
|
|||||||
data-testid="analysis-file-tree"
|
data-testid="analysis-file-tree"
|
||||||
>
|
>
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
{projectAnalysis.fileTree.map((node: FileTreeNode) => renderNode(node))}
|
{projectAnalysis.fileTree.map((node: FileTreeNode) =>
|
||||||
|
renderNode(node)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -529,16 +529,22 @@ export function BoardView() {
|
|||||||
const projectId = currentProject.id;
|
const projectId = currentProject.id;
|
||||||
|
|
||||||
const unsubscribe = api.autoMode.onEvent((event) => {
|
const unsubscribe = api.autoMode.onEvent((event) => {
|
||||||
// Use event's projectId if available, otherwise use current project
|
// Use event's projectPath or projectId if available, otherwise use current project
|
||||||
const eventProjectId = event.projectId || projectId;
|
// 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") {
|
if (event.type === "auto_mode_feature_complete") {
|
||||||
// Reload features when a feature is completed
|
// Reload features when a feature is completed
|
||||||
console.log("[Board] Feature completed, reloading features...");
|
console.log("[Board] Feature completed, reloading features...");
|
||||||
loadFeatures();
|
loadFeatures();
|
||||||
// Play ding sound when feature is done
|
// Play ding sound when feature is done (unless muted)
|
||||||
const audio = new Audio("/sounds/ding.mp3");
|
const { muteDoneSound } = useAppStore.getState();
|
||||||
audio.play().catch((err) => console.warn("Could not play ding sound:", err));
|
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") {
|
} else if (event.type === "auto_mode_error") {
|
||||||
// Reload features when an error occurs (feature moved to waiting_approval)
|
// Reload features when an error occurs (feature moved to waiting_approval)
|
||||||
console.log(
|
console.log(
|
||||||
@@ -580,22 +586,36 @@ export function BoardView() {
|
|||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api?.autoMode?.status) return;
|
if (!api?.autoMode?.status) return;
|
||||||
|
|
||||||
const status = await api.autoMode.status();
|
const status = await api.autoMode.status(currentProject.path);
|
||||||
if (status.success && status.runningFeatures) {
|
if (status.success) {
|
||||||
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 projectId = currentProject.id;
|
const projectId = currentProject.id;
|
||||||
clearRunningTasks(projectId);
|
const { clearRunningTasks, addRunningTask, setAutoModeRunning } =
|
||||||
|
useAppStore.getState();
|
||||||
|
|
||||||
// Add each running feature to the store
|
// Sync running features if available
|
||||||
status.runningFeatures.forEach((featureId: string) => {
|
if (status.runningFeatures) {
|
||||||
addRunningTask(projectId, featureId);
|
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) {
|
} catch (error) {
|
||||||
console.error("[Board] Failed to sync running tasks:", error);
|
console.error("[Board] Failed to sync running tasks:", error);
|
||||||
@@ -1899,7 +1919,7 @@ export function BoardView() {
|
|||||||
data-testid="start-next-button"
|
data-testid="start-next-button"
|
||||||
>
|
>
|
||||||
<FastForward className="w-3 h-3 mr-1" />
|
<FastForward className="w-3 h-3 mr-1" />
|
||||||
Start Next
|
Pull Top
|
||||||
</HotkeyButton>
|
</HotkeyButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,8 +20,11 @@ import {
|
|||||||
StopCircle,
|
StopCircle,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
RefreshCw,
|
||||||
|
Shield,
|
||||||
|
Zap,
|
||||||
} from "lucide-react";
|
} 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 { useAppStore, Feature } from "@/store/app-store";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -36,6 +39,39 @@ interface FeatureSuggestionsDialogProps {
|
|||||||
setIsGenerating: (generating: boolean) => void;
|
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({
|
export function FeatureSuggestionsDialog({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -49,6 +85,7 @@ export function FeatureSuggestionsDialog({
|
|||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||||
const [isImporting, setIsImporting] = useState(false);
|
const [isImporting, setIsImporting] = useState(false);
|
||||||
|
const [currentSuggestionType, setCurrentSuggestionType] = useState<SuggestionType | null>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const autoScrollRef = useRef(true);
|
const autoScrollRef = useRef(true);
|
||||||
|
|
||||||
@@ -87,7 +124,8 @@ export function FeatureSuggestionsDialog({
|
|||||||
setSuggestions(event.suggestions);
|
setSuggestions(event.suggestions);
|
||||||
// Select all by default
|
// Select all by default
|
||||||
setSelectedIds(new Set(event.suggestions.map((s) => s.id)));
|
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 {
|
} else {
|
||||||
toast.info("No suggestions generated. Try again.");
|
toast.info("No suggestions generated. Try again.");
|
||||||
}
|
}
|
||||||
@@ -100,10 +138,10 @@ export function FeatureSuggestionsDialog({
|
|||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}, [open, setSuggestions, setIsGenerating]);
|
}, [open, setSuggestions, setIsGenerating, currentSuggestionType]);
|
||||||
|
|
||||||
// Start generating suggestions
|
// Start generating suggestions for a specific type
|
||||||
const handleGenerate = useCallback(async () => {
|
const handleGenerate = useCallback(async (suggestionType: SuggestionType) => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api?.suggestions) {
|
if (!api?.suggestions) {
|
||||||
toast.error("Suggestions API not available");
|
toast.error("Suggestions API not available");
|
||||||
@@ -114,9 +152,10 @@ export function FeatureSuggestionsDialog({
|
|||||||
setProgress([]);
|
setProgress([]);
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
|
setCurrentSuggestionType(suggestionType);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await api.suggestions.generate(projectPath);
|
const result = await api.suggestions.generate(projectPath, suggestionType);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
toast.error(result.error || "Failed to start generation");
|
toast.error(result.error || "Failed to start generation");
|
||||||
setIsGenerating(false);
|
setIsGenerating(false);
|
||||||
@@ -203,8 +242,10 @@ export function FeatureSuggestionsDialog({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Create each new feature using the features API
|
// Create each new feature using the features API
|
||||||
for (const feature of newFeatures) {
|
if (api.features) {
|
||||||
await api.features.create(projectPath, feature);
|
for (const feature of newFeatures) {
|
||||||
|
await api.features.create(projectPath, feature);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge with existing features for store update
|
// Merge with existing features for store update
|
||||||
@@ -219,6 +260,7 @@ export function FeatureSuggestionsDialog({
|
|||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
setProgress([]);
|
setProgress([]);
|
||||||
|
setCurrentSuggestionType(null);
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -238,16 +280,17 @@ export function FeatureSuggestionsDialog({
|
|||||||
autoScrollRef.current = isAtBottom;
|
autoScrollRef.current = isAtBottom;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset state when dialog closes
|
// Go back to type selection
|
||||||
useEffect(() => {
|
const handleBackToSelection = useCallback(() => {
|
||||||
if (!open) {
|
setSuggestions([]);
|
||||||
// Don't reset immediately - allow re-open to see results
|
setSelectedIds(new Set());
|
||||||
// Only reset if explicitly closed without importing
|
setProgress([]);
|
||||||
}
|
setCurrentSuggestionType(null);
|
||||||
}, [open]);
|
}, [setSuggestions]);
|
||||||
|
|
||||||
const hasStarted = progress.length > 0 || suggestions.length > 0;
|
const hasStarted = progress.length > 0 || suggestions.length > 0;
|
||||||
const hasSuggestions = suggestions.length > 0;
|
const hasSuggestions = suggestions.length > 0;
|
||||||
|
const currentConfig = currentSuggestionType ? suggestionTypeConfig[currentSuggestionType] : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onClose}>
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
@@ -257,31 +300,56 @@ export function FeatureSuggestionsDialog({
|
|||||||
>
|
>
|
||||||
<DialogHeader className="flex-shrink-0">
|
<DialogHeader className="flex-shrink-0">
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<Lightbulb className="w-5 h-5 text-yellow-500" />
|
{currentConfig ? (
|
||||||
Feature Suggestions
|
<>
|
||||||
|
<currentConfig.icon className={`w-5 h-5 ${currentConfig.color}`} />
|
||||||
|
{currentConfig.label}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Lightbulb className="w-5 h-5 text-yellow-500" />
|
||||||
|
AI Suggestions
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Analyze your project to discover missing features and improvements.
|
{currentConfig
|
||||||
The AI will scan your codebase and suggest features ordered by priority.
|
? currentConfig.description
|
||||||
|
: "Analyze your project to discover improvements. Choose a suggestion type below."}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{!hasStarted ? (
|
{!hasStarted ? (
|
||||||
// Initial state - show explanation and generate button
|
// Initial state - show suggestion type buttons
|
||||||
<div className="flex-1 flex flex-col items-center justify-center py-8 text-center">
|
<div className="flex-1 flex flex-col items-center justify-center py-8">
|
||||||
<Lightbulb className="w-16 h-16 text-yellow-500/50 mb-4" />
|
<p className="text-muted-foreground text-center max-w-lg mb-8">
|
||||||
<h3 className="text-lg font-semibold mb-2">
|
Our AI will analyze your project and generate actionable suggestions.
|
||||||
Discover Missing Features
|
Choose what type of analysis you want to perform:
|
||||||
</h3>
|
|
||||||
<p className="text-muted-foreground max-w-md mb-6">
|
|
||||||
Our AI will analyze your project structure, code patterns, and
|
|
||||||
existing features to generate a prioritized list of suggestions
|
|
||||||
for new features you could add.
|
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={handleGenerate} size="lg">
|
<div className="grid grid-cols-2 gap-4 w-full max-w-2xl">
|
||||||
<Lightbulb className="w-4 h-4 mr-2" />
|
{(Object.entries(suggestionTypeConfig) as [SuggestionType, typeof suggestionTypeConfig[SuggestionType]][]).map(
|
||||||
Generate Suggestions
|
([type, config]) => {
|
||||||
</Button>
|
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>
|
</div>
|
||||||
) : isGenerating ? (
|
) : isGenerating ? (
|
||||||
// Generating state - show progress
|
// Generating state - show progress
|
||||||
@@ -410,20 +478,34 @@ export function FeatureSuggestionsDialog({
|
|||||||
<p className="text-muted-foreground mb-4">
|
<p className="text-muted-foreground mb-4">
|
||||||
No suggestions were generated. Try running the analysis again.
|
No suggestions were generated. Try running the analysis again.
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={handleGenerate}>
|
<div className="flex gap-2">
|
||||||
<Lightbulb className="w-4 h-4 mr-2" />
|
<Button variant="outline" onClick={handleBackToSelection}>
|
||||||
Try Again
|
Back to Selection
|
||||||
</Button>
|
</Button>
|
||||||
|
{currentSuggestionType && (
|
||||||
|
<Button onClick={() => handleGenerate(currentSuggestionType)}>
|
||||||
|
<Lightbulb className="w-4 h-4 mr-2" />
|
||||||
|
Try Again
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogFooter className="flex-shrink-0">
|
<DialogFooter className="flex-shrink-0">
|
||||||
{hasSuggestions && (
|
{hasSuggestions && (
|
||||||
<div className="flex gap-2 w-full justify-between">
|
<div className="flex gap-2 w-full justify-between">
|
||||||
<Button variant="outline" onClick={handleGenerate}>
|
<div className="flex gap-2">
|
||||||
<Lightbulb className="w-4 h-4 mr-2" />
|
<Button variant="outline" onClick={handleBackToSelection}>
|
||||||
Regenerate
|
Back
|
||||||
</Button>
|
</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">
|
<div className="flex gap-2">
|
||||||
<Button variant="ghost" onClick={onClose}>
|
<Button variant="ghost" onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@@ -248,21 +248,12 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
{...attributes}
|
{...attributes}
|
||||||
{...(isDraggable ? listeners : {})}
|
{...(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 */}
|
{/* Skip Tests indicator badge */}
|
||||||
{feature.skipTests && !feature.error && (
|
{feature.skipTests && !feature.error && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
|
"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"
|
"bg-orange-500/20 border border-orange-500/50 text-orange-400"
|
||||||
)}
|
)}
|
||||||
data-testid={`skip-tests-badge-${feature.id}`}
|
data-testid={`skip-tests-badge-${feature.id}`}
|
||||||
@@ -277,7 +268,7 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
|
"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"
|
"bg-red-500/20 border border-red-500/50 text-red-400"
|
||||||
)}
|
)}
|
||||||
data-testid={`error-badge-${feature.id}`}
|
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
|
// Position below error badge if present, otherwise use normal position
|
||||||
feature.error || feature.skipTests
|
feature.error || feature.skipTests
|
||||||
? "top-8 left-2"
|
? "top-8 left-2"
|
||||||
: shortcutKey
|
: "top-2 left-2"
|
||||||
? "top-2 left-10"
|
|
||||||
: "top-2 left-2"
|
|
||||||
)}
|
)}
|
||||||
data-testid={`branch-badge-${feature.id}`}
|
data-testid={`branch-badge-${feature.id}`}
|
||||||
>
|
>
|
||||||
@@ -319,7 +308,7 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"p-3 pb-2 block", // Reset grid layout to block for custom kanban card layout
|
"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
|
// 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
|
// Add even more top padding when both badges and branch are shown
|
||||||
hasWorktree && (feature.skipTests || feature.error) && "pt-14"
|
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" />
|
<FileText className="w-3 h-3 mr-1" />
|
||||||
Logs
|
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>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{onForceStop && (
|
{onForceStop && (
|
||||||
|
|||||||
210
app/src/components/views/running-agents-view.tsx
Normal file
210
app/src/components/views/running-agents-view.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,9 +2,23 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useAppStore } from "@/store/app-store";
|
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 { useCliStatus } from "./settings-view/hooks/use-cli-status";
|
||||||
import { useScrollTracking } from "@/hooks/use-scroll-tracking";
|
import { useScrollTracking } from "@/hooks/use-scroll-tracking";
|
||||||
import { NAV_ITEMS } from "./settings-view/config/navigation";
|
|
||||||
import { SettingsHeader } from "./settings-view/components/settings-header";
|
import { SettingsHeader } from "./settings-view/components/settings-header";
|
||||||
import { KeyboardMapDialog } from "./settings-view/components/keyboard-map-dialog";
|
import { KeyboardMapDialog } from "./settings-view/components/keyboard-map-dialog";
|
||||||
import { DeleteProjectDialog } from "./settings-view/components/delete-project-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 { KeyboardShortcutsSection } from "./settings-view/keyboard-shortcuts/keyboard-shortcuts-section";
|
||||||
import { FeatureDefaultsSection } from "./settings-view/feature-defaults/feature-defaults-section";
|
import { FeatureDefaultsSection } from "./settings-view/feature-defaults/feature-defaults-section";
|
||||||
import { DangerZoneSection } from "./settings-view/danger-zone/danger-zone-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";
|
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() {
|
export function SettingsView() {
|
||||||
const {
|
const {
|
||||||
theme,
|
theme,
|
||||||
@@ -33,12 +63,16 @@ export function SettingsView() {
|
|||||||
setUseWorktrees,
|
setUseWorktrees,
|
||||||
showProfilesOnly,
|
showProfilesOnly,
|
||||||
setShowProfilesOnly,
|
setShowProfilesOnly,
|
||||||
|
muteDoneSound,
|
||||||
|
setMuteDoneSound,
|
||||||
currentProject,
|
currentProject,
|
||||||
moveProjectToTrash,
|
moveProjectToTrash,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
|
|
||||||
// Convert electron Project to settings-view Project type
|
// 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;
|
if (!project) return null;
|
||||||
return {
|
return {
|
||||||
id: project.id,
|
id: project.id,
|
||||||
@@ -143,6 +177,55 @@ export function SettingsView() {
|
|||||||
onOpenKeyboardMap={() => setShowKeyboardMapDialog(true)}
|
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 "ding" 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 */}
|
{/* Feature Defaults Section */}
|
||||||
<FeatureDefaultsSection
|
<FeatureDefaultsSection
|
||||||
showProfilesOnly={showProfilesOnly}
|
showProfilesOnly={showProfilesOnly}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useCallback, useMemo } from "react";
|
import { useEffect, useCallback, useMemo } from "react";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useAppStore } from "@/store/app-store";
|
import { useAppStore } from "@/store/app-store";
|
||||||
import { getElectronAPI, type AutoModeEvent } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
|
import type { AutoModeEvent } from "@/types/electron";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for managing auto mode (scoped per project)
|
* Hook for managing auto mode (scoped per project)
|
||||||
@@ -16,6 +17,7 @@ export function useAutoMode() {
|
|||||||
currentProject,
|
currentProject,
|
||||||
addAutoModeActivity,
|
addAutoModeActivity,
|
||||||
maxConcurrency,
|
maxConcurrency,
|
||||||
|
projects,
|
||||||
} = useAppStore(
|
} = useAppStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
autoModeByProject: state.autoModeByProject,
|
autoModeByProject: state.autoModeByProject,
|
||||||
@@ -26,9 +28,16 @@ export function useAutoMode() {
|
|||||||
currentProject: state.currentProject,
|
currentProject: state.currentProject,
|
||||||
addAutoModeActivity: state.addAutoModeActivity,
|
addAutoModeActivity: state.addAutoModeActivity,
|
||||||
maxConcurrency: state.maxConcurrency,
|
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
|
// Get project-specific auto mode state
|
||||||
const projectId = currentProject?.id;
|
const projectId = currentProject?.id;
|
||||||
const projectAutoModeState = useMemo(() => {
|
const projectAutoModeState = useMemo(() => {
|
||||||
@@ -42,17 +51,32 @@ export function useAutoMode() {
|
|||||||
// Check if we can start a new task based on concurrency limit
|
// Check if we can start a new task based on concurrency limit
|
||||||
const canStartNewTask = runningAutoTasks.length < maxConcurrency;
|
const canStartNewTask = runningAutoTasks.length < maxConcurrency;
|
||||||
|
|
||||||
// Handle auto mode events
|
// Handle auto mode events - listen globally for all projects
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api?.autoMode || !projectId) return;
|
if (!api?.autoMode) return;
|
||||||
|
|
||||||
const unsubscribe = api.autoMode.onEvent((event: AutoModeEvent) => {
|
const unsubscribe = api.autoMode.onEvent((event: AutoModeEvent) => {
|
||||||
console.log("[AutoMode Event]", event);
|
console.log("[AutoMode Event]", event);
|
||||||
|
|
||||||
// Events include projectId from backend, use it to scope updates
|
// Events include projectPath from backend - use it to look up project ID
|
||||||
// Fall back to current projectId if not provided in event
|
// 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) {
|
switch (event.type) {
|
||||||
case "auto_mode_feature_start":
|
case "auto_mode_feature_start":
|
||||||
@@ -153,8 +177,47 @@ export function useAutoMode() {
|
|||||||
clearRunningTasks,
|
clearRunningTasks,
|
||||||
setAutoModeRunning,
|
setAutoModeRunning,
|
||||||
addAutoModeActivity,
|
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
|
// Start auto mode
|
||||||
const start = useCallback(async () => {
|
const start = useCallback(async () => {
|
||||||
if (!currentProject) {
|
if (!currentProject) {
|
||||||
@@ -199,7 +262,7 @@ export function useAutoMode() {
|
|||||||
throw new Error("Auto mode API not available");
|
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) {
|
if (result.success) {
|
||||||
setAutoModeRunning(currentProject.id, false);
|
setAutoModeRunning(currentProject.id, false);
|
||||||
|
|||||||
@@ -58,6 +58,26 @@ import type {
|
|||||||
// Feature type - Import from app-store
|
// Feature type - Import from app-store
|
||||||
import type { Feature } from "@/store/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
|
// Feature Suggestions types
|
||||||
export interface FeatureSuggestion {
|
export interface FeatureSuggestion {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -81,9 +101,12 @@ export interface SuggestionsEvent {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SuggestionType = "features" | "refactoring" | "security" | "performance";
|
||||||
|
|
||||||
export interface SuggestionsAPI {
|
export interface SuggestionsAPI {
|
||||||
generate: (
|
generate: (
|
||||||
projectPath: string
|
projectPath: string,
|
||||||
|
suggestionType?: SuggestionType
|
||||||
) => Promise<{ success: boolean; error?: string }>;
|
) => Promise<{ success: boolean; error?: string }>;
|
||||||
stop: () => Promise<{ success: boolean; error?: string }>;
|
stop: () => Promise<{ success: boolean; error?: string }>;
|
||||||
status: () => Promise<{
|
status: () => Promise<{
|
||||||
@@ -153,15 +176,18 @@ export interface AutoModeAPI {
|
|||||||
projectPath: string,
|
projectPath: string,
|
||||||
maxConcurrency?: number
|
maxConcurrency?: number
|
||||||
) => Promise<{ success: boolean; error?: string }>;
|
) => Promise<{ success: boolean; error?: string }>;
|
||||||
stop: () => Promise<{ success: boolean; error?: string }>;
|
stop: (projectPath: string) => Promise<{ success: boolean; error?: string; runningFeatures?: number }>;
|
||||||
stopFeature: (
|
stopFeature: (
|
||||||
featureId: string
|
featureId: string
|
||||||
) => Promise<{ success: boolean; error?: string }>;
|
) => Promise<{ success: boolean; error?: string }>;
|
||||||
status: () => Promise<{
|
status: (projectPath?: string) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
isRunning?: boolean;
|
isRunning?: boolean;
|
||||||
|
autoLoopRunning?: boolean; // Backend uses this name instead of isRunning
|
||||||
currentFeatureId?: string | null;
|
currentFeatureId?: string | null;
|
||||||
runningFeatures?: string[];
|
runningFeatures?: string[];
|
||||||
|
runningProjects?: string[];
|
||||||
|
runningCount?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
runFeature: (
|
runFeature: (
|
||||||
@@ -205,6 +231,7 @@ export interface SaveImageResult {
|
|||||||
|
|
||||||
export interface ElectronAPI {
|
export interface ElectronAPI {
|
||||||
ping: () => Promise<string>;
|
ping: () => Promise<string>;
|
||||||
|
openExternalLink: (url: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
openDirectory: () => Promise<DialogResult>;
|
openDirectory: () => Promise<DialogResult>;
|
||||||
openFile: (options?: object) => Promise<DialogResult>;
|
openFile: (options?: object) => Promise<DialogResult>;
|
||||||
readFile: (filePath: string) => Promise<FileResult>;
|
readFile: (filePath: string) => Promise<FileResult>;
|
||||||
@@ -276,6 +303,7 @@ export interface ElectronAPI {
|
|||||||
specRegeneration?: SpecRegenerationAPI;
|
specRegeneration?: SpecRegenerationAPI;
|
||||||
autoMode?: AutoModeAPI;
|
autoMode?: AutoModeAPI;
|
||||||
features?: FeaturesAPI;
|
features?: FeaturesAPI;
|
||||||
|
runningAgents?: RunningAgentsAPI;
|
||||||
setup?: {
|
setup?: {
|
||||||
getClaudeStatus: () => Promise<{
|
getClaudeStatus: () => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -397,6 +425,12 @@ export const getElectronAPI = (): ElectronAPI => {
|
|||||||
return {
|
return {
|
||||||
ping: async () => "pong (mock)",
|
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 () => {
|
openDirectory: async () => {
|
||||||
// In web mode, we'll use a prompt to simulate directory selection
|
// In web mode, we'll use a prompt to simulate directory selection
|
||||||
const path = prompt(
|
const path = prompt(
|
||||||
@@ -675,6 +709,9 @@ export const getElectronAPI = (): ElectronAPI => {
|
|||||||
|
|
||||||
// Mock Features API
|
// Mock Features API
|
||||||
features: createMockFeaturesAPI(),
|
features: createMockFeaturesAPI(),
|
||||||
|
|
||||||
|
// Mock Running Agents API
|
||||||
|
runningAgents: createMockRunningAgentsAPI(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1020,13 +1057,14 @@ function createMockAutoModeAPI(): AutoModeAPI {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
stop: async () => {
|
stop: async (_projectPath: string) => {
|
||||||
mockAutoModeRunning = false;
|
mockAutoModeRunning = false;
|
||||||
|
const runningCount = mockRunningFeatures.size;
|
||||||
mockRunningFeatures.clear();
|
mockRunningFeatures.clear();
|
||||||
// Clear all timeouts
|
// Clear all timeouts
|
||||||
mockAutoModeTimeouts.forEach((timeout) => clearTimeout(timeout));
|
mockAutoModeTimeouts.forEach((timeout) => clearTimeout(timeout));
|
||||||
mockAutoModeTimeouts.clear();
|
mockAutoModeTimeouts.clear();
|
||||||
return { success: true };
|
return { success: true, runningFeatures: runningCount };
|
||||||
},
|
},
|
||||||
|
|
||||||
stopFeature: async (featureId: string) => {
|
stopFeature: async (featureId: string) => {
|
||||||
@@ -1055,12 +1093,14 @@ function createMockAutoModeAPI(): AutoModeAPI {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
status: async () => {
|
status: async (_projectPath?: string) => {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
isRunning: mockAutoModeRunning,
|
isRunning: mockAutoModeRunning,
|
||||||
|
autoLoopRunning: mockAutoModeRunning,
|
||||||
currentFeatureId: mockAutoModeRunning ? "feature-0" : null,
|
currentFeatureId: mockAutoModeRunning ? "feature-0" : null,
|
||||||
runningFeatures: Array.from(mockRunningFeatures),
|
runningFeatures: Array.from(mockRunningFeatures),
|
||||||
|
runningCount: mockRunningFeatures.size,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1441,7 +1481,7 @@ let mockSuggestionsTimeout: NodeJS.Timeout | null = null;
|
|||||||
|
|
||||||
function createMockSuggestionsAPI(): SuggestionsAPI {
|
function createMockSuggestionsAPI(): SuggestionsAPI {
|
||||||
return {
|
return {
|
||||||
generate: async (projectPath: string) => {
|
generate: async (projectPath: string, suggestionType: SuggestionType = "features") => {
|
||||||
if (mockSuggestionsRunning) {
|
if (mockSuggestionsRunning) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -1450,10 +1490,10 @@ function createMockSuggestionsAPI(): SuggestionsAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mockSuggestionsRunning = true;
|
mockSuggestionsRunning = true;
|
||||||
console.log(`[Mock] Generating suggestions for: ${projectPath}`);
|
console.log(`[Mock] Generating ${suggestionType} suggestions for: ${projectPath}`);
|
||||||
|
|
||||||
// Simulate async suggestion generation
|
// Simulate async suggestion generation
|
||||||
simulateSuggestionsGeneration();
|
simulateSuggestionsGeneration(suggestionType);
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
@@ -1489,11 +1529,18 @@ function emitSuggestionsEvent(event: SuggestionsEvent) {
|
|||||||
mockSuggestionsCallbacks.forEach((cb) => cb(event));
|
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
|
// Emit progress events
|
||||||
emitSuggestionsEvent({
|
emitSuggestionsEvent({
|
||||||
type: "suggestions_progress",
|
type: "suggestions_progress",
|
||||||
content: "Starting project analysis...\n",
|
content: `Starting project analysis for ${typeLabels[suggestionType]}...\n`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -1524,7 +1571,7 @@ async function simulateSuggestionsGeneration() {
|
|||||||
|
|
||||||
emitSuggestionsEvent({
|
emitSuggestionsEvent({
|
||||||
type: "suggestions_progress",
|
type: "suggestions_progress",
|
||||||
content: "Identifying missing features...\n",
|
content: `Identifying ${typeLabels[suggestionType]}...\n`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -1532,75 +1579,184 @@ async function simulateSuggestionsGeneration() {
|
|||||||
});
|
});
|
||||||
if (!mockSuggestionsRunning) return;
|
if (!mockSuggestionsRunning) return;
|
||||||
|
|
||||||
// Generate mock suggestions
|
// Generate mock suggestions based on type
|
||||||
const mockSuggestions: FeatureSuggestion[] = [
|
let mockSuggestions: FeatureSuggestion[];
|
||||||
{
|
|
||||||
id: `suggestion-${Date.now()}-0`,
|
switch (suggestionType) {
|
||||||
category: "User Experience",
|
case "refactoring":
|
||||||
description: "Add dark mode toggle with system preference detection",
|
mockSuggestions = [
|
||||||
steps: [
|
{
|
||||||
"Create a ThemeProvider context to manage theme state",
|
id: `suggestion-${Date.now()}-0`,
|
||||||
"Add a toggle component in the settings or header",
|
category: "Code Smell",
|
||||||
"Implement CSS variables for theme colors",
|
description: "Extract duplicate validation logic into reusable utility",
|
||||||
"Add localStorage persistence for user preference",
|
steps: [
|
||||||
],
|
"Identify all files with similar validation patterns",
|
||||||
priority: 1,
|
"Create a validation utilities module",
|
||||||
reasoning:
|
"Replace duplicate code with utility calls",
|
||||||
"Dark mode is a standard feature that improves accessibility and user comfort",
|
"Add unit tests for the new utilities",
|
||||||
},
|
],
|
||||||
{
|
priority: 1,
|
||||||
id: `suggestion-${Date.now()}-1`,
|
reasoning: "Reduces code duplication and improves maintainability",
|
||||||
category: "Performance",
|
},
|
||||||
description: "Implement lazy loading for heavy components",
|
{
|
||||||
steps: [
|
id: `suggestion-${Date.now()}-1`,
|
||||||
"Identify components that are heavy or rarely used",
|
category: "Complexity",
|
||||||
"Use React.lazy() and Suspense for code splitting",
|
description: "Break down large handleSubmit function into smaller functions",
|
||||||
"Add loading states for lazy-loaded components",
|
steps: [
|
||||||
],
|
"Identify the handleSubmit function in form components",
|
||||||
priority: 2,
|
"Extract validation logic into separate function",
|
||||||
reasoning: "Improves initial load time and reduces bundle size",
|
"Extract API call logic into separate function",
|
||||||
},
|
"Extract success/error handling into separate functions",
|
||||||
{
|
],
|
||||||
id: `suggestion-${Date.now()}-2`,
|
priority: 2,
|
||||||
category: "Accessibility",
|
reasoning: "Function is too long and handles multiple responsibilities",
|
||||||
description: "Add keyboard navigation support throughout the app",
|
},
|
||||||
steps: [
|
{
|
||||||
"Implement focus management for modals and dialogs",
|
id: `suggestion-${Date.now()}-2`,
|
||||||
"Add keyboard shortcuts for common actions",
|
category: "Architecture",
|
||||||
"Ensure all interactive elements are focusable",
|
description: "Move business logic out of React components into hooks",
|
||||||
"Add ARIA labels and roles where needed",
|
steps: [
|
||||||
],
|
"Identify business logic in component files",
|
||||||
priority: 3,
|
"Create custom hooks for reusable logic",
|
||||||
reasoning:
|
"Update components to use the new hooks",
|
||||||
"Improves accessibility for users who rely on keyboard navigation",
|
"Add tests for the extracted hooks",
|
||||||
},
|
],
|
||||||
{
|
priority: 3,
|
||||||
id: `suggestion-${Date.now()}-3`,
|
reasoning: "Improves separation of concerns and testability",
|
||||||
category: "Testing",
|
},
|
||||||
description: "Add comprehensive unit test coverage",
|
];
|
||||||
steps: [
|
break;
|
||||||
"Set up Jest and React Testing Library",
|
|
||||||
"Create tests for all utility functions",
|
case "security":
|
||||||
"Add component tests for critical UI elements",
|
mockSuggestions = [
|
||||||
"Set up CI pipeline for automated testing",
|
{
|
||||||
],
|
id: `suggestion-${Date.now()}-0`,
|
||||||
priority: 4,
|
category: "High",
|
||||||
reasoning: "Ensures code quality and prevents regressions",
|
description: "Sanitize user input before rendering to prevent XSS",
|
||||||
},
|
steps: [
|
||||||
{
|
"Audit all places where user input is rendered",
|
||||||
id: `suggestion-${Date.now()}-4`,
|
"Implement input sanitization using DOMPurify",
|
||||||
category: "Developer Experience",
|
"Add Content-Security-Policy headers",
|
||||||
description: "Add Storybook for component documentation",
|
"Test with common XSS payloads",
|
||||||
steps: [
|
],
|
||||||
"Install and configure Storybook",
|
priority: 1,
|
||||||
"Create stories for all UI components",
|
reasoning: "User input is rendered without proper sanitization",
|
||||||
"Add interaction tests using play functions",
|
},
|
||||||
"Set up Chromatic for visual regression testing",
|
{
|
||||||
],
|
id: `suggestion-${Date.now()}-1`,
|
||||||
priority: 5,
|
category: "Medium",
|
||||||
reasoning: "Improves component development workflow and documentation",
|
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({
|
emitSuggestionsEvent({
|
||||||
type: "suggestions_complete",
|
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
|
// Utility functions for project management
|
||||||
|
|
||||||
export interface Project {
|
export interface Project {
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export type ViewMode =
|
|||||||
| "tools"
|
| "tools"
|
||||||
| "interview"
|
| "interview"
|
||||||
| "context"
|
| "context"
|
||||||
| "profiles";
|
| "profiles"
|
||||||
|
| "running-agents";
|
||||||
|
|
||||||
export type ThemeMode =
|
export type ThemeMode =
|
||||||
| "light"
|
| "light"
|
||||||
@@ -328,6 +329,9 @@ export interface AppState {
|
|||||||
// Keyboard Shortcuts
|
// Keyboard Shortcuts
|
||||||
keyboardShortcuts: KeyboardShortcuts; // User-defined 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
|
// Project Analysis
|
||||||
projectAnalysis: ProjectAnalysis | null;
|
projectAnalysis: ProjectAnalysis | null;
|
||||||
isAnalyzing: boolean;
|
isAnalyzing: boolean;
|
||||||
@@ -435,6 +439,9 @@ export interface AppActions {
|
|||||||
setKeyboardShortcuts: (shortcuts: Partial<KeyboardShortcuts>) => void;
|
setKeyboardShortcuts: (shortcuts: Partial<KeyboardShortcuts>) => void;
|
||||||
resetKeyboardShortcuts: () => void;
|
resetKeyboardShortcuts: () => void;
|
||||||
|
|
||||||
|
// Audio Settings actions
|
||||||
|
setMuteDoneSound: (muted: boolean) => void;
|
||||||
|
|
||||||
// AI Profile actions
|
// AI Profile actions
|
||||||
addAIProfile: (profile: Omit<AIProfile, "id">) => void;
|
addAIProfile: (profile: Omit<AIProfile, "id">) => void;
|
||||||
updateAIProfile: (id: string, updates: Partial<AIProfile>) => void;
|
updateAIProfile: (id: string, updates: Partial<AIProfile>) => void;
|
||||||
@@ -537,6 +544,7 @@ const initialState: AppState = {
|
|||||||
useWorktrees: false, // Default to disabled (worktree feature is experimental)
|
useWorktrees: false, // Default to disabled (worktree feature is experimental)
|
||||||
showProfilesOnly: false, // Default to showing all options (not profiles only)
|
showProfilesOnly: false, // Default to showing all options (not profiles only)
|
||||||
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts
|
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts
|
||||||
|
muteDoneSound: false, // Default to sound enabled (not muted)
|
||||||
aiProfiles: DEFAULT_AI_PROFILES,
|
aiProfiles: DEFAULT_AI_PROFILES,
|
||||||
projectAnalysis: null,
|
projectAnalysis: null,
|
||||||
isAnalyzing: false,
|
isAnalyzing: false,
|
||||||
@@ -1065,6 +1073,9 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
set({ keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS });
|
set({ keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Audio Settings actions
|
||||||
|
setMuteDoneSound: (muted) => set({ muteDoneSound: muted }),
|
||||||
|
|
||||||
// AI Profile actions
|
// AI Profile actions
|
||||||
addAIProfile: (profile) => {
|
addAIProfile: (profile) => {
|
||||||
const id = `profile-${Date.now()}-${Math.random()
|
const id = `profile-${Date.now()}-${Math.random()
|
||||||
@@ -1139,11 +1150,13 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
chatSessions: state.chatSessions,
|
chatSessions: state.chatSessions,
|
||||||
chatHistoryOpen: state.chatHistoryOpen,
|
chatHistoryOpen: state.chatHistoryOpen,
|
||||||
maxConcurrency: state.maxConcurrency,
|
maxConcurrency: state.maxConcurrency,
|
||||||
|
autoModeByProject: state.autoModeByProject,
|
||||||
kanbanCardDetailLevel: state.kanbanCardDetailLevel,
|
kanbanCardDetailLevel: state.kanbanCardDetailLevel,
|
||||||
defaultSkipTests: state.defaultSkipTests,
|
defaultSkipTests: state.defaultSkipTests,
|
||||||
useWorktrees: state.useWorktrees,
|
useWorktrees: state.useWorktrees,
|
||||||
showProfilesOnly: state.showProfilesOnly,
|
showProfilesOnly: state.showProfilesOnly,
|
||||||
keyboardShortcuts: state.keyboardShortcuts,
|
keyboardShortcuts: state.keyboardShortcuts,
|
||||||
|
muteDoneSound: state.muteDoneSound,
|
||||||
aiProfiles: state.aiProfiles,
|
aiProfiles: state.aiProfiles,
|
||||||
lastSelectedSessionByProject: state.lastSelectedSessionByProject,
|
lastSelectedSessionByProject: state.lastSelectedSessionByProject,
|
||||||
}),
|
}),
|
||||||
|
|||||||
18
app/src/types/electron.d.ts
vendored
18
app/src/types/electron.d.ts
vendored
@@ -163,18 +163,21 @@ export type AutoModeEvent =
|
|||||||
type: "auto_mode_feature_start";
|
type: "auto_mode_feature_start";
|
||||||
featureId: string;
|
featureId: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
projectPath?: string;
|
||||||
feature: unknown;
|
feature: unknown;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "auto_mode_progress";
|
type: "auto_mode_progress";
|
||||||
featureId: string;
|
featureId: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
projectPath?: string;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "auto_mode_tool";
|
type: "auto_mode_tool";
|
||||||
featureId: string;
|
featureId: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
projectPath?: string;
|
||||||
tool: string;
|
tool: string;
|
||||||
input: unknown;
|
input: unknown;
|
||||||
}
|
}
|
||||||
@@ -182,6 +185,7 @@ export type AutoModeEvent =
|
|||||||
type: "auto_mode_feature_complete";
|
type: "auto_mode_feature_complete";
|
||||||
featureId: string;
|
featureId: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
projectPath?: string;
|
||||||
passes: boolean;
|
passes: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
@@ -190,22 +194,26 @@ export type AutoModeEvent =
|
|||||||
error: string;
|
error: string;
|
||||||
featureId?: string;
|
featureId?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
projectPath?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "auto_mode_complete";
|
type: "auto_mode_complete";
|
||||||
message: string;
|
message: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
projectPath?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "auto_mode_phase";
|
type: "auto_mode_phase";
|
||||||
featureId: string;
|
featureId: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
projectPath?: string;
|
||||||
phase: "planning" | "action" | "verification";
|
phase: "planning" | "action" | "verification";
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "auto_mode_ultrathink_preparation";
|
type: "auto_mode_ultrathink_preparation";
|
||||||
featureId: string;
|
featureId: string;
|
||||||
|
projectPath?: string;
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
recommendations: string[];
|
recommendations: string[];
|
||||||
estimatedCost?: number;
|
estimatedCost?: number;
|
||||||
@@ -264,14 +272,15 @@ export interface SpecRegenerationAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AutoModeAPI {
|
export interface AutoModeAPI {
|
||||||
start: (projectPath: string) => Promise<{
|
start: (projectPath: string, maxConcurrency?: number) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
stop: () => Promise<{
|
stop: (projectPath: string) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
runningFeatures?: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
stopFeature: (featureId: string) => Promise<{
|
stopFeature: (featureId: string) => Promise<{
|
||||||
@@ -279,11 +288,14 @@ export interface AutoModeAPI {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
status: () => Promise<{
|
status: (projectPath?: string) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
autoLoopRunning?: boolean;
|
||||||
isRunning?: boolean;
|
isRunning?: boolean;
|
||||||
currentFeatureId?: string | null;
|
currentFeatureId?: string | null;
|
||||||
runningFeatures?: string[];
|
runningFeatures?: string[];
|
||||||
|
runningProjects?: string[];
|
||||||
|
runningCount?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user