diff --git a/apps/server/src/services/auto-mode-service.ts b/apps/server/src/services/auto-mode-service.ts index 2736e198..1f5407c8 100644 --- a/apps/server/src/services/auto-mode-service.ts +++ b/apps/server/src/services/auto-mode-service.ts @@ -534,7 +534,11 @@ export class AutoModeService { const autoModeByWorktree = settings.autoModeByWorktree; if (projectId && autoModeByWorktree && typeof autoModeByWorktree === 'object') { - const key = `${projectId}::${branchName ?? '__main__'}`; + // Normalize branch name to match UI convention: + // - null or "main" -> "__main__" (UI treats "main" as the main worktree) + // This ensures consistency with how the UI stores worktree settings + const normalizedBranch = branchName === 'main' ? null : branchName; + const key = `${projectId}::${normalizedBranch ?? '__main__'}`; const entry = autoModeByWorktree[key]; if (entry && typeof entry.maxConcurrency === 'number') { return entry.maxConcurrency; @@ -1039,7 +1043,9 @@ export class AutoModeService { }> { // Load feature to get branchName const feature = await this.loadFeature(projectPath, featureId); - const branchName = feature?.branchName ?? null; + const rawBranchName = feature?.branchName ?? null; + // Normalize "main" to null to match UI convention for main worktree + const branchName = rawBranchName === 'main' ? null : rawBranchName; // Get per-worktree limit const maxAgents = await this.resolveMaxConcurrency(projectPath, branchName); diff --git a/apps/server/src/services/settings-service.ts b/apps/server/src/services/settings-service.ts index 7f9b54e4..d436dc8f 100644 --- a/apps/server/src/services/settings-service.ts +++ b/apps/server/src/services/settings-service.ts @@ -621,6 +621,21 @@ export class SettingsService { }; } + // Deep merge autoModeByWorktree if provided (preserves other worktree entries) + if (sanitizedUpdates.autoModeByWorktree) { + type WorktreeEntry = { maxConcurrency: number; branchName: string | null }; + const mergedAutoModeByWorktree: Record = { + ...current.autoModeByWorktree, + }; + for (const [key, value] of Object.entries(sanitizedUpdates.autoModeByWorktree)) { + mergedAutoModeByWorktree[key] = { + ...mergedAutoModeByWorktree[key], + ...value, + }; + } + updated.autoModeByWorktree = mergedAutoModeByWorktree; + } + await writeSettingsJson(settingsPath, updated); logger.info('Global settings updated'); diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 2ed3ba98..30df9657 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -87,6 +87,7 @@ import { usePipelineConfig } from '@/hooks/queries'; import { useQueryClient } from '@tanstack/react-query'; import { queryKeys } from '@/lib/query-keys'; import { useAutoModeQueryInvalidation } from '@/hooks/use-query-invalidation'; +import { useUpdateGlobalSettings } from '@/hooks/mutations/use-settings-mutations'; // Stable empty array to avoid infinite loop in selector const EMPTY_WORKTREES: ReturnType['getWorktrees']> = []; @@ -451,6 +452,8 @@ export function BoardView() { const maxConcurrency = autoMode.maxConcurrency; // Get worktree-specific setter const setMaxConcurrencyForWorktree = useAppStore((state) => state.setMaxConcurrencyForWorktree); + // Mutation to persist maxConcurrency to server settings + const updateGlobalSettings = useUpdateGlobalSettings({ showSuccessToast: false }); // Get the current branch from the selected worktree (not from store which may be stale) const currentWorktreeBranch = selectedWorktree?.branch ?? null; @@ -1277,6 +1280,15 @@ export function BoardView() { if (currentProject && selectedWorktree) { const branchName = selectedWorktree.isMain ? null : selectedWorktree.branch; setMaxConcurrencyForWorktree(currentProject.id, branchName, newMaxConcurrency); + + // Persist to server settings so capacity checks use the correct value + const worktreeKey = `${currentProject.id}::${branchName ?? '__main__'}`; + updateGlobalSettings.mutate({ + autoModeByWorktree: { + [worktreeKey]: { maxConcurrency: newMaxConcurrency }, + }, + }); + // Also update backend if auto mode is running if (autoMode.isRunning) { // Restart auto mode with new concurrency (backend will handle this) diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts b/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts index 3fbdfd5d..1af61f09 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts @@ -553,6 +553,11 @@ export function useBoardActions({ }; updateFeature(feature.id, rollbackUpdates); + // Also persist the rollback so it survives page refresh + persistFeatureUpdate(feature.id, rollbackUpdates).catch((persistError) => { + logger.error('Failed to persist rollback:', persistError); + }); + // If server is offline (connection refused), redirect to login page if (isConnectionError(error)) { handleServerOffline(); diff --git a/apps/ui/src/lib/log-parser.ts b/apps/ui/src/lib/log-parser.ts index a6fa3278..8a873b5f 100644 --- a/apps/ui/src/lib/log-parser.ts +++ b/apps/ui/src/lib/log-parser.ts @@ -1326,13 +1326,21 @@ export function getLogTypeColors(type: LogEntryType): { icon: 'text-primary', badge: 'bg-primary/20 text-primary', }; + case 'info': + return { + bg: 'bg-zinc-500/10', + border: 'border-zinc-500/30', + text: 'text-primary', + icon: 'text-zinc-400', + badge: 'bg-zinc-500/20 text-primary', + }; default: return { bg: 'bg-zinc-500/10', border: 'border-zinc-500/30', - text: 'text-zinc-300', + text: 'text-black', icon: 'text-zinc-400', - badge: 'bg-zinc-500/20 text-zinc-300', + badge: 'bg-zinc-500/20 text-black', }; } }