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

@@ -169,7 +169,8 @@ export function WorktreeActionsDropdown({
Commit Changes
</DropdownMenuItem>
)}
{(worktree.branch !== "main" || worktree.hasChanges) && (
{/* Show PR option for non-primary worktrees, or primary worktree with changes */}
{(!worktree.isMain || worktree.hasChanges) && (
<DropdownMenuItem onClick={() => onCreatePR(worktree)} className="text-xs">
<GitPullRequest className="w-3.5 h-3.5 mr-2" />
Create Pull Request

View File

@@ -1,46 +1,34 @@
import { useCallback } from "react";
import { pathsEqual } from "@/lib/utils";
import type { WorktreeInfo, FeatureInfo } from "../types";
interface UseRunningFeaturesOptions {
projectPath: string;
runningFeatureIds: string[];
features: FeatureInfo[];
getWorktreeKey: (worktree: WorktreeInfo) => string;
}
export function useRunningFeatures({
projectPath,
runningFeatureIds,
features,
getWorktreeKey,
}: UseRunningFeaturesOptions) {
const hasRunningFeatures = useCallback(
(worktree: WorktreeInfo) => {
if (runningFeatureIds.length === 0) return false;
const worktreeKey = getWorktreeKey(worktree);
return runningFeatureIds.some((featureId) => {
const feature = features.find((f) => f.id === featureId);
if (!feature) return false;
if (feature.worktreePath) {
if (worktree.isMain) {
return pathsEqual(feature.worktreePath, projectPath);
}
return pathsEqual(feature.worktreePath, worktreeKey);
}
// Match by branchName only (worktreePath is no longer stored)
if (feature.branchName) {
return worktree.branch === feature.branchName;
}
// No branch assigned - belongs to main worktree
return worktree.isMain;
});
},
[runningFeatureIds, features, projectPath, getWorktreeKey]
[runningFeatureIds, features]
);
return {

View File

@@ -5,7 +5,7 @@ import { toast } from "sonner";
import type { WorktreeInfo } from "../types";
interface UseWorktreeActionsOptions {
fetchWorktrees: () => Promise<void>;
fetchWorktrees: () => Promise<Array<{ path: string; branch: string }> | undefined>;
fetchBranches: (worktreePath: string) => Promise<void>;
}

View File

@@ -8,9 +8,10 @@ import type { WorktreeInfo } from "../types";
interface UseWorktreesOptions {
projectPath: string;
refreshTrigger?: number;
onRemovedWorktrees?: (removedWorktrees: Array<{ path: string; branch: string }>) => void;
}
export function useWorktrees({ projectPath, refreshTrigger = 0 }: UseWorktreesOptions) {
export function useWorktrees({ projectPath, refreshTrigger = 0, onRemovedWorktrees }: UseWorktreesOptions) {
const [isLoading, setIsLoading] = useState(false);
const [worktrees, setWorktrees] = useState<WorktreeInfo[]>([]);
@@ -33,8 +34,11 @@ export function useWorktrees({ projectPath, refreshTrigger = 0 }: UseWorktreesOp
setWorktrees(result.worktrees);
setWorktreesInStore(projectPath, result.worktrees);
}
// Return removed worktrees so they can be handled by the caller
return result.removedWorktrees;
} catch (error) {
console.error("Failed to fetch worktrees:", error);
return undefined;
} finally {
setIsLoading(false);
}
@@ -46,9 +50,13 @@ export function useWorktrees({ projectPath, refreshTrigger = 0 }: UseWorktreesOp
useEffect(() => {
if (refreshTrigger > 0) {
fetchWorktrees();
fetchWorktrees().then((removedWorktrees) => {
if (removedWorktrees && removedWorktrees.length > 0 && onRemovedWorktrees) {
onRemovedWorktrees(removedWorktrees);
}
});
}
}, [refreshTrigger, fetchWorktrees]);
}, [refreshTrigger, fetchWorktrees, onRemovedWorktrees]);
useEffect(() => {
if (worktrees.length > 0) {
@@ -58,6 +66,8 @@ export function useWorktrees({ projectPath, refreshTrigger = 0 }: UseWorktreesOp
: worktrees.some((w) => !w.isMain && pathsEqual(w.path, currentPath));
if (currentWorktree == null || (currentPath !== null && !currentWorktreeExists)) {
// Find the primary worktree and get its branch name
// Fallback to "main" only if worktrees haven't loaded yet
const mainWorktree = worktrees.find((w) => w.isMain);
const mainBranch = mainWorktree?.branch || "main";
setCurrentWorktree(projectPath, null, mainBranch);

View File

@@ -22,7 +22,6 @@ export interface DevServerInfo {
export interface FeatureInfo {
id: string;
worktreePath?: string;
branchName?: string;
}
@@ -33,6 +32,7 @@ export interface WorktreePanelProps {
onCommit: (worktree: WorktreeInfo) => void;
onCreatePR: (worktree: WorktreeInfo) => void;
onCreateBranch: (worktree: WorktreeInfo) => void;
onRemovedWorktrees?: (removedWorktrees: Array<{ path: string; branch: string }>) => void;
runningFeatureIds?: string[];
features?: FeatureInfo[];
refreshTrigger?: number;

View File

@@ -20,6 +20,7 @@ export function WorktreePanel({
onCommit,
onCreatePR,
onCreateBranch,
onRemovedWorktrees,
runningFeatureIds = [],
features = [],
refreshTrigger = 0,
@@ -32,7 +33,7 @@ export function WorktreePanel({
useWorktreesEnabled,
fetchWorktrees,
handleSelectWorktree,
} = useWorktrees({ projectPath, refreshTrigger });
} = useWorktrees({ projectPath, refreshTrigger, onRemovedWorktrees });
const {
isStartingDevServer,
@@ -73,10 +74,8 @@ export function WorktreePanel({
const { defaultEditorName } = useDefaultEditor();
const { hasRunningFeatures } = useRunningFeatures({
projectPath,
runningFeatureIds,
features,
getWorktreeKey,
});
const isWorktreeSelected = (worktree: WorktreeInfo) => {
@@ -162,7 +161,12 @@ export function WorktreePanel({
variant="ghost"
size="sm"
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
onClick={fetchWorktrees}
onClick={async () => {
const removedWorktrees = await fetchWorktrees();
if (removedWorktrees && removedWorktrees.length > 0 && onRemovedWorktrees) {
onRemovedWorktrees(removedWorktrees);
}
}}
disabled={isLoading}
title="Refresh worktrees"
>