feat: enhance worktree management and feature filtering

- Added logic to show all local branches as suggestions in the branch autocomplete, allowing users to type new branch names.
- Implemented current worktree information retrieval for filtering features based on the selected worktree's branch.
- Updated feature handling to filter backlog features by the currently selected worktree branch, ensuring only relevant features are displayed.
- Enhanced the WorktreeSelector component to utilize branch names for determining the appropriate worktree for features.
- Introduced integration tests for worktree creation, deletion, and feature management to ensure robust functionality.
This commit is contained in:
Cody Seibert
2025-12-16 16:36:47 -05:00
parent 04d263b1ed
commit 30db67f89c
4 changed files with 693 additions and 24 deletions

View File

@@ -215,6 +215,8 @@ export function BoardView() {
}, [hookFeatures, persistedCategories]);
// Branch suggestions for the branch autocomplete
// Shows all local branches as suggestions, but users can type any new branch name
// When the feature is started, a worktree will be created if needed
const [branchSuggestions, setBranchSuggestions] = useState<string[]>([]);
// Fetch branches when project changes or worktrees are created/modified
@@ -283,6 +285,23 @@ export function BoardView() {
});
}, [hookFeatures, runningAutoTasks]);
// Get current worktree info (path and branch) for filtering features
// This needs to be before useBoardActions so we can pass currentWorktreeBranch
const currentWorktreeInfo = currentProject ? getCurrentWorktree(currentProject.path) : null;
const currentWorktreePath = currentWorktreeInfo?.path ?? null;
const currentWorktreeBranch = currentWorktreeInfo?.branch ?? null;
const worktreesByProject = useAppStore((s) => s.worktreesByProject);
const worktrees = useMemo(
() => (currentProject ? (worktreesByProject[currentProject.path] ?? EMPTY_WORKTREES) : EMPTY_WORKTREES),
[currentProject, worktreesByProject]
);
// Get the branch for the currently selected worktree (for defaulting new features)
// Use the branch from currentWorktreeInfo, or fall back to main worktree's branch
const selectedWorktreeBranch = currentWorktreeBranch
|| worktrees.find(w => w.isMain)?.branch
|| "main";
// Extract all action handlers into a hook
const {
handleAddFeature,
@@ -329,6 +348,7 @@ export function BoardView() {
outputFeature,
projectPath: currentProject?.path || null,
onWorktreeCreated: () => setWorktreeRefreshKey((k) => k + 1),
currentWorktreeBranch,
});
// Use keyboard shortcuts hook (after actions hook)
@@ -341,22 +361,6 @@ export function BoardView() {
});
// Use drag and drop hook
// Get current worktree info (path and branch) for filtering features
const currentWorktreeInfo = currentProject ? getCurrentWorktree(currentProject.path) : null;
const currentWorktreePath = currentWorktreeInfo?.path ?? null;
const currentWorktreeBranch = currentWorktreeInfo?.branch ?? null;
const worktreesByProject = useAppStore((s) => s.worktreesByProject);
const worktrees = useMemo(
() => (currentProject ? (worktreesByProject[currentProject.path] ?? EMPTY_WORKTREES) : EMPTY_WORKTREES),
[currentProject, worktreesByProject]
);
// Get the branch for the currently selected worktree (for defaulting new features)
// Use the branch from currentWorktreeInfo, or fall back to main worktree's branch
const selectedWorktreeBranch = currentWorktreeBranch
|| worktrees.find(w => w.isMain)?.branch
|| "main";
const { activeFeature, handleDragStart, handleDragEnd } = useBoardDragDrop({
features: hookFeatures,
currentProject,
@@ -448,7 +452,7 @@ export function BoardView() {
setShowCreateBranchDialog(true);
}}
runningFeatureIds={runningAutoTasks}
features={hookFeatures.map(f => ({ id: f.id, worktreePath: f.worktreePath }))}
features={hookFeatures.map(f => ({ id: f.id, worktreePath: f.worktreePath, branchName: f.branchName }))}
/>
{/* Main Content Area */}

View File

@@ -62,6 +62,7 @@ interface DevServerInfo {
interface FeatureInfo {
id: string;
worktreePath?: string;
branchName?: string; // Used as fallback to determine which worktree the spinner should show on
}
interface WorktreeSelectorProps {
@@ -302,14 +303,25 @@ export function WorktreeSelector({
const feature = features.find((f) => f.id === featureId);
if (!feature) return false;
// For main worktree, check features with no worktreePath or matching projectPath
// First, check if worktreePath is set and matches
// Use pathsEqual for cross-platform compatibility (Windows uses backslashes)
if (worktree.isMain) {
return !feature.worktreePath || pathsEqual(feature.worktreePath, projectPath);
if (feature.worktreePath) {
if (worktree.isMain) {
// Feature has worktreePath - show on main only if it matches projectPath
return pathsEqual(feature.worktreePath, projectPath);
}
// For non-main worktrees, check if worktreePath matches
return pathsEqual(feature.worktreePath, worktreeKey);
}
// For other worktrees, check if worktreePath matches
return pathsEqual(feature.worktreePath, worktreeKey);
// If worktreePath is not set, use branchName as fallback
if (feature.branchName) {
// Feature has a branchName - show spinner on the worktree with matching branch
return worktree.branch === feature.branchName;
}
// No worktreePath and no branchName - default to main
return worktree.isMain;
});
};

View File

@@ -39,6 +39,7 @@ interface UseBoardActionsProps {
outputFeature: Feature | null;
projectPath: string | null;
onWorktreeCreated?: () => void;
currentWorktreeBranch: string | null; // Branch name of the selected worktree for filtering
}
export function useBoardActions({
@@ -65,6 +66,7 @@ export function useBoardActions({
outputFeature,
projectPath,
onWorktreeCreated,
currentWorktreeBranch,
}: UseBoardActionsProps) {
const {
addFeature,
@@ -720,7 +722,24 @@ export function useBoardActions({
);
const handleStartNextFeatures = useCallback(async () => {
const backlogFeatures = features.filter((f) => f.status === "backlog");
// Filter backlog features by the currently selected worktree branch
// This ensures "G" only starts features from the filtered list
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";
// If no worktree is selected (currentWorktreeBranch is null or main-like),
// show features with no branch or "main"/"master" branch
if (!currentWorktreeBranch || currentWorktreeBranch === "main" || currentWorktreeBranch === "master") {
return !f.branchName || featureBranch === "main" || featureBranch === "master";
}
// Otherwise, only show features matching the selected worktree branch
return featureBranch === currentWorktreeBranch;
});
const availableSlots =
useAppStore.getState().maxConcurrency - runningAutoTasks.length;
@@ -734,7 +753,9 @@ export function useBoardActions({
if (backlogFeatures.length === 0) {
toast.info("Backlog empty", {
description: "No features in backlog to start.",
description: currentWorktreeBranch && currentWorktreeBranch !== "main" && currentWorktreeBranch !== "master"
? `No features in backlog for branch "${currentWorktreeBranch}".`
: "No features in backlog to start.",
});
return;
}
@@ -764,6 +785,7 @@ export function useBoardActions({
getOrCreateWorktreeForFeature,
persistFeatureUpdate,
onWorktreeCreated,
currentWorktreeBranch,
]);
const handleDeleteAllVerified = useCallback(async () => {