From 8facdc66a93b91dc670c5f6b983da52a1fbbf84d Mon Sep 17 00:00:00 2001 From: webdevcody Date: Tue, 20 Jan 2026 13:39:38 -0500 Subject: [PATCH] feat: enhance auto mode service and UI components for branch handling and verification - Added a new function to retrieve the current branch name in the auto mode service, improving branch management. - Updated the `getRunningCountForWorktree` method to utilize the current branch name for accurate feature counting. - Modified UI components to include a toggle for skipping verification in auto mode, enhancing user control. - Refactored various hooks and components to ensure consistent handling of branch names across the application. - Introduced a new utility file for string operations, providing common functions for text manipulation. --- apps/server/src/services/auto-mode-service.ts | 61 ++++-- .../views/board-view/board-header.tsx | 3 +- .../components/kanban-card/kanban-card.tsx | 2 + .../views/board-view/header-mobile-menu.tsx | 130 ++++++------- .../board-view/hooks/use-board-actions.ts | 11 +- .../board-view/hooks/use-board-drag-drop.ts | 17 +- .../hooks/use-running-features.ts | 7 +- apps/ui/src/hooks/use-auto-mode.ts | 17 +- apps/ui/src/routes/__root.tsx | 5 +- libs/utils/src/string-utils.ts | 178 ++++++++++++++++++ 10 files changed, 337 insertions(+), 94 deletions(-) create mode 100644 libs/utils/src/string-utils.ts diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 9eeefc14..a7753dc8 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -74,6 +74,21 @@ import { getNotificationService } from './notification-service.js'; const execAsync = promisify(exec); +/** + * Get the current branch name for a git repository + * @param projectPath - Path to the git repository + * @returns The current branch name, or null if not in a git repo or on detached HEAD + */ +async function getCurrentBranch(projectPath: string): Promise { + try { + const { stdout } = await execAsync('git branch --show-current', { cwd: projectPath }); + const branch = stdout.trim(); + return branch || null; + } catch { + return null; + } +} + // PlanningMode type is imported from @automaker/types interface ParsedTask { @@ -635,7 +650,7 @@ export class AutoModeService { iterationCount++; try { // Count running features for THIS project/worktree only - const projectRunningCount = this.getRunningCountForWorktree(projectPath, branchName); + const projectRunningCount = await this.getRunningCountForWorktree(projectPath, branchName); // Check if we have capacity for this project/worktree if (projectRunningCount >= projectState.config.maxConcurrency) { @@ -728,20 +743,24 @@ export class AutoModeService { /** * Get count of running features for a specific worktree * @param projectPath - The project path - * @param branchName - The branch name, or null for main worktree (features without branchName or with "main") + * @param branchName - The branch name, or null for main worktree (features without branchName or matching primary branch) */ - private getRunningCountForWorktree(projectPath: string, branchName: string | null): number { - const normalizedBranch = branchName === 'main' ? null : branchName; + private async getRunningCountForWorktree( + projectPath: string, + branchName: string | null + ): Promise { + // Get the actual primary branch name for the project + const primaryBranch = await getCurrentBranch(projectPath); + let count = 0; for (const [, feature] of this.runningFeatures) { // Filter by project path AND branchName to get accurate worktree-specific count const featureBranch = feature.branchName ?? null; - if (normalizedBranch === null) { - // Main worktree: match features with branchName === null OR branchName === "main" - if ( - feature.projectPath === projectPath && - (featureBranch === null || featureBranch === 'main') - ) { + if (branchName === null) { + // Main worktree: match features with branchName === null OR branchName matching primary branch + const isPrimaryBranch = + featureBranch === null || (primaryBranch && featureBranch === primaryBranch); + if (feature.projectPath === projectPath && isPrimaryBranch) { count++; } } else { @@ -790,7 +809,7 @@ export class AutoModeService { // Remove from map this.autoLoopsByProject.delete(worktreeKey); - return this.getRunningCountForWorktree(projectPath, branchName); + return await this.getRunningCountForWorktree(projectPath, branchName); } /** @@ -1025,7 +1044,7 @@ export class AutoModeService { const maxAgents = await this.resolveMaxConcurrency(projectPath, branchName); // Get current running count for this worktree - const currentAgents = this.getRunningCountForWorktree(projectPath, branchName); + const currentAgents = await this.getRunningCountForWorktree(projectPath, branchName); return { hasCapacity: currentAgents < maxAgents, @@ -2952,6 +2971,10 @@ Format your response as a structured markdown document.`; // Features are stored in .automaker directory const featuresDir = getFeaturesDir(projectPath); + // Get the actual primary branch name for the project (e.g., "main", "master", "develop") + // This is needed to correctly match features when branchName is null (main worktree) + const primaryBranch = await getCurrentBranch(projectPath); + try { const entries = await secureFs.readdir(featuresDir, { withFileTypes: true, @@ -2991,17 +3014,21 @@ Format your response as a structured markdown document.`; (feature.planSpec.tasksCompleted ?? 0) < (feature.planSpec.tasksTotal ?? 0)) ) { // Filter by branchName: - // - If branchName is null (main worktree), include features with branchName === null OR branchName === "main" + // - If branchName is null (main worktree), include features with: + // - branchName === null, OR + // - branchName === primaryBranch (e.g., "main", "master", "develop") // - If branchName is set, only include features with matching branchName const featureBranch = feature.branchName ?? null; if (branchName === null) { - // Main worktree: include features without branchName OR with branchName === "main" - // This handles both correct (null) and legacy ("main") cases - if (featureBranch === null || featureBranch === 'main') { + // Main worktree: include features without branchName OR with branchName matching primary branch + // This handles repos where the primary branch is named something other than "main" + const isPrimaryBranch = + featureBranch === null || (primaryBranch && featureBranch === primaryBranch); + if (isPrimaryBranch) { pendingFeatures.push(feature); } else { logger.debug( - `[loadPendingFeatures] Filtering out feature ${feature.id} (branchName: ${featureBranch}) for main worktree` + `[loadPendingFeatures] Filtering out feature ${feature.id} (branchName: ${featureBranch}, primaryBranch: ${primaryBranch}) for main worktree` ); } } else { diff --git a/apps/ui/src/components/views/board-view/board-header.tsx b/apps/ui/src/components/views/board-view/board-header.tsx index 00e36af2..77a272c9 100644 --- a/apps/ui/src/components/views/board-view/board-header.tsx +++ b/apps/ui/src/components/views/board-view/board-header.tsx @@ -142,7 +142,8 @@ export function BoardHeader({ onConcurrencyChange={onConcurrencyChange} isAutoModeRunning={isAutoModeRunning} onAutoModeToggle={onAutoModeToggle} - onOpenAutoModeSettings={() => {}} + skipVerificationInAutoMode={skipVerificationInAutoMode} + onSkipVerificationChange={setSkipVerificationInAutoMode} onOpenPlanDialog={onOpenPlanDialog} showClaudeUsage={showClaudeUsage} showCodexUsage={showCodexUsage} diff --git a/apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx b/apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx index ea078dd6..ba1dd97e 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx @@ -180,8 +180,10 @@ export const KanbanCard = memo(function KanbanCard({ 'kanban-card-content h-full relative', reduceEffects ? 'shadow-none' : 'shadow-sm', 'transition-all duration-200 ease-out', + // Disable hover translate for in-progress cards to prevent gap showing gradient isInteractive && !reduceEffects && + !isCurrentAutoTask && 'hover:-translate-y-0.5 hover:shadow-md hover:shadow-black/10 bg-transparent', !glassmorphism && 'backdrop-blur-[0px]!', !isCurrentAutoTask && diff --git a/apps/ui/src/components/views/board-view/header-mobile-menu.tsx b/apps/ui/src/components/views/board-view/header-mobile-menu.tsx index 37c4c9fc..f3c2c19d 100644 --- a/apps/ui/src/components/views/board-view/header-mobile-menu.tsx +++ b/apps/ui/src/components/views/board-view/header-mobile-menu.tsx @@ -5,7 +5,7 @@ import { HeaderActionsPanel, HeaderActionsPanelTrigger, } from '@/components/ui/header-actions-panel'; -import { Bot, Wand2, Settings2, GitBranch, Zap } from 'lucide-react'; +import { Bot, Wand2, GitBranch, Zap, FastForward } from 'lucide-react'; import { cn } from '@/lib/utils'; import { MobileUsageBar } from './mobile-usage-bar'; @@ -23,7 +23,8 @@ interface HeaderMobileMenuProps { // Auto mode isAutoModeRunning: boolean; onAutoModeToggle: (enabled: boolean) => void; - onOpenAutoModeSettings: () => void; + skipVerificationInAutoMode: boolean; + onSkipVerificationChange: (value: boolean) => void; // Plan button onOpenPlanDialog: () => void; // Usage bar visibility @@ -41,7 +42,8 @@ export function HeaderMobileMenu({ onConcurrencyChange, isAutoModeRunning, onAutoModeToggle, - onOpenAutoModeSettings, + skipVerificationInAutoMode, + onSkipVerificationChange, onOpenPlanDialog, showClaudeUsage, showCodexUsage, @@ -66,29 +68,23 @@ export function HeaderMobileMenu({ Controls - {/* Auto Mode Toggle */} -
onAutoModeToggle(!isAutoModeRunning)} - data-testid="mobile-auto-mode-toggle-container" - > -
- - Auto Mode - - {maxConcurrency} - -
-
+ {/* Auto Mode Section */} +
+ {/* Auto Mode Toggle */} +
onAutoModeToggle(!isAutoModeRunning)} + data-testid="mobile-auto-mode-toggle-container" + > +
+ + Auto Mode +
e.stopPropagation()} data-testid="mobile-auto-mode-toggle" /> - +
+ + {/* Skip Verification Toggle */} +
onSkipVerificationChange(!skipVerificationInAutoMode)} + data-testid="mobile-skip-verification-toggle-container" + > +
+ + Skip Verification +
+ e.stopPropagation()} + data-testid="mobile-skip-verification-toggle" + /> +
+ + {/* Concurrency Control */} +
+
+ + Max Agents + + {runningAgentsCount}/{maxConcurrency} + +
+ onConcurrencyChange(value[0])} + min={1} + max={10} + step={1} + className="w-full" + data-testid="mobile-concurrency-slider" + />
@@ -129,32 +159,6 @@ export function HeaderMobileMenu({ />
- {/* Concurrency Control */} -
-
- - Max Agents - - {runningAgentsCount}/{maxConcurrency} - -
- onConcurrencyChange(value[0])} - min={1} - max={10} - step={1} - className="w-full" - data-testid="mobile-concurrency-slider" - /> -
- {/* Plan Button */}