From 056d94f6036ccdbafdd215398366fa350febe03c Mon Sep 17 00:00:00 2001 From: Questler Date: Tue, 10 Feb 2026 23:22:46 +0100 Subject: [PATCH 1/4] feat: add "All Worktrees" overview option to worktree selector Adds an "All" option to the worktree selector that shows features from all worktrees and main combined. Uses a sentinel value ALL_WORKTREES_BRANCH to bypass worktree filtering. Supports dropdown (3+ worktrees), tab (1-2 worktrees), and mobile layouts. Co-Authored-By: Claude Opus 4.6 --- apps/ui/src/components/views/board-view.tsx | 26 +- .../hooks/use-board-column-features.ts | 7 +- .../components/worktree-dropdown.tsx | 226 ++++++++++++------ .../components/worktree-mobile-dropdown.tsx | 72 +++++- .../components/worktree-tab.tsx | 45 ++-- .../worktree-panel/hooks/use-worktrees.ts | 21 +- .../worktree-panel/worktree-panel.tsx | 59 ++++- apps/ui/src/store/app-store.ts | 3 + 8 files changed, 346 insertions(+), 113 deletions(-) diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index d8be006dd..62d7dcad2 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -27,7 +27,7 @@ class DialogAwarePointerSensor extends PointerSensor { }, ]; } -import { useAppStore, Feature } from '@/store/app-store'; +import { useAppStore, Feature, ALL_WORKTREES_BRANCH } from '@/store/app-store'; import { getElectronAPI } from '@/lib/electron'; import { getHttpApiClient } from '@/lib/http-api-client'; import type { BacklogPlanResult, FeatureStatusWithPipeline } from '@automaker/types'; @@ -413,8 +413,14 @@ export function BoardView() { ); // Get the branch for the currently selected worktree - // Find the worktree that matches the current selection, or use main worktree + // Find the worktree that matches the current selection, or use main worktree. + // When "All Worktrees" is selected, there is no single selected worktree — + // return undefined so consumers can handle the virtual selection gracefully. const selectedWorktree = useMemo((): WorktreeInfo | undefined => { + // ALL mode — no single worktree is selected + if (currentWorktreeInfo?.branch === ALL_WORKTREES_BRANCH) { + return undefined; + } let found; if (currentWorktreePath === null) { // Primary worktree selected - find the main worktree @@ -432,7 +438,7 @@ export function BoardView() { (currentWorktreePath !== null ? pathsEqual(found.path, currentWorktreePath) : found.isMain), hasWorktree: found.hasWorktree ?? true, }; - }, [worktrees, currentWorktreePath]); + }, [worktrees, currentWorktreePath, currentWorktreeInfo?.branch]); // Auto mode hook - pass current worktree to get worktree-specific state // Must be after selectedWorktree is defined @@ -503,8 +509,12 @@ export function BoardView() { // 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; + // Derive the current branch from the selected worktree. + // When selectedWorktree is undefined (ALL mode), fall back to the store's branch value + // which carries the ALL_WORKTREES_BRANCH sentinel so downstream hooks bypass filtering. + // Otherwise prefer the branch from the resolved selectedWorktree object. + const currentWorktreeBranch = + selectedWorktree?.branch ?? currentWorktreeInfo?.branch ?? null; // Get the branch for the currently selected worktree (for defaulting new features) // Use the branch from selectedWorktree, or fall back to main worktree's branch @@ -772,6 +782,9 @@ export function BoardView() { // Only backlog features if (f.status !== 'backlog') return false; + // "All Worktrees" view — all backlog features are selectable + if (currentWorktreeBranch === ALL_WORKTREES_BRANCH) return true; + // Filter by current worktree branch const featureBranch = f.branchName; if (!featureBranch) { @@ -803,6 +816,9 @@ export function BoardView() { // Only waiting_approval features if (f.status !== 'waiting_approval') return false; + // "All Worktrees" view — all waiting_approval features are selectable + if (currentWorktreeBranch === ALL_WORKTREES_BRANCH) return true; + // Filter by current worktree branch const featureBranch = f.branchName; if (!featureBranch) { diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-column-features.ts b/apps/ui/src/components/views/board-view/hooks/use-board-column-features.ts index 508cb9483..c330769bd 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-column-features.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-column-features.ts @@ -1,6 +1,6 @@ // @ts-nocheck - column filtering logic with dependency resolution and status mapping import { useMemo, useCallback } from 'react'; -import { Feature, useAppStore } from '@/store/app-store'; +import { Feature, useAppStore, ALL_WORKTREES_BRANCH } from '@/store/app-store'; import { createFeatureMap, getBlockingDependenciesFromMap, @@ -67,7 +67,10 @@ export function useBoardColumnFeatures({ const featureBranch = f.branchName; let matchesWorktree: boolean; - if (!featureBranch) { + if (effectiveBranch === ALL_WORKTREES_BRANCH) { + // "All Worktrees" view — show every feature regardless of branch assignment + matchesWorktree = true; + } else if (!featureBranch) { // No branch assigned - show only on primary worktree const isViewingPrimary = currentWorktreePath === null; matchesWorktree = isViewingPrimary; diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx index fd1c2ba33..8ca12cde9 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx @@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, + DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, @@ -16,6 +17,8 @@ import { Globe, GitPullRequest, FlaskConical, + Layers, + Check, } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; @@ -60,6 +63,10 @@ export interface WorktreeDropdownProps { getTestSessionInfo: (worktree: WorktreeInfo) => TestSessionInfo | undefined; /** Callback when a worktree is selected */ onSelectWorktree: (worktree: WorktreeInfo) => void; + /** Whether the "All Worktrees" virtual selection is active */ + isAllWorktreesSelected?: boolean; + /** Callback when "All Worktrees" is selected */ + onSelectAllWorktrees?: () => void; // Branch switching props branches: BranchInfo[]; @@ -120,6 +127,7 @@ const MAX_TRIGGER_BRANCH_NAME_LENGTH = 24; * Used when there are 3+ worktrees to avoid horizontal tab wrapping. * * Features: + * - "All Worktrees" option at top with Layers icon and total card count * - Compact dropdown trigger showing current worktree with indicators * - Grouped display (main branch + worktrees) * - Full status indicators (PR, dev server, auto mode, changes) @@ -139,6 +147,8 @@ export function WorktreeDropdown({ isTestRunningForWorktree, getTestSessionInfo, onSelectWorktree, + isAllWorktreesSelected = false, + onSelectAllWorktrees, // Branch switching props branches, filteredBranches, @@ -186,13 +196,23 @@ export function WorktreeDropdown({ onViewTestLogs, }: WorktreeDropdownProps) { // Find the currently selected worktree to display in the trigger - const selectedWorktree = worktrees.find((w) => isWorktreeSelected(w)); - const displayBranch = selectedWorktree?.branch || 'Select worktree'; + const selectedWorktree = isAllWorktreesSelected + ? undefined + : worktrees.find((w) => isWorktreeSelected(w)); + const displayBranch = isAllWorktreesSelected + ? 'All' + : selectedWorktree?.branch || 'Select worktree'; const { truncated: truncatedBranch, isTruncated: isBranchNameTruncated } = truncateBranchName( displayBranch, MAX_TRIGGER_BRANCH_NAME_LENGTH ); + // Compute total card count across all branches for "All Worktrees" display + const totalCardCount = useMemo(() => { + if (!branchCardCounts) return 0; + return Object.values(branchCardCounts).reduce((sum, count) => sum + count, 0); + }, [branchCardCounts]); + // Separate main worktree from others for grouping const mainWorktree = worktrees.find((w) => w.isMain); const otherWorktrees = worktrees.filter((w) => !w.isMain); @@ -234,101 +254,117 @@ export function WorktreeDropdown({ variant="outline" size="sm" className={cn( - 'h-7 px-3 gap-1.5 font-mono text-xs bg-secondary/50 hover:bg-secondary min-w-0 border-r-0 rounded-r-none' + 'h-7 px-3 gap-1.5 font-mono text-xs bg-secondary/50 hover:bg-secondary min-w-0', + !isAllWorktreesSelected && 'border-r-0 rounded-r-none' )} disabled={isActivating} > {/* Running/Activating indicator */} {(selectedStatus.isRunning || isActivating) && } - {/* Branch icon */} - + {/* Branch/Layers icon */} + {isAllWorktreesSelected ? ( + + ) : ( + + )} {/* Branch name with optional tooltip */} {truncatedBranch} - {/* Card count badge */} - {selectedWorktree && - branchCardCounts?.[selectedWorktree.branch] !== undefined && - branchCardCounts[selectedWorktree.branch] > 0 && ( - - {branchCardCounts[selectedWorktree.branch]} - - )} + {/* Card count badge - show total when "All Worktrees" is selected */} + {isAllWorktreesSelected + ? totalCardCount > 0 && ( + + {totalCardCount} + + ) + : selectedWorktree && + branchCardCounts?.[selectedWorktree.branch] !== undefined && + branchCardCounts[selectedWorktree.branch] > 0 && ( + + {branchCardCounts[selectedWorktree.branch]} + + )} - {/* Uncommitted changes indicator */} - {selectedWorktree?.hasChanges && ( - - )} - {/* Dev server indicator */} - {selectedStatus.devServerRunning && ( - - - - )} + {/* Dev server indicator */} + {selectedStatus.devServerRunning && ( + + + + )} - {/* Test running indicator */} - {selectedStatus.testRunning && ( - - - - )} + {/* Test running indicator */} + {selectedStatus.testRunning && ( + + + + )} - {/* Last test result indicator (when not running) */} - {!selectedStatus.testRunning && selectedStatus.testSessionInfo && ( - + + )} - title={`Last test: ${selectedStatus.testSessionInfo.status}`} - > - - - )} - {/* Auto mode indicator */} - {selectedStatus.autoModeRunning && ( - - - - )} + {/* Auto mode indicator */} + {selectedStatus.autoModeRunning && ( + + + + )} - {/* PR badge */} - {selectedWorktree?.pr && ( - + #{selectedWorktree.pr.number} + )} - > - #{selectedWorktree.pr.number} - + )} {/* Dropdown chevron */} ), - [isActivating, selectedStatus, truncatedBranch, selectedWorktree, branchCardCounts] + [isActivating, selectedStatus, truncatedBranch, selectedWorktree, branchCardCounts, isAllWorktreesSelected, totalCardCount] ); // Wrap trigger button with dropdown trigger first to ensure ref is passed correctly @@ -354,6 +390,52 @@ export function WorktreeDropdown({ className="w-80 max-h-96 overflow-y-auto" aria-label="Worktree selection" > + {/* All Worktrees option */} + {onSelectAllWorktrees && ( + <> + +
+ {/* Selection indicator */} + {isAllWorktreesSelected ? ( + + ) : ( +
+ )} + + {/* Layers icon */} + + + {/* Label */} + + All Worktrees + +
+ + {/* Total card count badge */} +
+ {totalCardCount > 0 && ( + + {totalCardCount} + + )} +
+ + + + )} + {/* Main worktree section */} {mainWorktree && ( <> diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-mobile-dropdown.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-mobile-dropdown.tsx index 079c9b11b..3848461cb 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-mobile-dropdown.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-mobile-dropdown.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { Button } from '@/components/ui/button'; import { DropdownMenu, @@ -7,7 +8,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { GitBranch, ChevronDown, CircleDot, Check } from 'lucide-react'; +import { GitBranch, ChevronDown, CircleDot, Check, Layers } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import type { WorktreeInfo } from '../types'; @@ -19,6 +20,10 @@ interface WorktreeMobileDropdownProps { isActivating: boolean; branchCardCounts?: Record; onSelectWorktree: (worktree: WorktreeInfo) => void; + /** Whether the "All Worktrees" virtual selection is active */ + isAllWorktreesSelected?: boolean; + /** Callback when "All Worktrees" is selected */ + onSelectAllWorktrees?: () => void; } export function WorktreeMobileDropdown({ @@ -28,10 +33,22 @@ export function WorktreeMobileDropdown({ isActivating, branchCardCounts, onSelectWorktree, + isAllWorktreesSelected = false, + onSelectAllWorktrees, }: WorktreeMobileDropdownProps) { // Find the currently selected worktree to display in the trigger - const selectedWorktree = worktrees.find((w) => isWorktreeSelected(w)); - const displayBranch = selectedWorktree?.branch || 'Select branch'; + const selectedWorktree = isAllWorktreesSelected + ? undefined + : worktrees.find((w) => isWorktreeSelected(w)); + const displayBranch = isAllWorktreesSelected + ? 'All' + : selectedWorktree?.branch || 'Select branch'; + + // Compute total card count across all branches for "All Worktrees" display + const totalCardCount = useMemo(() => { + if (!branchCardCounts) return 0; + return Object.values(branchCardCounts).reduce((sum, count) => sum + count, 0); + }, [branchCardCounts]); return ( @@ -42,8 +59,17 @@ export function WorktreeMobileDropdown({ className="h-8 px-3 gap-2 font-mono text-xs bg-secondary/50 hover:bg-secondary flex-1 min-w-0" disabled={isActivating} > - + {isAllWorktreesSelected ? ( + + ) : ( + + )} {displayBranch} + {isAllWorktreesSelected && totalCardCount > 0 && ( + + {totalCardCount} + + )} {isActivating ? ( ) : ( @@ -52,6 +78,44 @@ export function WorktreeMobileDropdown({ + {/* All Worktrees option */} + {onSelectAllWorktrees && ( + <> + +
+ {isAllWorktreesSelected ? ( + + ) : ( +
+ )} + + + All Worktrees + +
+
+ {totalCardCount > 0 && ( + + {totalCardCount} + + )} +
+ + + + )} Branches & Worktrees diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx index 9d508ed34..9acc9487f 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx @@ -81,6 +81,8 @@ interface WorktreeTabProps { hasInitScript: boolean; /** Whether a test command is configured in project settings */ hasTestCommand?: boolean; + /** Whether the "All Worktrees" virtual selection is active – hides branch switch & actions dropdowns */ + isAllWorktreesSelected?: boolean; } export function WorktreeTab({ @@ -140,6 +142,7 @@ export function WorktreeTab({ onViewTestLogs, hasInitScript, hasTestCommand = false, + isAllWorktreesSelected = false, }: WorktreeTabProps) { // Make the worktree tab a drop target for feature cards const { setNodeRef, isOver } = useDroppable({ @@ -250,7 +253,8 @@ export function WorktreeTab({ variant={isSelected ? 'default' : 'outline'} size="sm" className={cn( - 'h-7 px-3 text-xs font-mono gap-1.5 border-r-0 rounded-l-md rounded-r-none', + 'h-7 px-3 text-xs font-mono gap-1.5', + !isAllWorktreesSelected && 'border-r-0 rounded-l-md rounded-r-none', isSelected && 'bg-primary text-primary-foreground', !isSelected && 'bg-secondary/50 hover:bg-secondary' )} @@ -295,26 +299,29 @@ export function WorktreeTab({ )} {prBadge} - + {!isAllWorktreesSelected && ( + + )} ) : (
); } diff --git a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts index ab3a87d0d..a766159cd 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts +++ b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts @@ -1,6 +1,6 @@ import { useEffect, useCallback, useRef } from 'react'; import { useQueryClient } from '@tanstack/react-query'; -import { useAppStore } from '@/store/app-store'; +import { useAppStore, ALL_WORKTREES_BRANCH } from '@/store/app-store'; import { useWorktrees as useWorktreesQuery } from '@/hooks/queries'; import { queryKeys } from '@/lib/query-keys'; import { pathsEqual } from '@/lib/utils'; @@ -71,6 +71,13 @@ export function useWorktrees({ useEffect(() => { if (worktrees.length > 0) { const current = currentWorktreeRef.current; + + // Skip validation when "All Worktrees" is selected — it's a virtual + // selection that doesn't correspond to a real worktree path/branch + if (current?.branch === ALL_WORKTREES_BRANCH) { + return; + } + const currentPath = current?.path; const currentWorktreeExists = currentPath === null @@ -111,9 +118,15 @@ export function useWorktrees({ ); const currentWorktreePath = currentWorktree?.path ?? null; - const selectedWorktree = currentWorktreePath - ? worktrees.find((w) => pathsEqual(w.path, currentWorktreePath)) - : worktrees.find((w) => w.isMain); + + // When "All Worktrees" is selected, there is no single selected worktree — + // return undefined so consumers can handle the virtual selection gracefully. + const selectedWorktree = + currentWorktree?.branch === ALL_WORKTREES_BRANCH + ? undefined + : currentWorktreePath + ? worktrees.find((w) => pathsEqual(w.path, currentWorktreePath)) + : worktrees.find((w) => w.isMain); return { isLoading, diff --git a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx index f3aebce69..65a7806d7 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx @@ -1,6 +1,6 @@ -import { useEffect, useRef, useCallback, useState } from 'react'; +import { useEffect, useRef, useCallback, useState, useMemo } from 'react'; import { Button } from '@/components/ui/button'; -import { GitBranch, Plus, RefreshCw } from 'lucide-react'; +import { GitBranch, Layers, Plus, RefreshCw } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { pathsEqual } from '@/lib/utils'; import { toast } from 'sonner'; @@ -30,7 +30,7 @@ import { BranchSwitchDropdown, WorktreeDropdown, } from './components'; -import { useAppStore } from '@/store/app-store'; +import { useAppStore, ALL_WORKTREES_BRANCH } from '@/store/app-store'; import { ViewWorktreeChangesDialog, PushToRemoteDialog, MergeWorktreeDialog } from '../dialogs'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { TestLogsPanel } from '@/components/ui/test-logs-panel'; @@ -67,6 +67,22 @@ export function WorktreePanel({ handleSelectWorktree, } = useWorktrees({ projectPath, refreshTrigger, onRemovedWorktrees }); + // Derive whether "All Worktrees" is the active selection + const isAllWorktreesSelected = currentWorktree?.branch === ALL_WORKTREES_BRANCH; + + const setCurrentWorktree = useAppStore((s) => s.setCurrentWorktree); + + // Handler to select the "All Worktrees" virtual view + const handleSelectAllWorktrees = useCallback(() => { + setCurrentWorktree(projectPath, null, ALL_WORKTREES_BRANCH); + }, [projectPath, setCurrentWorktree]); + + // Compute total card count across all branches for "All" tab display + const totalCardCount = useMemo(() => { + if (!branchCardCounts) return 0; + return Object.values(branchCardCounts).reduce((sum, count) => sum + count, 0); + }, [branchCardCounts]); + const { isStartingDevServer, isDevServerRunning, @@ -399,6 +415,10 @@ export function WorktreePanel({ }, [fetchWorktrees]); const isWorktreeSelected = (worktree: WorktreeInfo) => { + // When "All Worktrees" is selected, no individual worktree is selected. + // We check both the derived flag and the branch sentinel directly so + // the function remains correct even if called before the flag is computed. + if (isAllWorktreesSelected || currentWorktree?.branch === ALL_WORKTREES_BRANCH) return false; return worktree.isMain ? currentWorktree === null || currentWorktree === undefined || currentWorktree.path === null : pathsEqual(worktree.path, currentWorktreePath); @@ -556,10 +576,12 @@ export function WorktreePanel({ isActivating={isActivating} branchCardCounts={branchCardCounts} onSelectWorktree={handleSelectWorktree} + isAllWorktreesSelected={isAllWorktreesSelected} + onSelectAllWorktrees={useWorktreesEnabled ? handleSelectAllWorktrees : undefined} /> - {/* Branch switch dropdown for the selected worktree */} - {selectedWorktree && ( + {/* Branch switch dropdown for the selected worktree (hidden when ALL is selected) */} + {selectedWorktree && !isAllWorktreesSelected && ( )} - {/* Actions menu for the selected worktree */} - {selectedWorktree && ( + {/* Actions menu for the selected worktree (hidden when ALL is selected) */} + {selectedWorktree && !isAllWorktreesSelected && (
+ {/* "All" tab - shown when worktrees are enabled */} + {useWorktreesEnabled && ( + + )} + {mainWorktree && ( )}
@@ -955,6 +999,7 @@ export function WorktreePanel({ onViewTestLogs={handleViewTestLogs} hasInitScript={hasInitScript} hasTestCommand={hasTestCommand} + isAllWorktreesSelected={isAllWorktreesSelected} /> ); })} diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index c07353554..7296dc94b 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -124,6 +124,9 @@ const logger = createLogger('AppStore'); const OPENCODE_BEDROCK_PROVIDER_ID = 'amazon-bedrock'; const OPENCODE_BEDROCK_MODEL_PREFIX = `${OPENCODE_BEDROCK_PROVIDER_ID}/`; +/** Sentinel value for "show features from all worktrees/branches" instead of filtering to a specific branch */ +export const ALL_WORKTREES_BRANCH = '__all_worktrees__' as const; + // Re-export types from @automaker/types for convenience export type { ModelAlias, From e4b0a9e41991047ca73756f623226c2a4ad11edd Mon Sep 17 00:00:00 2001 From: Questler Date: Tue, 10 Feb 2026 23:56:05 +0100 Subject: [PATCH 2/4] fix: show running agent state in All Worktrees view Use aggregated running tasks from all worktrees when ALL_WORKTREES_BRANCH is selected, so feature cards correctly show Logs/Stop instead of Resume. Co-Authored-By: Claude Opus 4.6 --- apps/ui/src/components/views/board-view.tsx | 28 +++++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 62d7dcad2..796e7e133 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -513,8 +513,7 @@ export function BoardView() { // When selectedWorktree is undefined (ALL mode), fall back to the store's branch value // which carries the ALL_WORKTREES_BRANCH sentinel so downstream hooks bypass filtering. // Otherwise prefer the branch from the resolved selectedWorktree object. - const currentWorktreeBranch = - selectedWorktree?.branch ?? currentWorktreeInfo?.branch ?? null; + const currentWorktreeBranch = selectedWorktree?.branch ?? currentWorktreeInfo?.branch ?? null; // Get the branch for the currently selected worktree (for defaulting new features) // Use the branch from selectedWorktree, or fall back to main worktree's branch @@ -531,14 +530,21 @@ export function BoardView() { .flatMap(([, state]) => state.runningTasks ?? []); }, [autoModeByWorktree, currentProject?.id]); + // When "All Worktrees" is selected, use aggregated running tasks from all worktrees + // so feature cards correctly show running agent state instead of "Resume" + const effectiveRunningAutoTasks = + currentWorktreeBranch === ALL_WORKTREES_BRANCH + ? runningAutoTasksAllWorktrees + : runningAutoTasks; + // Get in-progress features for keyboard shortcuts (needed before actions hook) // Must be after runningAutoTasks is defined const inProgressFeaturesForShortcuts = useMemo(() => { return hookFeatures.filter((f) => { - const isRunning = runningAutoTasks.includes(f.id); + const isRunning = effectiveRunningAutoTasks.includes(f.id); return isRunning || f.status === 'in_progress'; }); - }, [hookFeatures, runningAutoTasks]); + }, [hookFeatures, effectiveRunningAutoTasks]); // Calculate unarchived card counts per branch const branchCardCounts = useMemo(() => { @@ -603,7 +609,7 @@ export function BoardView() { } = useBoardActions({ currentProject, features: hookFeatures, - runningAutoTasks, + runningAutoTasks: effectiveRunningAutoTasks, loadFeatures, persistFeatureCreate, persistFeatureUpdate, @@ -1092,7 +1098,7 @@ export function BoardView() { // Use keyboard shortcuts hook (after actions hook) useBoardKeyboardShortcuts({ features: hookFeatures, - runningAutoTasks, + runningAutoTasks: effectiveRunningAutoTasks, onAddFeature: () => setShowAddDialog(true), onStartNextFeatures: handleStartNextFeatures, onViewOutput: handleViewOutput, @@ -1108,7 +1114,7 @@ export function BoardView() { } = useBoardDragDrop({ features: hookFeatures, currentProject, - runningAutoTasks, + runningAutoTasks: effectiveRunningAutoTasks, persistFeatureUpdate, handleStartImplementation, }); @@ -1160,7 +1166,7 @@ export function BoardView() { // Use column features hook const { getColumnFeatures, completedFeatures } = useBoardColumnFeatures({ features: hookFeatures, - runningAutoTasks, + runningAutoTasks: effectiveRunningAutoTasks, searchQuery, currentWorktreePath, currentWorktreeBranch, @@ -1346,7 +1352,7 @@ export function BoardView() { { if (currentProject) { // If selectedWorktree is undefined or it's the main worktree, branchName will be null. @@ -1482,7 +1488,7 @@ export function BoardView() { setShowAddDialog(true); }, }} - runningAutoTasks={runningAutoTasks} + runningAutoTasks={effectiveRunningAutoTasks} pipelineConfig={pipelineConfig} onAddFeature={() => setShowAddDialog(true)} isSelectionMode={isSelectionMode} @@ -1521,7 +1527,7 @@ export function BoardView() { setShowAddDialog(true); }} featuresWithContext={featuresWithContext} - runningAutoTasks={runningAutoTasks} + runningAutoTasks={effectiveRunningAutoTasks} onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)} onAddFeature={() => setShowAddDialog(true)} onShowCompletedModal={() => setShowCompletedModal(true)} From 09c9e1ac0e9591e1e02e6bffa34c83f32b48baf7 Mon Sep 17 00:00:00 2001 From: Questler Date: Wed, 11 Feb 2026 00:25:29 +0100 Subject: [PATCH 3/4] fix: address PR review feedback for All Worktrees feature - Sanitize sentinel branch value so it never leaks into feature defaults - Pass totalCardCount as prop from parent instead of recalculating in each child - Only show "All" option when at least 1 non-main worktree exists Co-Authored-By: Claude Opus 4.6 --- apps/ui/src/components/views/board-view.tsx | 7 +++-- .../components/worktree-dropdown.tsx | 26 +++++++++---------- .../components/worktree-mobile-dropdown.tsx | 17 +++--------- .../worktree-panel/worktree-panel.tsx | 16 +++++++++--- 4 files changed, 34 insertions(+), 32 deletions(-) diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 796e7e133..2cbad93a3 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -516,9 +516,12 @@ export function BoardView() { const currentWorktreeBranch = selectedWorktree?.branch ?? currentWorktreeInfo?.branch ?? null; // Get the branch for the currently selected worktree (for defaulting new features) - // Use the branch from selectedWorktree, or fall back to main worktree's branch + // When ALL_WORKTREES_BRANCH is active, fall back to main branch to avoid + // assigning the sentinel value as a real branch on new features. + const effectiveWorktreeBranch = + currentWorktreeBranch === ALL_WORKTREES_BRANCH ? null : currentWorktreeBranch; const selectedWorktreeBranch = - currentWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || 'main'; + effectiveWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || 'main'; // Aggregate running auto tasks across all worktrees for this project const autoModeByWorktree = useAppStore((state) => state.autoModeByWorktree); diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx index 8ca12cde9..7af70cd01 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx @@ -67,6 +67,8 @@ export interface WorktreeDropdownProps { isAllWorktreesSelected?: boolean; /** Callback when "All Worktrees" is selected */ onSelectAllWorktrees?: () => void; + /** Pre-computed total card count across all branches (for "All Worktrees" display) */ + totalCardCount?: number; // Branch switching props branches: BranchInfo[]; @@ -149,6 +151,7 @@ export function WorktreeDropdown({ onSelectWorktree, isAllWorktreesSelected = false, onSelectAllWorktrees, + totalCardCount = 0, // Branch switching props branches, filteredBranches, @@ -207,12 +210,6 @@ export function WorktreeDropdown({ MAX_TRIGGER_BRANCH_NAME_LENGTH ); - // Compute total card count across all branches for "All Worktrees" display - const totalCardCount = useMemo(() => { - if (!branchCardCounts) return 0; - return Object.values(branchCardCounts).reduce((sum, count) => sum + count, 0); - }, [branchCardCounts]); - // Separate main worktree from others for grouping const mainWorktree = worktrees.find((w) => w.isMain); const otherWorktrees = worktrees.filter((w) => !w.isMain); @@ -364,7 +361,15 @@ export function WorktreeDropdown({ ), - [isActivating, selectedStatus, truncatedBranch, selectedWorktree, branchCardCounts, isAllWorktreesSelected, totalCardCount] + [ + isActivating, + selectedStatus, + truncatedBranch, + selectedWorktree, + branchCardCounts, + isAllWorktreesSelected, + totalCardCount, + ] ); // Wrap trigger button with dropdown trigger first to ensure ref is passed correctly @@ -413,12 +418,7 @@ export function WorktreeDropdown({ {/* Label */} - + All Worktrees
diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-mobile-dropdown.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-mobile-dropdown.tsx index 3848461cb..0b8c5757f 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-mobile-dropdown.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-mobile-dropdown.tsx @@ -1,4 +1,3 @@ -import { useMemo } from 'react'; import { Button } from '@/components/ui/button'; import { DropdownMenu, @@ -24,6 +23,8 @@ interface WorktreeMobileDropdownProps { isAllWorktreesSelected?: boolean; /** Callback when "All Worktrees" is selected */ onSelectAllWorktrees?: () => void; + /** Pre-computed total card count across all branches (for "All Worktrees" display) */ + totalCardCount?: number; } export function WorktreeMobileDropdown({ @@ -35,6 +36,7 @@ export function WorktreeMobileDropdown({ onSelectWorktree, isAllWorktreesSelected = false, onSelectAllWorktrees, + totalCardCount = 0, }: WorktreeMobileDropdownProps) { // Find the currently selected worktree to display in the trigger const selectedWorktree = isAllWorktreesSelected @@ -44,12 +46,6 @@ export function WorktreeMobileDropdown({ ? 'All' : selectedWorktree?.branch || 'Select branch'; - // Compute total card count across all branches for "All Worktrees" display - const totalCardCount = useMemo(() => { - if (!branchCardCounts) return 0; - return Object.values(branchCardCounts).reduce((sum, count) => sum + count, 0); - }, [branchCardCounts]); - return ( @@ -96,12 +92,7 @@ export function WorktreeMobileDropdown({
)} - + All Worktrees
diff --git a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx index 65a7806d7..b7988a2fa 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx @@ -577,7 +577,12 @@ export function WorktreePanel({ branchCardCounts={branchCardCounts} onSelectWorktree={handleSelectWorktree} isAllWorktreesSelected={isAllWorktreesSelected} - onSelectAllWorktrees={useWorktreesEnabled ? handleSelectAllWorktrees : undefined} + onSelectAllWorktrees={ + useWorktreesEnabled && nonMainWorktrees.length > 0 + ? handleSelectAllWorktrees + : undefined + } + totalCardCount={totalCardCount} /> {/* Branch switch dropdown for the selected worktree (hidden when ALL is selected) */} @@ -765,7 +770,10 @@ export function WorktreePanel({ getTestSessionInfo={getTestSessionInfo} onSelectWorktree={handleSelectWorktree} isAllWorktreesSelected={isAllWorktreesSelected} - onSelectAllWorktrees={handleSelectAllWorktrees} + onSelectAllWorktrees={ + nonMainWorktrees.length > 0 ? handleSelectAllWorktrees : undefined + } + totalCardCount={totalCardCount} // Branch switching props branches={branches} filteredBranches={filteredBranches} @@ -847,8 +855,8 @@ export function WorktreePanel({ /* Standard tabs layout for 1-2 worktrees */ <>
- {/* "All" tab - shown when worktrees are enabled */} - {useWorktreesEnabled && ( + {/* "All" tab - shown when worktrees are enabled and at least 1 non-main worktree exists */} + {useWorktreesEnabled && nonMainWorktrees.length > 0 && (