diff --git a/apps/app/src/components/session-manager.tsx b/apps/app/src/components/session-manager.tsx index ce8b95a41..aa59a0558 100644 --- a/apps/app/src/components/session-manager.tsx +++ b/apps/app/src/components/session-manager.tsx @@ -1,12 +1,7 @@ "use client"; import { useState, useEffect } from "react"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { HotkeyButton } from "@/components/ui/hotkey-button"; import { Input } from "@/components/ui/input"; @@ -116,8 +111,10 @@ export function SessionManager({ new Set() ); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [sessionToDelete, setSessionToDelete] = useState(null); - const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] = useState(false); + const [sessionToDelete, setSessionToDelete] = + useState(null); + const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] = + useState(false); // Check running state for all sessions const checkRunningSessions = async (sessionList: SessionListItem[]) => { @@ -234,11 +231,7 @@ export function SessionManager({ const api = getElectronAPI(); if (!editingName.trim() || !api?.sessions) return; - const result = await api.sessions.update( - sessionId, - editingName, - undefined - ); + const result = await api.sessions.update(sessionId, editingName, undefined); if (result.success) { setEditingSessionId(null); diff --git a/apps/app/src/components/ui/branch-autocomplete.tsx b/apps/app/src/components/ui/branch-autocomplete.tsx index b2d76913b..7b2b5bd51 100644 --- a/apps/app/src/components/ui/branch-autocomplete.tsx +++ b/apps/app/src/components/ui/branch-autocomplete.tsx @@ -8,6 +8,7 @@ interface BranchAutocompleteProps { value: string; onChange: (value: string) => void; branches: string[]; + branchCardCounts?: Record; // Map of branch name to unarchived card count placeholder?: string; className?: string; disabled?: boolean; @@ -19,6 +20,7 @@ export function BranchAutocomplete({ value, onChange, branches, + branchCardCounts, placeholder = "Select a branch...", className, disabled = false, @@ -28,12 +30,22 @@ export function BranchAutocomplete({ // Always include "main" at the top of suggestions const branchOptions: AutocompleteOption[] = React.useMemo(() => { const branchSet = new Set(["main", ...branches]); - return Array.from(branchSet).map((branch) => ({ - value: branch, - label: branch, - badge: branch === "main" ? "default" : undefined, - })); - }, [branches]); + return Array.from(branchSet).map((branch) => { + const cardCount = branchCardCounts?.[branch]; + // Show card count if available, otherwise show "default" for main branch only + const badge = branchCardCounts !== undefined + ? String(cardCount ?? 0) + : branch === "main" + ? "default" + : undefined; + + return { + value: branch, + label: branch, + badge, + }; + }); + }, [branches, branchCardCounts]); return ( { + return hookFeatures.reduce((counts, feature) => { + if (feature.status !== "completed") { + const branch = feature.branchName ?? "main"; + counts[branch] = (counts[branch] || 0) + 1; + } + return counts; + }, {} as Record); + }, [hookFeatures]); + // Custom collision detection that prioritizes columns over cards const collisionDetectionStrategy = useCallback((args: any) => { // First, check if pointer is within a column @@ -302,14 +313,14 @@ export function BoardView() { }); if (matchesRemovedWorktree) { - // Reset the feature's branch assignment - persistFeatureUpdate(feature.id, { - branchName: null as unknown as string | undefined, - }); + // Reset the feature's branch assignment - update both local state and persist + const updates = { branchName: null as unknown as string | undefined }; + updateFeature(feature.id, updates); + persistFeatureUpdate(feature.id, updates); } }); }, - [hookFeatures, persistFeatureUpdate] + [hookFeatures, updateFeature, persistFeatureUpdate] ); // Get in-progress features for keyboard shortcuts (needed before actions hook) @@ -418,6 +429,18 @@ export function BoardView() { hookFeaturesRef.current = hookFeatures; }, [hookFeatures]); + // Use a ref to track running tasks to avoid effect re-runs that clear pendingFeaturesRef + const runningAutoTasksRef = useRef(runningAutoTasks); + useEffect(() => { + runningAutoTasksRef.current = runningAutoTasks; + }, [runningAutoTasks]); + + // Keep latest start handler without retriggering the auto mode effect + const handleStartImplementationRef = useRef(handleStartImplementation); + useEffect(() => { + handleStartImplementationRef.current = handleStartImplementation; + }, [handleStartImplementation]); + // Track features that are pending (started but not yet confirmed running) const pendingFeaturesRef = useRef>(new Set()); @@ -485,8 +508,9 @@ export function BoardView() { } // Count currently running tasks + pending features + // Use ref to get the latest running tasks without causing effect re-runs const currentRunning = - runningAutoTasks.length + pendingFeaturesRef.current.size; + runningAutoTasksRef.current.length + pendingFeaturesRef.current.size; const availableSlots = maxConcurrency - currentRunning; // No available slots, skip check @@ -541,6 +565,10 @@ export function BoardView() { // Start features up to available slots const featuresToStart = eligibleFeatures.slice(0, availableSlots); + const startImplementation = handleStartImplementationRef.current; + if (!startImplementation) { + return; + } for (const feature of featuresToStart) { // Check again before starting each feature @@ -566,7 +594,7 @@ export function BoardView() { } // Start the implementation - server will derive workDir from feature.branchName - const started = await handleStartImplementation(feature); + const started = await startImplementation(feature); // If successfully started, track it as pending until we receive the start event if (started) { @@ -580,7 +608,7 @@ export function BoardView() { // Check immediately, then every 3 seconds checkAndStartFeatures(); - const interval = setInterval(checkAndStartFeatures, 3000); + const interval = setInterval(checkAndStartFeatures, 1000); return () => { // Mark as inactive to prevent any pending async operations from continuing @@ -592,7 +620,8 @@ export function BoardView() { }, [ autoMode.isRunning, currentProject, - runningAutoTasks, + // runningAutoTasks is accessed via runningAutoTasksRef to prevent effect re-runs + // that would clear pendingFeaturesRef and cause concurrency issues maxConcurrency, // hookFeatures is accessed via hookFeaturesRef to prevent effect re-runs currentWorktreeBranch, @@ -601,7 +630,6 @@ export function BoardView() { isPrimaryWorktreeBranch, enableDependencyBlocking, persistFeatureUpdate, - handleStartImplementation, ]); // Use keyboard shortcuts hook (after actions hook) @@ -640,7 +668,9 @@ export function BoardView() { // Find feature for pending plan approval const pendingApprovalFeature = useMemo(() => { if (!pendingPlanApproval) return null; - return hookFeatures.find((f) => f.id === pendingPlanApproval.featureId) || null; + return ( + hookFeatures.find((f) => f.id === pendingPlanApproval.featureId) || null + ); }, [pendingPlanApproval, hookFeatures]); // Handle plan approval @@ -666,10 +696,10 @@ export function BoardView() { if (result.success) { // Immediately update local feature state to hide "Approve Plan" button // Get current feature to preserve version - const currentFeature = hookFeatures.find(f => f.id === featureId); + const currentFeature = hookFeatures.find((f) => f.id === featureId); updateFeature(featureId, { planSpec: { - status: 'approved', + status: "approved", content: editedPlan || pendingPlanApproval.planContent, version: currentFeature?.planSpec?.version || 1, approvedAt: new Date().toISOString(), @@ -688,7 +718,14 @@ export function BoardView() { setPendingPlanApproval(null); } }, - [pendingPlanApproval, currentProject, setPendingPlanApproval, updateFeature, loadFeatures, hookFeatures] + [ + pendingPlanApproval, + currentProject, + setPendingPlanApproval, + updateFeature, + loadFeatures, + hookFeatures, + ] ); // Handle plan rejection @@ -715,11 +752,11 @@ export function BoardView() { if (result.success) { // Immediately update local feature state // Get current feature to preserve version - const currentFeature = hookFeatures.find(f => f.id === featureId); + const currentFeature = hookFeatures.find((f) => f.id === featureId); updateFeature(featureId, { - status: 'backlog', + status: "backlog", planSpec: { - status: 'rejected', + status: "rejected", content: pendingPlanApproval.planContent, version: currentFeature?.planSpec?.version || 1, reviewedByUser: true, @@ -737,7 +774,14 @@ export function BoardView() { setPendingPlanApproval(null); } }, - [pendingPlanApproval, currentProject, setPendingPlanApproval, updateFeature, loadFeatures, hookFeatures] + [ + pendingPlanApproval, + currentProject, + setPendingPlanApproval, + updateFeature, + loadFeatures, + hookFeatures, + ] ); // Handle opening approval dialog from feature card button @@ -748,7 +792,7 @@ export function BoardView() { // Determine the planning mode for approval (skip should never have a plan requiring approval) const mode = feature.planningMode; const approvalMode: "lite" | "spec" | "full" = - mode === 'lite' || mode === 'spec' || mode === 'full' ? mode : 'spec'; + mode === "lite" || mode === "spec" || mode === "full" ? mode : "spec"; // Re-open the approval dialog with the feature's plan data setPendingPlanApproval({ @@ -833,6 +877,7 @@ export function BoardView() { }} onRemovedWorktrees={handleRemovedWorktrees} runningFeatureIds={runningAutoTasks} + branchCardCounts={branchCardCounts} features={hookFeatures.map((f) => ({ id: f.id, branchName: f.branchName, @@ -929,6 +974,7 @@ export function BoardView() { onAdd={handleAddFeature} categorySuggestions={categorySuggestions} branchSuggestions={branchSuggestions} + branchCardCounts={branchCardCounts} defaultSkipTests={defaultSkipTests} defaultBranch={selectedWorktreeBranch} currentBranch={currentWorktreeBranch || undefined} @@ -944,6 +990,7 @@ export function BoardView() { onUpdate={handleUpdateFeature} categorySuggestions={categorySuggestions} branchSuggestions={branchSuggestions} + branchCardCounts={branchCardCounts} currentBranch={currentWorktreeBranch || undefined} isMaximized={isMaximized} showProfilesOnly={showProfilesOnly} @@ -1065,15 +1112,24 @@ export function BoardView() { onOpenChange={setShowDeleteWorktreeDialog} projectPath={currentProject.path} worktree={selectedWorktreeForAction} + affectedFeatureCount={ + selectedWorktreeForAction + ? hookFeatures.filter( + (f) => f.branchName === selectedWorktreeForAction.branch + ).length + : 0 + } onDeleted={(deletedWorktree, _deletedBranch) => { // Reset features that were assigned to the deleted worktree (by branch) hookFeatures.forEach((feature) => { // Match by branch name since worktreePath is no longer stored if (feature.branchName === deletedWorktree.branch) { - // Reset the feature's branch assignment - persistFeatureUpdate(feature.id, { + // Reset the feature's branch assignment - update both local state and persist + const updates = { branchName: null as unknown as string | undefined, - }); + }; + updateFeature(feature.id, updates); + persistFeatureUpdate(feature.id, updates); } }); diff --git a/apps/app/src/components/views/board-view/dialogs/add-feature-dialog.tsx b/apps/app/src/components/views/board-view/dialogs/add-feature-dialog.tsx index 51dfd4bd4..388c95827 100644 --- a/apps/app/src/components/views/board-view/dialogs/add-feature-dialog.tsx +++ b/apps/app/src/components/views/board-view/dialogs/add-feature-dialog.tsx @@ -73,6 +73,7 @@ interface AddFeatureDialogProps { }) => void; categorySuggestions: string[]; branchSuggestions: string[]; + branchCardCounts?: Record; // Map of branch name to unarchived card count defaultSkipTests: boolean; defaultBranch?: string; currentBranch?: string; @@ -87,6 +88,7 @@ export function AddFeatureDialog({ onAdd, categorySuggestions, branchSuggestions, + branchCardCounts, defaultSkipTests, defaultBranch = "main", currentBranch, @@ -115,11 +117,16 @@ export function AddFeatureDialog({ const [enhancementMode, setEnhancementMode] = useState< "improve" | "technical" | "simplify" | "acceptance" >("improve"); - const [planningMode, setPlanningMode] = useState('skip'); + const [planningMode, setPlanningMode] = useState("skip"); const [requirePlanApproval, setRequirePlanApproval] = useState(false); // Get enhancement model, planning mode defaults, and worktrees setting from store - const { enhancementModel, defaultPlanningMode, defaultRequirePlanApproval, useWorktrees } = useAppStore(); + const { + enhancementModel, + defaultPlanningMode, + defaultRequirePlanApproval, + useWorktrees, + } = useAppStore(); // Sync defaults when dialog opens useEffect(() => { @@ -133,7 +140,13 @@ export function AddFeatureDialog({ setPlanningMode(defaultPlanningMode); setRequirePlanApproval(defaultRequirePlanApproval); } - }, [open, defaultSkipTests, defaultBranch, defaultPlanningMode, defaultRequirePlanApproval]); + }, [ + open, + defaultSkipTests, + defaultBranch, + defaultPlanningMode, + defaultRequirePlanApproval, + ]); const handleAdd = () => { if (!newFeature.description.trim()) { @@ -157,7 +170,7 @@ export function AddFeatureDialog({ // If currentBranch is provided (non-primary worktree), use it // Otherwise (primary worktree), use empty string which means "unassigned" (show only on primary) const finalBranchName = useCurrentBranch - ? (currentBranch || "") + ? currentBranch || "" : newFeature.branchName || ""; onAdd({ @@ -398,6 +411,7 @@ export function AddFeatureDialog({ setNewFeature({ ...newFeature, branchName: value }) } branchSuggestions={branchSuggestions} + branchCardCounts={branchCardCounts} currentBranch={currentBranch} testIdPrefix="feature" /> @@ -480,7 +494,10 @@ export function AddFeatureDialog({ {/* Options Tab */} - + {/* Planning Mode Section */} Add Feature diff --git a/apps/app/src/components/views/board-view/dialogs/delete-worktree-dialog.tsx b/apps/app/src/components/views/board-view/dialogs/delete-worktree-dialog.tsx index 0e228ab2c..203de8191 100644 --- a/apps/app/src/components/views/board-view/dialogs/delete-worktree-dialog.tsx +++ b/apps/app/src/components/views/board-view/dialogs/delete-worktree-dialog.tsx @@ -12,7 +12,7 @@ import { import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; -import { Loader2, Trash2, AlertTriangle } from "lucide-react"; +import { Loader2, Trash2, AlertTriangle, FileWarning } from "lucide-react"; import { getElectronAPI } from "@/lib/electron"; import { toast } from "sonner"; @@ -30,6 +30,8 @@ interface DeleteWorktreeDialogProps { projectPath: string; worktree: WorktreeInfo | null; onDeleted: (deletedWorktree: WorktreeInfo, deletedBranch: boolean) => void; + /** Number of features assigned to this worktree's branch */ + affectedFeatureCount?: number; } export function DeleteWorktreeDialog({ @@ -38,6 +40,7 @@ export function DeleteWorktreeDialog({ projectPath, worktree, onDeleted, + affectedFeatureCount = 0, }: DeleteWorktreeDialogProps) { const [deleteBranch, setDeleteBranch] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -100,6 +103,18 @@ export function DeleteWorktreeDialog({ ? + {affectedFeatureCount > 0 && ( +
+ + + {affectedFeatureCount} feature{affectedFeatureCount !== 1 ? "s" : ""}{" "} + {affectedFeatureCount !== 1 ? "are" : "is"} assigned to this + branch. {affectedFeatureCount !== 1 ? "They" : "It"} will be + unassigned and moved to the main worktree. + +
+ )} + {worktree.hasChanges && (
diff --git a/apps/app/src/components/views/board-view/dialogs/edit-feature-dialog.tsx b/apps/app/src/components/views/board-view/dialogs/edit-feature-dialog.tsx index 56d2757b2..6e63bb35c 100644 --- a/apps/app/src/components/views/board-view/dialogs/edit-feature-dialog.tsx +++ b/apps/app/src/components/views/board-view/dialogs/edit-feature-dialog.tsx @@ -77,6 +77,7 @@ interface EditFeatureDialogProps { ) => void; categorySuggestions: string[]; branchSuggestions: string[]; + branchCardCounts?: Record; // Map of branch name to unarchived card count currentBranch?: string; isMaximized: boolean; showProfilesOnly: boolean; @@ -90,6 +91,7 @@ export function EditFeatureDialog({ onUpdate, categorySuggestions, branchSuggestions, + branchCardCounts, currentBranch, isMaximized, showProfilesOnly, @@ -389,6 +391,7 @@ export function EditFeatureDialog({ }) } branchSuggestions={branchSuggestions} + branchCardCounts={branchCardCounts} currentBranch={currentBranch} disabled={editingFeature.status !== "backlog"} testIdPrefix="edit-feature" diff --git a/apps/app/src/components/views/board-view/hooks/use-board-actions.ts b/apps/app/src/components/views/board-view/hooks/use-board-actions.ts index 9deb8a40c..8370d96f8 100644 --- a/apps/app/src/components/views/board-view/hooks/use-board-actions.ts +++ b/apps/app/src/components/views/board-view/hooks/use-board-actions.ts @@ -82,8 +82,8 @@ export function useBoardActions({ } = useAppStore(); const autoMode = useAutoMode(); - // Note: getOrCreateWorktreeForFeature removed - worktrees are now created server-side - // at execution time based on feature.branchName + // Worktrees are created when adding/editing features with a branch name + // This ensures the worktree exists before the feature starts execution const handleAddFeature = useCallback( async (featureData: { @@ -100,24 +100,58 @@ export function useBoardActions({ planningMode: PlanningMode; requirePlanApproval: boolean; }) => { - // Simplified: Only store branchName, no worktree creation on add - // Worktrees are created at execution time (when feature starts) // Empty string means "unassigned" (show only on primary worktree) - convert to undefined // Non-empty string is the actual branch name (for non-primary worktrees) const finalBranchName = featureData.branchName || undefined; + // If worktrees enabled and a branch is specified, create the worktree now + // This ensures the worktree exists before the feature starts + if (useWorktrees && finalBranchName && currentProject) { + try { + const api = getElectronAPI(); + if (api?.worktree?.create) { + const result = await api.worktree.create( + currentProject.path, + finalBranchName + ); + if (result.success) { + console.log( + `[Board] Worktree for branch "${finalBranchName}" ${ + result.worktree?.isNew ? "created" : "already exists" + }` + ); + // Refresh worktree list in UI + onWorktreeCreated?.(); + } else { + console.error( + `[Board] Failed to create worktree for branch "${finalBranchName}":`, + result.error + ); + toast.error("Failed to create worktree", { + description: result.error || "An error occurred", + }); + } + } + } catch (error) { + console.error("[Board] Error creating worktree:", error); + toast.error("Failed to create worktree", { + description: + error instanceof Error ? error.message : "An error occurred", + }); + } + } + const newFeatureData = { ...featureData, status: "backlog" as const, branchName: finalBranchName, - // No worktreePath - derived at runtime from branchName }; const createdFeature = addFeature(newFeatureData); // Must await to ensure feature exists on server before user can drag it await persistFeatureCreate(createdFeature); saveCategory(featureData.category); }, - [addFeature, persistFeatureCreate, saveCategory] + [addFeature, persistFeatureCreate, saveCategory, useWorktrees, currentProject, onWorktreeCreated] ); const handleUpdateFeature = useCallback( @@ -139,6 +173,43 @@ export function useBoardActions({ ) => { const finalBranchName = updates.branchName || undefined; + // If worktrees enabled and a branch is specified, create the worktree now + // This ensures the worktree exists before the feature starts + if (useWorktrees && finalBranchName && currentProject) { + try { + const api = getElectronAPI(); + if (api?.worktree?.create) { + const result = await api.worktree.create( + currentProject.path, + finalBranchName + ); + if (result.success) { + console.log( + `[Board] Worktree for branch "${finalBranchName}" ${ + result.worktree?.isNew ? "created" : "already exists" + }` + ); + // Refresh worktree list in UI + onWorktreeCreated?.(); + } else { + console.error( + `[Board] Failed to create worktree for branch "${finalBranchName}":`, + result.error + ); + toast.error("Failed to create worktree", { + description: result.error || "An error occurred", + }); + } + } + } catch (error) { + console.error("[Board] Error creating worktree:", error); + toast.error("Failed to create worktree", { + description: + error instanceof Error ? error.message : "An error occurred", + }); + } + } + const finalUpdates = { ...updates, branchName: finalBranchName, @@ -151,7 +222,7 @@ export function useBoardActions({ } setEditingFeature(null); }, - [updateFeature, persistFeatureUpdate, saveCategory, setEditingFeature] + [updateFeature, persistFeatureUpdate, saveCategory, setEditingFeature, useWorktrees, currentProject, onWorktreeCreated] ); const handleDeleteFeature = useCallback( diff --git a/apps/app/src/components/views/board-view/shared/branch-selector.tsx b/apps/app/src/components/views/board-view/shared/branch-selector.tsx index a395edf5b..0ba0848b5 100644 --- a/apps/app/src/components/views/board-view/shared/branch-selector.tsx +++ b/apps/app/src/components/views/board-view/shared/branch-selector.tsx @@ -10,6 +10,7 @@ interface BranchSelectorProps { branchName: string; onBranchNameChange: (branchName: string) => void; branchSuggestions: string[]; + branchCardCounts?: Record; // Map of branch name to unarchived card count currentBranch?: string; disabled?: boolean; testIdPrefix?: string; @@ -21,6 +22,7 @@ export function BranchSelector({ branchName, onBranchNameChange, branchSuggestions, + branchCardCounts, currentBranch, disabled = false, testIdPrefix = "branch", @@ -69,6 +71,7 @@ export function BranchSelector({ value={branchName} onChange={onBranchNameChange} branches={branchSuggestions} + branchCardCounts={branchCardCounts} placeholder="Select or create branch..." data-testid={`${testIdPrefix}-input`} disabled={disabled} diff --git a/apps/app/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx b/apps/app/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx index 8332e71d8..4f130db75 100644 --- a/apps/app/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx +++ b/apps/app/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx @@ -9,6 +9,7 @@ import { WorktreeActionsDropdown } from "./worktree-actions-dropdown"; interface WorktreeTabProps { worktree: WorktreeInfo; + cardCount?: number; // Number of unarchived cards for this branch isSelected: boolean; isRunning: boolean; isActivating: boolean; @@ -44,6 +45,7 @@ interface WorktreeTabProps { export function WorktreeTab({ worktree, + cardCount, isSelected, isRunning, isActivating, @@ -97,9 +99,9 @@ export function WorktreeTab({ )} {worktree.branch} - {worktree.hasChanges && ( + {cardCount !== undefined && cardCount > 0 && ( - {worktree.changedFilesCount} + {cardCount} )} @@ -140,9 +142,9 @@ export function WorktreeTab({ )} {worktree.branch} - {worktree.hasChanges && ( + {cardCount !== undefined && cardCount > 0 && ( - {worktree.changedFilesCount} + {cardCount} )} diff --git a/apps/app/src/components/views/board-view/worktree-panel/types.ts b/apps/app/src/components/views/board-view/worktree-panel/types.ts index e143ae733..c1beaf5fa 100644 --- a/apps/app/src/components/views/board-view/worktree-panel/types.ts +++ b/apps/app/src/components/views/board-view/worktree-panel/types.ts @@ -35,5 +35,6 @@ export interface WorktreePanelProps { onRemovedWorktrees?: (removedWorktrees: Array<{ path: string; branch: string }>) => void; runningFeatureIds?: string[]; features?: FeatureInfo[]; + branchCardCounts?: Record; // Map of branch name to unarchived card count refreshTrigger?: number; } diff --git a/apps/app/src/components/views/board-view/worktree-panel/worktree-panel.tsx b/apps/app/src/components/views/board-view/worktree-panel/worktree-panel.tsx index ddd27892f..910b87935 100644 --- a/apps/app/src/components/views/board-view/worktree-panel/worktree-panel.tsx +++ b/apps/app/src/components/views/board-view/worktree-panel/worktree-panel.tsx @@ -24,6 +24,7 @@ export function WorktreePanel({ onRemovedWorktrees, runningFeatureIds = [], features = [], + branchCardCounts, refreshTrigger = 0, }: WorktreePanelProps) { const { @@ -110,43 +111,47 @@ export function WorktreePanel({ Branch:
- {worktrees.map((worktree) => ( - - ))} + {worktrees.map((worktree) => { + const cardCount = branchCardCounts?.[worktree.branch]; + return ( + + ); + })}