Skip to content

Commit 46c3dd2

Browse files
authored
Merge pull request #168 from AutoMaker-Org/feature/cards-in-worktrees
feat: add branch card counts to UI components
2 parents 6c31f72 + 18a2ed2 commit 46c3dd2

17 files changed

Lines changed: 442 additions & 189 deletions

File tree

apps/app/src/components/session-manager.tsx

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
"use client";
22

33
import { useState, useEffect } from "react";
4-
import {
5-
Card,
6-
CardContent,
7-
CardHeader,
8-
CardTitle,
9-
} from "@/components/ui/card";
4+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
105
import { Button } from "@/components/ui/button";
116
import { HotkeyButton } from "@/components/ui/hotkey-button";
127
import { Input } from "@/components/ui/input";
@@ -116,8 +111,10 @@ export function SessionManager({
116111
new Set()
117112
);
118113
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
119-
const [sessionToDelete, setSessionToDelete] = useState<SessionListItem | null>(null);
120-
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] = useState(false);
114+
const [sessionToDelete, setSessionToDelete] =
115+
useState<SessionListItem | null>(null);
116+
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] =
117+
useState(false);
121118

122119
// Check running state for all sessions
123120
const checkRunningSessions = async (sessionList: SessionListItem[]) => {
@@ -234,11 +231,7 @@ export function SessionManager({
234231
const api = getElectronAPI();
235232
if (!editingName.trim() || !api?.sessions) return;
236233

237-
const result = await api.sessions.update(
238-
sessionId,
239-
editingName,
240-
undefined
241-
);
234+
const result = await api.sessions.update(sessionId, editingName, undefined);
242235

243236
if (result.success) {
244237
setEditingSessionId(null);

apps/app/src/components/ui/branch-autocomplete.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface BranchAutocompleteProps {
88
value: string;
99
onChange: (value: string) => void;
1010
branches: string[];
11+
branchCardCounts?: Record<string, number>; // Map of branch name to unarchived card count
1112
placeholder?: string;
1213
className?: string;
1314
disabled?: boolean;
@@ -19,6 +20,7 @@ export function BranchAutocomplete({
1920
value,
2021
onChange,
2122
branches,
23+
branchCardCounts,
2224
placeholder = "Select a branch...",
2325
className,
2426
disabled = false,
@@ -28,12 +30,22 @@ export function BranchAutocomplete({
2830
// Always include "main" at the top of suggestions
2931
const branchOptions: AutocompleteOption[] = React.useMemo(() => {
3032
const branchSet = new Set(["main", ...branches]);
31-
return Array.from(branchSet).map((branch) => ({
32-
value: branch,
33-
label: branch,
34-
badge: branch === "main" ? "default" : undefined,
35-
}));
36-
}, [branches]);
33+
return Array.from(branchSet).map((branch) => {
34+
const cardCount = branchCardCounts?.[branch];
35+
// Show card count if available, otherwise show "default" for main branch only
36+
const badge = branchCardCounts !== undefined
37+
? String(cardCount ?? 0)
38+
: branch === "main"
39+
? "default"
40+
: undefined;
41+
42+
return {
43+
value: branch,
44+
label: branch,
45+
badge,
46+
};
47+
});
48+
}, [branches, branchCardCounts]);
3749

3850
return (
3951
<Autocomplete

apps/app/src/components/views/board-view.tsx

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,17 @@ export function BoardView() {
270270
fetchBranches();
271271
}, [currentProject, worktreeRefreshKey]);
272272

273+
// Calculate unarchived card counts per branch
274+
const branchCardCounts = useMemo(() => {
275+
return hookFeatures.reduce((counts, feature) => {
276+
if (feature.status !== "completed") {
277+
const branch = feature.branchName ?? "main";
278+
counts[branch] = (counts[branch] || 0) + 1;
279+
}
280+
return counts;
281+
}, {} as Record<string, number>);
282+
}, [hookFeatures]);
283+
273284
// Custom collision detection that prioritizes columns over cards
274285
const collisionDetectionStrategy = useCallback((args: any) => {
275286
// First, check if pointer is within a column
@@ -302,14 +313,14 @@ export function BoardView() {
302313
});
303314

304315
if (matchesRemovedWorktree) {
305-
// Reset the feature's branch assignment
306-
persistFeatureUpdate(feature.id, {
307-
branchName: null as unknown as string | undefined,
308-
});
316+
// Reset the feature's branch assignment - update both local state and persist
317+
const updates = { branchName: null as unknown as string | undefined };
318+
updateFeature(feature.id, updates);
319+
persistFeatureUpdate(feature.id, updates);
309320
}
310321
});
311322
},
312-
[hookFeatures, persistFeatureUpdate]
323+
[hookFeatures, updateFeature, persistFeatureUpdate]
313324
);
314325

315326
// Get in-progress features for keyboard shortcuts (needed before actions hook)
@@ -418,6 +429,18 @@ export function BoardView() {
418429
hookFeaturesRef.current = hookFeatures;
419430
}, [hookFeatures]);
420431

432+
// Use a ref to track running tasks to avoid effect re-runs that clear pendingFeaturesRef
433+
const runningAutoTasksRef = useRef(runningAutoTasks);
434+
useEffect(() => {
435+
runningAutoTasksRef.current = runningAutoTasks;
436+
}, [runningAutoTasks]);
437+
438+
// Keep latest start handler without retriggering the auto mode effect
439+
const handleStartImplementationRef = useRef(handleStartImplementation);
440+
useEffect(() => {
441+
handleStartImplementationRef.current = handleStartImplementation;
442+
}, [handleStartImplementation]);
443+
421444
// Track features that are pending (started but not yet confirmed running)
422445
const pendingFeaturesRef = useRef<Set<string>>(new Set());
423446

@@ -485,8 +508,9 @@ export function BoardView() {
485508
}
486509

487510
// Count currently running tasks + pending features
511+
// Use ref to get the latest running tasks without causing effect re-runs
488512
const currentRunning =
489-
runningAutoTasks.length + pendingFeaturesRef.current.size;
513+
runningAutoTasksRef.current.length + pendingFeaturesRef.current.size;
490514
const availableSlots = maxConcurrency - currentRunning;
491515

492516
// No available slots, skip check
@@ -541,6 +565,10 @@ export function BoardView() {
541565

542566
// Start features up to available slots
543567
const featuresToStart = eligibleFeatures.slice(0, availableSlots);
568+
const startImplementation = handleStartImplementationRef.current;
569+
if (!startImplementation) {
570+
return;
571+
}
544572

545573
for (const feature of featuresToStart) {
546574
// Check again before starting each feature
@@ -566,7 +594,7 @@ export function BoardView() {
566594
}
567595

568596
// Start the implementation - server will derive workDir from feature.branchName
569-
const started = await handleStartImplementation(feature);
597+
const started = await startImplementation(feature);
570598

571599
// If successfully started, track it as pending until we receive the start event
572600
if (started) {
@@ -580,7 +608,7 @@ export function BoardView() {
580608

581609
// Check immediately, then every 3 seconds
582610
checkAndStartFeatures();
583-
const interval = setInterval(checkAndStartFeatures, 3000);
611+
const interval = setInterval(checkAndStartFeatures, 1000);
584612

585613
return () => {
586614
// Mark as inactive to prevent any pending async operations from continuing
@@ -592,7 +620,8 @@ export function BoardView() {
592620
}, [
593621
autoMode.isRunning,
594622
currentProject,
595-
runningAutoTasks,
623+
// runningAutoTasks is accessed via runningAutoTasksRef to prevent effect re-runs
624+
// that would clear pendingFeaturesRef and cause concurrency issues
596625
maxConcurrency,
597626
// hookFeatures is accessed via hookFeaturesRef to prevent effect re-runs
598627
currentWorktreeBranch,
@@ -601,7 +630,6 @@ export function BoardView() {
601630
isPrimaryWorktreeBranch,
602631
enableDependencyBlocking,
603632
persistFeatureUpdate,
604-
handleStartImplementation,
605633
]);
606634

607635
// Use keyboard shortcuts hook (after actions hook)
@@ -640,7 +668,9 @@ export function BoardView() {
640668
// Find feature for pending plan approval
641669
const pendingApprovalFeature = useMemo(() => {
642670
if (!pendingPlanApproval) return null;
643-
return hookFeatures.find((f) => f.id === pendingPlanApproval.featureId) || null;
671+
return (
672+
hookFeatures.find((f) => f.id === pendingPlanApproval.featureId) || null
673+
);
644674
}, [pendingPlanApproval, hookFeatures]);
645675

646676
// Handle plan approval
@@ -666,10 +696,10 @@ export function BoardView() {
666696
if (result.success) {
667697
// Immediately update local feature state to hide "Approve Plan" button
668698
// Get current feature to preserve version
669-
const currentFeature = hookFeatures.find(f => f.id === featureId);
699+
const currentFeature = hookFeatures.find((f) => f.id === featureId);
670700
updateFeature(featureId, {
671701
planSpec: {
672-
status: 'approved',
702+
status: "approved",
673703
content: editedPlan || pendingPlanApproval.planContent,
674704
version: currentFeature?.planSpec?.version || 1,
675705
approvedAt: new Date().toISOString(),
@@ -688,7 +718,14 @@ export function BoardView() {
688718
setPendingPlanApproval(null);
689719
}
690720
},
691-
[pendingPlanApproval, currentProject, setPendingPlanApproval, updateFeature, loadFeatures, hookFeatures]
721+
[
722+
pendingPlanApproval,
723+
currentProject,
724+
setPendingPlanApproval,
725+
updateFeature,
726+
loadFeatures,
727+
hookFeatures,
728+
]
692729
);
693730

694731
// Handle plan rejection
@@ -715,11 +752,11 @@ export function BoardView() {
715752
if (result.success) {
716753
// Immediately update local feature state
717754
// Get current feature to preserve version
718-
const currentFeature = hookFeatures.find(f => f.id === featureId);
755+
const currentFeature = hookFeatures.find((f) => f.id === featureId);
719756
updateFeature(featureId, {
720-
status: 'backlog',
757+
status: "backlog",
721758
planSpec: {
722-
status: 'rejected',
759+
status: "rejected",
723760
content: pendingPlanApproval.planContent,
724761
version: currentFeature?.planSpec?.version || 1,
725762
reviewedByUser: true,
@@ -737,7 +774,14 @@ export function BoardView() {
737774
setPendingPlanApproval(null);
738775
}
739776
},
740-
[pendingPlanApproval, currentProject, setPendingPlanApproval, updateFeature, loadFeatures, hookFeatures]
777+
[
778+
pendingPlanApproval,
779+
currentProject,
780+
setPendingPlanApproval,
781+
updateFeature,
782+
loadFeatures,
783+
hookFeatures,
784+
]
741785
);
742786

743787
// Handle opening approval dialog from feature card button
@@ -748,7 +792,7 @@ export function BoardView() {
748792
// Determine the planning mode for approval (skip should never have a plan requiring approval)
749793
const mode = feature.planningMode;
750794
const approvalMode: "lite" | "spec" | "full" =
751-
mode === 'lite' || mode === 'spec' || mode === 'full' ? mode : 'spec';
795+
mode === "lite" || mode === "spec" || mode === "full" ? mode : "spec";
752796

753797
// Re-open the approval dialog with the feature's plan data
754798
setPendingPlanApproval({
@@ -833,6 +877,7 @@ export function BoardView() {
833877
}}
834878
onRemovedWorktrees={handleRemovedWorktrees}
835879
runningFeatureIds={runningAutoTasks}
880+
branchCardCounts={branchCardCounts}
836881
features={hookFeatures.map((f) => ({
837882
id: f.id,
838883
branchName: f.branchName,
@@ -929,6 +974,7 @@ export function BoardView() {
929974
onAdd={handleAddFeature}
930975
categorySuggestions={categorySuggestions}
931976
branchSuggestions={branchSuggestions}
977+
branchCardCounts={branchCardCounts}
932978
defaultSkipTests={defaultSkipTests}
933979
defaultBranch={selectedWorktreeBranch}
934980
currentBranch={currentWorktreeBranch || undefined}
@@ -944,6 +990,7 @@ export function BoardView() {
944990
onUpdate={handleUpdateFeature}
945991
categorySuggestions={categorySuggestions}
946992
branchSuggestions={branchSuggestions}
993+
branchCardCounts={branchCardCounts}
947994
currentBranch={currentWorktreeBranch || undefined}
948995
isMaximized={isMaximized}
949996
showProfilesOnly={showProfilesOnly}
@@ -1065,15 +1112,24 @@ export function BoardView() {
10651112
onOpenChange={setShowDeleteWorktreeDialog}
10661113
projectPath={currentProject.path}
10671114
worktree={selectedWorktreeForAction}
1115+
affectedFeatureCount={
1116+
selectedWorktreeForAction
1117+
? hookFeatures.filter(
1118+
(f) => f.branchName === selectedWorktreeForAction.branch
1119+
).length
1120+
: 0
1121+
}
10681122
onDeleted={(deletedWorktree, _deletedBranch) => {
10691123
// Reset features that were assigned to the deleted worktree (by branch)
10701124
hookFeatures.forEach((feature) => {
10711125
// Match by branch name since worktreePath is no longer stored
10721126
if (feature.branchName === deletedWorktree.branch) {
1073-
// Reset the feature's branch assignment
1074-
persistFeatureUpdate(feature.id, {
1127+
// Reset the feature's branch assignment - update both local state and persist
1128+
const updates = {
10751129
branchName: null as unknown as string | undefined,
1076-
});
1130+
};
1131+
updateFeature(feature.id, updates);
1132+
persistFeatureUpdate(feature.id, updates);
10771133
}
10781134
});
10791135

0 commit comments

Comments
 (0)