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.
This commit is contained in:
webdevcody
2026-01-20 13:39:38 -05:00
parent 2ab78dd590
commit 8facdc66a9
10 changed files with 337 additions and 94 deletions

View File

@@ -142,7 +142,8 @@ export function BoardHeader({
onConcurrencyChange={onConcurrencyChange}
isAutoModeRunning={isAutoModeRunning}
onAutoModeToggle={onAutoModeToggle}
onOpenAutoModeSettings={() => {}}
skipVerificationInAutoMode={skipVerificationInAutoMode}
onSkipVerificationChange={setSkipVerificationInAutoMode}
onOpenPlanDialog={onOpenPlanDialog}
showClaudeUsage={showClaudeUsage}
showCodexUsage={showCodexUsage}

View File

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

View File

@@ -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
</span>
{/* Auto Mode Toggle */}
<div
className="flex items-center justify-between p-3 cursor-pointer hover:bg-accent/50 rounded-lg border border-border/50 transition-colors"
onClick={() => onAutoModeToggle(!isAutoModeRunning)}
data-testid="mobile-auto-mode-toggle-container"
>
<div className="flex items-center gap-2">
<Zap
className={cn(
'w-4 h-4',
isAutoModeRunning ? 'text-yellow-500' : 'text-muted-foreground'
)}
/>
<span className="text-sm font-medium">Auto Mode</span>
<span
className="text-[10px] font-medium text-muted-foreground bg-muted/60 px-1.5 py-0.5 rounded"
data-testid="mobile-auto-mode-max-concurrency"
title="Max concurrent agents"
>
{maxConcurrency}
</span>
</div>
<div className="flex items-center gap-2">
{/* Auto Mode Section */}
<div className="rounded-lg border border-border/50 overflow-hidden">
{/* Auto Mode Toggle */}
<div
className="flex items-center justify-between p-3 cursor-pointer hover:bg-accent/50 transition-colors"
onClick={() => onAutoModeToggle(!isAutoModeRunning)}
data-testid="mobile-auto-mode-toggle-container"
>
<div className="flex items-center gap-2">
<Zap
className={cn(
'w-4 h-4',
isAutoModeRunning ? 'text-yellow-500' : 'text-muted-foreground'
)}
/>
<span className="text-sm font-medium">Auto Mode</span>
</div>
<Switch
id="mobile-auto-mode-toggle"
checked={isAutoModeRunning}
@@ -96,17 +92,51 @@ export function HeaderMobileMenu({
onClick={(e) => e.stopPropagation()}
data-testid="mobile-auto-mode-toggle"
/>
<button
onClick={(e) => {
e.stopPropagation();
onOpenAutoModeSettings();
}}
className="p-1 rounded hover:bg-accent/50 transition-colors"
title="Auto Mode Settings"
data-testid="mobile-auto-mode-settings-button"
>
<Settings2 className="w-4 h-4 text-muted-foreground" />
</button>
</div>
{/* Skip Verification Toggle */}
<div
className="flex items-center justify-between p-3 pl-9 cursor-pointer hover:bg-accent/50 border-t border-border/30 transition-colors"
onClick={() => onSkipVerificationChange(!skipVerificationInAutoMode)}
data-testid="mobile-skip-verification-toggle-container"
>
<div className="flex items-center gap-2">
<FastForward className="w-4 h-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">Skip Verification</span>
</div>
<Switch
id="mobile-skip-verification-toggle"
checked={skipVerificationInAutoMode}
onCheckedChange={onSkipVerificationChange}
onClick={(e) => e.stopPropagation()}
data-testid="mobile-skip-verification-toggle"
/>
</div>
{/* Concurrency Control */}
<div
className="p-3 pl-9 border-t border-border/30"
data-testid="mobile-concurrency-control"
>
<div className="flex items-center gap-2 mb-3">
<Bot className="w-4 h-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">Max Agents</span>
<span
className="text-sm text-muted-foreground ml-auto"
data-testid="mobile-concurrency-value"
>
{runningAgentsCount}/{maxConcurrency}
</span>
</div>
<Slider
value={[maxConcurrency]}
onValueChange={(value) => onConcurrencyChange(value[0])}
min={1}
max={10}
step={1}
className="w-full"
data-testid="mobile-concurrency-slider"
/>
</div>
</div>
@@ -129,32 +159,6 @@ export function HeaderMobileMenu({
/>
</div>
{/* Concurrency Control */}
<div
className="p-3 rounded-lg border border-border/50"
data-testid="mobile-concurrency-control"
>
<div className="flex items-center gap-2 mb-3">
<Bot className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium">Max Agents</span>
<span
className="text-sm text-muted-foreground ml-auto"
data-testid="mobile-concurrency-value"
>
{runningAgentsCount}/{maxConcurrency}
</span>
</div>
<Slider
value={[maxConcurrency]}
onValueChange={(value) => onConcurrencyChange(value[0])}
min={1}
max={10}
step={1}
className="w-full"
data-testid="mobile-concurrency-slider"
/>
</div>
{/* Plan Button */}
<Button
variant="outline"

View File

@@ -487,7 +487,15 @@ export function useBoardActions({
const handleStartImplementation = useCallback(
async (feature: Feature) => {
// Check capacity for the feature's specific worktree, not the current view
const featureBranchName = feature.branchName ?? null;
// Normalize the branch name: if the feature's branch is the primary worktree branch,
// treat it as null (main worktree) to match how running tasks are stored
const rawBranchName = feature.branchName ?? null;
const featureBranchName =
currentProject?.path &&
rawBranchName &&
isPrimaryWorktreeBranch(currentProject.path, rawBranchName)
? null
: rawBranchName;
const featureWorktreeState = currentProject
? getAutoModeState(currentProject.id, featureBranchName)
: null;
@@ -567,6 +575,7 @@ export function useBoardActions({
handleRunFeature,
currentProject,
getAutoModeState,
isPrimaryWorktreeBranch,
]
);

View File

@@ -128,15 +128,22 @@ export function useBoardDragDrop({
const targetBranch = worktreeData.branch;
const currentBranch = draggedFeature.branchName;
// For main worktree, set branchName to null to indicate it should use main
// (must use null not undefined so it serializes to JSON for the API call)
// For other worktrees, set branchName to the target branch
const newBranchName = worktreeData.isMain ? null : targetBranch;
// If already on the same branch, nothing to do
if (currentBranch === targetBranch) {
// For main worktree: feature with null/undefined branchName is already on main
// For other worktrees: compare branch names directly
const isAlreadyOnTarget = worktreeData.isMain
? !currentBranch // null or undefined means already on main
: currentBranch === targetBranch;
if (isAlreadyOnTarget) {
return;
}
// For main worktree, set branchName to undefined/null to indicate it should use main
// For other worktrees, set branchName to the target branch
const newBranchName = worktreeData.isMain ? undefined : targetBranch;
// Update feature's branchName
updateFeature(featureId, { branchName: newBranchName });
await persistFeatureUpdate(featureId, { branchName: newBranchName });

View File

@@ -17,11 +17,8 @@ export function useRunningFeatures({ runningFeatureIds, features }: UseRunningFe
// Match by branchName only (worktreePath is no longer stored)
if (feature.branchName) {
// Special case: if feature is on 'main' branch, it belongs to main worktree
// irrespective of whether the branch name matches exactly (it should, but strict equality might fail if refs differ)
if (worktree.isMain && feature.branchName === 'main') {
return true;
}
// Check if branch names match - this handles both main worktree (any primary branch name)
// and feature worktrees
return worktree.branch === feature.branchName;
}

View File

@@ -77,6 +77,7 @@ export function useAutoMode(worktree?: WorktreeInfo) {
getWorktreeKey,
getMaxConcurrencyForWorktree,
setMaxConcurrencyForWorktree,
isPrimaryWorktreeBranch,
} = useAppStore(
useShallow((state) => ({
autoModeByWorktree: state.autoModeByWorktree,
@@ -90,6 +91,7 @@ export function useAutoMode(worktree?: WorktreeInfo) {
getWorktreeKey: state.getWorktreeKey,
getMaxConcurrencyForWorktree: state.getMaxConcurrencyForWorktree,
setMaxConcurrencyForWorktree: state.setMaxConcurrencyForWorktree,
isPrimaryWorktreeBranch: state.isPrimaryWorktreeBranch,
}))
);
@@ -197,9 +199,21 @@ export function useAutoMode(worktree?: WorktreeInfo) {
}
// Extract branchName from event, defaulting to null (main worktree)
const eventBranchName: string | null =
const rawEventBranchName: string | null =
'branchName' in event && event.branchName !== undefined ? event.branchName : null;
// Get projectPath for worktree lookup
const eventProjectPath = 'projectPath' in event ? event.projectPath : currentProject?.path;
// Normalize branchName: convert primary worktree branch to null for consistent key lookup
// This handles cases where the main branch is named something other than 'main' (e.g., 'master', 'develop')
const eventBranchName: string | null =
eventProjectPath &&
rawEventBranchName &&
isPrimaryWorktreeBranch(eventProjectPath, rawEventBranchName)
? null
: rawEventBranchName;
// Skip event if we couldn't determine the project
if (!eventProjectId) {
logger.warn('Could not determine project for event:', event);
@@ -493,6 +507,7 @@ export function useAutoMode(worktree?: WorktreeInfo) {
currentProject?.path,
getMaxConcurrencyForWorktree,
setMaxConcurrencyForWorktree,
isPrimaryWorktreeBranch,
]);
// Start auto mode - calls backend to start the auto loop for this worktree

View File

@@ -895,12 +895,15 @@ function RootLayoutContent() {
}
function RootLayout() {
// Hide devtools on compact screens (mobile/tablet) to avoid overlap with sidebar settings
const isCompact = useIsCompact();
return (
<QueryClientProvider client={queryClient}>
<FileBrowserProvider>
<RootLayoutContent />
</FileBrowserProvider>
{SHOW_QUERY_DEVTOOLS ? (
{SHOW_QUERY_DEVTOOLS && !isCompact ? (
<ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-left" />
) : null}
</QueryClientProvider>