mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
refactor: streamline session and board view components
- Consolidated imports in session-manager.tsx for cleaner code. - Improved state initialization formatting for better readability. - Updated board-view.tsx to enhance feature management, including the use of refs to track running tasks and prevent unnecessary effect re-runs. - Added affectedFeatureCount prop to DeleteWorktreeDialog for better user feedback on feature assignments. - Refactored useBoardActions to ensure worktrees are created when features are added or updated, improving overall workflow efficiency.
This commit is contained in:
@@ -1,12 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import {
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { HotkeyButton } from "@/components/ui/hotkey-button";
|
import { HotkeyButton } from "@/components/ui/hotkey-button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -116,8 +111,10 @@ export function SessionManager({
|
|||||||
new Set()
|
new Set()
|
||||||
);
|
);
|
||||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
const [sessionToDelete, setSessionToDelete] = useState<SessionListItem | null>(null);
|
const [sessionToDelete, setSessionToDelete] =
|
||||||
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] = useState(false);
|
useState<SessionListItem | null>(null);
|
||||||
|
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
// Check running state for all sessions
|
// Check running state for all sessions
|
||||||
const checkRunningSessions = async (sessionList: SessionListItem[]) => {
|
const checkRunningSessions = async (sessionList: SessionListItem[]) => {
|
||||||
@@ -234,11 +231,7 @@ export function SessionManager({
|
|||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!editingName.trim() || !api?.sessions) return;
|
if (!editingName.trim() || !api?.sessions) return;
|
||||||
|
|
||||||
const result = await api.sessions.update(
|
const result = await api.sessions.update(sessionId, editingName, undefined);
|
||||||
sessionId,
|
|
||||||
editingName,
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setEditingSessionId(null);
|
setEditingSessionId(null);
|
||||||
|
|||||||
@@ -313,14 +313,14 @@ export function BoardView() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (matchesRemovedWorktree) {
|
if (matchesRemovedWorktree) {
|
||||||
// Reset the feature's branch assignment
|
// Reset the feature's branch assignment - update both local state and persist
|
||||||
persistFeatureUpdate(feature.id, {
|
const updates = { branchName: null as unknown as string | undefined };
|
||||||
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)
|
// Get in-progress features for keyboard shortcuts (needed before actions hook)
|
||||||
@@ -429,6 +429,18 @@ export function BoardView() {
|
|||||||
hookFeaturesRef.current = hookFeatures;
|
hookFeaturesRef.current = hookFeatures;
|
||||||
}, [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)
|
// Track features that are pending (started but not yet confirmed running)
|
||||||
const pendingFeaturesRef = useRef<Set<string>>(new Set());
|
const pendingFeaturesRef = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
@@ -496,8 +508,9 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Count currently running tasks + pending features
|
// Count currently running tasks + pending features
|
||||||
|
// Use ref to get the latest running tasks without causing effect re-runs
|
||||||
const currentRunning =
|
const currentRunning =
|
||||||
runningAutoTasks.length + pendingFeaturesRef.current.size;
|
runningAutoTasksRef.current.length + pendingFeaturesRef.current.size;
|
||||||
const availableSlots = maxConcurrency - currentRunning;
|
const availableSlots = maxConcurrency - currentRunning;
|
||||||
|
|
||||||
// No available slots, skip check
|
// No available slots, skip check
|
||||||
@@ -552,6 +565,10 @@ export function BoardView() {
|
|||||||
|
|
||||||
// Start features up to available slots
|
// Start features up to available slots
|
||||||
const featuresToStart = eligibleFeatures.slice(0, availableSlots);
|
const featuresToStart = eligibleFeatures.slice(0, availableSlots);
|
||||||
|
const startImplementation = handleStartImplementationRef.current;
|
||||||
|
if (!startImplementation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const feature of featuresToStart) {
|
for (const feature of featuresToStart) {
|
||||||
// Check again before starting each feature
|
// Check again before starting each feature
|
||||||
@@ -577,7 +594,7 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start the implementation - server will derive workDir from feature.branchName
|
// 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 successfully started, track it as pending until we receive the start event
|
||||||
if (started) {
|
if (started) {
|
||||||
@@ -591,7 +608,7 @@ export function BoardView() {
|
|||||||
|
|
||||||
// Check immediately, then every 3 seconds
|
// Check immediately, then every 3 seconds
|
||||||
checkAndStartFeatures();
|
checkAndStartFeatures();
|
||||||
const interval = setInterval(checkAndStartFeatures, 3000);
|
const interval = setInterval(checkAndStartFeatures, 1000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Mark as inactive to prevent any pending async operations from continuing
|
// Mark as inactive to prevent any pending async operations from continuing
|
||||||
@@ -603,7 +620,8 @@ export function BoardView() {
|
|||||||
}, [
|
}, [
|
||||||
autoMode.isRunning,
|
autoMode.isRunning,
|
||||||
currentProject,
|
currentProject,
|
||||||
runningAutoTasks,
|
// runningAutoTasks is accessed via runningAutoTasksRef to prevent effect re-runs
|
||||||
|
// that would clear pendingFeaturesRef and cause concurrency issues
|
||||||
maxConcurrency,
|
maxConcurrency,
|
||||||
// hookFeatures is accessed via hookFeaturesRef to prevent effect re-runs
|
// hookFeatures is accessed via hookFeaturesRef to prevent effect re-runs
|
||||||
currentWorktreeBranch,
|
currentWorktreeBranch,
|
||||||
@@ -612,7 +630,6 @@ export function BoardView() {
|
|||||||
isPrimaryWorktreeBranch,
|
isPrimaryWorktreeBranch,
|
||||||
enableDependencyBlocking,
|
enableDependencyBlocking,
|
||||||
persistFeatureUpdate,
|
persistFeatureUpdate,
|
||||||
handleStartImplementation,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Use keyboard shortcuts hook (after actions hook)
|
// Use keyboard shortcuts hook (after actions hook)
|
||||||
@@ -651,7 +668,9 @@ export function BoardView() {
|
|||||||
// Find feature for pending plan approval
|
// Find feature for pending plan approval
|
||||||
const pendingApprovalFeature = useMemo(() => {
|
const pendingApprovalFeature = useMemo(() => {
|
||||||
if (!pendingPlanApproval) return null;
|
if (!pendingPlanApproval) return null;
|
||||||
return hookFeatures.find((f) => f.id === pendingPlanApproval.featureId) || null;
|
return (
|
||||||
|
hookFeatures.find((f) => f.id === pendingPlanApproval.featureId) || null
|
||||||
|
);
|
||||||
}, [pendingPlanApproval, hookFeatures]);
|
}, [pendingPlanApproval, hookFeatures]);
|
||||||
|
|
||||||
// Handle plan approval
|
// Handle plan approval
|
||||||
@@ -677,10 +696,10 @@ export function BoardView() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Immediately update local feature state to hide "Approve Plan" button
|
// Immediately update local feature state to hide "Approve Plan" button
|
||||||
// Get current feature to preserve version
|
// Get current feature to preserve version
|
||||||
const currentFeature = hookFeatures.find(f => f.id === featureId);
|
const currentFeature = hookFeatures.find((f) => f.id === featureId);
|
||||||
updateFeature(featureId, {
|
updateFeature(featureId, {
|
||||||
planSpec: {
|
planSpec: {
|
||||||
status: 'approved',
|
status: "approved",
|
||||||
content: editedPlan || pendingPlanApproval.planContent,
|
content: editedPlan || pendingPlanApproval.planContent,
|
||||||
version: currentFeature?.planSpec?.version || 1,
|
version: currentFeature?.planSpec?.version || 1,
|
||||||
approvedAt: new Date().toISOString(),
|
approvedAt: new Date().toISOString(),
|
||||||
@@ -699,7 +718,14 @@ export function BoardView() {
|
|||||||
setPendingPlanApproval(null);
|
setPendingPlanApproval(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[pendingPlanApproval, currentProject, setPendingPlanApproval, updateFeature, loadFeatures, hookFeatures]
|
[
|
||||||
|
pendingPlanApproval,
|
||||||
|
currentProject,
|
||||||
|
setPendingPlanApproval,
|
||||||
|
updateFeature,
|
||||||
|
loadFeatures,
|
||||||
|
hookFeatures,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle plan rejection
|
// Handle plan rejection
|
||||||
@@ -726,11 +752,11 @@ export function BoardView() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Immediately update local feature state
|
// Immediately update local feature state
|
||||||
// Get current feature to preserve version
|
// Get current feature to preserve version
|
||||||
const currentFeature = hookFeatures.find(f => f.id === featureId);
|
const currentFeature = hookFeatures.find((f) => f.id === featureId);
|
||||||
updateFeature(featureId, {
|
updateFeature(featureId, {
|
||||||
status: 'backlog',
|
status: "backlog",
|
||||||
planSpec: {
|
planSpec: {
|
||||||
status: 'rejected',
|
status: "rejected",
|
||||||
content: pendingPlanApproval.planContent,
|
content: pendingPlanApproval.planContent,
|
||||||
version: currentFeature?.planSpec?.version || 1,
|
version: currentFeature?.planSpec?.version || 1,
|
||||||
reviewedByUser: true,
|
reviewedByUser: true,
|
||||||
@@ -748,7 +774,14 @@ export function BoardView() {
|
|||||||
setPendingPlanApproval(null);
|
setPendingPlanApproval(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[pendingPlanApproval, currentProject, setPendingPlanApproval, updateFeature, loadFeatures, hookFeatures]
|
[
|
||||||
|
pendingPlanApproval,
|
||||||
|
currentProject,
|
||||||
|
setPendingPlanApproval,
|
||||||
|
updateFeature,
|
||||||
|
loadFeatures,
|
||||||
|
hookFeatures,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle opening approval dialog from feature card button
|
// Handle opening approval dialog from feature card button
|
||||||
@@ -759,7 +792,7 @@ export function BoardView() {
|
|||||||
// Determine the planning mode for approval (skip should never have a plan requiring approval)
|
// Determine the planning mode for approval (skip should never have a plan requiring approval)
|
||||||
const mode = feature.planningMode;
|
const mode = feature.planningMode;
|
||||||
const approvalMode: "lite" | "spec" | "full" =
|
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
|
// Re-open the approval dialog with the feature's plan data
|
||||||
setPendingPlanApproval({
|
setPendingPlanApproval({
|
||||||
@@ -1079,15 +1112,24 @@ export function BoardView() {
|
|||||||
onOpenChange={setShowDeleteWorktreeDialog}
|
onOpenChange={setShowDeleteWorktreeDialog}
|
||||||
projectPath={currentProject.path}
|
projectPath={currentProject.path}
|
||||||
worktree={selectedWorktreeForAction}
|
worktree={selectedWorktreeForAction}
|
||||||
|
affectedFeatureCount={
|
||||||
|
selectedWorktreeForAction
|
||||||
|
? hookFeatures.filter(
|
||||||
|
(f) => f.branchName === selectedWorktreeForAction.branch
|
||||||
|
).length
|
||||||
|
: 0
|
||||||
|
}
|
||||||
onDeleted={(deletedWorktree, _deletedBranch) => {
|
onDeleted={(deletedWorktree, _deletedBranch) => {
|
||||||
// Reset features that were assigned to the deleted worktree (by branch)
|
// Reset features that were assigned to the deleted worktree (by branch)
|
||||||
hookFeatures.forEach((feature) => {
|
hookFeatures.forEach((feature) => {
|
||||||
// Match by branch name since worktreePath is no longer stored
|
// Match by branch name since worktreePath is no longer stored
|
||||||
if (feature.branchName === deletedWorktree.branch) {
|
if (feature.branchName === deletedWorktree.branch) {
|
||||||
// Reset the feature's branch assignment
|
// Reset the feature's branch assignment - update both local state and persist
|
||||||
persistFeatureUpdate(feature.id, {
|
const updates = {
|
||||||
branchName: null as unknown as string | undefined,
|
branchName: null as unknown as string | undefined,
|
||||||
});
|
};
|
||||||
|
updateFeature(feature.id, updates);
|
||||||
|
persistFeatureUpdate(feature.id, updates);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Loader2, Trash2, AlertTriangle } from "lucide-react";
|
import { Loader2, Trash2, AlertTriangle, FileWarning } from "lucide-react";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -30,6 +30,8 @@ interface DeleteWorktreeDialogProps {
|
|||||||
projectPath: string;
|
projectPath: string;
|
||||||
worktree: WorktreeInfo | null;
|
worktree: WorktreeInfo | null;
|
||||||
onDeleted: (deletedWorktree: WorktreeInfo, deletedBranch: boolean) => void;
|
onDeleted: (deletedWorktree: WorktreeInfo, deletedBranch: boolean) => void;
|
||||||
|
/** Number of features assigned to this worktree's branch */
|
||||||
|
affectedFeatureCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DeleteWorktreeDialog({
|
export function DeleteWorktreeDialog({
|
||||||
@@ -38,6 +40,7 @@ export function DeleteWorktreeDialog({
|
|||||||
projectPath,
|
projectPath,
|
||||||
worktree,
|
worktree,
|
||||||
onDeleted,
|
onDeleted,
|
||||||
|
affectedFeatureCount = 0,
|
||||||
}: DeleteWorktreeDialogProps) {
|
}: DeleteWorktreeDialogProps) {
|
||||||
const [deleteBranch, setDeleteBranch] = useState(false);
|
const [deleteBranch, setDeleteBranch] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -100,6 +103,18 @@ export function DeleteWorktreeDialog({
|
|||||||
?
|
?
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{affectedFeatureCount > 0 && (
|
||||||
|
<div className="flex items-start gap-2 p-3 rounded-md bg-orange-500/10 border border-orange-500/20 mt-2">
|
||||||
|
<FileWarning className="w-4 h-4 text-orange-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span className="text-orange-500 text-sm">
|
||||||
|
{affectedFeatureCount} feature{affectedFeatureCount !== 1 ? "s" : ""}{" "}
|
||||||
|
{affectedFeatureCount !== 1 ? "are" : "is"} assigned to this
|
||||||
|
branch. {affectedFeatureCount !== 1 ? "They" : "It"} will be
|
||||||
|
unassigned and moved to the main worktree.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{worktree.hasChanges && (
|
{worktree.hasChanges && (
|
||||||
<div className="flex items-start gap-2 p-3 rounded-md bg-yellow-500/10 border border-yellow-500/20 mt-2">
|
<div className="flex items-start gap-2 p-3 rounded-md bg-yellow-500/10 border border-yellow-500/20 mt-2">
|
||||||
<AlertTriangle className="w-4 h-4 text-yellow-500 mt-0.5 flex-shrink-0" />
|
<AlertTriangle className="w-4 h-4 text-yellow-500 mt-0.5 flex-shrink-0" />
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ export function useBoardActions({
|
|||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
const autoMode = useAutoMode();
|
const autoMode = useAutoMode();
|
||||||
|
|
||||||
// Note: getOrCreateWorktreeForFeature removed - worktrees are now created server-side
|
// Worktrees are created when adding/editing features with a branch name
|
||||||
// at execution time based on feature.branchName
|
// This ensures the worktree exists before the feature starts execution
|
||||||
|
|
||||||
const handleAddFeature = useCallback(
|
const handleAddFeature = useCallback(
|
||||||
async (featureData: {
|
async (featureData: {
|
||||||
@@ -100,24 +100,58 @@ export function useBoardActions({
|
|||||||
planningMode: PlanningMode;
|
planningMode: PlanningMode;
|
||||||
requirePlanApproval: boolean;
|
requirePlanApproval: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
// Simplified: Only store branchName, no worktree creation on add
|
|
||||||
// Worktrees are created at execution time (when feature starts)
|
|
||||||
// Empty string means "unassigned" (show only on primary worktree) - convert to undefined
|
// Empty string means "unassigned" (show only on primary worktree) - convert to undefined
|
||||||
// Non-empty string is the actual branch name (for non-primary worktrees)
|
// Non-empty string is the actual branch name (for non-primary worktrees)
|
||||||
const finalBranchName = featureData.branchName || undefined;
|
const finalBranchName = featureData.branchName || undefined;
|
||||||
|
|
||||||
|
// If worktrees enabled and a branch is specified, create the worktree now
|
||||||
|
// This ensures the worktree exists before the feature starts
|
||||||
|
if (useWorktrees && finalBranchName && currentProject) {
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (api?.worktree?.create) {
|
||||||
|
const result = await api.worktree.create(
|
||||||
|
currentProject.path,
|
||||||
|
finalBranchName
|
||||||
|
);
|
||||||
|
if (result.success) {
|
||||||
|
console.log(
|
||||||
|
`[Board] Worktree for branch "${finalBranchName}" ${
|
||||||
|
result.worktree?.isNew ? "created" : "already exists"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
// Refresh worktree list in UI
|
||||||
|
onWorktreeCreated?.();
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`[Board] Failed to create worktree for branch "${finalBranchName}":`,
|
||||||
|
result.error
|
||||||
|
);
|
||||||
|
toast.error("Failed to create worktree", {
|
||||||
|
description: result.error || "An error occurred",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Board] Error creating worktree:", error);
|
||||||
|
toast.error("Failed to create worktree", {
|
||||||
|
description:
|
||||||
|
error instanceof Error ? error.message : "An error occurred",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newFeatureData = {
|
const newFeatureData = {
|
||||||
...featureData,
|
...featureData,
|
||||||
status: "backlog" as const,
|
status: "backlog" as const,
|
||||||
branchName: finalBranchName,
|
branchName: finalBranchName,
|
||||||
// No worktreePath - derived at runtime from branchName
|
|
||||||
};
|
};
|
||||||
const createdFeature = addFeature(newFeatureData);
|
const createdFeature = addFeature(newFeatureData);
|
||||||
// Must await to ensure feature exists on server before user can drag it
|
// Must await to ensure feature exists on server before user can drag it
|
||||||
await persistFeatureCreate(createdFeature);
|
await persistFeatureCreate(createdFeature);
|
||||||
saveCategory(featureData.category);
|
saveCategory(featureData.category);
|
||||||
},
|
},
|
||||||
[addFeature, persistFeatureCreate, saveCategory]
|
[addFeature, persistFeatureCreate, saveCategory, useWorktrees, currentProject, onWorktreeCreated]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleUpdateFeature = useCallback(
|
const handleUpdateFeature = useCallback(
|
||||||
@@ -139,6 +173,43 @@ export function useBoardActions({
|
|||||||
) => {
|
) => {
|
||||||
const finalBranchName = updates.branchName || undefined;
|
const finalBranchName = updates.branchName || undefined;
|
||||||
|
|
||||||
|
// If worktrees enabled and a branch is specified, create the worktree now
|
||||||
|
// This ensures the worktree exists before the feature starts
|
||||||
|
if (useWorktrees && finalBranchName && currentProject) {
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (api?.worktree?.create) {
|
||||||
|
const result = await api.worktree.create(
|
||||||
|
currentProject.path,
|
||||||
|
finalBranchName
|
||||||
|
);
|
||||||
|
if (result.success) {
|
||||||
|
console.log(
|
||||||
|
`[Board] Worktree for branch "${finalBranchName}" ${
|
||||||
|
result.worktree?.isNew ? "created" : "already exists"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
// Refresh worktree list in UI
|
||||||
|
onWorktreeCreated?.();
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`[Board] Failed to create worktree for branch "${finalBranchName}":`,
|
||||||
|
result.error
|
||||||
|
);
|
||||||
|
toast.error("Failed to create worktree", {
|
||||||
|
description: result.error || "An error occurred",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Board] Error creating worktree:", error);
|
||||||
|
toast.error("Failed to create worktree", {
|
||||||
|
description:
|
||||||
|
error instanceof Error ? error.message : "An error occurred",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const finalUpdates = {
|
const finalUpdates = {
|
||||||
...updates,
|
...updates,
|
||||||
branchName: finalBranchName,
|
branchName: finalBranchName,
|
||||||
@@ -151,7 +222,7 @@ export function useBoardActions({
|
|||||||
}
|
}
|
||||||
setEditingFeature(null);
|
setEditingFeature(null);
|
||||||
},
|
},
|
||||||
[updateFeature, persistFeatureUpdate, saveCategory, setEditingFeature]
|
[updateFeature, persistFeatureUpdate, saveCategory, setEditingFeature, useWorktrees, currentProject, onWorktreeCreated]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteFeature = useCallback(
|
const handleDeleteFeature = useCallback(
|
||||||
|
|||||||
@@ -520,29 +520,28 @@ export class AutoModeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Derive workDir from feature.branchName
|
// Derive workDir from feature.branchName
|
||||||
// If no branchName, derive from feature ID: feature/{featureId}
|
// Worktrees should already be created when the feature is added/edited
|
||||||
let worktreePath: string | null = null;
|
let worktreePath: string | null = null;
|
||||||
const branchName = feature.branchName || `feature/${featureId}`;
|
const branchName = feature.branchName;
|
||||||
|
|
||||||
if (useWorktrees && branchName) {
|
if (useWorktrees && branchName) {
|
||||||
// Try to find existing worktree for this branch
|
// Try to find existing worktree for this branch
|
||||||
|
// Worktree should already exist (created when feature was added/edited)
|
||||||
worktreePath = await this.findExistingWorktreeForBranch(
|
worktreePath = await this.findExistingWorktreeForBranch(
|
||||||
projectPath,
|
projectPath,
|
||||||
branchName
|
branchName
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!worktreePath) {
|
if (worktreePath) {
|
||||||
// Create worktree for this branch
|
console.log(
|
||||||
worktreePath = await this.setupWorktree(
|
`[AutoMode] Using worktree for branch "${branchName}": ${worktreePath}`
|
||||||
projectPath,
|
);
|
||||||
featureId,
|
} else {
|
||||||
branchName
|
// Worktree doesn't exist - log warning and continue with project path
|
||||||
|
console.warn(
|
||||||
|
`[AutoMode] Worktree for branch "${branchName}" not found, using project path`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[AutoMode] Using worktree for branch "${branchName}": ${worktreePath}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure workDir is always an absolute path for cross-platform compatibility
|
// Ensure workDir is always an absolute path for cross-platform compatibility
|
||||||
@@ -1410,60 +1409,6 @@ Format your response as a structured markdown document.`;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setupWorktree(
|
|
||||||
projectPath: string,
|
|
||||||
featureId: string,
|
|
||||||
branchName: string
|
|
||||||
): Promise<string> {
|
|
||||||
// First, check if git already has a worktree for this branch (anywhere)
|
|
||||||
const existingWorktree = await this.findExistingWorktreeForBranch(
|
|
||||||
projectPath,
|
|
||||||
branchName
|
|
||||||
);
|
|
||||||
if (existingWorktree) {
|
|
||||||
// Path is already resolved to absolute in findExistingWorktreeForBranch
|
|
||||||
console.log(
|
|
||||||
`[AutoMode] Found existing worktree for branch "${branchName}" at: ${existingWorktree}`
|
|
||||||
);
|
|
||||||
return existingWorktree;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Git worktrees stay in project directory
|
|
||||||
const worktreesDir = path.join(projectPath, ".worktrees");
|
|
||||||
const worktreePath = path.join(worktreesDir, featureId);
|
|
||||||
|
|
||||||
await fs.mkdir(worktreesDir, { recursive: true });
|
|
||||||
|
|
||||||
// Check if worktree directory already exists (might not be linked to branch)
|
|
||||||
try {
|
|
||||||
await fs.access(worktreePath);
|
|
||||||
// Return absolute path for cross-platform compatibility
|
|
||||||
return path.resolve(worktreePath);
|
|
||||||
} catch {
|
|
||||||
// Create new worktree
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create branch if it doesn't exist
|
|
||||||
try {
|
|
||||||
await execAsync(`git branch ${branchName}`, { cwd: projectPath });
|
|
||||||
} catch {
|
|
||||||
// Branch may already exist
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create worktree
|
|
||||||
try {
|
|
||||||
await execAsync(`git worktree add "${worktreePath}" ${branchName}`, {
|
|
||||||
cwd: projectPath,
|
|
||||||
});
|
|
||||||
// Return absolute path for cross-platform compatibility
|
|
||||||
return path.resolve(worktreePath);
|
|
||||||
} catch (error) {
|
|
||||||
// Worktree creation failed, fall back to direct execution
|
|
||||||
console.error(`[AutoMode] Worktree creation failed:`, error);
|
|
||||||
return path.resolve(projectPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadFeature(
|
private async loadFeature(
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
featureId: string
|
featureId: string
|
||||||
|
|||||||
Reference in New Issue
Block a user