feat: add branch card counts to UI components

- Introduced branchCardCounts prop to various components to display unarchived card counts per branch.
- Updated BranchAutocomplete, BoardView, AddFeatureDialog, EditFeatureDialog, BranchSelector, WorktreePanel, and WorktreeTab to utilize the new prop for enhanced branch management visibility.
- Enhanced user experience by showing card counts alongside branch names in relevant UI elements.
This commit is contained in:
Cody Seibert
2025-12-18 17:34:37 -05:00
parent a0efa5d351
commit f0bea76141
8 changed files with 114 additions and 55 deletions

View File

@@ -8,6 +8,7 @@ interface BranchAutocompleteProps {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
branches: string[]; branches: string[];
branchCardCounts?: Record<string, number>; // Map of branch name to unarchived card count
placeholder?: string; placeholder?: string;
className?: string; className?: string;
disabled?: boolean; disabled?: boolean;
@@ -19,6 +20,7 @@ export function BranchAutocomplete({
value, value,
onChange, onChange,
branches, branches,
branchCardCounts,
placeholder = "Select a branch...", placeholder = "Select a branch...",
className, className,
disabled = false, disabled = false,
@@ -28,12 +30,22 @@ export function BranchAutocomplete({
// Always include "main" at the top of suggestions // Always include "main" at the top of suggestions
const branchOptions: AutocompleteOption[] = React.useMemo(() => { const branchOptions: AutocompleteOption[] = React.useMemo(() => {
const branchSet = new Set(["main", ...branches]); const branchSet = new Set(["main", ...branches]);
return Array.from(branchSet).map((branch) => ({ return Array.from(branchSet).map((branch) => {
value: branch, const cardCount = branchCardCounts?.[branch];
label: branch, // Show card count if available, otherwise show "default" for main branch only
badge: branch === "main" ? "default" : undefined, const badge = cardCount !== undefined
})); ? String(cardCount)
}, [branches]); : branch === "main"
? "default"
: undefined;
return {
value: branch,
label: branch,
badge,
};
});
}, [branches, branchCardCounts]);
return ( return (
<Autocomplete <Autocomplete

View File

@@ -270,6 +270,21 @@ export function BoardView() {
fetchBranches(); fetchBranches();
}, [currentProject, worktreeRefreshKey]); }, [currentProject, worktreeRefreshKey]);
// Calculate unarchived card counts per branch
const branchCardCounts = useMemo(() => {
const counts: Record<string, number> = {};
// Count unarchived features (status !== "completed") per branch
hookFeatures.forEach((feature) => {
if (feature.status !== "completed") {
const branch = feature.branchName || "main";
counts[branch] = (counts[branch] || 0) + 1;
}
});
return counts;
}, [hookFeatures]);
// Custom collision detection that prioritizes columns over cards // Custom collision detection that prioritizes columns over cards
const collisionDetectionStrategy = useCallback((args: any) => { const collisionDetectionStrategy = useCallback((args: any) => {
// First, check if pointer is within a column // First, check if pointer is within a column
@@ -833,6 +848,7 @@ export function BoardView() {
}} }}
onRemovedWorktrees={handleRemovedWorktrees} onRemovedWorktrees={handleRemovedWorktrees}
runningFeatureIds={runningAutoTasks} runningFeatureIds={runningAutoTasks}
branchCardCounts={branchCardCounts}
features={hookFeatures.map((f) => ({ features={hookFeatures.map((f) => ({
id: f.id, id: f.id,
branchName: f.branchName, branchName: f.branchName,
@@ -929,6 +945,7 @@ export function BoardView() {
onAdd={handleAddFeature} onAdd={handleAddFeature}
categorySuggestions={categorySuggestions} categorySuggestions={categorySuggestions}
branchSuggestions={branchSuggestions} branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
defaultSkipTests={defaultSkipTests} defaultSkipTests={defaultSkipTests}
defaultBranch={selectedWorktreeBranch} defaultBranch={selectedWorktreeBranch}
currentBranch={currentWorktreeBranch || undefined} currentBranch={currentWorktreeBranch || undefined}
@@ -944,6 +961,7 @@ export function BoardView() {
onUpdate={handleUpdateFeature} onUpdate={handleUpdateFeature}
categorySuggestions={categorySuggestions} categorySuggestions={categorySuggestions}
branchSuggestions={branchSuggestions} branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
currentBranch={currentWorktreeBranch || undefined} currentBranch={currentWorktreeBranch || undefined}
isMaximized={isMaximized} isMaximized={isMaximized}
showProfilesOnly={showProfilesOnly} showProfilesOnly={showProfilesOnly}

View File

@@ -73,6 +73,7 @@ interface AddFeatureDialogProps {
}) => void; }) => void;
categorySuggestions: string[]; categorySuggestions: string[];
branchSuggestions: string[]; branchSuggestions: string[];
branchCardCounts?: Record<string, number>; // Map of branch name to unarchived card count
defaultSkipTests: boolean; defaultSkipTests: boolean;
defaultBranch?: string; defaultBranch?: string;
currentBranch?: string; currentBranch?: string;
@@ -87,6 +88,7 @@ export function AddFeatureDialog({
onAdd, onAdd,
categorySuggestions, categorySuggestions,
branchSuggestions, branchSuggestions,
branchCardCounts,
defaultSkipTests, defaultSkipTests,
defaultBranch = "main", defaultBranch = "main",
currentBranch, currentBranch,
@@ -115,11 +117,16 @@ export function AddFeatureDialog({
const [enhancementMode, setEnhancementMode] = useState< const [enhancementMode, setEnhancementMode] = useState<
"improve" | "technical" | "simplify" | "acceptance" "improve" | "technical" | "simplify" | "acceptance"
>("improve"); >("improve");
const [planningMode, setPlanningMode] = useState<PlanningMode>('skip'); const [planningMode, setPlanningMode] = useState<PlanningMode>("skip");
const [requirePlanApproval, setRequirePlanApproval] = useState(false); const [requirePlanApproval, setRequirePlanApproval] = useState(false);
// Get enhancement model, planning mode defaults, and worktrees setting from store // 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 // Sync defaults when dialog opens
useEffect(() => { useEffect(() => {
@@ -133,7 +140,13 @@ export function AddFeatureDialog({
setPlanningMode(defaultPlanningMode); setPlanningMode(defaultPlanningMode);
setRequirePlanApproval(defaultRequirePlanApproval); setRequirePlanApproval(defaultRequirePlanApproval);
} }
}, [open, defaultSkipTests, defaultBranch, defaultPlanningMode, defaultRequirePlanApproval]); }, [
open,
defaultSkipTests,
defaultBranch,
defaultPlanningMode,
defaultRequirePlanApproval,
]);
const handleAdd = () => { const handleAdd = () => {
if (!newFeature.description.trim()) { if (!newFeature.description.trim()) {
@@ -157,7 +170,7 @@ export function AddFeatureDialog({
// If currentBranch is provided (non-primary worktree), use it // If currentBranch is provided (non-primary worktree), use it
// Otherwise (primary worktree), use empty string which means "unassigned" (show only on primary) // Otherwise (primary worktree), use empty string which means "unassigned" (show only on primary)
const finalBranchName = useCurrentBranch const finalBranchName = useCurrentBranch
? (currentBranch || "") ? currentBranch || ""
: newFeature.branchName || ""; : newFeature.branchName || "";
onAdd({ onAdd({
@@ -398,6 +411,7 @@ export function AddFeatureDialog({
setNewFeature({ ...newFeature, branchName: value }) setNewFeature({ ...newFeature, branchName: value })
} }
branchSuggestions={branchSuggestions} branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
currentBranch={currentBranch} currentBranch={currentBranch}
testIdPrefix="feature" testIdPrefix="feature"
/> />
@@ -480,7 +494,10 @@ export function AddFeatureDialog({
</TabsContent> </TabsContent>
{/* Options Tab */} {/* Options Tab */}
<TabsContent value="options" className="space-y-4 overflow-y-auto cursor-default"> <TabsContent
value="options"
className="space-y-4 overflow-y-auto cursor-default"
>
{/* Planning Mode Section */} {/* Planning Mode Section */}
<PlanningModeSelector <PlanningModeSelector
mode={planningMode} mode={planningMode}
@@ -515,9 +532,7 @@ export function AddFeatureDialog({
hotkeyActive={open} hotkeyActive={open}
data-testid="confirm-add-feature" data-testid="confirm-add-feature"
disabled={ disabled={
useWorktrees && useWorktrees && !useCurrentBranch && !newFeature.branchName.trim()
!useCurrentBranch &&
!newFeature.branchName.trim()
} }
> >
Add Feature Add Feature

View File

@@ -77,6 +77,7 @@ interface EditFeatureDialogProps {
) => void; ) => void;
categorySuggestions: string[]; categorySuggestions: string[];
branchSuggestions: string[]; branchSuggestions: string[];
branchCardCounts?: Record<string, number>; // Map of branch name to unarchived card count
currentBranch?: string; currentBranch?: string;
isMaximized: boolean; isMaximized: boolean;
showProfilesOnly: boolean; showProfilesOnly: boolean;
@@ -90,6 +91,7 @@ export function EditFeatureDialog({
onUpdate, onUpdate,
categorySuggestions, categorySuggestions,
branchSuggestions, branchSuggestions,
branchCardCounts,
currentBranch, currentBranch,
isMaximized, isMaximized,
showProfilesOnly, showProfilesOnly,
@@ -389,6 +391,7 @@ export function EditFeatureDialog({
}) })
} }
branchSuggestions={branchSuggestions} branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
currentBranch={currentBranch} currentBranch={currentBranch}
disabled={editingFeature.status !== "backlog"} disabled={editingFeature.status !== "backlog"}
testIdPrefix="edit-feature" testIdPrefix="edit-feature"

View File

@@ -10,6 +10,7 @@ interface BranchSelectorProps {
branchName: string; branchName: string;
onBranchNameChange: (branchName: string) => void; onBranchNameChange: (branchName: string) => void;
branchSuggestions: string[]; branchSuggestions: string[];
branchCardCounts?: Record<string, number>; // Map of branch name to unarchived card count
currentBranch?: string; currentBranch?: string;
disabled?: boolean; disabled?: boolean;
testIdPrefix?: string; testIdPrefix?: string;
@@ -21,6 +22,7 @@ export function BranchSelector({
branchName, branchName,
onBranchNameChange, onBranchNameChange,
branchSuggestions, branchSuggestions,
branchCardCounts,
currentBranch, currentBranch,
disabled = false, disabled = false,
testIdPrefix = "branch", testIdPrefix = "branch",
@@ -69,6 +71,7 @@ export function BranchSelector({
value={branchName} value={branchName}
onChange={onBranchNameChange} onChange={onBranchNameChange}
branches={branchSuggestions} branches={branchSuggestions}
branchCardCounts={branchCardCounts}
placeholder="Select or create branch..." placeholder="Select or create branch..."
data-testid={`${testIdPrefix}-input`} data-testid={`${testIdPrefix}-input`}
disabled={disabled} disabled={disabled}

View File

@@ -9,6 +9,7 @@ import { WorktreeActionsDropdown } from "./worktree-actions-dropdown";
interface WorktreeTabProps { interface WorktreeTabProps {
worktree: WorktreeInfo; worktree: WorktreeInfo;
cardCount?: number; // Number of unarchived cards for this branch
isSelected: boolean; isSelected: boolean;
isRunning: boolean; isRunning: boolean;
isActivating: boolean; isActivating: boolean;
@@ -44,6 +45,7 @@ interface WorktreeTabProps {
export function WorktreeTab({ export function WorktreeTab({
worktree, worktree,
cardCount,
isSelected, isSelected,
isRunning, isRunning,
isActivating, isActivating,
@@ -97,9 +99,9 @@ export function WorktreeTab({
<RefreshCw className="w-3 h-3 animate-spin" /> <RefreshCw className="w-3 h-3 animate-spin" />
)} )}
{worktree.branch} {worktree.branch}
{worktree.hasChanges && ( {cardCount !== undefined && cardCount > 0 && (
<span className="inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded bg-background/80 text-foreground border border-border"> <span className="inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded bg-background/80 text-foreground border border-border">
{worktree.changedFilesCount} {cardCount}
</span> </span>
)} )}
</Button> </Button>
@@ -140,9 +142,9 @@ export function WorktreeTab({
<RefreshCw className="w-3 h-3 animate-spin" /> <RefreshCw className="w-3 h-3 animate-spin" />
)} )}
{worktree.branch} {worktree.branch}
{worktree.hasChanges && ( {cardCount !== undefined && cardCount > 0 && (
<span className="inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded bg-background/80 text-foreground border border-border"> <span className="inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded bg-background/80 text-foreground border border-border">
{worktree.changedFilesCount} {cardCount}
</span> </span>
)} )}
</Button> </Button>

View File

@@ -35,5 +35,6 @@ export interface WorktreePanelProps {
onRemovedWorktrees?: (removedWorktrees: Array<{ path: string; branch: string }>) => void; onRemovedWorktrees?: (removedWorktrees: Array<{ path: string; branch: string }>) => void;
runningFeatureIds?: string[]; runningFeatureIds?: string[];
features?: FeatureInfo[]; features?: FeatureInfo[];
branchCardCounts?: Record<string, number>; // Map of branch name to unarchived card count
refreshTrigger?: number; refreshTrigger?: number;
} }

View File

@@ -24,6 +24,7 @@ export function WorktreePanel({
onRemovedWorktrees, onRemovedWorktrees,
runningFeatureIds = [], runningFeatureIds = [],
features = [], features = [],
branchCardCounts,
refreshTrigger = 0, refreshTrigger = 0,
}: WorktreePanelProps) { }: WorktreePanelProps) {
const { const {
@@ -110,43 +111,47 @@ export function WorktreePanel({
<span className="text-sm text-muted-foreground mr-2">Branch:</span> <span className="text-sm text-muted-foreground mr-2">Branch:</span>
<div className="flex items-center gap-1 flex-wrap"> <div className="flex items-center gap-1 flex-wrap">
{worktrees.map((worktree) => ( {worktrees.map((worktree) => {
<WorktreeTab const cardCount = branchCardCounts?.[worktree.branch];
key={worktree.path} return (
worktree={worktree} <WorktreeTab
isSelected={isWorktreeSelected(worktree)} key={worktree.path}
isRunning={hasRunningFeatures(worktree)} worktree={worktree}
isActivating={isActivating} cardCount={cardCount}
isDevServerRunning={isDevServerRunning(worktree)} isSelected={isWorktreeSelected(worktree)}
devServerInfo={getDevServerInfo(worktree)} isRunning={hasRunningFeatures(worktree)}
defaultEditorName={defaultEditorName} isActivating={isActivating}
branches={branches} isDevServerRunning={isDevServerRunning(worktree)}
filteredBranches={filteredBranches} devServerInfo={getDevServerInfo(worktree)}
branchFilter={branchFilter} defaultEditorName={defaultEditorName}
isLoadingBranches={isLoadingBranches} branches={branches}
isSwitching={isSwitching} filteredBranches={filteredBranches}
isPulling={isPulling} branchFilter={branchFilter}
isPushing={isPushing} isLoadingBranches={isLoadingBranches}
isStartingDevServer={isStartingDevServer} isSwitching={isSwitching}
aheadCount={aheadCount} isPulling={isPulling}
behindCount={behindCount} isPushing={isPushing}
onSelectWorktree={handleSelectWorktree} isStartingDevServer={isStartingDevServer}
onBranchDropdownOpenChange={handleBranchDropdownOpenChange(worktree)} aheadCount={aheadCount}
onActionsDropdownOpenChange={handleActionsDropdownOpenChange(worktree)} behindCount={behindCount}
onBranchFilterChange={setBranchFilter} onSelectWorktree={handleSelectWorktree}
onSwitchBranch={handleSwitchBranch} onBranchDropdownOpenChange={handleBranchDropdownOpenChange(worktree)}
onCreateBranch={onCreateBranch} onActionsDropdownOpenChange={handleActionsDropdownOpenChange(worktree)}
onPull={handlePull} onBranchFilterChange={setBranchFilter}
onPush={handlePush} onSwitchBranch={handleSwitchBranch}
onOpenInEditor={handleOpenInEditor} onCreateBranch={onCreateBranch}
onCommit={onCommit} onPull={handlePull}
onCreatePR={onCreatePR} onPush={handlePush}
onDeleteWorktree={onDeleteWorktree} onOpenInEditor={handleOpenInEditor}
onStartDevServer={handleStartDevServer} onCommit={onCommit}
onStopDevServer={handleStopDevServer} onCreatePR={onCreatePR}
onOpenDevServerUrl={handleOpenDevServerUrl} onDeleteWorktree={onDeleteWorktree}
/> onStartDevServer={handleStartDevServer}
))} onStopDevServer={handleStopDevServer}
onOpenDevServerUrl={handleOpenDevServerUrl}
/>
);
})}
<Button <Button
variant="ghost" variant="ghost"