Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions apps/app/src/components/session-manager.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -116,8 +111,10 @@ export function SessionManager({
new Set()
);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [sessionToDelete, setSessionToDelete] = useState<SessionListItem | null>(null);
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] = useState(false);
const [sessionToDelete, setSessionToDelete] =
useState<SessionListItem | null>(null);
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] =
useState(false);

// Check running state for all sessions
const checkRunningSessions = async (sessionList: SessionListItem[]) => {
Expand Down Expand Up @@ -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);
Expand Down
24 changes: 18 additions & 6 deletions apps/app/src/components/ui/branch-autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,6 +20,7 @@ export function BranchAutocomplete({
value,
onChange,
branches,
branchCardCounts,
placeholder = "Select a branch...",
className,
disabled = false,
Expand All @@ -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 (
<Autocomplete
Expand Down
100 changes: 78 additions & 22 deletions apps/app/src/components/views/board-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,17 @@ export function BoardView() {
fetchBranches();
}, [currentProject, worktreeRefreshKey]);

// Calculate unarchived card counts per branch
const branchCardCounts = useMemo(() => {
return hookFeatures.reduce((counts, feature) => {
if (feature.status !== "completed") {
const branch = feature.branchName ?? "main";
counts[branch] = (counts[branch] || 0) + 1;
}
return counts;
}, {} as Record<string, number>);
}, [hookFeatures]);

// Custom collision detection that prioritizes columns over cards
const collisionDetectionStrategy = useCallback((args: any) => {
// First, check if pointer is within a column
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<Set<string>>(new Set());

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -580,7 +608,7 @@ export function BoardView() {

// Check immediately, then every 3 seconds
checkAndStartFeatures();
const interval = setInterval(checkAndStartFeatures, 3000);
const interval = setInterval(checkAndStartFeatures, 1000);
Comment on lines 610 to +611
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the file around lines 610-611 to see the actual comment and code
sed -n '600,620p' apps/app/src/components/views/board-view.tsx

Repository: AutoMaker-Org/automaker

Length of output: 659


Update comment to reflect actual interval timing.

The comment states "every 3 seconds" but the interval is set to 1000ms (1 second). Update the comment on line 609 to: // Check immediately, then every 1 second to match the implemented behavior.

🤖 Prompt for AI Agents
In apps/app/src/components/views/board-view.tsx around lines 609 to 611, the
inline comment incorrectly says "every 3 seconds" while the code calls
checkAndStartFeatures() immediately then sets setInterval(checkAndStartFeatures,
1000) (1 second). Update the comment on line 609 to read "// Check immediately,
then every 1 second" so it matches the implemented interval.


return () => {
// Mark as inactive to prevent any pending async operations from continuing
Expand All @@ -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,
Expand All @@ -601,7 +630,6 @@ export function BoardView() {
isPrimaryWorktreeBranch,
enableDependencyBlocking,
persistFeatureUpdate,
handleStartImplementation,
]);

// Use keyboard shortcuts hook (after actions hook)
Expand Down Expand Up @@ -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
Expand All @@ -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(),
Expand All @@ -688,7 +718,14 @@ export function BoardView() {
setPendingPlanApproval(null);
}
},
[pendingPlanApproval, currentProject, setPendingPlanApproval, updateFeature, loadFeatures, hookFeatures]
[
pendingPlanApproval,
currentProject,
setPendingPlanApproval,
updateFeature,
loadFeatures,
hookFeatures,
]
);

// Handle plan rejection
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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({
Expand Down Expand Up @@ -833,6 +877,7 @@ export function BoardView() {
}}
onRemovedWorktrees={handleRemovedWorktrees}
runningFeatureIds={runningAutoTasks}
branchCardCounts={branchCardCounts}
features={hookFeatures.map((f) => ({
id: f.id,
branchName: f.branchName,
Expand Down Expand Up @@ -929,6 +974,7 @@ export function BoardView() {
onAdd={handleAddFeature}
categorySuggestions={categorySuggestions}
branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
defaultSkipTests={defaultSkipTests}
defaultBranch={selectedWorktreeBranch}
currentBranch={currentWorktreeBranch || undefined}
Expand All @@ -944,6 +990,7 @@ export function BoardView() {
onUpdate={handleUpdateFeature}
categorySuggestions={categorySuggestions}
branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
currentBranch={currentWorktreeBranch || undefined}
isMaximized={isMaximized}
showProfilesOnly={showProfilesOnly}
Expand Down Expand Up @@ -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);
}
});

Expand Down
Loading