feat: enhance UI components and branch management

- Added new RadioGroup and Switch components for better UI interaction.
- Introduced BranchSelector for improved branch selection in feature dialogs.
- Updated Autocomplete and BranchAutocomplete components to handle error states.
- Refactored feature management to archive verified features instead of deleting them.
- Enhanced worktree handling by removing worktreePath from features, relying on branchName instead.
- Improved auto mode functionality by integrating branch management and worktree updates.
- Cleaned up unused code and optimized existing logic for better performance.
This commit is contained in:
Cody Seibert
2025-12-17 22:29:39 -05:00
parent cffdec91f1
commit 0549b8085a
45 changed files with 1669 additions and 1346 deletions

View File

@@ -6,8 +6,6 @@
import { Router } from "express";
import type { AutoModeService } from "../../services/auto-mode-service.js";
import { createStartHandler } from "./routes/start.js";
import { createStopHandler } from "./routes/stop.js";
import { createStopFeatureHandler } from "./routes/stop-feature.js";
import { createStatusHandler } from "./routes/status.js";
import { createRunFeatureHandler } from "./routes/run-feature.js";
@@ -21,8 +19,6 @@ import { createCommitFeatureHandler } from "./routes/commit-feature.js";
export function createAutoModeRoutes(autoModeService: AutoModeService): Router {
const router = Router();
router.post("/start", createStartHandler(autoModeService));
router.post("/stop", createStopHandler(autoModeService));
router.post("/stop-feature", createStopFeatureHandler(autoModeService));
router.post("/status", createStatusHandler(autoModeService));
router.post("/run-feature", createRunFeatureHandler(autoModeService));

View File

@@ -12,13 +12,14 @@ const logger = createLogger("AutoMode");
export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId, prompt, imagePaths, worktreePath } = req.body as {
projectPath: string;
featureId: string;
prompt: string;
imagePaths?: string[];
worktreePath?: string;
};
const { projectPath, featureId, prompt, imagePaths, useWorktrees } =
req.body as {
projectPath: string;
featureId: string;
prompt: string;
imagePaths?: string[];
useWorktrees?: boolean;
};
if (!projectPath || !featureId || !prompt) {
res.status(400).json({
@@ -28,14 +29,25 @@ export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
return;
}
// Start follow-up in background, using the feature's worktreePath for correct branch
// Start follow-up in background
// followUpFeature derives workDir from feature.branchName
autoModeService
.followUpFeature(projectPath, featureId, prompt, imagePaths, worktreePath)
.followUpFeature(
projectPath,
featureId,
prompt,
imagePaths,
useWorktrees ?? true
)
.catch((error) => {
logger.error(
`[AutoMode] Follow up feature ${featureId} error:`,
error
);
})
.finally(() => {
// Release the starting slot when follow-up completes (success or error)
// Note: The feature should be in runningFeatures by this point
});
res.json({ success: true });

View File

@@ -19,12 +19,10 @@ export function createResumeFeatureHandler(autoModeService: AutoModeService) {
};
if (!projectPath || !featureId) {
res
.status(400)
.json({
success: false,
error: "projectPath and featureId are required",
});
res.status(400).json({
success: false,
error: "projectPath and featureId are required",
});
return;
}
@@ -34,7 +32,8 @@ export function createResumeFeatureHandler(autoModeService: AutoModeService) {
.resumeFeature(projectPath, featureId, useWorktrees ?? false)
.catch((error) => {
logger.error(`[AutoMode] Resume feature ${featureId} error:`, error);
});
})
.finally(() => {});
res.json({ success: true });
} catch (error) {

View File

@@ -12,30 +12,30 @@ const logger = createLogger("AutoMode");
export function createRunFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId, useWorktrees, worktreePath } = req.body as {
const { projectPath, featureId, useWorktrees } = req.body as {
projectPath: string;
featureId: string;
useWorktrees?: boolean;
worktreePath?: string;
};
if (!projectPath || !featureId) {
res
.status(400)
.json({
success: false,
error: "projectPath and featureId are required",
});
res.status(400).json({
success: false,
error: "projectPath and featureId are required",
});
return;
}
// Start execution in background
// If worktreePath is provided, use it directly; otherwise let the service decide
// Default to false - worktrees should only be used when explicitly enabled
// executeFeature derives workDir from feature.branchName
autoModeService
.executeFeature(projectPath, featureId, useWorktrees ?? false, false, worktreePath)
.executeFeature(projectPath, featureId, useWorktrees ?? false, false)
.catch((error) => {
logger.error(`[AutoMode] Feature ${featureId} error:`, error);
})
.finally(() => {
// Release the starting slot when execution completes (success or error)
// Note: The feature should be in runningFeatures by this point
});
res.json({ success: true });

View File

@@ -1,31 +0,0 @@
/**
* POST /start endpoint - Start auto mode loop
*/
import type { Request, Response } from "express";
import type { AutoModeService } from "../../../services/auto-mode-service.js";
import { getErrorMessage, logError } from "../common.js";
export function createStartHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, maxConcurrency } = req.body as {
projectPath: string;
maxConcurrency?: number;
};
if (!projectPath) {
res
.status(400)
.json({ success: false, error: "projectPath is required" });
return;
}
await autoModeService.startAutoLoop(projectPath, maxConcurrency || 3);
res.json({ success: true });
} catch (error) {
logError(error, "Start auto loop failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -1,19 +0,0 @@
/**
* POST /stop endpoint - Stop auto mode loop
*/
import type { Request, Response } from "express";
import type { AutoModeService } from "../../../services/auto-mode-service.js";
import { getErrorMessage, logError } from "../common.js";
export function createStopHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const runningCount = await autoModeService.stopAutoLoop();
res.json({ success: true, runningFeatures: runningCount });
} catch (error) {
logError(error, "Stop auto loop failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -16,7 +16,6 @@ export function createIndexHandler(autoModeService: AutoModeService) {
success: true,
runningAgents,
totalCount: runningAgents.length,
autoLoopRunning: status.autoLoopRunning,
});
} catch (error) {
logError(error, "Get running agents failed");

View File

@@ -8,6 +8,7 @@
import type { Request, Response } from "express";
import { exec } from "child_process";
import { promisify } from "util";
import { existsSync } from "fs";
import { isGitRepo, getErrorMessage, logError, normalizePath } from "../common.js";
const execAsync = promisify(exec);
@@ -58,10 +59,12 @@ export function createListHandler() {
});
const worktrees: WorktreeInfo[] = [];
const removedWorktrees: Array<{ path: string; branch: string }> = [];
const lines = stdout.split("\n");
let current: { path?: string; branch?: string } = {};
let isFirst = true;
// First pass: detect removed worktrees
for (const line of lines) {
if (line.startsWith("worktree ")) {
current.path = normalizePath(line.slice(9));
@@ -69,19 +72,40 @@ export function createListHandler() {
current.branch = line.slice(7).replace("refs/heads/", "");
} else if (line === "") {
if (current.path && current.branch) {
worktrees.push({
path: current.path,
branch: current.branch,
isMain: isFirst,
isCurrent: current.branch === currentBranch,
hasWorktree: true,
});
isFirst = false;
const isMainWorktree = isFirst;
// Check if the worktree directory actually exists
// Skip checking/pruning the main worktree (projectPath itself)
if (!isMainWorktree && !existsSync(current.path)) {
// Worktree directory doesn't exist - it was manually deleted
removedWorktrees.push({
path: current.path,
branch: current.branch,
});
} else {
// Worktree exists (or is main worktree), add it to the list
worktrees.push({
path: current.path,
branch: current.branch,
isMain: isMainWorktree,
isCurrent: current.branch === currentBranch,
hasWorktree: true,
});
isFirst = false;
}
}
current = {};
}
}
// Prune removed worktrees from git (only if any were detected)
if (removedWorktrees.length > 0) {
try {
await execAsync("git worktree prune", { cwd: projectPath });
} catch {
// Prune failed, but we'll still report the removed worktrees
}
}
// If includeDetails is requested, fetch change status for each worktree
if (includeDetails) {
for (const worktree of worktrees) {
@@ -103,7 +127,11 @@ export function createListHandler() {
}
}
res.json({ success: true, worktrees });
res.json({
success: true,
worktrees,
removedWorktrees: removedWorktrees.length > 0 ? removedWorktrees : undefined,
});
} catch (error) {
logError(error, "List worktrees failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });