diff --git a/apps/server/src/routes/auto-mode/routes/status.ts b/apps/server/src/routes/auto-mode/routes/status.ts index aad4b248..765ff73a 100644 --- a/apps/server/src/routes/auto-mode/routes/status.ts +++ b/apps/server/src/routes/auto-mode/routes/status.ts @@ -25,7 +25,7 @@ export function createStatusHandler(autoModeService: AutoModeServiceCompat) { // Normalize branchName: undefined becomes null const normalizedBranchName = branchName ?? null; - const projectStatus = autoModeService.getStatusForProject( + const projectStatus = await autoModeService.getStatusForProject( projectPath, normalizedBranchName ); diff --git a/apps/server/src/routes/projects/routes/overview.ts b/apps/server/src/routes/projects/routes/overview.ts index 3ace44cf..436e683f 100644 --- a/apps/server/src/routes/projects/routes/overview.ts +++ b/apps/server/src/routes/projects/routes/overview.ts @@ -173,7 +173,7 @@ export function createOverviewHandler( const totalFeatures = features.length; // Get auto-mode status for this project (main worktree, branchName = null) - const autoModeStatus: ProjectAutoModeStatus = autoModeService.getStatusForProject( + const autoModeStatus: ProjectAutoModeStatus = await autoModeService.getStatusForProject( projectRef.path, null ); diff --git a/apps/server/src/services/auto-mode/compat.ts b/apps/server/src/services/auto-mode/compat.ts index 011c654e..cece2475 100644 --- a/apps/server/src/services/auto-mode/compat.ts +++ b/apps/server/src/services/auto-mode/compat.ts @@ -89,16 +89,16 @@ export class AutoModeServiceCompat { // PER-PROJECT OPERATIONS (delegated to facades) // =========================================================================== - getStatusForProject( + async getStatusForProject( projectPath: string, branchName: string | null = null - ): { + ): Promise<{ isAutoLoopRunning: boolean; runningFeatures: string[]; runningCount: number; maxConcurrency: number; branchName: string | null; - } { + }> { const facade = this.createFacade(projectPath); return facade.getStatusForProject(branchName); } diff --git a/apps/server/src/services/auto-mode/facade.ts b/apps/server/src/services/auto-mode/facade.ts index d56985a5..29902a76 100644 --- a/apps/server/src/services/auto-mode/facade.ts +++ b/apps/server/src/services/auto-mode/facade.ts @@ -757,7 +757,7 @@ Address the follow-up instructions above. Review the previous work and make the * Get status for this project/worktree * @param branchName - The branch name, or null for main worktree */ - getStatusForProject(branchName: string | null = null): ProjectAutoModeStatus { + async getStatusForProject(branchName: string | null = null): Promise { const isAutoLoopRunning = this.autoLoopCoordinator.isAutoLoopRunningForProject( this.projectPath, branchName @@ -766,10 +766,12 @@ Address the follow-up instructions above. Review the previous work and make the this.projectPath, branchName ); - const runningFeatures = this.concurrencyManager - .getAllRunning() - .filter((f) => f.projectPath === this.projectPath && f.branchName === branchName) - .map((f) => f.featureId); + // Use branchName-normalized filter so features with branchName "main" + // are correctly matched when querying for the main worktree (null) + const runningFeatures = await this.concurrencyManager.getRunningFeaturesForWorktree( + this.projectPath, + branchName + ); return { isAutoLoopRunning, diff --git a/apps/server/src/services/concurrency-manager.ts b/apps/server/src/services/concurrency-manager.ts index 909727e3..6c5c0bd0 100644 --- a/apps/server/src/services/concurrency-manager.ts +++ b/apps/server/src/services/concurrency-manager.ts @@ -209,6 +209,41 @@ export class ConcurrencyManager { return Array.from(this.runningFeatures.values()); } + /** + * Get running feature IDs for a specific worktree, with proper primary branch normalization. + * + * When branchName is null (main worktree), matches features with branchName === null + * OR branchName matching the primary branch (e.g., "main", "master"). + * + * @param projectPath - The project path + * @param branchName - The branch name, or null for main worktree + * @returns Array of feature IDs running in the specified worktree + */ + async getRunningFeaturesForWorktree( + projectPath: string, + branchName: string | null + ): Promise { + const primaryBranch = await this.getCurrentBranch(projectPath); + const featureIds: string[] = []; + + for (const [, feature] of this.runningFeatures) { + if (feature.projectPath !== projectPath) continue; + const featureBranch = feature.branchName ?? null; + + if (branchName === null) { + // Main worktree: match features with null branchName OR primary branch name + const isPrimaryBranch = + featureBranch === null || (primaryBranch && featureBranch === primaryBranch); + if (isPrimaryBranch) featureIds.push(feature.featureId); + } else { + // Feature worktree: exact match + if (featureBranch === branchName) featureIds.push(feature.featureId); + } + } + + return featureIds; + } + /** * Update properties of a running feature * diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 4f00337e..86e300d4 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -593,7 +593,7 @@ export function BoardView() { } = useBoardActions({ currentProject, features: hookFeatures, - runningAutoTasks, + runningAutoTasks: runningAutoTasksAllWorktrees, loadFeatures, persistFeatureCreate, persistFeatureUpdate, @@ -1092,7 +1092,7 @@ export function BoardView() { } = useBoardDragDrop({ features: hookFeatures, currentProject, - runningAutoTasks, + runningAutoTasks: runningAutoTasksAllWorktrees, persistFeatureUpdate, handleStartImplementation, }); @@ -1472,7 +1472,7 @@ export function BoardView() { setShowAddDialog(true); }, }} - runningAutoTasks={runningAutoTasks} + runningAutoTasks={runningAutoTasksAllWorktrees} pipelineConfig={pipelineConfig} onAddFeature={() => setShowAddDialog(true)} isSelectionMode={isSelectionMode} @@ -1511,7 +1511,7 @@ export function BoardView() { setShowAddDialog(true); }} featuresWithContext={featuresWithContext} - runningAutoTasks={runningAutoTasks} + runningAutoTasks={runningAutoTasksAllWorktrees} onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)} onAddFeature={() => setShowAddDialog(true)} onShowCompletedModal={() => setShowCompletedModal(true)} diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-drag-drop.ts b/apps/ui/src/components/views/board-view/hooks/use-board-drag-drop.ts index dd00e3e0..7b9b5abc 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-drag-drop.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-drag-drop.ts @@ -3,6 +3,7 @@ import { createLogger } from '@automaker/utils/logger'; import { DragStartEvent, DragEndEvent } from '@dnd-kit/core'; import { Feature } from '@/store/app-store'; import { useAppStore } from '@/store/app-store'; +import { useAutoMode } from '@/hooks/use-auto-mode'; import { toast } from 'sonner'; import { COLUMNS, ColumnId } from '../constants'; @@ -33,6 +34,7 @@ export function useBoardDragDrop({ null ); const { moveFeature, updateFeature } = useAppStore(); + const autoMode = useAutoMode(); // Note: getOrCreateWorktreeForFeature removed - worktrees are now created server-side // at execution time based on feature.branchName @@ -155,19 +157,9 @@ export function useBoardDragDrop({ } } - // Determine if dragging is allowed based on status and skipTests - // - Backlog items can always be dragged - // - waiting_approval items can always be dragged (to allow manual verification via drag) - // - verified items can always be dragged (to allow moving back to waiting_approval) - // - in_progress items can be dragged (but not if they're currently running) - // - Non-skipTests (TDD) items that are in progress cannot be dragged if they are running - if (draggedFeature.status === 'in_progress') { - // Only allow dragging in_progress if it's not currently running - if (isRunningTask) { - logger.debug('Cannot drag feature - currently running'); - return; - } - } + // Determine if dragging is allowed based on status + // Running in_progress features CAN be dragged to backlog (stops the agent) + // but cannot be dragged to other columns let targetStatus: ColumnId | null = null; @@ -235,15 +227,38 @@ export function useBoardDragDrop({ } else if (draggedFeature.status === 'in_progress') { // Handle in_progress features being moved if (targetStatus === 'backlog') { - // Allow moving in_progress cards back to backlog + // If the feature is currently running, stop it first + if (isRunningTask) { + try { + await autoMode.stopFeature(featureId); + logger.info('Stopped running feature via drag to backlog:', featureId); + } catch (error) { + logger.error('Error stopping feature during drag to backlog:', error); + toast.error('Failed to stop agent', { + description: 'The feature will still be moved to backlog.', + }); + } + } moveFeature(featureId, 'backlog'); persistFeatureUpdate(featureId, { status: 'backlog' }); - toast.info('Feature moved to backlog', { - description: `Moved to Backlog: ${draggedFeature.description.slice( - 0, - 50 - )}${draggedFeature.description.length > 50 ? '...' : ''}`, + toast.info( + isRunningTask + ? 'Agent stopped and feature moved to backlog' + : 'Feature moved to backlog', + { + description: `Moved to Backlog: ${draggedFeature.description.slice( + 0, + 50 + )}${draggedFeature.description.length > 50 ? '...' : ''}`, + } + ); + } else if (isRunningTask) { + // Running features can only be dragged to backlog, not other columns + logger.debug('Cannot drag running feature to', targetStatus); + toast.error('Cannot move running feature', { + description: 'Stop the agent first or drag to Backlog to stop and move.', }); + return; } else if (targetStatus === 'verified' && draggedFeature.skipTests) { // Manual verify via drag (only for skipTests features) moveFeature(featureId, 'verified'); @@ -310,6 +325,7 @@ export function useBoardDragDrop({ updateFeature, persistFeatureUpdate, handleStartImplementation, + autoMode, ] ); diff --git a/apps/ui/src/hooks/use-query-invalidation.ts b/apps/ui/src/hooks/use-query-invalidation.ts index a91dd5d6..0c09977c 100644 --- a/apps/ui/src/hooks/use-query-invalidation.ts +++ b/apps/ui/src/hooks/use-query-invalidation.ts @@ -28,8 +28,11 @@ const PROGRESS_DEBOUNCE_MAX_WAIT = 2000; * feature moving to custom pipeline columns (fixes GitHub issue #668) */ const FEATURE_LIST_INVALIDATION_EVENTS: AutoModeEvent['type'][] = [ + 'auto_mode_feature_start', 'auto_mode_feature_complete', 'auto_mode_error', + 'auto_mode_started', + 'auto_mode_stopped', 'plan_approval_required', 'plan_approved', 'plan_rejected', @@ -39,11 +42,11 @@ const FEATURE_LIST_INVALIDATION_EVENTS: AutoModeEvent['type'][] = [ /** * Events that should invalidate a specific feature (features.single query) - * Note: pipeline_step_started is NOT included here because it already invalidates - * features.all() above, which also invalidates child queries (features.single) + * Note: auto_mode_feature_start and pipeline_step_started are NOT included here + * because they already invalidate features.all() above, which also invalidates + * child queries (features.single) */ const SINGLE_FEATURE_INVALIDATION_EVENTS: AutoModeEvent['type'][] = [ - 'auto_mode_feature_start', 'auto_mode_phase', 'auto_mode_phase_complete', 'auto_mode_task_status',