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;
onChange: (value: string) => void;
branches: string[];
branchCardCounts?: Record<string, number>; // 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 = cardCount !== undefined
? String(cardCount)
: branch === "main"
? "default"
: undefined;
return {
value: branch,
label: branch,
badge,
};
});
}, [branches, branchCardCounts]);
return (
<Autocomplete

View File

@@ -270,6 +270,21 @@ export function BoardView() {
fetchBranches();
}, [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
const collisionDetectionStrategy = useCallback((args: any) => {
// First, check if pointer is within a column
@@ -833,6 +848,7 @@ export function BoardView() {
}}
onRemovedWorktrees={handleRemovedWorktrees}
runningFeatureIds={runningAutoTasks}
branchCardCounts={branchCardCounts}
features={hookFeatures.map((f) => ({
id: f.id,
branchName: f.branchName,
@@ -929,6 +945,7 @@ export function BoardView() {
onAdd={handleAddFeature}
categorySuggestions={categorySuggestions}
branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
defaultSkipTests={defaultSkipTests}
defaultBranch={selectedWorktreeBranch}
currentBranch={currentWorktreeBranch || undefined}
@@ -944,6 +961,7 @@ export function BoardView() {
onUpdate={handleUpdateFeature}
categorySuggestions={categorySuggestions}
branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
currentBranch={currentWorktreeBranch || undefined}
isMaximized={isMaximized}
showProfilesOnly={showProfilesOnly}

View File

@@ -73,6 +73,7 @@ interface AddFeatureDialogProps {
}) => void;
categorySuggestions: string[];
branchSuggestions: string[];
branchCardCounts?: Record<string, number>; // 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<PlanningMode>('skip');
const [planningMode, setPlanningMode] = useState<PlanningMode>("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({
</TabsContent>
{/* 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 */}
<PlanningModeSelector
mode={planningMode}
@@ -515,9 +532,7 @@ export function AddFeatureDialog({
hotkeyActive={open}
data-testid="confirm-add-feature"
disabled={
useWorktrees &&
!useCurrentBranch &&
!newFeature.branchName.trim()
useWorktrees && !useCurrentBranch && !newFeature.branchName.trim()
}
>
Add Feature

View File

@@ -77,6 +77,7 @@ interface EditFeatureDialogProps {
) => void;
categorySuggestions: string[];
branchSuggestions: string[];
branchCardCounts?: Record<string, number>; // 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"

View File

@@ -10,6 +10,7 @@ interface BranchSelectorProps {
branchName: string;
onBranchNameChange: (branchName: string) => void;
branchSuggestions: string[];
branchCardCounts?: Record<string, number>; // 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}

View File

@@ -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({
<RefreshCw className="w-3 h-3 animate-spin" />
)}
{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">
{worktree.changedFilesCount}
{cardCount}
</span>
)}
</Button>
@@ -140,9 +142,9 @@ export function WorktreeTab({
<RefreshCw className="w-3 h-3 animate-spin" />
)}
{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">
{worktree.changedFilesCount}
{cardCount}
</span>
)}
</Button>

View File

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

View File

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