mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
Implement branch selection and worktree management features
- Added a new BranchAutocomplete component for selecting branches in feature dialogs. - Enhanced BoardView to fetch and display branch suggestions. - Updated CreateWorktreeDialog and EditFeatureDialog to include branch selection. - Modified worktree management to ensure proper handling of branch-specific worktrees. - Refactored related components and hooks to support the new branch management functionality. - Removed unused revert and merge handlers from Kanban components for cleaner code.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { Feature, FeatureImage, AgentModel, ThinkingLevel, useAppStore } from "@/store/app-store";
|
||||
import { FeatureImagePath as DescriptionImagePath } from "@/components/ui/description-image-dropzone";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
@@ -28,6 +28,8 @@ interface UseBoardActionsProps {
|
||||
setShowFollowUpDialog: (show: boolean) => void;
|
||||
inProgressFeaturesForShortcuts: Feature[];
|
||||
outputFeature: Feature | null;
|
||||
projectPath: string | null;
|
||||
onWorktreeCreated?: () => void;
|
||||
}
|
||||
|
||||
export function useBoardActions({
|
||||
@@ -52,10 +54,67 @@ export function useBoardActions({
|
||||
setShowFollowUpDialog,
|
||||
inProgressFeaturesForShortcuts,
|
||||
outputFeature,
|
||||
projectPath,
|
||||
onWorktreeCreated,
|
||||
}: UseBoardActionsProps) {
|
||||
const { addFeature, updateFeature, removeFeature, moveFeature, useWorktrees } = useAppStore();
|
||||
const autoMode = useAutoMode();
|
||||
|
||||
/**
|
||||
* Get or create the worktree path for a feature based on its branchName.
|
||||
* - If branchName is "main" or empty, returns the project path
|
||||
* - Otherwise, creates a worktree for that branch if needed
|
||||
*/
|
||||
const getOrCreateWorktreeForFeature = useCallback(
|
||||
async (feature: Feature): Promise<string | null> => {
|
||||
if (!projectPath) return null;
|
||||
|
||||
const branchName = feature.branchName || "main";
|
||||
|
||||
// If targeting main branch, use the project path directly
|
||||
if (branchName === "main" || branchName === "master") {
|
||||
return projectPath;
|
||||
}
|
||||
|
||||
// For other branches, create a worktree if it doesn't exist
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.worktree?.create) {
|
||||
console.error("[BoardActions] Worktree API not available");
|
||||
return projectPath;
|
||||
}
|
||||
|
||||
// Try to create the worktree (will return existing if already exists)
|
||||
const result = await api.worktree.create(projectPath, branchName);
|
||||
|
||||
if (result.success && result.worktree) {
|
||||
console.log(
|
||||
`[BoardActions] Worktree ready for branch "${branchName}": ${result.worktree.path}`
|
||||
);
|
||||
if (result.worktree.isNew) {
|
||||
toast.success(`Worktree created for branch "${branchName}"`, {
|
||||
description: "A new worktree was created for this feature.",
|
||||
});
|
||||
}
|
||||
return result.worktree.path;
|
||||
} else {
|
||||
console.error("[BoardActions] Failed to create worktree:", result.error);
|
||||
toast.error("Failed to create worktree", {
|
||||
description: result.error || "Could not create worktree for this branch.",
|
||||
});
|
||||
return projectPath; // Fall back to project path
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[BoardActions] Error creating worktree:", error);
|
||||
toast.error("Error creating worktree", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
return projectPath; // Fall back to project path
|
||||
}
|
||||
},
|
||||
[projectPath]
|
||||
);
|
||||
|
||||
const handleAddFeature = useCallback(
|
||||
(featureData: {
|
||||
category: string;
|
||||
@@ -66,6 +125,7 @@ export function useBoardActions({
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
branchName: string;
|
||||
}) => {
|
||||
const newFeatureData = {
|
||||
...featureData,
|
||||
@@ -89,6 +149,7 @@ export function useBoardActions({
|
||||
model: AgentModel;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
imagePaths: DescriptionImagePath[];
|
||||
branchName: string;
|
||||
}
|
||||
) => {
|
||||
updateFeature(featureId, updates);
|
||||
@@ -155,14 +216,19 @@ export function useBoardActions({
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the feature's assigned worktreePath (set when moving to in_progress)
|
||||
// This ensures work happens in the correct worktree based on the feature's branchName
|
||||
const featureWorktreePath = feature.worktreePath;
|
||||
|
||||
const result = await api.autoMode.runFeature(
|
||||
currentProject.path,
|
||||
feature.id,
|
||||
useWorktrees
|
||||
useWorktrees,
|
||||
featureWorktreePath || undefined
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log("[Board] Feature run started successfully");
|
||||
console.log("[Board] Feature run started successfully in worktree:", featureWorktreePath || "main");
|
||||
} else {
|
||||
console.error("[Board] Failed to run feature:", result.error);
|
||||
await loadFeatures();
|
||||
@@ -327,8 +393,10 @@ export function useBoardActions({
|
||||
});
|
||||
|
||||
const imagePaths = followUpImagePaths.map((img) => img.path);
|
||||
// Use the feature's worktreePath to ensure work happens in the correct branch
|
||||
const featureWorktreePath = followUpFeature.worktreePath;
|
||||
api.autoMode
|
||||
.followUpFeature(currentProject.path, followUpFeature.id, followUpPrompt, imagePaths)
|
||||
.followUpFeature(currentProject.path, followUpFeature.id, followUpPrompt, imagePaths, featureWorktreePath)
|
||||
.catch((error) => {
|
||||
console.error("[Board] Error sending follow-up:", error);
|
||||
toast.error("Failed to send follow-up", {
|
||||
@@ -365,7 +433,8 @@ export function useBoardActions({
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.autoMode.commitFeature(currentProject.path, feature.id);
|
||||
// Pass the feature's worktreePath to ensure commits happen in the correct worktree
|
||||
const result = await api.autoMode.commitFeature(currentProject.path, feature.id, feature.worktreePath);
|
||||
|
||||
if (result.success) {
|
||||
moveFeature(feature.id, "verified");
|
||||
@@ -373,6 +442,8 @@ export function useBoardActions({
|
||||
toast.success("Feature committed", {
|
||||
description: `Committed and verified: ${truncateDescription(feature.description)}`,
|
||||
});
|
||||
// Refresh worktree selector to update commit counts
|
||||
onWorktreeCreated?.();
|
||||
} else {
|
||||
console.error("[Board] Failed to commit feature:", result.error);
|
||||
toast.error("Failed to commit feature", {
|
||||
@@ -388,7 +459,7 @@ export function useBoardActions({
|
||||
await loadFeatures();
|
||||
}
|
||||
},
|
||||
[currentProject, moveFeature, persistFeatureUpdate, loadFeatures]
|
||||
[currentProject, moveFeature, persistFeatureUpdate, loadFeatures, onWorktreeCreated]
|
||||
);
|
||||
|
||||
const handleRevertFeature = useCallback(
|
||||
@@ -565,12 +636,29 @@ export function useBoardActions({
|
||||
return;
|
||||
}
|
||||
|
||||
const featuresToStart = backlogFeatures.slice(0, availableSlots);
|
||||
if (backlogFeatures.length === 0) {
|
||||
toast.info("Backlog empty", {
|
||||
description: "No features in backlog to start.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Start only one feature per keypress (user must press again for next)
|
||||
const featuresToStart = backlogFeatures.slice(0, 1);
|
||||
|
||||
for (const feature of featuresToStart) {
|
||||
await handleStartImplementation(feature);
|
||||
// Get or create worktree based on the feature's assigned branch (same as drag-to-in-progress)
|
||||
const worktreePath = await getOrCreateWorktreeForFeature(feature);
|
||||
if (worktreePath) {
|
||||
await persistFeatureUpdate(feature.id, { worktreePath });
|
||||
}
|
||||
// Refresh worktree selector after creating worktree
|
||||
onWorktreeCreated?.();
|
||||
// Start the implementation
|
||||
// Pass feature with worktreePath so handleRunFeature uses the correct path
|
||||
await handleStartImplementation({ ...feature, worktreePath: worktreePath || undefined });
|
||||
}
|
||||
}, [features, runningAutoTasks, handleStartImplementation]);
|
||||
}, [features, runningAutoTasks, handleStartImplementation, getOrCreateWorktreeForFeature, persistFeatureUpdate, onWorktreeCreated]);
|
||||
|
||||
const handleDeleteAllVerified = useCallback(async () => {
|
||||
const verifiedFeatures = features.filter((f) => f.status === "verified");
|
||||
|
||||
@@ -8,6 +8,7 @@ interface UseBoardColumnFeaturesProps {
|
||||
runningAutoTasks: string[];
|
||||
searchQuery: string;
|
||||
currentWorktreePath: string | null; // Currently selected worktree path
|
||||
currentWorktreeBranch: string | null; // Branch name of the selected worktree (null = main)
|
||||
projectPath: string | null; // Main project path (for main worktree)
|
||||
}
|
||||
|
||||
@@ -16,6 +17,7 @@ export function useBoardColumnFeatures({
|
||||
runningAutoTasks,
|
||||
searchQuery,
|
||||
currentWorktreePath,
|
||||
currentWorktreeBranch,
|
||||
projectPath,
|
||||
}: UseBoardColumnFeaturesProps) {
|
||||
// Memoize column features to prevent unnecessary re-renders
|
||||
@@ -38,18 +40,32 @@ export function useBoardColumnFeatures({
|
||||
)
|
||||
: features;
|
||||
|
||||
// Determine the effective worktree path for filtering
|
||||
// If currentWorktreePath is null, we're on the main worktree (use projectPath)
|
||||
// Determine the effective worktree path and branch for filtering
|
||||
// If currentWorktreePath is null, we're on the main worktree
|
||||
const effectiveWorktreePath = currentWorktreePath || projectPath;
|
||||
const effectiveBranch = currentWorktreeBranch || "main";
|
||||
|
||||
filteredFeatures.forEach((f) => {
|
||||
// If feature has a running agent, always show it in "in_progress"
|
||||
const isRunning = runningAutoTasks.includes(f.id);
|
||||
|
||||
// Check if feature matches the current worktree
|
||||
// Features without a worktreePath are considered unassigned (backlog items)
|
||||
// Features with a worktreePath should only show if it matches the selected worktree
|
||||
const matchesWorktree = !f.worktreePath || f.worktreePath === effectiveWorktreePath;
|
||||
// Match by worktreePath if set, OR by branchName if set
|
||||
// Features with neither are considered unassigned (show on main only)
|
||||
const featureBranch = f.branchName || "main";
|
||||
const hasWorktreeAssigned = f.worktreePath || f.branchName;
|
||||
|
||||
let matchesWorktree: boolean;
|
||||
if (!hasWorktreeAssigned) {
|
||||
// No worktree or branch assigned - show only on main
|
||||
matchesWorktree = !currentWorktreePath;
|
||||
} else if (f.worktreePath) {
|
||||
// Has worktreePath - match by path
|
||||
matchesWorktree = f.worktreePath === effectiveWorktreePath;
|
||||
} else {
|
||||
// Has branchName but no worktreePath - match by branch name
|
||||
matchesWorktree = featureBranch === effectiveBranch;
|
||||
}
|
||||
|
||||
if (isRunning) {
|
||||
// Only show running tasks if they match the current worktree
|
||||
@@ -84,7 +100,7 @@ export function useBoardColumnFeatures({
|
||||
});
|
||||
|
||||
return map;
|
||||
}, [features, runningAutoTasks, searchQuery, currentWorktreePath, projectPath]);
|
||||
}, [features, runningAutoTasks, searchQuery, currentWorktreePath, currentWorktreeBranch, projectPath]);
|
||||
|
||||
const getColumnFeatures = useCallback(
|
||||
(columnId: ColumnId) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Feature } from "@/store/app-store";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { toast } from "sonner";
|
||||
import { COLUMNS, ColumnId } from "../constants";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
|
||||
interface UseBoardDragDropProps {
|
||||
features: Feature[];
|
||||
@@ -14,8 +15,8 @@ interface UseBoardDragDropProps {
|
||||
updates: Partial<Feature>
|
||||
) => Promise<void>;
|
||||
handleStartImplementation: (feature: Feature) => Promise<boolean>;
|
||||
currentWorktreePath: string | null; // Currently selected worktree path
|
||||
projectPath: string | null; // Main project path
|
||||
onWorktreeCreated?: () => void; // Callback when a new worktree is created
|
||||
}
|
||||
|
||||
export function useBoardDragDrop({
|
||||
@@ -24,14 +25,66 @@ export function useBoardDragDrop({
|
||||
runningAutoTasks,
|
||||
persistFeatureUpdate,
|
||||
handleStartImplementation,
|
||||
currentWorktreePath,
|
||||
projectPath,
|
||||
onWorktreeCreated,
|
||||
}: UseBoardDragDropProps) {
|
||||
const [activeFeature, setActiveFeature] = useState<Feature | null>(null);
|
||||
const { moveFeature } = useAppStore();
|
||||
|
||||
// Determine the effective worktree path for assigning to features
|
||||
const effectiveWorktreePath = currentWorktreePath || projectPath;
|
||||
/**
|
||||
* Get or create the worktree path for a feature based on its branchName.
|
||||
* - If branchName is "main" or empty, returns the project path
|
||||
* - Otherwise, creates a worktree for that branch if needed
|
||||
*/
|
||||
const getOrCreateWorktreeForFeature = useCallback(
|
||||
async (feature: Feature): Promise<string | null> => {
|
||||
if (!projectPath) return null;
|
||||
|
||||
const branchName = feature.branchName || "main";
|
||||
|
||||
// If targeting main branch, use the project path directly
|
||||
if (branchName === "main" || branchName === "master") {
|
||||
return projectPath;
|
||||
}
|
||||
|
||||
// For other branches, create a worktree if it doesn't exist
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.worktree?.create) {
|
||||
console.error("[DragDrop] Worktree API not available");
|
||||
return projectPath;
|
||||
}
|
||||
|
||||
// Try to create the worktree (will return existing if already exists)
|
||||
const result = await api.worktree.create(projectPath, branchName);
|
||||
|
||||
if (result.success && result.worktree) {
|
||||
console.log(
|
||||
`[DragDrop] Worktree ready for branch "${branchName}": ${result.worktree.path}`
|
||||
);
|
||||
if (result.worktree.isNew) {
|
||||
toast.success(`Worktree created for branch "${branchName}"`, {
|
||||
description: "A new worktree was created for this feature.",
|
||||
});
|
||||
}
|
||||
return result.worktree.path;
|
||||
} else {
|
||||
console.error("[DragDrop] Failed to create worktree:", result.error);
|
||||
toast.error("Failed to create worktree", {
|
||||
description: result.error || "Could not create worktree for this branch.",
|
||||
});
|
||||
return projectPath; // Fall back to project path
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[DragDrop] Error creating worktree:", error);
|
||||
toast.error("Error creating worktree", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
return projectPath; // Fall back to project path
|
||||
}
|
||||
},
|
||||
[projectPath]
|
||||
);
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
(event: DragStartEvent) => {
|
||||
@@ -104,12 +157,16 @@ export function useBoardDragDrop({
|
||||
if (draggedFeature.status === "backlog") {
|
||||
// From backlog
|
||||
if (targetStatus === "in_progress") {
|
||||
// Assign the current worktree to this feature when moving to in_progress
|
||||
if (effectiveWorktreePath) {
|
||||
await persistFeatureUpdate(featureId, { worktreePath: effectiveWorktreePath });
|
||||
// Get or create worktree based on the feature's assigned branch
|
||||
const worktreePath = await getOrCreateWorktreeForFeature(draggedFeature);
|
||||
if (worktreePath) {
|
||||
await persistFeatureUpdate(featureId, { worktreePath });
|
||||
}
|
||||
// Always refresh worktree selector after moving to in_progress
|
||||
onWorktreeCreated?.();
|
||||
// Use helper function to handle concurrency check and start implementation
|
||||
await handleStartImplementation(draggedFeature);
|
||||
// Pass feature with worktreePath so handleRunFeature uses the correct path
|
||||
await handleStartImplementation({ ...draggedFeature, worktreePath: worktreePath || undefined });
|
||||
} else {
|
||||
moveFeature(featureId, targetStatus);
|
||||
persistFeatureUpdate(featureId, { status: targetStatus });
|
||||
@@ -219,7 +276,8 @@ export function useBoardDragDrop({
|
||||
moveFeature,
|
||||
persistFeatureUpdate,
|
||||
handleStartImplementation,
|
||||
effectiveWorktreePath,
|
||||
getOrCreateWorktreeForFeature,
|
||||
onWorktreeCreated,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user