Merge main into refactor/frontend

Merge latest features from main including:
- PR #161 (worktree-confusion): Clarified branch handling in dialogs
- PR #160 (speckits-rebase): Planning mode functionality

Resolved conflicts:
- add-feature-dialog.tsx: Combined TanStack Router navigation with branch selection state
- worktree-integration.spec.ts: Updated tests for new worktree behavior (created at execution time)
- package-lock.json: Regenerated after merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kacper
2025-12-18 12:00:45 +01:00
71 changed files with 10632 additions and 1192 deletions

View File

@@ -4,6 +4,7 @@ import {
FeatureImage,
AgentModel,
ThinkingLevel,
PlanningMode,
useAppStore,
} from "@/store/app-store";
import { FeatureImagePath as DescriptionImagePath } from "@/components/ui/description-image-dropzone";
@@ -76,67 +77,13 @@ export function useBoardActions({
moveFeature,
useWorktrees,
enableDependencyBlocking,
isPrimaryWorktreeBranch,
getPrimaryWorktreeBranch,
} = 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]
);
// Note: getOrCreateWorktreeForFeature removed - worktrees are now created server-side
// at execution time based on feature.branchName
const handleAddFeature = useCallback(
async (featureData: {
@@ -150,35 +97,27 @@ export function useBoardActions({
thinkingLevel: ThinkingLevel;
branchName: string;
priority: number;
planningMode: PlanningMode;
requirePlanApproval: boolean;
}) => {
let worktreePath: string | undefined;
// If worktrees are enabled and a non-main branch is selected, create the worktree
if (useWorktrees && featureData.branchName) {
const branchName = featureData.branchName;
if (branchName !== "main" && branchName !== "master") {
// Create a temporary feature-like object for getOrCreateWorktreeForFeature
const tempFeature = { branchName } as Feature;
const path = await getOrCreateWorktreeForFeature(tempFeature);
if (path && path !== projectPath) {
worktreePath = path;
// Refresh worktree selector after creating worktree
onWorktreeCreated?.();
}
}
}
// 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
// Non-empty string is the actual branch name (for non-primary worktrees)
const finalBranchName = featureData.branchName || undefined;
const newFeatureData = {
...featureData,
status: "backlog" as const,
worktreePath,
branchName: finalBranchName,
// No worktreePath - derived at runtime from branchName
};
const createdFeature = addFeature(newFeatureData);
// Must await to ensure feature exists on server before user can drag it
await persistFeatureCreate(createdFeature);
saveCategory(featureData.category);
},
[addFeature, persistFeatureCreate, saveCategory, useWorktrees, getOrCreateWorktreeForFeature, projectPath, onWorktreeCreated]
[addFeature, persistFeatureCreate, saveCategory]
);
const handleUpdateFeature = useCallback(
@@ -194,46 +133,16 @@ export function useBoardActions({
imagePaths: DescriptionImagePath[];
branchName: string;
priority: number;
planningMode?: PlanningMode;
requirePlanApproval?: boolean;
}
) => {
// Get the current feature to check if branch is changing
const currentFeature = features.find((f) => f.id === featureId);
const currentBranch = currentFeature?.branchName || "main";
const newBranch = updates.branchName || "main";
const branchIsChanging = currentBranch !== newBranch;
const finalBranchName = updates.branchName || undefined;
let worktreePath: string | undefined;
let shouldClearWorktreePath = false;
// If worktrees are enabled and branch is changing to a non-main branch, create worktree
if (useWorktrees && branchIsChanging) {
if (newBranch === "main" || newBranch === "master") {
// Changing to main - clear the worktreePath
shouldClearWorktreePath = true;
} else {
// Changing to a feature branch - create worktree if needed
const tempFeature = { branchName: newBranch } as Feature;
const path = await getOrCreateWorktreeForFeature(tempFeature);
if (path && path !== projectPath) {
worktreePath = path;
// Refresh worktree selector after creating worktree
onWorktreeCreated?.();
}
}
}
// Build final updates with worktreePath if it was changed
let finalUpdates: typeof updates & { worktreePath?: string };
if (branchIsChanging && useWorktrees) {
if (shouldClearWorktreePath) {
// Use null to clear the value in persistence (cast to work around type system)
finalUpdates = { ...updates, worktreePath: null as unknown as string | undefined };
} else {
finalUpdates = { ...updates, worktreePath };
}
} else {
finalUpdates = updates;
}
const finalUpdates = {
...updates,
branchName: finalBranchName,
};
updateFeature(featureId, finalUpdates);
persistFeatureUpdate(featureId, finalUpdates);
@@ -242,7 +151,7 @@ export function useBoardActions({
}
setEditingFeature(null);
},
[updateFeature, persistFeatureUpdate, saveCategory, setEditingFeature, features, useWorktrees, getOrCreateWorktreeForFeature, projectPath, onWorktreeCreated]
[updateFeature, persistFeatureUpdate, saveCategory, setEditingFeature]
);
const handleDeleteFeature = useCallback(
@@ -307,21 +216,18 @@ 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;
// Server derives workDir from feature.branchName at execution time
const result = await api.autoMode.runFeature(
currentProject.path,
feature.id,
useWorktrees,
featureWorktreePath || undefined
useWorktrees
// No worktreePath - server derives from feature.branchName
);
if (result.success) {
console.log(
"[Board] Feature run started successfully in worktree:",
featureWorktreePath || "main"
"[Board] Feature run started successfully, branch:",
feature.branchName || "default"
);
} else {
console.error("[Board] Failed to run feature:", result.error);
@@ -350,10 +256,12 @@ export function useBoardActions({
if (enableDependencyBlocking) {
const blockingDeps = getBlockingDependencies(feature, features);
if (blockingDeps.length > 0) {
const depDescriptions = blockingDeps.map(depId => {
const dep = features.find(f => f.id === depId);
return dep ? truncateDescription(dep.description, 40) : depId;
}).join(", ");
const depDescriptions = blockingDeps
.map((depId) => {
const dep = features.find((f) => f.id === depId);
return dep ? truncateDescription(dep.description, 40) : depId;
})
.join(", ");
toast.warning("Starting feature with incomplete dependencies", {
description: `This feature depends on: ${depDescriptions}`,
@@ -372,7 +280,14 @@ export function useBoardActions({
await handleRunFeature(feature);
return true;
},
[autoMode, enableDependencyBlocking, features, updateFeature, persistFeatureUpdate, handleRunFeature]
[
autoMode,
enableDependencyBlocking,
features,
updateFeature,
persistFeatureUpdate,
handleRunFeature,
]
);
const handleVerifyFeature = useCallback(
@@ -489,7 +404,6 @@ export function useBoardActions({
const featureId = followUpFeature.id;
const featureDescription = followUpFeature.description;
const prompt = followUpPrompt;
const api = getElectronAPI();
if (!api?.autoMode?.followUpFeature) {
@@ -521,15 +435,14 @@ 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;
// Server derives workDir from feature.branchName at execution time
api.autoMode
.followUpFeature(
currentProject.path,
followUpFeature.id,
followUpPrompt,
imagePaths,
featureWorktreePath
imagePaths
// No worktreePath - server derives from feature.branchName
)
.catch((error) => {
console.error("[Board] Error sending follow-up:", error);
@@ -569,11 +482,11 @@ export function useBoardActions({
return;
}
// Pass the feature's worktreePath to ensure commits happen in the correct worktree
// Server derives workDir from feature.branchName
const result = await api.autoMode.commitFeature(
currentProject.path,
feature.id,
feature.worktreePath
feature.id
// No worktreePath - server derives from feature.branchName
);
if (result.success) {
@@ -758,23 +671,25 @@ export function useBoardActions({
const handleStartNextFeatures = useCallback(async () => {
// Filter backlog features by the currently selected worktree branch
// This ensures "G" only starts features from the filtered list
const primaryBranch = projectPath
? getPrimaryWorktreeBranch(projectPath)
: null;
const backlogFeatures = features.filter((f) => {
if (f.status !== "backlog") return false;
// Determine the feature's branch (default to "main" if not set)
const featureBranch = f.branchName || "main";
// Determine the feature's branch (default to primary branch if not set)
const featureBranch = f.branchName || primaryBranch || "main";
// If no worktree is selected (currentWorktreeBranch is null or main-like),
// show features with no branch or "main"/"master" branch
// If no worktree is selected (currentWorktreeBranch is null or matches primary),
// show features with no branch or primary branch
if (
!currentWorktreeBranch ||
currentWorktreeBranch === "main" ||
currentWorktreeBranch === "master"
(projectPath &&
isPrimaryWorktreeBranch(projectPath, currentWorktreeBranch))
) {
return (
!f.branchName ||
featureBranch === "main" ||
featureBranch === "master"
(projectPath && isPrimaryWorktreeBranch(projectPath, featureBranch))
);
}
@@ -794,57 +709,65 @@ export function useBoardActions({
}
if (backlogFeatures.length === 0) {
const isOnPrimaryBranch =
!currentWorktreeBranch ||
(projectPath &&
isPrimaryWorktreeBranch(projectPath, currentWorktreeBranch));
toast.info("Backlog empty", {
description:
currentWorktreeBranch &&
currentWorktreeBranch !== "main" &&
currentWorktreeBranch !== "master"
? `No features in backlog for branch "${currentWorktreeBranch}".`
: "No features in backlog to start.",
description: !isOnPrimaryBranch
? `No features in backlog for branch "${currentWorktreeBranch}".`
: "No features in backlog to start.",
});
return;
}
// Sort by priority (lower number = higher priority, priority 1 is highest)
// This matches the auto mode service behavior for consistency
const sortedBacklog = [...backlogFeatures].sort(
(a, b) => (a.priority || 999) - (b.priority || 999)
);
// Features with blocking dependencies are sorted to the end
const sortedBacklog = [...backlogFeatures].sort((a, b) => {
const aBlocked = enableDependencyBlocking
? getBlockingDependencies(a, features).length > 0
: false;
const bBlocked = enableDependencyBlocking
? getBlockingDependencies(b, features).length > 0
: false;
// Blocked features go to the end
if (aBlocked && !bBlocked) return 1;
if (!aBlocked && bBlocked) return -1;
// Within same blocked/unblocked group, sort by priority
return (a.priority || 999) - (b.priority || 999);
});
// Find the first feature without blocking dependencies
const featureToStart = sortedBacklog.find((f) => {
if (!enableDependencyBlocking) return true;
return getBlockingDependencies(f, features).length === 0;
});
if (!featureToStart) {
toast.info("No eligible features", {
description:
"All backlog features have unmet dependencies. Complete their dependencies first.",
});
return;
}
// Start only one feature per keypress (user must press again for next)
const featuresToStart = sortedBacklog.slice(0, 1);
for (const feature of featuresToStart) {
// Only create worktrees if the feature is enabled
let worktreePath: string | null = null;
if (useWorktrees) {
// Get or create worktree based on the feature's assigned branch (same as drag-to-in-progress)
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,
});
}
// Simplified: No worktree creation on client - server derives workDir from feature.branchName
await handleStartImplementation(featureToStart);
}, [
features,
runningAutoTasks,
handleStartImplementation,
getOrCreateWorktreeForFeature,
persistFeatureUpdate,
onWorktreeCreated,
currentWorktreeBranch,
useWorktrees,
projectPath,
isPrimaryWorktreeBranch,
getPrimaryWorktreeBranch,
enableDependencyBlocking,
]);
const handleDeleteAllVerified = useCallback(async () => {
const handleArchiveAllVerified = useCallback(async () => {
const verifiedFeatures = features.filter((f) => f.status === "verified");
for (const feature of verifiedFeatures) {
@@ -853,22 +776,29 @@ export function useBoardActions({
try {
await autoMode.stopFeature(feature.id);
} catch (error) {
console.error("[Board] Error stopping feature before delete:", error);
console.error(
"[Board] Error stopping feature before archive:",
error
);
}
}
removeFeature(feature.id);
persistFeatureDelete(feature.id);
// Archive the feature by setting status to completed
const updates = {
status: "completed" as const,
};
updateFeature(feature.id, updates);
persistFeatureUpdate(feature.id, updates);
}
toast.success("All verified features deleted", {
description: `Deleted ${verifiedFeatures.length} feature(s).`,
toast.success("All verified features archived", {
description: `Archived ${verifiedFeatures.length} feature(s).`,
});
}, [
features,
runningAutoTasks,
autoMode,
removeFeature,
persistFeatureDelete,
updateFeature,
persistFeatureUpdate,
]);
return {
@@ -890,6 +820,6 @@ export function useBoardActions({
handleOutputModalNumberKeyPress,
handleForceStopFeature,
handleStartNextFeatures,
handleDeleteAllVerified,
handleArchiveAllVerified,
};
}

View File

@@ -1,7 +1,6 @@
import { useMemo, useCallback } from "react";
import { Feature } from "@/store/app-store";
import { resolveDependencies } from "@/lib/dependency-resolver";
import { pathsEqual } from "@/lib/utils";
import { Feature, useAppStore } from "@/store/app-store";
import { resolveDependencies, getBlockingDependencies } from "@/lib/dependency-resolver";
type ColumnId = Feature["status"];
@@ -56,26 +55,24 @@ export function useBoardColumnFeatures({
// 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
// Match by worktreePath if set, OR by branchName if set
// Features with neither are considered unassigned (show on ALL worktrees)
const featureBranch = f.branchName || "main";
const hasWorktreeAssigned = f.worktreePath || f.branchName;
// Check if feature matches the current worktree by branchName
// Features without branchName are considered unassigned (show only on primary worktree)
const featureBranch = f.branchName;
let matchesWorktree: boolean;
if (!hasWorktreeAssigned) {
// No worktree or branch assigned - show on ALL worktrees (unassigned)
matchesWorktree = true;
} else if (f.worktreePath) {
// Has worktreePath - match by path (use pathsEqual for cross-platform compatibility)
matchesWorktree = pathsEqual(f.worktreePath, effectiveWorktreePath);
if (!featureBranch) {
// No branch assigned - show only on primary worktree
const isViewingPrimary = currentWorktreePath === null;
matchesWorktree = isViewingPrimary;
} else if (effectiveBranch === null) {
// We're viewing main but branch hasn't been initialized yet
// (worktrees disabled or haven't loaded yet).
// Show features assigned to main/master branch since we're on the main worktree.
matchesWorktree = featureBranch === "main" || featureBranch === "master";
// Show features assigned to primary worktree's branch.
matchesWorktree = projectPath
? useAppStore.getState().isPrimaryWorktreeBranch(projectPath, featureBranch)
: false;
} else {
// Has branchName but no worktreePath - match by branch name
// Match by branch name
matchesWorktree = featureBranch === effectiveBranch;
}
@@ -101,7 +98,9 @@ export function useBoardColumnFeatures({
}
} else {
// Unknown status, default to backlog
map.backlog.push(f);
if (matchesWorktree) {
map.backlog.push(f);
}
}
}
});
@@ -111,7 +110,29 @@ export function useBoardColumnFeatures({
// Within the same dependency level, features are sorted by priority
if (map.backlog.length > 0) {
const { orderedFeatures } = resolveDependencies(map.backlog);
map.backlog = orderedFeatures;
// Get all features to check blocking dependencies against
const allFeatures = features;
const enableDependencyBlocking = useAppStore.getState().enableDependencyBlocking;
// Sort blocked features to the end of the backlog
// This keeps the dependency order within each group (unblocked/blocked)
if (enableDependencyBlocking) {
const unblocked: Feature[] = [];
const blocked: Feature[] = [];
for (const f of orderedFeatures) {
if (getBlockingDependencies(f, allFeatures).length > 0) {
blocked.push(f);
} else {
unblocked.push(f);
}
}
map.backlog = [...unblocked, ...blocked];
} else {
map.backlog = orderedFeatures;
}
}
return map;

View File

@@ -4,7 +4,6 @@ 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[];
@@ -15,8 +14,6 @@ interface UseBoardDragDropProps {
updates: Partial<Feature>
) => Promise<void>;
handleStartImplementation: (feature: Feature) => Promise<boolean>;
projectPath: string | null; // Main project path
onWorktreeCreated?: () => void; // Callback when a new worktree is created
}
export function useBoardDragDrop({
@@ -25,66 +22,12 @@ export function useBoardDragDrop({
runningAutoTasks,
persistFeatureUpdate,
handleStartImplementation,
projectPath,
onWorktreeCreated,
}: UseBoardDragDropProps) {
const [activeFeature, setActiveFeature] = useState<Feature | null>(null);
const { moveFeature, useWorktrees } = useAppStore();
const { moveFeature } = useAppStore();
/**
* 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]
);
// Note: getOrCreateWorktreeForFeature removed - worktrees are now created server-side
// at execution time based on feature.branchName
const handleDragStart = useCallback(
(event: DragStartEvent) => {
@@ -118,17 +61,13 @@ export function useBoardDragDrop({
// - Backlog items can always be dragged
// - waiting_approval items can always be dragged (to allow manual verification via drag)
// - verified items can always be dragged (to allow moving back to waiting_approval)
// - skipTests (non-TDD) items can be dragged between in_progress and verified
// - Non-skipTests (TDD) items that are in progress cannot be dragged (they are running)
if (
draggedFeature.status !== "backlog" &&
draggedFeature.status !== "waiting_approval" &&
draggedFeature.status !== "verified"
) {
// Only allow dragging in_progress if it's a skipTests feature and not currently running
if (!draggedFeature.skipTests || isRunningTask) {
// - in_progress items can be dragged (but not if they're currently running)
// - Non-skipTests (TDD) items that are in progress cannot be dragged if they are running
if (draggedFeature.status === "in_progress") {
// Only allow dragging in_progress if it's not currently running
if (isRunningTask) {
console.log(
"[Board] Cannot drag feature - TDD feature or currently running"
"[Board] Cannot drag feature - currently running"
);
return;
}
@@ -154,23 +93,13 @@ export function useBoardDragDrop({
if (targetStatus === draggedFeature.status) return;
// Handle different drag scenarios
// Note: Worktrees are created server-side at execution time based on feature.branchName
if (draggedFeature.status === "backlog") {
// From backlog
if (targetStatus === "in_progress") {
// Only create worktrees if the feature is enabled
let worktreePath: string | null = null;
if (useWorktrees) {
// Get or create worktree based on the feature's assigned branch
worktreePath = await getOrCreateWorktreeForFeature(draggedFeature);
if (worktreePath) {
await persistFeatureUpdate(featureId, { worktreePath });
}
// Refresh worktree selector after moving to in_progress
onWorktreeCreated?.();
}
// Use helper function to handle concurrency check and start implementation
// Pass feature with worktreePath so handleRunFeature uses the correct path
await handleStartImplementation({ ...draggedFeature, worktreePath: worktreePath || undefined });
// Server will derive workDir from feature.branchName
await handleStartImplementation(draggedFeature);
} else {
moveFeature(featureId, targetStatus);
persistFeatureUpdate(featureId, { status: targetStatus });
@@ -195,11 +124,10 @@ export function useBoardDragDrop({
} else if (targetStatus === "backlog") {
// Allow moving waiting_approval cards back to backlog
moveFeature(featureId, "backlog");
// Clear justFinishedAt timestamp and worktreePath when moving back to backlog
// Clear justFinishedAt timestamp when moving back to backlog
persistFeatureUpdate(featureId, {
status: "backlog",
justFinishedAt: undefined,
worktreePath: undefined,
});
toast.info("Feature moved to backlog", {
description: `Moved to Backlog: ${draggedFeature.description.slice(
@@ -208,13 +136,23 @@ export function useBoardDragDrop({
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
});
}
} else if (draggedFeature.skipTests) {
// skipTests feature being moved between in_progress and verified
if (
} else if (draggedFeature.status === "in_progress") {
// Handle in_progress features being moved
if (targetStatus === "backlog") {
// Allow moving in_progress cards back to backlog
moveFeature(featureId, "backlog");
persistFeatureUpdate(featureId, { status: "backlog" });
toast.info("Feature moved to backlog", {
description: `Moved to Backlog: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
});
} else if (
targetStatus === "verified" &&
draggedFeature.status === "in_progress"
draggedFeature.skipTests
) {
// Manual verify via drag
// Manual verify via drag (only for skipTests features)
moveFeature(featureId, "verified");
persistFeatureUpdate(featureId, { status: "verified" });
toast.success("Feature verified", {
@@ -223,7 +161,10 @@ export function useBoardDragDrop({
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
});
} else if (
}
} else if (draggedFeature.skipTests) {
// skipTests feature being moved between verified and waiting_approval
if (
targetStatus === "waiting_approval" &&
draggedFeature.status === "verified"
) {
@@ -237,10 +178,9 @@ export function useBoardDragDrop({
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
});
} else if (targetStatus === "backlog") {
// Allow moving skipTests cards back to backlog
// Allow moving skipTests cards back to backlog (from verified)
moveFeature(featureId, "backlog");
// Clear worktreePath when moving back to backlog
persistFeatureUpdate(featureId, { status: "backlog", worktreePath: undefined });
persistFeatureUpdate(featureId, { status: "backlog" });
toast.info("Feature moved to backlog", {
description: `Moved to Backlog: ${draggedFeature.description.slice(
0,
@@ -263,8 +203,7 @@ export function useBoardDragDrop({
} else if (targetStatus === "backlog") {
// Allow moving verified cards back to backlog
moveFeature(featureId, "backlog");
// Clear worktreePath when moving back to backlog
persistFeatureUpdate(featureId, { status: "backlog", worktreePath: undefined });
persistFeatureUpdate(featureId, { status: "backlog" });
toast.info("Feature moved to backlog", {
description: `Moved to Backlog: ${draggedFeature.description.slice(
0,
@@ -280,9 +219,6 @@ export function useBoardDragDrop({
moveFeature,
persistFeatureUpdate,
handleStartImplementation,
getOrCreateWorktreeForFeature,
onWorktreeCreated,
useWorktrees,
]
);

View File

@@ -1,7 +1,6 @@
import { useEffect, useRef } from "react";
import { useEffect } from "react";
import { getElectronAPI } from "@/lib/electron";
import { useAppStore } from "@/store/app-store";
import { useAutoMode } from "@/hooks/use-auto-mode";
interface UseBoardEffectsProps {
currentProject: { path: string; id: string } | null;
@@ -28,8 +27,6 @@ export function useBoardEffects({
isLoading,
setFeaturesWithContext,
}: UseBoardEffectsProps) {
const autoMode = useAutoMode();
// Make current project available globally for modal
useEffect(() => {
if (currentProject) {
@@ -101,8 +98,7 @@ export function useBoardEffects({
const status = await api.autoMode.status(currentProject.path);
if (status.success) {
const projectId = currentProject.id;
const { clearRunningTasks, addRunningTask, setAutoModeRunning } =
useAppStore.getState();
const { clearRunningTasks, addRunningTask } = useAppStore.getState();
if (status.runningFeatures) {
console.log(
@@ -116,14 +112,6 @@ export function useBoardEffects({
addRunningTask(projectId, featureId);
});
}
const isAutoModeRunning =
status.autoLoopRunning ?? status.isRunning ?? false;
console.log(
"[Board] Syncing auto mode running state:",
isAutoModeRunning
);
setAutoModeRunning(projectId, isAutoModeRunning);
}
} catch (error) {
console.error("[Board] Failed to sync running tasks:", error);

View File

@@ -210,6 +210,11 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
.play()
.catch((err) => console.warn("Could not play ding sound:", err));
}
} else if (event.type === "plan_approval_required") {
// Reload features when plan is generated and requires approval
// This ensures the feature card shows the "Approve Plan" button
console.log("[Board] Plan approval required, reloading features...");
loadFeatures();
} else if (event.type === "auto_mode_error") {
// Reload features when an error occurs (feature moved to waiting_approval)
console.log(