mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
feat: enhance UI components and branch management
- Added new RadioGroup and Switch components for better UI interaction. - Introduced BranchSelector for improved branch selection in feature dialogs. - Updated Autocomplete and BranchAutocomplete components to handle error states. - Refactored feature management to archive verified features instead of deleting them. - Enhanced worktree handling by removing worktreePath from features, relying on branchName instead. - Improved auto mode functionality by integrating branch management and worktree updates. - Cleaned up unused code and optimized existing logic for better performance.
This commit is contained in:
@@ -45,8 +45,10 @@
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
|
||||
@@ -35,6 +35,7 @@ interface AutocompleteProps {
|
||||
emptyMessage?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
error?: boolean;
|
||||
icon?: LucideIcon;
|
||||
allowCreate?: boolean;
|
||||
createLabel?: (value: string) => string;
|
||||
@@ -58,6 +59,7 @@ export function Autocomplete({
|
||||
emptyMessage = "No results found.",
|
||||
className,
|
||||
disabled = false,
|
||||
error = false,
|
||||
icon: Icon,
|
||||
allowCreate = false,
|
||||
createLabel = (v) => `Create "${v}"`,
|
||||
@@ -130,6 +132,7 @@ export function Autocomplete({
|
||||
className={cn(
|
||||
"w-full justify-between",
|
||||
Icon && "font-mono text-sm",
|
||||
error && "border-destructive focus-visible:ring-destructive",
|
||||
className
|
||||
)}
|
||||
data-testid={testId}
|
||||
|
||||
@@ -11,6 +11,7 @@ interface BranchAutocompleteProps {
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
error?: boolean;
|
||||
"data-testid"?: string;
|
||||
}
|
||||
|
||||
@@ -21,6 +22,7 @@ export function BranchAutocomplete({
|
||||
placeholder = "Select a branch...",
|
||||
className,
|
||||
disabled = false,
|
||||
error = false,
|
||||
"data-testid": testId,
|
||||
}: BranchAutocompleteProps) {
|
||||
// Always include "main" at the top of suggestions
|
||||
@@ -43,6 +45,7 @@ export function BranchAutocomplete({
|
||||
emptyMessage="No branches found."
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
error={error}
|
||||
icon={GitBranch}
|
||||
allowCreate
|
||||
createLabel={(v) => `Create "${v}"`}
|
||||
|
||||
45
apps/app/src/components/ui/radio-group.tsx
Normal file
45
apps/app/src/components/ui/radio-group.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
});
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||
|
||||
export { RadioGroup, RadioGroupItem };
|
||||
|
||||
30
apps/app/src/components/ui/switch.tsx
Normal file
30
apps/app/src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { Switch };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import { useEffect, useState, useCallback, useMemo, useRef } from "react";
|
||||
import {
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
} from "@dnd-kit/core";
|
||||
import { useAppStore, Feature } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import type { AutoModeEvent } from "@/types/electron";
|
||||
import { pathsEqual } from "@/lib/utils";
|
||||
import { getBlockingDependencies } from "@/lib/dependency-resolver";
|
||||
import { BoardBackgroundModal } from "@/components/dialogs/board-background-modal";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { useAutoMode } from "@/hooks/use-auto-mode";
|
||||
@@ -25,7 +27,7 @@ import {
|
||||
AddFeatureDialog,
|
||||
AgentOutputModal,
|
||||
CompletedFeaturesModal,
|
||||
DeleteAllVerifiedDialog,
|
||||
ArchiveAllVerifiedDialog,
|
||||
DeleteCompletedFeatureDialog,
|
||||
EditFeatureDialog,
|
||||
FeatureSuggestionsDialog,
|
||||
@@ -72,6 +74,10 @@ export function BoardView() {
|
||||
setCurrentWorktree,
|
||||
getWorktrees,
|
||||
setWorktrees,
|
||||
useWorktrees,
|
||||
enableDependencyBlocking,
|
||||
isPrimaryWorktreeBranch,
|
||||
getPrimaryWorktreeBranch,
|
||||
} = useAppStore();
|
||||
const shortcuts = useKeyboardShortcutsConfig();
|
||||
const {
|
||||
@@ -89,7 +95,7 @@ export function BoardView() {
|
||||
const [featuresWithContext, setFeaturesWithContext] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
const [showDeleteAllVerifiedDialog, setShowDeleteAllVerifiedDialog] =
|
||||
const [showArchiveAllVerifiedDialog, setShowArchiveAllVerifiedDialog] =
|
||||
useState(false);
|
||||
const [showBoardBackgroundModal, setShowBoardBackgroundModal] =
|
||||
useState(false);
|
||||
@@ -277,6 +283,27 @@ export function BoardView() {
|
||||
const { persistFeatureCreate, persistFeatureUpdate, persistFeatureDelete } =
|
||||
useBoardPersistence({ currentProject });
|
||||
|
||||
// Memoize the removed worktrees handler to prevent infinite loops
|
||||
const handleRemovedWorktrees = useCallback(
|
||||
(removedWorktrees: Array<{ path: string; branch: string }>) => {
|
||||
// Reset features that were assigned to the removed worktrees (by branch)
|
||||
hookFeatures.forEach((feature) => {
|
||||
const matchesRemovedWorktree = removedWorktrees.some((removed) => {
|
||||
// Match by branch name since worktreePath is no longer stored
|
||||
return feature.branchName === removed.branch;
|
||||
});
|
||||
|
||||
if (matchesRemovedWorktree) {
|
||||
// Reset the feature's branch assignment
|
||||
persistFeatureUpdate(feature.id, {
|
||||
branchName: null as unknown as string | undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
[hookFeatures, persistFeatureUpdate]
|
||||
);
|
||||
|
||||
// Get in-progress features for keyboard shortcuts (needed before actions hook)
|
||||
const inProgressFeaturesForShortcuts = useMemo(() => {
|
||||
return hookFeatures.filter((f) => {
|
||||
@@ -285,13 +312,12 @@ export function BoardView() {
|
||||
});
|
||||
}, [hookFeatures, runningAutoTasks]);
|
||||
|
||||
// Get current worktree info (path and branch) for filtering features
|
||||
// Get current worktree info (path) 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(
|
||||
() =>
|
||||
@@ -301,8 +327,25 @@ export function BoardView() {
|
||||
[currentProject, worktreesByProject]
|
||||
);
|
||||
|
||||
// Get the branch for the currently selected worktree
|
||||
// Find the worktree that matches the current selection, or use main worktree
|
||||
const selectedWorktree = useMemo(() => {
|
||||
if (currentWorktreePath === null) {
|
||||
// Primary worktree selected - find the main worktree
|
||||
return worktrees.find((w) => w.isMain);
|
||||
} else {
|
||||
// Specific worktree selected - find it by path
|
||||
return worktrees.find(
|
||||
(w) => !w.isMain && pathsEqual(w.path, currentWorktreePath)
|
||||
);
|
||||
}
|
||||
}, [worktrees, currentWorktreePath]);
|
||||
|
||||
// Get the current branch from the selected worktree (not from store which may be stale)
|
||||
const currentWorktreeBranch = selectedWorktree?.branch ?? null;
|
||||
|
||||
// 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
|
||||
// Use the branch from selectedWorktree, or fall back to main worktree's branch
|
||||
const selectedWorktreeBranch =
|
||||
currentWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || "main";
|
||||
|
||||
@@ -326,7 +369,7 @@ export function BoardView() {
|
||||
handleOutputModalNumberKeyPress,
|
||||
handleForceStopFeature,
|
||||
handleStartNextFeatures,
|
||||
handleDeleteAllVerified,
|
||||
handleArchiveAllVerified,
|
||||
} = useBoardActions({
|
||||
currentProject,
|
||||
features: hookFeatures,
|
||||
@@ -354,6 +397,202 @@ export function BoardView() {
|
||||
currentWorktreeBranch,
|
||||
});
|
||||
|
||||
// Client-side auto mode: periodically check for backlog items and move them to in-progress
|
||||
// Use a ref to track the latest auto mode state so async operations always check the current value
|
||||
const autoModeRunningRef = useRef(autoMode.isRunning);
|
||||
useEffect(() => {
|
||||
autoModeRunningRef.current = autoMode.isRunning;
|
||||
}, [autoMode.isRunning]);
|
||||
|
||||
// Track features that are pending (started but not yet confirmed running)
|
||||
const pendingFeaturesRef = useRef<Set<string>>(new Set());
|
||||
|
||||
// Listen to auto mode events to remove features from pending when they start running
|
||||
useEffect(() => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) return;
|
||||
|
||||
const unsubscribe = api.autoMode.onEvent((event: AutoModeEvent) => {
|
||||
if (!currentProject) return;
|
||||
|
||||
// Only process events for the current project
|
||||
const eventProjectPath =
|
||||
"projectPath" in event ? event.projectPath : undefined;
|
||||
if (eventProjectPath && eventProjectPath !== currentProject.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case "auto_mode_feature_start":
|
||||
// Feature is now confirmed running - remove from pending
|
||||
if (event.featureId) {
|
||||
pendingFeaturesRef.current.delete(event.featureId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "auto_mode_feature_complete":
|
||||
case "auto_mode_error":
|
||||
// Feature completed or errored - remove from pending if still there
|
||||
if (event.featureId) {
|
||||
pendingFeaturesRef.current.delete(event.featureId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [currentProject]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoMode.isRunning || !currentProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isChecking = false;
|
||||
let isActive = true; // Track if this effect is still active
|
||||
|
||||
const checkAndStartFeatures = async () => {
|
||||
// Check if auto mode is still running and effect is still active
|
||||
// Use ref to get the latest value, not the closure value
|
||||
if (!isActive || !autoModeRunningRef.current || !currentProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent concurrent executions
|
||||
if (isChecking) {
|
||||
return;
|
||||
}
|
||||
|
||||
isChecking = true;
|
||||
try {
|
||||
// Double-check auto mode is still running before proceeding
|
||||
if (!isActive || !autoModeRunningRef.current || !currentProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Count currently running tasks + pending features
|
||||
const currentRunning =
|
||||
runningAutoTasks.length + pendingFeaturesRef.current.size;
|
||||
const availableSlots = maxConcurrency - currentRunning;
|
||||
|
||||
// No available slots, skip check
|
||||
if (availableSlots <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter backlog features by the currently selected worktree branch
|
||||
const primaryBranch = currentProject.path
|
||||
? getPrimaryWorktreeBranch(currentProject.path)
|
||||
: null;
|
||||
const backlogFeatures = hookFeatures.filter((f) => {
|
||||
if (f.status !== "backlog") return false;
|
||||
|
||||
// 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 matches primary),
|
||||
// show features with no branch or primary branch
|
||||
if (
|
||||
!currentWorktreeBranch ||
|
||||
(currentProject.path &&
|
||||
isPrimaryWorktreeBranch(
|
||||
currentProject.path,
|
||||
currentWorktreeBranch
|
||||
))
|
||||
) {
|
||||
return (
|
||||
!f.branchName ||
|
||||
(currentProject.path &&
|
||||
isPrimaryWorktreeBranch(currentProject.path, featureBranch))
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, only show features matching the selected worktree branch
|
||||
return featureBranch === currentWorktreeBranch;
|
||||
});
|
||||
|
||||
if (backlogFeatures.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by priority (lower number = higher priority, priority 1 is highest)
|
||||
const sortedBacklog = [...backlogFeatures].sort(
|
||||
(a, b) => (a.priority || 999) - (b.priority || 999)
|
||||
);
|
||||
|
||||
// Filter out features with blocking dependencies if dependency blocking is enabled
|
||||
const eligibleFeatures = enableDependencyBlocking
|
||||
? sortedBacklog.filter((f) => {
|
||||
const blockingDeps = getBlockingDependencies(f, hookFeatures);
|
||||
return blockingDeps.length === 0;
|
||||
})
|
||||
: sortedBacklog;
|
||||
|
||||
// Start features up to available slots
|
||||
const featuresToStart = eligibleFeatures.slice(0, availableSlots);
|
||||
|
||||
for (const feature of featuresToStart) {
|
||||
// Check again before starting each feature
|
||||
if (!isActive || !autoModeRunningRef.current || !currentProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Simplified: No worktree creation on client - server derives workDir from feature.branchName
|
||||
// If feature has no branchName and primary worktree is selected, assign primary branch
|
||||
if (currentWorktreePath === null && !feature.branchName) {
|
||||
const primaryBranch =
|
||||
(currentProject.path
|
||||
? getPrimaryWorktreeBranch(currentProject.path)
|
||||
: null) || "main";
|
||||
await persistFeatureUpdate(feature.id, {
|
||||
branchName: primaryBranch,
|
||||
});
|
||||
}
|
||||
|
||||
// Final check before starting implementation
|
||||
if (!isActive || !autoModeRunningRef.current || !currentProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the implementation - server will derive workDir from feature.branchName
|
||||
const started = await handleStartImplementation(feature);
|
||||
|
||||
// If successfully started, track it as pending until we receive the start event
|
||||
if (started) {
|
||||
pendingFeaturesRef.current.add(feature.id);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isChecking = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Check immediately, then every 3 seconds
|
||||
checkAndStartFeatures();
|
||||
const interval = setInterval(checkAndStartFeatures, 3000);
|
||||
|
||||
return () => {
|
||||
// Mark as inactive to prevent any pending async operations from continuing
|
||||
isActive = false;
|
||||
clearInterval(interval);
|
||||
// Clear pending features when effect unmounts or dependencies change
|
||||
pendingFeaturesRef.current.clear();
|
||||
};
|
||||
}, [
|
||||
autoMode.isRunning,
|
||||
currentProject,
|
||||
runningAutoTasks,
|
||||
maxConcurrency,
|
||||
hookFeatures,
|
||||
currentWorktreeBranch,
|
||||
currentWorktreePath,
|
||||
getPrimaryWorktreeBranch,
|
||||
isPrimaryWorktreeBranch,
|
||||
enableDependencyBlocking,
|
||||
persistFeatureUpdate,
|
||||
handleStartImplementation,
|
||||
]);
|
||||
|
||||
// Use keyboard shortcuts hook (after actions hook)
|
||||
useBoardKeyboardShortcuts({
|
||||
features: hookFeatures,
|
||||
@@ -422,8 +661,13 @@ export function BoardView() {
|
||||
maxConcurrency={maxConcurrency}
|
||||
onConcurrencyChange={setMaxConcurrency}
|
||||
isAutoModeRunning={autoMode.isRunning}
|
||||
onStartAutoMode={() => autoMode.start()}
|
||||
onStopAutoMode={() => autoMode.stop()}
|
||||
onAutoModeToggle={(enabled) => {
|
||||
if (enabled) {
|
||||
autoMode.start();
|
||||
} else {
|
||||
autoMode.stop();
|
||||
}
|
||||
}}
|
||||
onAddFeature={() => setShowAddDialog(true)}
|
||||
addFeatureShortcut={{
|
||||
key: shortcuts.addFeature,
|
||||
@@ -454,10 +698,10 @@ export function BoardView() {
|
||||
setSelectedWorktreeForAction(worktree);
|
||||
setShowCreateBranchDialog(true);
|
||||
}}
|
||||
onRemovedWorktrees={handleRemovedWorktrees}
|
||||
runningFeatureIds={runningAutoTasks}
|
||||
features={hookFeatures.map((f) => ({
|
||||
id: f.id,
|
||||
worktreePath: f.worktreePath,
|
||||
branchName: f.branchName,
|
||||
}))}
|
||||
/>
|
||||
@@ -512,7 +756,7 @@ export function BoardView() {
|
||||
onStartNextFeatures={handleStartNextFeatures}
|
||||
onShowSuggestions={() => setShowSuggestionsDialog(true)}
|
||||
suggestionsCount={suggestionsCount}
|
||||
onDeleteAllVerified={() => setShowDeleteAllVerifiedDialog(true)}
|
||||
onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -552,6 +796,7 @@ export function BoardView() {
|
||||
branchSuggestions={branchSuggestions}
|
||||
defaultSkipTests={defaultSkipTests}
|
||||
defaultBranch={selectedWorktreeBranch}
|
||||
currentBranch={currentWorktreeBranch || undefined}
|
||||
isMaximized={isMaximized}
|
||||
showProfilesOnly={showProfilesOnly}
|
||||
aiProfiles={aiProfiles}
|
||||
@@ -564,6 +809,7 @@ export function BoardView() {
|
||||
onUpdate={handleUpdateFeature}
|
||||
categorySuggestions={categorySuggestions}
|
||||
branchSuggestions={branchSuggestions}
|
||||
currentBranch={currentWorktreeBranch || undefined}
|
||||
isMaximized={isMaximized}
|
||||
showProfilesOnly={showProfilesOnly}
|
||||
aiProfiles={aiProfiles}
|
||||
@@ -580,14 +826,14 @@ export function BoardView() {
|
||||
onNumberKeyPress={handleOutputModalNumberKeyPress}
|
||||
/>
|
||||
|
||||
{/* Delete All Verified Dialog */}
|
||||
<DeleteAllVerifiedDialog
|
||||
open={showDeleteAllVerifiedDialog}
|
||||
onOpenChange={setShowDeleteAllVerifiedDialog}
|
||||
{/* Archive All Verified Dialog */}
|
||||
<ArchiveAllVerifiedDialog
|
||||
open={showArchiveAllVerifiedDialog}
|
||||
onOpenChange={setShowArchiveAllVerifiedDialog}
|
||||
verifiedCount={getColumnFeatures("verified").length}
|
||||
onConfirm={async () => {
|
||||
await handleDeleteAllVerified();
|
||||
setShowDeleteAllVerifiedDialog(false);
|
||||
await handleArchiveAllVerified();
|
||||
setShowArchiveAllVerifiedDialog(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -657,19 +903,13 @@ export function BoardView() {
|
||||
projectPath={currentProject.path}
|
||||
worktree={selectedWorktreeForAction}
|
||||
onDeleted={(deletedWorktree, _deletedBranch) => {
|
||||
// Reset features that were assigned to the deleted worktree
|
||||
// Reset features that were assigned to the deleted worktree (by branch)
|
||||
hookFeatures.forEach((feature) => {
|
||||
const matchesByPath =
|
||||
feature.worktreePath &&
|
||||
pathsEqual(feature.worktreePath, deletedWorktree.path);
|
||||
const matchesByBranch =
|
||||
feature.branchName === deletedWorktree.branch;
|
||||
|
||||
if (matchesByPath || matchesByBranch) {
|
||||
// Reset the feature's worktree assignment
|
||||
// Match by branch name since worktreePath is no longer stored
|
||||
if (feature.branchName === deletedWorktree.branch) {
|
||||
// Reset the feature's branch assignment
|
||||
persistFeatureUpdate(feature.id, {
|
||||
branchName: null as unknown as string | undefined,
|
||||
worktreePath: null as unknown as string | undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { HotkeyButton } from "@/components/ui/hotkey-button";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Play, StopCircle, Plus, Users } from "lucide-react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Plus, Users } from "lucide-react";
|
||||
import { KeyboardShortcut } from "@/hooks/use-keyboard-shortcuts";
|
||||
|
||||
interface BoardHeaderProps {
|
||||
@@ -11,8 +13,7 @@ interface BoardHeaderProps {
|
||||
maxConcurrency: number;
|
||||
onConcurrencyChange: (value: number) => void;
|
||||
isAutoModeRunning: boolean;
|
||||
onStartAutoMode: () => void;
|
||||
onStopAutoMode: () => void;
|
||||
onAutoModeToggle: (enabled: boolean) => void;
|
||||
onAddFeature: () => void;
|
||||
addFeatureShortcut: KeyboardShortcut;
|
||||
isMounted: boolean;
|
||||
@@ -23,8 +24,7 @@ export function BoardHeader({
|
||||
maxConcurrency,
|
||||
onConcurrencyChange,
|
||||
isAutoModeRunning,
|
||||
onStartAutoMode,
|
||||
onStopAutoMode,
|
||||
onAutoModeToggle,
|
||||
onAddFeature,
|
||||
addFeatureShortcut,
|
||||
isMounted,
|
||||
@@ -63,29 +63,20 @@ export function BoardHeader({
|
||||
|
||||
{/* Auto Mode Toggle - only show after mount to prevent hydration issues */}
|
||||
{isMounted && (
|
||||
<>
|
||||
{isAutoModeRunning ? (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={onStopAutoMode}
|
||||
data-testid="stop-auto-mode"
|
||||
>
|
||||
<StopCircle className="w-4 h-4 mr-2" />
|
||||
Stop Auto Mode
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={onStartAutoMode}
|
||||
data-testid="start-auto-mode"
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Auto Mode
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-secondary border border-border">
|
||||
<Label
|
||||
htmlFor="auto-mode-toggle"
|
||||
className="text-sm font-medium cursor-pointer"
|
||||
>
|
||||
Auto Mode
|
||||
</Label>
|
||||
<Switch
|
||||
id="auto-mode-toggle"
|
||||
checked={isAutoModeRunning}
|
||||
onCheckedChange={onAutoModeToggle}
|
||||
data-testid="auto-mode-toggle"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<HotkeyButton
|
||||
|
||||
@@ -257,7 +257,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
feature.status === "backlog" ||
|
||||
feature.status === "waiting_approval" ||
|
||||
feature.status === "verified" ||
|
||||
(feature.skipTests && !isCurrentAutoTask);
|
||||
(feature.status === "in_progress" && !isCurrentAutoTask);
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
ProfileQuickSelect,
|
||||
TestingTabContent,
|
||||
PrioritySelector,
|
||||
BranchSelector,
|
||||
} from "../shared";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -63,13 +64,14 @@ interface AddFeatureDialogProps {
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
branchName: string;
|
||||
branchName: string; // Can be empty string to use current branch
|
||||
priority: number;
|
||||
}) => void;
|
||||
categorySuggestions: string[];
|
||||
branchSuggestions: string[];
|
||||
defaultSkipTests: boolean;
|
||||
defaultBranch?: string;
|
||||
currentBranch?: string;
|
||||
isMaximized: boolean;
|
||||
showProfilesOnly: boolean;
|
||||
aiProfiles: AIProfile[];
|
||||
@@ -83,10 +85,12 @@ export function AddFeatureDialog({
|
||||
branchSuggestions,
|
||||
defaultSkipTests,
|
||||
defaultBranch = "main",
|
||||
currentBranch,
|
||||
isMaximized,
|
||||
showProfilesOnly,
|
||||
aiProfiles,
|
||||
}: AddFeatureDialogProps) {
|
||||
const [useCurrentBranch, setUseCurrentBranch] = useState(true);
|
||||
const [newFeature, setNewFeature] = useState({
|
||||
category: "",
|
||||
description: "",
|
||||
@@ -96,7 +100,7 @@ export function AddFeatureDialog({
|
||||
skipTests: false,
|
||||
model: "opus" as AgentModel,
|
||||
thinkingLevel: "none" as ThinkingLevel,
|
||||
branchName: "main",
|
||||
branchName: "",
|
||||
priority: 2 as number, // Default to medium priority
|
||||
});
|
||||
const [newFeaturePreviewMap, setNewFeaturePreviewMap] =
|
||||
@@ -117,8 +121,9 @@ export function AddFeatureDialog({
|
||||
setNewFeature((prev) => ({
|
||||
...prev,
|
||||
skipTests: defaultSkipTests,
|
||||
branchName: defaultBranch,
|
||||
branchName: defaultBranch || "",
|
||||
}));
|
||||
setUseCurrentBranch(true);
|
||||
}
|
||||
}, [open, defaultSkipTests, defaultBranch]);
|
||||
|
||||
@@ -128,12 +133,24 @@ export function AddFeatureDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate branch selection when "other branch" is selected
|
||||
if (useWorktrees && !useCurrentBranch && !newFeature.branchName.trim()) {
|
||||
toast.error("Please select a branch name");
|
||||
return;
|
||||
}
|
||||
|
||||
const category = newFeature.category || "Uncategorized";
|
||||
const selectedModel = newFeature.model;
|
||||
const normalizedThinking = modelSupportsThinking(selectedModel)
|
||||
? newFeature.thinkingLevel
|
||||
: "none";
|
||||
|
||||
// Use current branch if toggle is on (empty string = use current), otherwise use selected branch
|
||||
// Important: Don't save the actual current branch name - empty string means "use current"
|
||||
const finalBranchName = useCurrentBranch
|
||||
? ""
|
||||
: newFeature.branchName || "";
|
||||
|
||||
onAdd({
|
||||
category,
|
||||
description: newFeature.description,
|
||||
@@ -143,7 +160,7 @@ export function AddFeatureDialog({
|
||||
skipTests: newFeature.skipTests,
|
||||
model: selectedModel,
|
||||
thinkingLevel: normalizedThinking,
|
||||
branchName: newFeature.branchName,
|
||||
branchName: finalBranchName,
|
||||
priority: newFeature.priority,
|
||||
});
|
||||
|
||||
@@ -158,8 +175,9 @@ export function AddFeatureDialog({
|
||||
model: "opus",
|
||||
priority: 2,
|
||||
thinkingLevel: "none",
|
||||
branchName: defaultBranch,
|
||||
branchName: "",
|
||||
});
|
||||
setUseCurrentBranch(true);
|
||||
setNewFeaturePreviewMap(new Map());
|
||||
setShowAdvancedOptions(false);
|
||||
setDescriptionError(false);
|
||||
@@ -359,22 +377,17 @@ export function AddFeatureDialog({
|
||||
/>
|
||||
</div>
|
||||
{useWorktrees && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="branch">Target Branch</Label>
|
||||
<BranchAutocomplete
|
||||
value={newFeature.branchName}
|
||||
onChange={(value) =>
|
||||
setNewFeature({ ...newFeature, branchName: value })
|
||||
}
|
||||
branches={branchSuggestions}
|
||||
placeholder="Select or create branch..."
|
||||
data-testid="feature-branch-input"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Work will be done in this branch. A worktree will be created if
|
||||
needed.
|
||||
</p>
|
||||
</div>
|
||||
<BranchSelector
|
||||
useCurrentBranch={useCurrentBranch}
|
||||
onUseCurrentBranchChange={setUseCurrentBranch}
|
||||
branchName={newFeature.branchName}
|
||||
onBranchNameChange={(value) =>
|
||||
setNewFeature({ ...newFeature, branchName: value })
|
||||
}
|
||||
branchSuggestions={branchSuggestions}
|
||||
currentBranch={currentBranch}
|
||||
testIdPrefix="feature"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Priority Selector */}
|
||||
@@ -477,6 +490,11 @@ export function AddFeatureDialog({
|
||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
||||
hotkeyActive={open}
|
||||
data-testid="confirm-add-feature"
|
||||
disabled={
|
||||
useWorktrees &&
|
||||
!useCurrentBranch &&
|
||||
!newFeature.branchName.trim()
|
||||
}
|
||||
>
|
||||
Add Feature
|
||||
</HotkeyButton>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Archive } from "lucide-react";
|
||||
|
||||
interface ArchiveAllVerifiedDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
verifiedCount: number;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export function ArchiveAllVerifiedDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
verifiedCount,
|
||||
onConfirm,
|
||||
}: ArchiveAllVerifiedDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent data-testid="archive-all-verified-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Archive All Verified Features</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to archive all verified features? They will be
|
||||
moved to the archive box.
|
||||
{verifiedCount > 0 && (
|
||||
<span className="block mt-2 text-yellow-500">
|
||||
{verifiedCount} feature(s) will be archived.
|
||||
</span>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="default" onClick={onConfirm} data-testid="confirm-archive-all-verified">
|
||||
<Archive className="w-4 h-4 mr-2" />
|
||||
Archive All
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,14 +53,18 @@ export function CreatePRDialog({
|
||||
// Reset state when dialog opens or worktree changes
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// Only reset form fields, not the result states (prUrl, browserUrl, showBrowserFallback)
|
||||
// These are set by the API response and should persist until dialog closes
|
||||
// Reset form fields
|
||||
setTitle("");
|
||||
setBody("");
|
||||
setCommitMessage("");
|
||||
setBaseBranch("main");
|
||||
setIsDraft(false);
|
||||
setError(null);
|
||||
// Also reset result states when opening for a new worktree
|
||||
// This prevents showing stale PR URLs from previous worktrees
|
||||
setPrUrl(null);
|
||||
setBrowserUrl(null);
|
||||
setShowBrowserFallback(false);
|
||||
} else {
|
||||
// Reset everything when dialog closes
|
||||
setTitle("");
|
||||
@@ -105,7 +109,8 @@ export function CreatePRDialog({
|
||||
onClick: () => window.open(result.result!.prUrl!, "_blank"),
|
||||
},
|
||||
});
|
||||
onCreated();
|
||||
// Don't call onCreated() here - keep dialog open to show success message
|
||||
// onCreated() will be called when user closes the dialog
|
||||
} else {
|
||||
// Branch was pushed successfully
|
||||
const prError = result.result.prError;
|
||||
@@ -182,6 +187,9 @@ export function CreatePRDialog({
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
// Call onCreated() to refresh worktrees when dialog closes
|
||||
// This ensures the worktree list is updated after any operation
|
||||
onCreated();
|
||||
onOpenChange(false);
|
||||
// Reset state after dialog closes
|
||||
setTimeout(() => {
|
||||
@@ -228,13 +236,18 @@ export function CreatePRDialog({
|
||||
Your PR is ready for review
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => window.open(prUrl, "_blank")}
|
||||
className="gap-2"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
View Pull Request
|
||||
</Button>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<Button
|
||||
onClick={() => window.open(prUrl, "_blank")}
|
||||
className="gap-2"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
View Pull Request
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : shouldShowBrowserFallback ? (
|
||||
<div className="py-6 text-center space-y-4">
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
ProfileQuickSelect,
|
||||
TestingTabContent,
|
||||
PrioritySelector,
|
||||
BranchSelector,
|
||||
} from "../shared";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -66,12 +67,13 @@ interface EditFeatureDialogProps {
|
||||
model: AgentModel;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
imagePaths: DescriptionImagePath[];
|
||||
branchName: string;
|
||||
branchName: string; // Can be empty string to use current branch
|
||||
priority: number;
|
||||
}
|
||||
) => void;
|
||||
categorySuggestions: string[];
|
||||
branchSuggestions: string[];
|
||||
currentBranch?: string;
|
||||
isMaximized: boolean;
|
||||
showProfilesOnly: boolean;
|
||||
aiProfiles: AIProfile[];
|
||||
@@ -84,12 +86,17 @@ export function EditFeatureDialog({
|
||||
onUpdate,
|
||||
categorySuggestions,
|
||||
branchSuggestions,
|
||||
currentBranch,
|
||||
isMaximized,
|
||||
showProfilesOnly,
|
||||
aiProfiles,
|
||||
allFeatures,
|
||||
}: EditFeatureDialogProps) {
|
||||
const [editingFeature, setEditingFeature] = useState<Feature | null>(feature);
|
||||
const [useCurrentBranch, setUseCurrentBranch] = useState(() => {
|
||||
// If feature has no branchName, default to using current branch
|
||||
return !feature?.branchName;
|
||||
});
|
||||
const [editFeaturePreviewMap, setEditFeaturePreviewMap] =
|
||||
useState<ImagePreviewMap>(() => new Map());
|
||||
const [showEditAdvancedOptions, setShowEditAdvancedOptions] = useState(false);
|
||||
@@ -107,12 +114,27 @@ export function EditFeatureDialog({
|
||||
if (!feature) {
|
||||
setEditFeaturePreviewMap(new Map());
|
||||
setShowEditAdvancedOptions(false);
|
||||
} else {
|
||||
// If feature has no branchName, default to using current branch
|
||||
setUseCurrentBranch(!feature.branchName);
|
||||
}
|
||||
}, [feature]);
|
||||
|
||||
const handleUpdate = () => {
|
||||
if (!editingFeature) return;
|
||||
|
||||
// Validate branch selection when "other branch" is selected and branch selector is enabled
|
||||
const isBranchSelectorEnabled = editingFeature.status === "backlog";
|
||||
if (
|
||||
useWorktrees &&
|
||||
isBranchSelectorEnabled &&
|
||||
!useCurrentBranch &&
|
||||
!editingFeature.branchName?.trim()
|
||||
) {
|
||||
toast.error("Please select a branch name");
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedModel = (editingFeature.model ?? "opus") as AgentModel;
|
||||
const normalizedThinking: ThinkingLevel = modelSupportsThinking(
|
||||
selectedModel
|
||||
@@ -120,6 +142,12 @@ export function EditFeatureDialog({
|
||||
? editingFeature.thinkingLevel ?? "none"
|
||||
: "none";
|
||||
|
||||
// Use current branch if toggle is on (empty string = use current), otherwise use selected branch
|
||||
// Important: Don't save the actual current branch name - empty string means "use current"
|
||||
const finalBranchName = useCurrentBranch
|
||||
? ""
|
||||
: editingFeature.branchName || "";
|
||||
|
||||
const updates = {
|
||||
category: editingFeature.category,
|
||||
description: editingFeature.description,
|
||||
@@ -128,7 +156,7 @@ export function EditFeatureDialog({
|
||||
model: selectedModel,
|
||||
thinkingLevel: normalizedThinking,
|
||||
imagePaths: editingFeature.imagePaths ?? [],
|
||||
branchName: editingFeature.branchName ?? "main",
|
||||
branchName: finalBranchName,
|
||||
priority: editingFeature.priority ?? 2,
|
||||
};
|
||||
|
||||
@@ -339,33 +367,21 @@ export function EditFeatureDialog({
|
||||
/>
|
||||
</div>
|
||||
{useWorktrees && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-branch">Target Branch</Label>
|
||||
<BranchAutocomplete
|
||||
value={editingFeature.branchName ?? "main"}
|
||||
onChange={(value) =>
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
branchName: value,
|
||||
})
|
||||
}
|
||||
branches={branchSuggestions}
|
||||
placeholder="Select or create branch..."
|
||||
data-testid="edit-feature-branch"
|
||||
disabled={editingFeature.status !== "backlog"}
|
||||
/>
|
||||
{editingFeature.status !== "backlog" && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Branch cannot be changed after work has started.
|
||||
</p>
|
||||
)}
|
||||
{editingFeature.status === "backlog" && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Work will be done in this branch. A worktree will be created
|
||||
if needed.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<BranchSelector
|
||||
useCurrentBranch={useCurrentBranch}
|
||||
onUseCurrentBranchChange={setUseCurrentBranch}
|
||||
branchName={editingFeature.branchName ?? ""}
|
||||
onBranchNameChange={(value) =>
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
branchName: value,
|
||||
})
|
||||
}
|
||||
branchSuggestions={branchSuggestions}
|
||||
currentBranch={currentBranch}
|
||||
disabled={editingFeature.status !== "backlog"}
|
||||
testIdPrefix="edit-feature"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Priority Selector */}
|
||||
@@ -486,6 +502,12 @@ export function EditFeatureDialog({
|
||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
||||
hotkeyActive={!!editingFeature}
|
||||
data-testid="confirm-edit-feature"
|
||||
disabled={
|
||||
useWorktrees &&
|
||||
editingFeature.status === "backlog" &&
|
||||
!useCurrentBranch &&
|
||||
!editingFeature.branchName?.trim()
|
||||
}
|
||||
>
|
||||
Save Changes
|
||||
</HotkeyButton>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export { AddFeatureDialog } from "./add-feature-dialog";
|
||||
export { AgentOutputModal } from "./agent-output-modal";
|
||||
export { CompletedFeaturesModal } from "./completed-features-modal";
|
||||
export { DeleteAllVerifiedDialog } from "./delete-all-verified-dialog";
|
||||
export { ArchiveAllVerifiedDialog } from "./archive-all-verified-dialog";
|
||||
export { DeleteCompletedFeatureDialog } from "./delete-completed-feature-dialog";
|
||||
export { EditFeatureDialog } from "./edit-feature-dialog";
|
||||
export { FeatureSuggestionsDialog } from "./feature-suggestions-dialog";
|
||||
|
||||
@@ -76,67 +76,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: {
|
||||
@@ -151,34 +97,26 @@ export function useBoardActions({
|
||||
branchName: string;
|
||||
priority: number;
|
||||
}) => {
|
||||
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 user chose "use current branch" - don't save a branch name
|
||||
const finalBranchName =
|
||||
featureData.branchName === ""
|
||||
? undefined
|
||||
: 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(
|
||||
@@ -196,44 +134,13 @@ export function useBoardActions({
|
||||
priority: number;
|
||||
}
|
||||
) => {
|
||||
// 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 : 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 +149,7 @@ export function useBoardActions({
|
||||
}
|
||||
setEditingFeature(null);
|
||||
},
|
||||
[updateFeature, persistFeatureUpdate, saveCategory, setEditingFeature, features, useWorktrees, getOrCreateWorktreeForFeature, projectPath, onWorktreeCreated]
|
||||
[updateFeature, persistFeatureUpdate, saveCategory, setEditingFeature]
|
||||
);
|
||||
|
||||
const handleDeleteFeature = useCallback(
|
||||
@@ -307,21 +214,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 +254,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 +278,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 +402,6 @@ export function useBoardActions({
|
||||
|
||||
const featureId = followUpFeature.id;
|
||||
const featureDescription = followUpFeature.description;
|
||||
const prompt = followUpPrompt;
|
||||
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.followUpFeature) {
|
||||
@@ -521,15 +433,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 +480,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 +669,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 +707,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 +774,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 +818,6 @@ export function useBoardActions({
|
||||
handleOutputModalNumberKeyPress,
|
||||
handleForceStopFeature,
|
||||
handleStartNextFeatures,
|
||||
handleDeleteAllVerified,
|
||||
handleArchiveAllVerified,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -111,7 +108,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;
|
||||
|
||||
@@ -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[];
|
||||
@@ -29,62 +28,10 @@ export function useBoardDragDrop({
|
||||
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 +65,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 +97,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 +128,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 +140,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 +165,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 +182,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 +207,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 +223,6 @@ export function useBoardDragDrop({
|
||||
moveFeature,
|
||||
persistFeatureUpdate,
|
||||
handleStartImplementation,
|
||||
getOrCreateWorktreeForFeature,
|
||||
onWorktreeCreated,
|
||||
useWorktrees,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { HotkeyButton } from "@/components/ui/hotkey-button";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { KanbanColumn, KanbanCard } from "./components";
|
||||
import { Feature } from "@/store/app-store";
|
||||
import { FastForward, Lightbulb, Trash2 } from "lucide-react";
|
||||
import { FastForward, Lightbulb, Archive } from "lucide-react";
|
||||
import { useKeyboardShortcutsConfig } from "@/hooks/use-keyboard-shortcuts";
|
||||
import { COLUMNS, ColumnId } from "./constants";
|
||||
|
||||
@@ -53,7 +53,7 @@ interface KanbanBoardProps {
|
||||
onStartNextFeatures: () => void;
|
||||
onShowSuggestions: () => void;
|
||||
suggestionsCount: number;
|
||||
onDeleteAllVerified: () => void;
|
||||
onArchiveAllVerified: () => void;
|
||||
}
|
||||
|
||||
export function KanbanBoard({
|
||||
@@ -83,7 +83,7 @@ export function KanbanBoard({
|
||||
onStartNextFeatures,
|
||||
onShowSuggestions,
|
||||
suggestionsCount,
|
||||
onDeleteAllVerified,
|
||||
onArchiveAllVerified,
|
||||
}: KanbanBoardProps) {
|
||||
return (
|
||||
<div
|
||||
@@ -115,12 +115,12 @@ export function KanbanBoard({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={onDeleteAllVerified}
|
||||
data-testid="delete-all-verified-button"
|
||||
className="h-6 px-2 text-xs"
|
||||
onClick={onArchiveAllVerified}
|
||||
data-testid="archive-all-verified-button"
|
||||
>
|
||||
<Trash2 className="w-3 h-3 mr-1" />
|
||||
Delete All
|
||||
<Archive className="w-3 h-3 mr-1" />
|
||||
Archive All
|
||||
</Button>
|
||||
) : column.id === "backlog" ? (
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { BranchAutocomplete } from "@/components/ui/branch-autocomplete";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface BranchSelectorProps {
|
||||
useCurrentBranch: boolean;
|
||||
onUseCurrentBranchChange: (useCurrent: boolean) => void;
|
||||
branchName: string;
|
||||
onBranchNameChange: (branchName: string) => void;
|
||||
branchSuggestions: string[];
|
||||
currentBranch?: string;
|
||||
disabled?: boolean;
|
||||
testIdPrefix?: string;
|
||||
}
|
||||
|
||||
export function BranchSelector({
|
||||
useCurrentBranch,
|
||||
onUseCurrentBranchChange,
|
||||
branchName,
|
||||
onBranchNameChange,
|
||||
branchSuggestions,
|
||||
currentBranch,
|
||||
disabled = false,
|
||||
testIdPrefix = "branch",
|
||||
}: BranchSelectorProps) {
|
||||
// Validate: if "other branch" is selected, branch name is required
|
||||
const isBranchRequired = !useCurrentBranch;
|
||||
const hasError = isBranchRequired && !branchName.trim();
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`${testIdPrefix}-selector`}>Target Branch</Label>
|
||||
<RadioGroup
|
||||
value={useCurrentBranch ? "current" : "other"}
|
||||
onValueChange={(value) => onUseCurrentBranchChange(value === "current")}
|
||||
disabled={disabled}
|
||||
data-testid={`${testIdPrefix}-radio-group`}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="current" id={`${testIdPrefix}-current`} />
|
||||
<Label
|
||||
htmlFor={`${testIdPrefix}-current`}
|
||||
className="font-normal cursor-pointer"
|
||||
>
|
||||
Use current selected branch
|
||||
{currentBranch && (
|
||||
<span className="text-muted-foreground ml-1">
|
||||
({currentBranch})
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="other" id={`${testIdPrefix}-other`} />
|
||||
<Label
|
||||
htmlFor={`${testIdPrefix}-other`}
|
||||
className="font-normal cursor-pointer"
|
||||
>
|
||||
Other branch
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
{!useCurrentBranch && (
|
||||
<div className="ml-6 space-y-1">
|
||||
<BranchAutocomplete
|
||||
value={branchName}
|
||||
onChange={onBranchNameChange}
|
||||
branches={branchSuggestions}
|
||||
placeholder="Select or create branch..."
|
||||
data-testid={`${testIdPrefix}-input`}
|
||||
disabled={disabled}
|
||||
error={hasError}
|
||||
/>
|
||||
{hasError && (
|
||||
<p className="text-xs text-destructive">
|
||||
Branch name is required when "Other branch" is selected.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{disabled ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Branch cannot be changed after work has started.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{useCurrentBranch
|
||||
? "Work will be done in the currently selected branch. A worktree will be created if needed."
|
||||
: "Work will be done in this branch. A worktree will be created if needed."}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from "./thinking-level-selector";
|
||||
export * from "./profile-quick-select";
|
||||
export * from "./testing-tab-content";
|
||||
export * from "./priority-selector";
|
||||
export * from "./branch-selector";
|
||||
|
||||
@@ -170,7 +170,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
|
||||
|
||||
@@ -1,47 +1,35 @@
|
||||
"use client";
|
||||
|
||||
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 {
|
||||
|
||||
@@ -9,9 +9,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[]>([]);
|
||||
|
||||
@@ -34,8 +35,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);
|
||||
}
|
||||
@@ -47,9 +51,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) {
|
||||
@@ -59,6 +67,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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -21,6 +21,7 @@ export function WorktreePanel({
|
||||
onCommit,
|
||||
onCreatePR,
|
||||
onCreateBranch,
|
||||
onRemovedWorktrees,
|
||||
runningFeatureIds = [],
|
||||
features = [],
|
||||
refreshTrigger = 0,
|
||||
@@ -33,7 +34,7 @@ export function WorktreePanel({
|
||||
useWorktreesEnabled,
|
||||
fetchWorktrees,
|
||||
handleSelectWorktree,
|
||||
} = useWorktrees({ projectPath, refreshTrigger });
|
||||
} = useWorktrees({ projectPath, refreshTrigger, onRemovedWorktrees });
|
||||
|
||||
const {
|
||||
isStartingDevServer,
|
||||
@@ -74,10 +75,8 @@ export function WorktreePanel({
|
||||
const { defaultEditorName } = useDefaultEditor();
|
||||
|
||||
const { hasRunningFeatures } = useRunningFeatures({
|
||||
projectPath,
|
||||
runningFeatureIds,
|
||||
features,
|
||||
getWorktreeKey,
|
||||
});
|
||||
|
||||
const isWorktreeSelected = (worktree: WorktreeInfo) => {
|
||||
@@ -163,7 +162,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"
|
||||
>
|
||||
|
||||
@@ -213,15 +213,25 @@ export function ContextView() {
|
||||
await api.writeFile(filePath, newFileContent);
|
||||
}
|
||||
|
||||
// Close dialog and reset state immediately after successful file write
|
||||
setIsAddDialogOpen(false);
|
||||
setNewFileName("");
|
||||
setNewFileType("text");
|
||||
setUploadedImageData(null);
|
||||
setNewFileContent("");
|
||||
setIsDropHovering(false);
|
||||
|
||||
// Load files after dialog is closed
|
||||
await loadContextFiles();
|
||||
} catch (error) {
|
||||
console.error("Failed to add file:", error);
|
||||
// Still close the dialog even if loadContextFiles fails
|
||||
setIsAddDialogOpen(false);
|
||||
setNewFileName("");
|
||||
setNewFileType("text");
|
||||
setUploadedImageData(null);
|
||||
setNewFileContent("");
|
||||
setIsDropHovering(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ export function useAutoMode() {
|
||||
setAutoModeRunning,
|
||||
addRunningTask,
|
||||
removeRunningTask,
|
||||
clearRunningTasks,
|
||||
currentProject,
|
||||
addAutoModeActivity,
|
||||
maxConcurrency,
|
||||
@@ -24,7 +23,6 @@ export function useAutoMode() {
|
||||
setAutoModeRunning: state.setAutoModeRunning,
|
||||
addRunningTask: state.addRunningTask,
|
||||
removeRunningTask: state.removeRunningTask,
|
||||
clearRunningTasks: state.clearRunningTasks,
|
||||
currentProject: state.currentProject,
|
||||
addAutoModeActivity: state.addAutoModeActivity,
|
||||
maxConcurrency: state.maxConcurrency,
|
||||
@@ -119,33 +117,6 @@ export function useAutoMode() {
|
||||
}
|
||||
break;
|
||||
|
||||
case "auto_mode_stopped":
|
||||
// Auto mode was explicitly stopped (by user or error)
|
||||
setAutoModeRunning(eventProjectId, false);
|
||||
clearRunningTasks(eventProjectId);
|
||||
console.log("[AutoMode] Auto mode stopped");
|
||||
break;
|
||||
|
||||
case "auto_mode_started":
|
||||
// Auto mode started - ensure UI reflects running state
|
||||
console.log("[AutoMode] Auto mode started:", event.message);
|
||||
break;
|
||||
|
||||
case "auto_mode_idle":
|
||||
// Auto mode is running but has no pending features to pick up
|
||||
// This is NOT a stop - auto mode keeps running and will pick up new features
|
||||
console.log("[AutoMode] Auto mode idle - waiting for new features");
|
||||
break;
|
||||
|
||||
case "auto_mode_complete":
|
||||
// Legacy event - only handle if it looks like a stop (for backwards compatibility)
|
||||
if (event.message === "Auto mode stopped") {
|
||||
setAutoModeRunning(eventProjectId, false);
|
||||
clearRunningTasks(eventProjectId);
|
||||
console.log("[AutoMode] Auto mode stopped (legacy event)");
|
||||
}
|
||||
break;
|
||||
|
||||
case "auto_mode_error":
|
||||
console.error("[AutoMode Error]", event.error);
|
||||
if (event.featureId && event.error) {
|
||||
@@ -218,128 +189,35 @@ export function useAutoMode() {
|
||||
projectId,
|
||||
addRunningTask,
|
||||
removeRunningTask,
|
||||
clearRunningTasks,
|
||||
setAutoModeRunning,
|
||||
addAutoModeActivity,
|
||||
getProjectIdFromPath,
|
||||
]);
|
||||
|
||||
// Restore auto mode for all projects that were running when app was closed
|
||||
// This runs once on mount to restart auto loops for persisted running states
|
||||
useEffect(() => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) return;
|
||||
|
||||
// Find all projects that have auto mode marked as running
|
||||
const projectsToRestart: Array<{ projectId: string; projectPath: string }> =
|
||||
[];
|
||||
for (const [projectId, state] of Object.entries(autoModeByProject)) {
|
||||
if (state.isRunning) {
|
||||
// Find the project path for this project ID
|
||||
const project = projects.find((p) => p.id === projectId);
|
||||
if (project) {
|
||||
projectsToRestart.push({ projectId, projectPath: project.path });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restart auto mode for each project
|
||||
for (const { projectId, projectPath } of projectsToRestart) {
|
||||
console.log(`[AutoMode] Restoring auto mode for project: ${projectPath}`);
|
||||
api.autoMode
|
||||
.start(projectPath, maxConcurrency)
|
||||
.then((result) => {
|
||||
if (!result.success) {
|
||||
console.error(
|
||||
`[AutoMode] Failed to restore auto mode for ${projectPath}:`,
|
||||
result.error
|
||||
);
|
||||
// Mark as not running if we couldn't restart
|
||||
setAutoModeRunning(projectId, false);
|
||||
} else {
|
||||
console.log(`[AutoMode] Restored auto mode for ${projectPath}`);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
`[AutoMode] Error restoring auto mode for ${projectPath}:`,
|
||||
error
|
||||
);
|
||||
setAutoModeRunning(projectId, false);
|
||||
});
|
||||
}
|
||||
// Only run once on mount - intentionally empty dependency array
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Start auto mode
|
||||
const start = useCallback(async () => {
|
||||
// Start auto mode - UI only, feature pickup is handled in board-view.tsx
|
||||
const start = useCallback(() => {
|
||||
if (!currentProject) {
|
||||
console.error("No project selected");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) {
|
||||
throw new Error("Auto mode API not available");
|
||||
}
|
||||
|
||||
const result = await api.autoMode.start(
|
||||
currentProject.path,
|
||||
maxConcurrency
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setAutoModeRunning(currentProject.id, true);
|
||||
console.log(
|
||||
`[AutoMode] Started successfully with maxConcurrency: ${maxConcurrency}`
|
||||
);
|
||||
} else {
|
||||
console.error("[AutoMode] Failed to start:", result.error);
|
||||
throw new Error(result.error || "Failed to start auto mode");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AutoMode] Error starting:", error);
|
||||
if (currentProject) {
|
||||
setAutoModeRunning(currentProject.id, false);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
setAutoModeRunning(currentProject.id, true);
|
||||
console.log(`[AutoMode] Started with maxConcurrency: ${maxConcurrency}`);
|
||||
}, [currentProject, setAutoModeRunning, maxConcurrency]);
|
||||
|
||||
// Stop auto mode - only turns off the toggle, running tasks continue
|
||||
const stop = useCallback(async () => {
|
||||
// Stop auto mode - UI only, running tasks continue until natural completion
|
||||
const stop = useCallback(() => {
|
||||
if (!currentProject) {
|
||||
console.error("No project selected");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) {
|
||||
throw new Error("Auto mode API not available");
|
||||
}
|
||||
|
||||
const result = await api.autoMode.stop(currentProject.path);
|
||||
|
||||
if (result.success) {
|
||||
setAutoModeRunning(currentProject.id, false);
|
||||
// NOTE: We intentionally do NOT clear running tasks here.
|
||||
// Stopping auto mode only turns off the toggle to prevent new features
|
||||
// from being picked up. Running tasks will complete naturally and be
|
||||
// removed via the auto_mode_feature_complete event.
|
||||
console.log(
|
||||
"[AutoMode] Stopped successfully - running tasks will continue"
|
||||
);
|
||||
} else {
|
||||
console.error("[AutoMode] Failed to stop:", result.error);
|
||||
throw new Error(result.error || "Failed to stop auto mode");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AutoMode] Error stopping:", error);
|
||||
throw error;
|
||||
}
|
||||
setAutoModeRunning(currentProject.id, false);
|
||||
// NOTE: We intentionally do NOT clear running tasks here.
|
||||
// Stopping auto mode only turns off the toggle to prevent new features
|
||||
// from being picked up. Running tasks will complete naturally and be
|
||||
// removed via the auto_mode_feature_complete event.
|
||||
console.log("[AutoMode] Stopped - running tasks will continue");
|
||||
}, [currentProject, setAutoModeRunning]);
|
||||
|
||||
// Stop a specific feature
|
||||
|
||||
@@ -80,7 +80,6 @@ export interface RunningAgentsResult {
|
||||
success: boolean;
|
||||
runningAgents?: RunningAgent[];
|
||||
totalCount?: number;
|
||||
autoLoopRunning?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -217,7 +216,6 @@ export interface AutoModeAPI {
|
||||
status: (projectPath?: string) => Promise<{
|
||||
success: boolean;
|
||||
isRunning?: boolean;
|
||||
autoLoopRunning?: boolean; // Backend uses this name instead of isRunning
|
||||
currentFeatureId?: string | null;
|
||||
runningFeatures?: string[];
|
||||
runningProjects?: string[];
|
||||
@@ -1442,7 +1440,6 @@ function createMockAutoModeAPI(): AutoModeAPI {
|
||||
return {
|
||||
success: true,
|
||||
isRunning: mockAutoModeRunning,
|
||||
autoLoopRunning: mockAutoModeRunning,
|
||||
currentFeatureId: mockAutoModeRunning ? "feature-0" : null,
|
||||
runningFeatures: Array.from(mockRunningFeatures),
|
||||
runningCount: mockRunningFeatures.size,
|
||||
@@ -2593,7 +2590,6 @@ function createMockRunningAgentsAPI(): RunningAgentsAPI {
|
||||
success: true,
|
||||
runningAgents,
|
||||
totalCount: runningAgents.length,
|
||||
autoLoopRunning: mockAutoModeRunning,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -738,7 +738,6 @@ export class HttpApiClient implements ElectronAPI {
|
||||
isAutoMode: boolean;
|
||||
}>;
|
||||
totalCount?: number;
|
||||
autoLoopRunning?: boolean;
|
||||
error?: string;
|
||||
}> => this.get("/api/running-agents"),
|
||||
};
|
||||
|
||||
@@ -296,9 +296,8 @@ export interface Feature {
|
||||
error?: string; // Error message if the agent errored during processing
|
||||
priority?: number; // Priority: 1 = high, 2 = medium, 3 = low
|
||||
dependencies?: string[]; // Array of feature IDs this feature depends on
|
||||
// Worktree info - set when a feature is being worked on in an isolated git worktree
|
||||
worktreePath?: string; // Path to the worktree directory
|
||||
branchName?: string; // Name of the feature branch
|
||||
// Branch info - worktree path is derived at runtime from branchName
|
||||
branchName?: string; // Name of the feature branch (undefined = use current worktree)
|
||||
justFinishedAt?: string; // ISO timestamp when agent just finished and moved to waiting_approval (shows badge for 2 minutes)
|
||||
}
|
||||
|
||||
@@ -404,7 +403,10 @@ export interface AppState {
|
||||
|
||||
// User-managed Worktrees (per-project)
|
||||
// projectPath -> { path: worktreePath or null for main, branch: branch name }
|
||||
currentWorktreeByProject: Record<string, { path: string | null; branch: string }>;
|
||||
currentWorktreeByProject: Record<
|
||||
string,
|
||||
{ path: string | null; branch: string }
|
||||
>;
|
||||
worktreesByProject: Record<
|
||||
string,
|
||||
Array<{
|
||||
@@ -588,7 +590,11 @@ export interface AppActions {
|
||||
|
||||
// Worktree Settings actions
|
||||
setUseWorktrees: (enabled: boolean) => void;
|
||||
setCurrentWorktree: (projectPath: string, worktreePath: string | null, branch: string) => void;
|
||||
setCurrentWorktree: (
|
||||
projectPath: string,
|
||||
worktreePath: string | null,
|
||||
branch: string
|
||||
) => void;
|
||||
setWorktrees: (
|
||||
projectPath: string,
|
||||
worktrees: Array<{
|
||||
@@ -599,7 +605,9 @@ export interface AppActions {
|
||||
changedFilesCount?: number;
|
||||
}>
|
||||
) => void;
|
||||
getCurrentWorktree: (projectPath: string) => { path: string | null; branch: string } | null;
|
||||
getCurrentWorktree: (
|
||||
projectPath: string
|
||||
) => { path: string | null; branch: string } | null;
|
||||
getWorktrees: (projectPath: string) => Array<{
|
||||
path: string;
|
||||
branch: string;
|
||||
@@ -607,6 +615,8 @@ export interface AppActions {
|
||||
hasChanges?: boolean;
|
||||
changedFilesCount?: number;
|
||||
}>;
|
||||
isPrimaryWorktreeBranch: (projectPath: string, branchName: string) => boolean;
|
||||
getPrimaryWorktreeBranch: (projectPath: string) => string | null;
|
||||
|
||||
// Profile Display Settings actions
|
||||
setShowProfilesOnly: (enabled: boolean) => void;
|
||||
@@ -1347,7 +1357,8 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
|
||||
// Feature Default Settings actions
|
||||
setDefaultSkipTests: (skip) => set({ defaultSkipTests: skip }),
|
||||
setEnableDependencyBlocking: (enabled) => set({ enableDependencyBlocking: enabled }),
|
||||
setEnableDependencyBlocking: (enabled) =>
|
||||
set({ enableDependencyBlocking: enabled }),
|
||||
|
||||
// Worktree Settings actions
|
||||
setUseWorktrees: (enabled) => set({ useWorktrees: enabled }),
|
||||
@@ -1380,6 +1391,18 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
return get().worktreesByProject[projectPath] ?? [];
|
||||
},
|
||||
|
||||
isPrimaryWorktreeBranch: (projectPath, branchName) => {
|
||||
const worktrees = get().worktreesByProject[projectPath] ?? [];
|
||||
const primary = worktrees.find((w) => w.isMain);
|
||||
return primary?.branch === branchName;
|
||||
},
|
||||
|
||||
getPrimaryWorktreeBranch: (projectPath) => {
|
||||
const worktrees = get().worktreesByProject[projectPath] ?? [];
|
||||
const primary = worktrees.find((w) => w.isMain);
|
||||
return primary?.branch ?? null;
|
||||
},
|
||||
|
||||
// Profile Display Settings actions
|
||||
setShowProfilesOnly: (enabled) => set({ showProfilesOnly: enabled }),
|
||||
|
||||
@@ -2237,7 +2260,8 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
// Settings
|
||||
apiKeys: state.apiKeys,
|
||||
maxConcurrency: state.maxConcurrency,
|
||||
autoModeByProject: state.autoModeByProject,
|
||||
// Note: autoModeByProject is intentionally NOT persisted
|
||||
// Auto-mode should always default to OFF on app refresh
|
||||
defaultSkipTests: state.defaultSkipTests,
|
||||
enableDependencyBlocking: state.enableDependencyBlocking,
|
||||
useWorktrees: state.useWorktrees,
|
||||
|
||||
48
apps/app/src/types/electron.d.ts
vendored
48
apps/app/src/types/electron.d.ts
vendored
@@ -198,30 +198,6 @@ export type AutoModeEvent =
|
||||
projectId?: string;
|
||||
projectPath?: string;
|
||||
}
|
||||
| {
|
||||
type: "auto_mode_complete";
|
||||
message: string;
|
||||
projectId?: string;
|
||||
projectPath?: string;
|
||||
}
|
||||
| {
|
||||
type: "auto_mode_stopped";
|
||||
message: string;
|
||||
projectId?: string;
|
||||
projectPath?: string;
|
||||
}
|
||||
| {
|
||||
type: "auto_mode_started";
|
||||
message: string;
|
||||
projectId?: string;
|
||||
projectPath?: string;
|
||||
}
|
||||
| {
|
||||
type: "auto_mode_idle";
|
||||
message: string;
|
||||
projectId?: string;
|
||||
projectPath?: string;
|
||||
}
|
||||
| {
|
||||
type: "auto_mode_phase";
|
||||
featureId: string;
|
||||
@@ -310,20 +286,6 @@ export interface SpecRegenerationAPI {
|
||||
}
|
||||
|
||||
export interface AutoModeAPI {
|
||||
start: (
|
||||
projectPath: string,
|
||||
maxConcurrency?: number
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
stop: (projectPath: string) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
runningFeatures?: number;
|
||||
}>;
|
||||
|
||||
stopFeature: (featureId: string) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
@@ -331,7 +293,6 @@ export interface AutoModeAPI {
|
||||
|
||||
status: (projectPath?: string) => Promise<{
|
||||
success: boolean;
|
||||
autoLoopRunning?: boolean;
|
||||
isRunning?: boolean;
|
||||
currentFeatureId?: string | null;
|
||||
runningFeatures?: string[];
|
||||
@@ -343,8 +304,7 @@ export interface AutoModeAPI {
|
||||
runFeature: (
|
||||
projectPath: string,
|
||||
featureId: string,
|
||||
useWorktrees?: boolean,
|
||||
worktreePath?: string
|
||||
useWorktrees?: boolean
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
passes?: boolean;
|
||||
@@ -390,7 +350,7 @@ export interface AutoModeAPI {
|
||||
featureId: string,
|
||||
prompt: string,
|
||||
imagePaths?: string[],
|
||||
worktreePath?: string
|
||||
useWorktrees?: boolean
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
passes?: boolean;
|
||||
@@ -632,6 +592,10 @@ export interface WorktreeAPI {
|
||||
hasChanges?: boolean;
|
||||
changedFilesCount?: number;
|
||||
}>;
|
||||
removedWorktrees?: Array<{
|
||||
path: string;
|
||||
branch: string;
|
||||
}>;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ export const TEST_IDS = {
|
||||
addFeatureButton: "add-feature-button",
|
||||
addFeatureDialog: "add-feature-dialog",
|
||||
confirmAddFeature: "confirm-add-feature",
|
||||
featureBranchInput: "feature-branch-input",
|
||||
featureBranchInput: "feature-input",
|
||||
featureCategoryInput: "feature-category-input",
|
||||
worktreeSelector: "worktree-selector",
|
||||
|
||||
|
||||
@@ -137,8 +137,17 @@ export async function fillAddFeatureDialog(
|
||||
|
||||
// Fill branch if provided (it's a combobox autocomplete)
|
||||
if (options?.branch) {
|
||||
const branchButton = page.locator('[data-testid="feature-branch-input"]');
|
||||
await branchButton.click();
|
||||
// First, select "Other branch" radio option if not already selected
|
||||
const otherBranchRadio = page.locator('[data-testid="feature-radio-group"]').locator('[id="feature-other"]');
|
||||
await otherBranchRadio.waitFor({ state: "visible", timeout: 5000 });
|
||||
await otherBranchRadio.click();
|
||||
// Wait for the branch input to appear
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Now click on the branch input (autocomplete)
|
||||
const branchInput = page.locator('[data-testid="feature-input"]');
|
||||
await branchInput.waitFor({ state: "visible", timeout: 5000 });
|
||||
await branchInput.click();
|
||||
// Wait for the popover to open
|
||||
await page.waitForTimeout(300);
|
||||
// Type in the command input
|
||||
|
||||
@@ -741,14 +741,15 @@ test.describe("Worktree Integration Tests", () => {
|
||||
await waitForNetworkIdle(page);
|
||||
await waitForBoardView(page);
|
||||
|
||||
// Create a worktree first
|
||||
// Note: Worktrees are created at execution time (when feature starts),
|
||||
// not when adding to backlog. We can specify a branch name without
|
||||
// creating a worktree first.
|
||||
const branchName = "feature/test-branch";
|
||||
await apiCreateWorktree(page, testRepo.path, branchName);
|
||||
|
||||
// Click add feature button
|
||||
await clickAddFeature(page);
|
||||
|
||||
// Fill in the feature details
|
||||
// Fill in the feature details with a branch name
|
||||
await fillAddFeatureDialog(page, "Test feature for worktree", {
|
||||
branch: branchName,
|
||||
category: "Testing",
|
||||
@@ -773,9 +774,12 @@ test.describe("Worktree Integration Tests", () => {
|
||||
expect(featureData.description).toBe("Test feature for worktree");
|
||||
expect(featureData.branchName).toBe(branchName);
|
||||
expect(featureData.status).toBe("backlog");
|
||||
// Verify worktreePath is not set when adding to backlog
|
||||
// (worktrees are created at execution time, not when adding to backlog)
|
||||
expect(featureData.worktreePath).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should create worktree automatically when adding feature with new branch", async ({
|
||||
test("should store branch name when adding feature with new branch (worktree created at execution)", async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupProjectWithPath(page, testRepo.path);
|
||||
@@ -783,12 +787,13 @@ test.describe("Worktree Integration Tests", () => {
|
||||
await waitForNetworkIdle(page);
|
||||
await waitForBoardView(page);
|
||||
|
||||
// Use a branch name that doesn't exist yet - NO worktree is pre-created
|
||||
// Use a branch name that doesn't exist yet
|
||||
// Note: Worktrees are now created at execution time, not when adding to backlog
|
||||
const branchName = "feature/auto-create-worktree";
|
||||
const expectedWorktreePath = getWorktreePath(testRepo.path, branchName);
|
||||
|
||||
// Verify worktree does NOT exist before we create the feature
|
||||
expect(fs.existsSync(expectedWorktreePath)).toBe(false);
|
||||
// Verify branch does NOT exist before we create the feature
|
||||
const branchesBefore = await listBranches(testRepo.path);
|
||||
expect(branchesBefore).not.toContain(branchName);
|
||||
|
||||
// Click add feature button
|
||||
await clickAddFeature(page);
|
||||
@@ -802,17 +807,14 @@ test.describe("Worktree Integration Tests", () => {
|
||||
// Confirm
|
||||
await confirmAddFeature(page);
|
||||
|
||||
// Wait for the worktree to be created
|
||||
await page.waitForTimeout(2000);
|
||||
// Wait for feature to be saved
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify worktree was automatically created when feature was added
|
||||
expect(fs.existsSync(expectedWorktreePath)).toBe(true);
|
||||
// Verify branch was NOT created when adding feature (created at execution time)
|
||||
const branchesAfter = await listBranches(testRepo.path);
|
||||
expect(branchesAfter).not.toContain(branchName);
|
||||
|
||||
// Verify the branch was created
|
||||
const branches = await listBranches(testRepo.path);
|
||||
expect(branches).toContain(branchName);
|
||||
|
||||
// Verify feature was created with correct branch
|
||||
// Verify feature was created with correct branch name stored
|
||||
const featuresDir = path.join(testRepo.path, ".automaker", "features");
|
||||
const featureDirs = fs.readdirSync(featuresDir);
|
||||
expect(featureDirs.length).toBeGreaterThan(0);
|
||||
@@ -829,8 +831,15 @@ test.describe("Worktree Integration Tests", () => {
|
||||
|
||||
const featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
|
||||
const featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
||||
|
||||
// Verify branch name is stored
|
||||
expect(featureData.branchName).toBe(branchName);
|
||||
expect(featureData.worktreePath).toBe(expectedWorktreePath);
|
||||
|
||||
// Verify worktreePath is NOT set (worktrees are created at execution time)
|
||||
expect(featureData.worktreePath).toBeUndefined();
|
||||
|
||||
// Verify feature is in backlog status
|
||||
expect(featureData.status).toBe("backlog");
|
||||
});
|
||||
|
||||
test("should reset feature branch and worktree when worktree is deleted", async ({
|
||||
@@ -887,8 +896,11 @@ test.describe("Worktree Integration Tests", () => {
|
||||
|
||||
let featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
|
||||
let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
||||
|
||||
// Verify feature was created with the branch name stored
|
||||
expect(featureData.branchName).toBe(branchName);
|
||||
expect(featureData.worktreePath).toBe(worktreePath);
|
||||
// Verify worktreePath is NOT set (worktrees are created at execution time, not when adding)
|
||||
expect(featureData.worktreePath).toBeUndefined();
|
||||
|
||||
// Delete the worktree via UI
|
||||
// Open the worktree actions menu
|
||||
@@ -911,10 +923,11 @@ test.describe("Worktree Integration Tests", () => {
|
||||
// Verify worktree is deleted
|
||||
expect(fs.existsSync(worktreePath)).toBe(false);
|
||||
|
||||
// Verify feature's branchName and worktreePath are reset to null
|
||||
// Verify feature's branchName is reset to null/undefined when worktree is deleted
|
||||
// (worktreePath was never stored, so it remains undefined)
|
||||
featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
||||
expect(featureData.branchName).toBeNull();
|
||||
expect(featureData.worktreePath).toBeNull();
|
||||
expect(featureData.worktreePath).toBeUndefined();
|
||||
|
||||
// Verify the feature appears in the backlog when main is selected
|
||||
const mainButton = page.getByRole("button", { name: "main" }).first();
|
||||
@@ -940,8 +953,9 @@ test.describe("Worktree Integration Tests", () => {
|
||||
await otherWorktreeButton.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Unassigned features should still be visible in the backlog
|
||||
await expect(featureText).toBeVisible({ timeout: 5000 });
|
||||
// Unassigned features should NOT be visible on non-primary worktrees
|
||||
// They should only show on the primary (main) worktree
|
||||
await expect(featureText).not.toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test("should filter features by selected worktree", async ({ page }) => {
|
||||
@@ -1062,9 +1076,11 @@ test.describe("Worktree Integration Tests", () => {
|
||||
// Open add feature dialog
|
||||
await clickAddFeature(page);
|
||||
|
||||
// Verify the branch input button shows the selected worktree's branch
|
||||
const branchButton = page.locator('[data-testid="feature-branch-input"]');
|
||||
await expect(branchButton).toContainText(branchName, { timeout: 5000 });
|
||||
// Verify the branch selector shows the selected worktree's branch
|
||||
// When a worktree is selected, "Use current selected branch" should be selected
|
||||
// and the branch name should be shown in the label
|
||||
const currentBranchLabel = page.locator('label[for="feature-current"]');
|
||||
await expect(currentBranchLabel).toContainText(branchName, { timeout: 5000 });
|
||||
|
||||
// Close dialog
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
import { Router } from "express";
|
||||
import type { AutoModeService } from "../../services/auto-mode-service.js";
|
||||
import { createStartHandler } from "./routes/start.js";
|
||||
import { createStopHandler } from "./routes/stop.js";
|
||||
import { createStopFeatureHandler } from "./routes/stop-feature.js";
|
||||
import { createStatusHandler } from "./routes/status.js";
|
||||
import { createRunFeatureHandler } from "./routes/run-feature.js";
|
||||
@@ -21,8 +19,6 @@ import { createCommitFeatureHandler } from "./routes/commit-feature.js";
|
||||
export function createAutoModeRoutes(autoModeService: AutoModeService): Router {
|
||||
const router = Router();
|
||||
|
||||
router.post("/start", createStartHandler(autoModeService));
|
||||
router.post("/stop", createStopHandler(autoModeService));
|
||||
router.post("/stop-feature", createStopFeatureHandler(autoModeService));
|
||||
router.post("/status", createStatusHandler(autoModeService));
|
||||
router.post("/run-feature", createRunFeatureHandler(autoModeService));
|
||||
|
||||
@@ -12,13 +12,14 @@ const logger = createLogger("AutoMode");
|
||||
export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, featureId, prompt, imagePaths, worktreePath } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
prompt: string;
|
||||
imagePaths?: string[];
|
||||
worktreePath?: string;
|
||||
};
|
||||
const { projectPath, featureId, prompt, imagePaths, useWorktrees } =
|
||||
req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
prompt: string;
|
||||
imagePaths?: string[];
|
||||
useWorktrees?: boolean;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId || !prompt) {
|
||||
res.status(400).json({
|
||||
@@ -28,14 +29,25 @@ export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start follow-up in background, using the feature's worktreePath for correct branch
|
||||
// Start follow-up in background
|
||||
// followUpFeature derives workDir from feature.branchName
|
||||
autoModeService
|
||||
.followUpFeature(projectPath, featureId, prompt, imagePaths, worktreePath)
|
||||
.followUpFeature(
|
||||
projectPath,
|
||||
featureId,
|
||||
prompt,
|
||||
imagePaths,
|
||||
useWorktrees ?? true
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error(
|
||||
`[AutoMode] Follow up feature ${featureId} error:`,
|
||||
error
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
// Release the starting slot when follow-up completes (success or error)
|
||||
// Note: The feature should be in runningFeatures by this point
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
|
||||
@@ -19,12 +19,10 @@ export function createResumeFeatureHandler(autoModeService: AutoModeService) {
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "projectPath and featureId are required",
|
||||
});
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath and featureId are required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,7 +32,8 @@ export function createResumeFeatureHandler(autoModeService: AutoModeService) {
|
||||
.resumeFeature(projectPath, featureId, useWorktrees ?? false)
|
||||
.catch((error) => {
|
||||
logger.error(`[AutoMode] Resume feature ${featureId} error:`, error);
|
||||
});
|
||||
})
|
||||
.finally(() => {});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
|
||||
@@ -12,30 +12,30 @@ const logger = createLogger("AutoMode");
|
||||
export function createRunFeatureHandler(autoModeService: AutoModeService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, featureId, useWorktrees, worktreePath } = req.body as {
|
||||
const { projectPath, featureId, useWorktrees } = req.body as {
|
||||
projectPath: string;
|
||||
featureId: string;
|
||||
useWorktrees?: boolean;
|
||||
worktreePath?: string;
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "projectPath and featureId are required",
|
||||
});
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath and featureId are required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Start execution in background
|
||||
// If worktreePath is provided, use it directly; otherwise let the service decide
|
||||
// Default to false - worktrees should only be used when explicitly enabled
|
||||
// executeFeature derives workDir from feature.branchName
|
||||
autoModeService
|
||||
.executeFeature(projectPath, featureId, useWorktrees ?? false, false, worktreePath)
|
||||
.executeFeature(projectPath, featureId, useWorktrees ?? false, false)
|
||||
.catch((error) => {
|
||||
logger.error(`[AutoMode] Feature ${featureId} error:`, error);
|
||||
})
|
||||
.finally(() => {
|
||||
// Release the starting slot when execution completes (success or error)
|
||||
// Note: The feature should be in runningFeatures by this point
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* POST /start endpoint - Start auto mode loop
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
|
||||
export function createStartHandler(autoModeService: AutoModeService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { projectPath, maxConcurrency } = req.body as {
|
||||
projectPath: string;
|
||||
maxConcurrency?: number;
|
||||
};
|
||||
|
||||
if (!projectPath) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
await autoModeService.startAutoLoop(projectPath, maxConcurrency || 3);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError(error, "Start auto loop failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* POST /stop endpoint - Stop auto mode loop
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
|
||||
export function createStopHandler(autoModeService: AutoModeService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const runningCount = await autoModeService.stopAutoLoop();
|
||||
res.json({ success: true, runningFeatures: runningCount });
|
||||
} catch (error) {
|
||||
logError(error, "Stop auto loop failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -16,7 +16,6 @@ export function createIndexHandler(autoModeService: AutoModeService) {
|
||||
success: true,
|
||||
runningAgents,
|
||||
totalCount: runningAgents.length,
|
||||
autoLoopRunning: status.autoLoopRunning,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Get running agents failed");
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { existsSync } from "fs";
|
||||
import { isGitRepo, getErrorMessage, logError, normalizePath } from "../common.js";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
@@ -58,10 +59,12 @@ export function createListHandler() {
|
||||
});
|
||||
|
||||
const worktrees: WorktreeInfo[] = [];
|
||||
const removedWorktrees: Array<{ path: string; branch: string }> = [];
|
||||
const lines = stdout.split("\n");
|
||||
let current: { path?: string; branch?: string } = {};
|
||||
let isFirst = true;
|
||||
|
||||
// First pass: detect removed worktrees
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("worktree ")) {
|
||||
current.path = normalizePath(line.slice(9));
|
||||
@@ -69,19 +72,40 @@ export function createListHandler() {
|
||||
current.branch = line.slice(7).replace("refs/heads/", "");
|
||||
} else if (line === "") {
|
||||
if (current.path && current.branch) {
|
||||
worktrees.push({
|
||||
path: current.path,
|
||||
branch: current.branch,
|
||||
isMain: isFirst,
|
||||
isCurrent: current.branch === currentBranch,
|
||||
hasWorktree: true,
|
||||
});
|
||||
isFirst = false;
|
||||
const isMainWorktree = isFirst;
|
||||
// Check if the worktree directory actually exists
|
||||
// Skip checking/pruning the main worktree (projectPath itself)
|
||||
if (!isMainWorktree && !existsSync(current.path)) {
|
||||
// Worktree directory doesn't exist - it was manually deleted
|
||||
removedWorktrees.push({
|
||||
path: current.path,
|
||||
branch: current.branch,
|
||||
});
|
||||
} else {
|
||||
// Worktree exists (or is main worktree), add it to the list
|
||||
worktrees.push({
|
||||
path: current.path,
|
||||
branch: current.branch,
|
||||
isMain: isMainWorktree,
|
||||
isCurrent: current.branch === currentBranch,
|
||||
hasWorktree: true,
|
||||
});
|
||||
isFirst = false;
|
||||
}
|
||||
}
|
||||
current = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Prune removed worktrees from git (only if any were detected)
|
||||
if (removedWorktrees.length > 0) {
|
||||
try {
|
||||
await execAsync("git worktree prune", { cwd: projectPath });
|
||||
} catch {
|
||||
// Prune failed, but we'll still report the removed worktrees
|
||||
}
|
||||
}
|
||||
|
||||
// If includeDetails is requested, fetch change status for each worktree
|
||||
if (includeDetails) {
|
||||
for (const worktree of worktrees) {
|
||||
@@ -103,7 +127,11 @@ export function createListHandler() {
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, worktrees });
|
||||
res.json({
|
||||
success: true,
|
||||
worktrees,
|
||||
removedWorktrees: removedWorktrees.length > 0 ? removedWorktrees : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "List worktrees failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
|
||||
@@ -20,14 +20,8 @@ import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
||||
import { resolveModelString, DEFAULT_MODELS } from "../lib/model-resolver.js";
|
||||
import { createAutoModeOptions } from "../lib/sdk-options.js";
|
||||
import { isAbortError, classifyError } from "../lib/error-handler.js";
|
||||
import { resolveDependencies, areDependenciesSatisfied } from "../lib/dependency-resolver.js";
|
||||
import type { Feature } from "./feature-loader.js";
|
||||
import {
|
||||
getFeatureDir,
|
||||
getFeaturesDir,
|
||||
getAutomakerDir,
|
||||
getWorktreesDir,
|
||||
} from "../lib/automaker-paths.js";
|
||||
import { getFeatureDir, getAutomakerDir } from "../lib/automaker-paths.js";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -41,196 +35,43 @@ interface RunningFeature {
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
interface AutoModeConfig {
|
||||
maxConcurrency: number;
|
||||
useWorktrees: boolean;
|
||||
projectPath: string;
|
||||
}
|
||||
|
||||
export class AutoModeService {
|
||||
private events: EventEmitter;
|
||||
private runningFeatures = new Map<string, RunningFeature>();
|
||||
private autoLoopRunning = false;
|
||||
private autoLoopAbortController: AbortController | null = null;
|
||||
private config: AutoModeConfig | null = null;
|
||||
|
||||
constructor(events: EventEmitter) {
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the auto mode loop - continuously picks and executes pending features
|
||||
*/
|
||||
async startAutoLoop(projectPath: string, maxConcurrency = 3): Promise<void> {
|
||||
if (this.autoLoopRunning) {
|
||||
throw new Error("Auto mode is already running");
|
||||
}
|
||||
|
||||
this.autoLoopRunning = true;
|
||||
this.autoLoopAbortController = new AbortController();
|
||||
this.config = {
|
||||
maxConcurrency,
|
||||
useWorktrees: true,
|
||||
projectPath,
|
||||
};
|
||||
|
||||
this.emitAutoModeEvent("auto_mode_started", {
|
||||
message: `Auto mode started with max ${maxConcurrency} concurrent features`,
|
||||
projectPath,
|
||||
});
|
||||
|
||||
// Run the loop in the background
|
||||
this.runAutoLoop().catch((error) => {
|
||||
console.error("[AutoMode] Loop error:", error);
|
||||
this.emitAutoModeEvent("auto_mode_error", {
|
||||
error: error.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async runAutoLoop(): Promise<void> {
|
||||
while (
|
||||
this.autoLoopRunning &&
|
||||
this.autoLoopAbortController &&
|
||||
!this.autoLoopAbortController.signal.aborted
|
||||
) {
|
||||
try {
|
||||
// Check if we have capacity
|
||||
if (this.runningFeatures.size >= (this.config?.maxConcurrency || 3)) {
|
||||
await this.sleep(5000);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Load pending features
|
||||
const pendingFeatures = await this.loadPendingFeatures(
|
||||
this.config!.projectPath
|
||||
);
|
||||
|
||||
if (pendingFeatures.length === 0) {
|
||||
this.emitAutoModeEvent("auto_mode_idle", {
|
||||
message: "No pending features - auto mode idle",
|
||||
projectPath: this.config!.projectPath,
|
||||
});
|
||||
await this.sleep(10000);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find a feature not currently running
|
||||
const nextFeature = pendingFeatures.find(
|
||||
(f) => !this.runningFeatures.has(f.id)
|
||||
);
|
||||
|
||||
if (nextFeature) {
|
||||
// Start feature execution in background
|
||||
this.executeFeature(
|
||||
this.config!.projectPath,
|
||||
nextFeature.id,
|
||||
this.config!.useWorktrees,
|
||||
true
|
||||
).catch((error) => {
|
||||
console.error(`[AutoMode] Feature ${nextFeature.id} error:`, error);
|
||||
});
|
||||
}
|
||||
|
||||
await this.sleep(2000);
|
||||
} catch (error) {
|
||||
console.error("[AutoMode] Loop iteration error:", error);
|
||||
await this.sleep(5000);
|
||||
}
|
||||
}
|
||||
|
||||
this.autoLoopRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the auto mode loop
|
||||
*/
|
||||
async stopAutoLoop(): Promise<number> {
|
||||
const wasRunning = this.autoLoopRunning;
|
||||
this.autoLoopRunning = false;
|
||||
if (this.autoLoopAbortController) {
|
||||
this.autoLoopAbortController.abort();
|
||||
this.autoLoopAbortController = null;
|
||||
}
|
||||
|
||||
// Emit stop event immediately when user explicitly stops
|
||||
if (wasRunning) {
|
||||
this.emitAutoModeEvent("auto_mode_stopped", {
|
||||
message: "Auto mode stopped",
|
||||
projectPath: this.config?.projectPath,
|
||||
});
|
||||
}
|
||||
|
||||
return this.runningFeatures.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single feature
|
||||
* @param projectPath - The main project path
|
||||
* @param featureId - The feature ID to execute
|
||||
* @param useWorktrees - Whether to use worktrees for isolation
|
||||
* @param isAutoMode - Whether this is running in auto mode
|
||||
* @param providedWorktreePath - Optional: use this worktree path instead of creating a new one
|
||||
*/
|
||||
async executeFeature(
|
||||
projectPath: string,
|
||||
featureId: string,
|
||||
useWorktrees = false,
|
||||
isAutoMode = false,
|
||||
providedWorktreePath?: string
|
||||
isAutoMode = false
|
||||
): Promise<void> {
|
||||
if (this.runningFeatures.has(featureId)) {
|
||||
throw new Error(`Feature ${featureId} is already running`);
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
const branchName = `feature/${featureId}`;
|
||||
let worktreePath: string | null = null;
|
||||
|
||||
// Use provided worktree path if given, otherwise setup new worktree if enabled
|
||||
if (providedWorktreePath) {
|
||||
// Resolve to absolute path - critical for cross-platform compatibility
|
||||
// On Windows, relative paths or paths with forward slashes may not work correctly with cwd
|
||||
// On all platforms, absolute paths ensure commands execute in the correct directory
|
||||
try {
|
||||
// Resolve relative paths relative to projectPath, absolute paths as-is
|
||||
const resolvedPath = path.isAbsolute(providedWorktreePath)
|
||||
? path.resolve(providedWorktreePath)
|
||||
: path.resolve(projectPath, providedWorktreePath);
|
||||
|
||||
// Verify the path exists before using it
|
||||
await fs.access(resolvedPath);
|
||||
worktreePath = resolvedPath;
|
||||
console.log(`[AutoMode] Using provided worktree path (resolved): ${worktreePath}`);
|
||||
} catch (error) {
|
||||
console.error(`[AutoMode] Provided worktree path invalid or doesn't exist: ${providedWorktreePath}`, error);
|
||||
// Fall through to create new worktree or use project path
|
||||
}
|
||||
}
|
||||
|
||||
if (!worktreePath && useWorktrees) {
|
||||
// No specific worktree provided, create a new one for this feature
|
||||
worktreePath = await this.setupWorktree(
|
||||
projectPath,
|
||||
featureId,
|
||||
branchName
|
||||
// Check if feature has existing context - if so, resume instead of starting fresh
|
||||
const hasExistingContext = await this.contextExists(projectPath, featureId);
|
||||
if (hasExistingContext) {
|
||||
console.log(
|
||||
`[AutoMode] Feature ${featureId} has existing context, resuming instead of starting fresh`
|
||||
);
|
||||
return this.resumeFeature(projectPath, featureId, useWorktrees);
|
||||
}
|
||||
|
||||
// Ensure workDir is always an absolute path for cross-platform compatibility
|
||||
const workDir = worktreePath ? path.resolve(worktreePath) : path.resolve(projectPath);
|
||||
const abortController = new AbortController();
|
||||
|
||||
this.runningFeatures.set(featureId, {
|
||||
featureId,
|
||||
projectPath,
|
||||
worktreePath,
|
||||
branchName,
|
||||
abortController,
|
||||
isAutoMode,
|
||||
startTime: Date.now(),
|
||||
});
|
||||
|
||||
// Emit feature start event
|
||||
// Emit feature start event early
|
||||
this.emitAutoModeEvent("auto_mode_feature_start", {
|
||||
featureId,
|
||||
projectPath,
|
||||
@@ -242,12 +83,53 @@ export class AutoModeService {
|
||||
});
|
||||
|
||||
try {
|
||||
// Load feature details
|
||||
// Load feature details FIRST to get branchName
|
||||
const feature = await this.loadFeature(projectPath, featureId);
|
||||
if (!feature) {
|
||||
throw new Error(`Feature ${featureId} not found`);
|
||||
}
|
||||
|
||||
// Derive workDir from feature.branchName
|
||||
// If no branchName, use the project path directly
|
||||
let worktreePath: string | null = null;
|
||||
const branchName = feature.branchName || null;
|
||||
|
||||
if (useWorktrees && branchName) {
|
||||
// Try to find existing worktree for this branch
|
||||
worktreePath = await this.findExistingWorktreeForBranch(
|
||||
projectPath,
|
||||
branchName
|
||||
);
|
||||
|
||||
if (!worktreePath) {
|
||||
// Create worktree for this branch
|
||||
worktreePath = await this.setupWorktree(
|
||||
projectPath,
|
||||
featureId,
|
||||
branchName
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[AutoMode] Using worktree for branch "${branchName}": ${worktreePath}`
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure workDir is always an absolute path for cross-platform compatibility
|
||||
const workDir = worktreePath
|
||||
? path.resolve(worktreePath)
|
||||
: path.resolve(projectPath);
|
||||
|
||||
this.runningFeatures.set(featureId, {
|
||||
featureId,
|
||||
projectPath,
|
||||
worktreePath,
|
||||
branchName,
|
||||
abortController,
|
||||
isAutoMode,
|
||||
startTime: Date.now(),
|
||||
});
|
||||
|
||||
// Update feature status to in_progress
|
||||
await this.updateFeatureStatus(projectPath, featureId, "in_progress");
|
||||
|
||||
@@ -262,7 +144,7 @@ export class AutoModeService {
|
||||
// Get model from feature
|
||||
const model = resolveModelString(feature.model, DEFAULT_MODELS.claude);
|
||||
console.log(
|
||||
`[AutoMode] Executing feature ${featureId} with model: ${model}`
|
||||
`[AutoMode] Executing feature ${featureId} with model: ${model} in ${workDir}`
|
||||
);
|
||||
|
||||
// Run the agent with the feature's model and images
|
||||
@@ -271,6 +153,7 @@ export class AutoModeService {
|
||||
featureId,
|
||||
prompt,
|
||||
abortController,
|
||||
projectPath,
|
||||
imagePaths,
|
||||
model
|
||||
);
|
||||
@@ -371,7 +254,7 @@ export class AutoModeService {
|
||||
featureId: string,
|
||||
prompt: string,
|
||||
imagePaths?: string[],
|
||||
providedWorktreePath?: string
|
||||
useWorktrees = true
|
||||
): Promise<void> {
|
||||
if (this.runningFeatures.has(featureId)) {
|
||||
throw new Error(`Feature ${featureId} is already running`);
|
||||
@@ -379,32 +262,29 @@ export class AutoModeService {
|
||||
|
||||
const abortController = new AbortController();
|
||||
|
||||
// Use the provided worktreePath (from the feature's assigned branch)
|
||||
// Fall back to project path if not provided
|
||||
// Load feature info for context FIRST to get branchName
|
||||
const feature = await this.loadFeature(projectPath, featureId);
|
||||
|
||||
// Derive workDir from feature.branchName
|
||||
let workDir = path.resolve(projectPath);
|
||||
let worktreePath: string | null = null;
|
||||
const branchName = feature?.branchName || null;
|
||||
|
||||
if (providedWorktreePath) {
|
||||
try {
|
||||
// Resolve to absolute path - critical for cross-platform compatibility
|
||||
// On Windows, relative paths or paths with forward slashes may not work correctly with cwd
|
||||
// On all platforms, absolute paths ensure commands execute in the correct directory
|
||||
const resolvedPath = path.isAbsolute(providedWorktreePath)
|
||||
? path.resolve(providedWorktreePath)
|
||||
: path.resolve(projectPath, providedWorktreePath);
|
||||
|
||||
await fs.access(resolvedPath);
|
||||
workDir = resolvedPath;
|
||||
worktreePath = resolvedPath;
|
||||
} catch {
|
||||
// Worktree path provided but doesn't exist, use project path
|
||||
console.log(`[AutoMode] Provided worktreePath doesn't exist: ${providedWorktreePath}, using project path`);
|
||||
if (useWorktrees && branchName) {
|
||||
// Try to find existing worktree for this branch
|
||||
worktreePath = await this.findExistingWorktreeForBranch(
|
||||
projectPath,
|
||||
branchName
|
||||
);
|
||||
|
||||
if (worktreePath) {
|
||||
workDir = worktreePath;
|
||||
console.log(
|
||||
`[AutoMode] Follow-up using worktree for branch "${branchName}": ${workDir}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Load feature info for context
|
||||
const feature = await this.loadFeature(projectPath, featureId);
|
||||
|
||||
// Load previous agent output if it exists
|
||||
const featureDir = getFeatureDir(projectPath, featureId);
|
||||
const contextPath = path.join(featureDir, "agent-output.md");
|
||||
@@ -441,7 +321,7 @@ Address the follow-up instructions above. Review the previous work and make the
|
||||
featureId,
|
||||
projectPath,
|
||||
worktreePath,
|
||||
branchName: worktreePath ? path.basename(worktreePath) : null,
|
||||
branchName,
|
||||
abortController,
|
||||
isAutoMode: false,
|
||||
startTime: Date.now(),
|
||||
@@ -537,6 +417,7 @@ Address the follow-up instructions above. Review the previous work and make the
|
||||
featureId,
|
||||
fullPrompt,
|
||||
abortController,
|
||||
projectPath,
|
||||
allImagePaths.length > 0 ? allImagePaths : imagePaths,
|
||||
model,
|
||||
previousContext || undefined
|
||||
@@ -653,17 +534,25 @@ Address the follow-up instructions above. Review the previous work and make the
|
||||
workDir = providedWorktreePath;
|
||||
console.log(`[AutoMode] Committing in provided worktree: ${workDir}`);
|
||||
} catch {
|
||||
console.log(`[AutoMode] Provided worktree path doesn't exist: ${providedWorktreePath}, using project path`);
|
||||
console.log(
|
||||
`[AutoMode] Provided worktree path doesn't exist: ${providedWorktreePath}, using project path`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Fallback: try to find worktree at legacy location
|
||||
const legacyWorktreePath = path.join(projectPath, ".worktrees", featureId);
|
||||
const legacyWorktreePath = path.join(
|
||||
projectPath,
|
||||
".worktrees",
|
||||
featureId
|
||||
);
|
||||
try {
|
||||
await fs.access(legacyWorktreePath);
|
||||
workDir = legacyWorktreePath;
|
||||
console.log(`[AutoMode] Committing in legacy worktree: ${workDir}`);
|
||||
} catch {
|
||||
console.log(`[AutoMode] No worktree found, committing in project path: ${workDir}`);
|
||||
console.log(
|
||||
`[AutoMode] No worktree found, committing in project path: ${workDir}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,13 +705,11 @@ Format your response as a structured markdown document.`;
|
||||
*/
|
||||
getStatus(): {
|
||||
isRunning: boolean;
|
||||
autoLoopRunning: boolean;
|
||||
runningFeatures: string[];
|
||||
runningCount: number;
|
||||
} {
|
||||
return {
|
||||
isRunning: this.autoLoopRunning || this.runningFeatures.size > 0,
|
||||
autoLoopRunning: this.autoLoopRunning,
|
||||
isRunning: this.runningFeatures.size > 0,
|
||||
runningFeatures: Array.from(this.runningFeatures.keys()),
|
||||
runningCount: this.runningFeatures.size,
|
||||
};
|
||||
@@ -905,10 +792,15 @@ Format your response as a structured markdown document.`;
|
||||
branchName: string
|
||||
): Promise<string> {
|
||||
// First, check if git already has a worktree for this branch (anywhere)
|
||||
const existingWorktree = await this.findExistingWorktreeForBranch(projectPath, branchName);
|
||||
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}`);
|
||||
console.log(
|
||||
`[AutoMode] Found existing worktree for branch "${branchName}" at: ${existingWorktree}`
|
||||
);
|
||||
return existingWorktree;
|
||||
}
|
||||
|
||||
@@ -992,56 +884,6 @@ Format your response as a structured markdown document.`;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadPendingFeatures(projectPath: string): Promise<Feature[]> {
|
||||
// Features are stored in .automaker directory
|
||||
const featuresDir = getFeaturesDir(projectPath);
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(featuresDir, { withFileTypes: true });
|
||||
const allFeatures: Feature[] = [];
|
||||
const pendingFeatures: Feature[] = [];
|
||||
|
||||
// Load all features (for dependency checking)
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const featurePath = path.join(
|
||||
featuresDir,
|
||||
entry.name,
|
||||
"feature.json"
|
||||
);
|
||||
try {
|
||||
const data = await fs.readFile(featurePath, "utf-8");
|
||||
const feature = JSON.parse(data);
|
||||
allFeatures.push(feature);
|
||||
|
||||
// Track pending features separately
|
||||
if (
|
||||
feature.status === "pending" ||
|
||||
feature.status === "ready" ||
|
||||
feature.status === "backlog"
|
||||
) {
|
||||
pendingFeatures.push(feature);
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid features
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply dependency-aware ordering
|
||||
const { orderedFeatures } = resolveDependencies(pendingFeatures);
|
||||
|
||||
// Filter to only features with satisfied dependencies
|
||||
const readyFeatures = orderedFeatures.filter(feature =>
|
||||
areDependenciesSatisfied(feature, allFeatures)
|
||||
);
|
||||
|
||||
return readyFeatures;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a title from feature description (first line or truncated)
|
||||
*/
|
||||
@@ -1060,31 +902,6 @@ Format your response as a structured markdown document.`;
|
||||
return firstLine.substring(0, 57) + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract image paths from feature's imagePaths array
|
||||
* Handles both string paths and objects with path property
|
||||
*/
|
||||
private extractImagePaths(
|
||||
imagePaths:
|
||||
| Array<string | { path: string; [key: string]: unknown }>
|
||||
| undefined,
|
||||
projectPath: string
|
||||
): string[] {
|
||||
if (!imagePaths || imagePaths.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return imagePaths
|
||||
.map((imgPath) => {
|
||||
const pathStr = typeof imgPath === "string" ? imgPath : imgPath.path;
|
||||
// Resolve relative paths to absolute paths
|
||||
return path.isAbsolute(pathStr)
|
||||
? pathStr
|
||||
: path.join(projectPath, pathStr);
|
||||
})
|
||||
.filter((p) => p); // Filter out any empty paths
|
||||
}
|
||||
|
||||
private buildFeaturePrompt(feature: Feature): string {
|
||||
const title = this.extractTitleFromDescription(feature.description);
|
||||
|
||||
@@ -1164,6 +981,7 @@ This helps parse your summary correctly in the output logs.`;
|
||||
featureId: string,
|
||||
prompt: string,
|
||||
abortController: AbortController,
|
||||
projectPath: string,
|
||||
imagePaths?: string[],
|
||||
model?: string,
|
||||
previousContent?: string
|
||||
@@ -1171,7 +989,9 @@ This helps parse your summary correctly in the output logs.`;
|
||||
// CI/CD Mock Mode: Return early with mock response when AUTOMAKER_MOCK_AGENT is set
|
||||
// This prevents actual API calls during automated testing
|
||||
if (process.env.AUTOMAKER_MOCK_AGENT === "true") {
|
||||
console.log(`[AutoMode] MOCK MODE: Skipping real agent execution for feature ${featureId}`);
|
||||
console.log(
|
||||
`[AutoMode] MOCK MODE: Skipping real agent execution for feature ${featureId}`
|
||||
);
|
||||
|
||||
// Simulate some work being done
|
||||
await this.sleep(500);
|
||||
@@ -1203,8 +1023,7 @@ This helps parse your summary correctly in the output logs.`;
|
||||
await this.sleep(200);
|
||||
|
||||
// Save mock agent output
|
||||
const configProjectPath = this.config?.projectPath || workDir;
|
||||
const featureDirForOutput = getFeatureDir(configProjectPath, featureId);
|
||||
const featureDirForOutput = getFeatureDir(projectPath, featureId);
|
||||
const outputPath = path.join(featureDirForOutput, "agent-output.md");
|
||||
|
||||
const mockOutput = `# Mock Agent Output
|
||||
@@ -1222,7 +1041,9 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await fs.writeFile(outputPath, mockOutput);
|
||||
|
||||
console.log(`[AutoMode] MOCK MODE: Completed mock execution for feature ${featureId}`);
|
||||
console.log(
|
||||
`[AutoMode] MOCK MODE: Completed mock execution for feature ${featureId}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1273,10 +1094,8 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
||||
? `${previousContent}\n\n---\n\n## Follow-up Session\n\n`
|
||||
: "";
|
||||
// Agent output goes to .automaker directory
|
||||
// Note: We use the original projectPath here (from config), not workDir
|
||||
// because workDir might be a worktree path
|
||||
const configProjectPath = this.config?.projectPath || workDir;
|
||||
const featureDirForOutput = getFeatureDir(configProjectPath, featureId);
|
||||
// Note: We use projectPath here, not workDir, because workDir might be a worktree path
|
||||
const featureDirForOutput = getFeatureDir(projectPath, featureId);
|
||||
const outputPath = path.join(featureDirForOutput, "agent-output.md");
|
||||
|
||||
// Incremental file writing state
|
||||
@@ -1290,7 +1109,10 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
||||
await fs.writeFile(outputPath, responseText);
|
||||
} catch (error) {
|
||||
// Log but don't crash - file write errors shouldn't stop execution
|
||||
console.error(`[AutoMode] Failed to write agent output for ${featureId}:`, error);
|
||||
console.error(
|
||||
`[AutoMode] Failed to write agent output for ${featureId}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1309,11 +1131,11 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === "text") {
|
||||
// Add separator before new text if we already have content and it doesn't end with newlines
|
||||
if (responseText.length > 0 && !responseText.endsWith('\n\n')) {
|
||||
if (responseText.endsWith('\n')) {
|
||||
responseText += '\n';
|
||||
if (responseText.length > 0 && !responseText.endsWith("\n\n")) {
|
||||
if (responseText.endsWith("\n")) {
|
||||
responseText += "\n";
|
||||
} else {
|
||||
responseText += '\n\n';
|
||||
responseText += "\n\n";
|
||||
}
|
||||
}
|
||||
responseText += block.text || "";
|
||||
@@ -1347,12 +1169,16 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
||||
});
|
||||
|
||||
// Also add to file output for persistence
|
||||
if (responseText.length > 0 && !responseText.endsWith('\n')) {
|
||||
responseText += '\n';
|
||||
if (responseText.length > 0 && !responseText.endsWith("\n")) {
|
||||
responseText += "\n";
|
||||
}
|
||||
responseText += `\n🔧 Tool: ${block.name}\n`;
|
||||
if (block.input) {
|
||||
responseText += `Input: ${JSON.stringify(block.input, null, 2)}\n`;
|
||||
responseText += `Input: ${JSON.stringify(
|
||||
block.input,
|
||||
null,
|
||||
2
|
||||
)}\n`;
|
||||
}
|
||||
scheduleWrite();
|
||||
}
|
||||
@@ -1382,12 +1208,68 @@ This mock response was generated because AUTOMAKER_MOCK_AGENT=true was set.
|
||||
context: string,
|
||||
useWorktrees: boolean
|
||||
): Promise<void> {
|
||||
const feature = await this.loadFeature(projectPath, featureId);
|
||||
if (!feature) {
|
||||
throw new Error(`Feature ${featureId} not found`);
|
||||
if (this.runningFeatures.has(featureId)) {
|
||||
throw new Error(`Feature ${featureId} is already running`);
|
||||
}
|
||||
|
||||
const prompt = `## Continuing Feature Implementation
|
||||
const abortController = new AbortController();
|
||||
|
||||
// Emit feature start event early
|
||||
this.emitAutoModeEvent("auto_mode_feature_start", {
|
||||
featureId,
|
||||
projectPath,
|
||||
feature: {
|
||||
id: featureId,
|
||||
title: "Resuming...",
|
||||
description: "Feature is resuming from previous context",
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const feature = await this.loadFeature(projectPath, featureId);
|
||||
if (!feature) {
|
||||
throw new Error(`Feature ${featureId} not found`);
|
||||
}
|
||||
|
||||
// Derive workDir from feature.branchName
|
||||
let worktreePath: string | null = null;
|
||||
const branchName = feature.branchName || null;
|
||||
|
||||
if (useWorktrees && branchName) {
|
||||
worktreePath = await this.findExistingWorktreeForBranch(
|
||||
projectPath,
|
||||
branchName
|
||||
);
|
||||
if (!worktreePath) {
|
||||
worktreePath = await this.setupWorktree(
|
||||
projectPath,
|
||||
featureId,
|
||||
branchName
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`[AutoMode] Resuming in worktree for branch "${branchName}": ${worktreePath}`
|
||||
);
|
||||
}
|
||||
|
||||
const workDir = worktreePath
|
||||
? path.resolve(worktreePath)
|
||||
: path.resolve(projectPath);
|
||||
|
||||
this.runningFeatures.set(featureId, {
|
||||
featureId,
|
||||
projectPath,
|
||||
worktreePath,
|
||||
branchName,
|
||||
abortController,
|
||||
isAutoMode: false,
|
||||
startTime: Date.now(),
|
||||
});
|
||||
|
||||
// Update feature status to in_progress
|
||||
await this.updateFeatureStatus(projectPath, featureId, "in_progress");
|
||||
|
||||
const prompt = `## Continuing Feature Implementation
|
||||
|
||||
${this.buildFeaturePrompt(feature)}
|
||||
|
||||
@@ -1399,7 +1281,67 @@ ${context}
|
||||
## Instructions
|
||||
Review the previous work and continue the implementation. If the feature appears complete, verify it works correctly.`;
|
||||
|
||||
return this.executeFeature(projectPath, featureId, useWorktrees, false);
|
||||
// Extract image paths from feature
|
||||
const imagePaths = feature.imagePaths?.map((img) =>
|
||||
typeof img === "string" ? img : img.path
|
||||
);
|
||||
|
||||
// Get model from feature
|
||||
const model = resolveModelString(feature.model, DEFAULT_MODELS.claude);
|
||||
console.log(
|
||||
`[AutoMode] Resuming feature ${featureId} with model: ${model} in ${workDir}`
|
||||
);
|
||||
|
||||
// Run the agent with context
|
||||
await this.runAgent(
|
||||
workDir,
|
||||
featureId,
|
||||
prompt,
|
||||
abortController,
|
||||
projectPath,
|
||||
imagePaths,
|
||||
model,
|
||||
context // Pass previous context for proper file output
|
||||
);
|
||||
|
||||
// Mark as waiting_approval for user review
|
||||
await this.updateFeatureStatus(
|
||||
projectPath,
|
||||
featureId,
|
||||
"waiting_approval"
|
||||
);
|
||||
|
||||
this.emitAutoModeEvent("auto_mode_feature_complete", {
|
||||
featureId,
|
||||
passes: true,
|
||||
message: `Feature resumed and completed in ${Math.round(
|
||||
(Date.now() - this.runningFeatures.get(featureId)!.startTime) / 1000
|
||||
)}s`,
|
||||
projectPath,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorInfo = classifyError(error);
|
||||
|
||||
if (errorInfo.isAbort) {
|
||||
this.emitAutoModeEvent("auto_mode_feature_complete", {
|
||||
featureId,
|
||||
passes: false,
|
||||
message: "Feature stopped by user",
|
||||
projectPath,
|
||||
});
|
||||
} else {
|
||||
console.error(`[AutoMode] Feature ${featureId} resume failed:`, error);
|
||||
await this.updateFeatureStatus(projectPath, featureId, "backlog");
|
||||
this.emitAutoModeEvent("auto_mode_error", {
|
||||
featureId,
|
||||
error: errorInfo.message,
|
||||
errorType: errorInfo.isAuth ? "authentication" : "execution",
|
||||
projectPath,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
this.runningFeatures.delete(featureId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1418,7 +1360,28 @@ Review the previous work and continue the implementation. If the feature appears
|
||||
});
|
||||
}
|
||||
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
private sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(resolve, ms);
|
||||
|
||||
// If signal is provided and already aborted, reject immediately
|
||||
if (signal?.aborted) {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error("Aborted"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for abort signal
|
||||
if (signal) {
|
||||
signal.addEventListener(
|
||||
"abort",
|
||||
() => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error("Aborted"));
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ export interface Feature {
|
||||
spec?: string;
|
||||
model?: string;
|
||||
imagePaths?: Array<string | { path: string; [key: string]: unknown }>;
|
||||
// Branch info - worktree path is derived at runtime from branchName
|
||||
branchName?: string; // Name of the feature branch (undefined = use current worktree)
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
629
package-lock.json
generated
629
package-lock.json
generated
@@ -33,8 +33,10 @@
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
@@ -1546,10 +1548,6 @@
|
||||
"version": "1.1.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.7",
|
||||
"license": "MIT",
|
||||
@@ -1599,72 +1597,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.15",
|
||||
"license": "MIT",
|
||||
@@ -1715,19 +1647,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.11",
|
||||
"license": "MIT",
|
||||
@@ -1816,22 +1735,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-label": {
|
||||
"version": "2.1.8",
|
||||
"license": "MIT",
|
||||
@@ -2031,94 +1934,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.11",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-slider": {
|
||||
"version": "1.3.6",
|
||||
"license": "MIT",
|
||||
@@ -2242,52 +2057,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-use-effect-event": {
|
||||
"version": "0.0.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
@@ -2304,32 +2073,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-use-previous": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-use-rect": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
@@ -2346,22 +2089,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-use-size": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/app/node_modules/@radix-ui/react-visually-hidden": {
|
||||
"version": "1.2.3",
|
||||
"license": "MIT",
|
||||
@@ -11195,6 +10922,358 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-radio-group": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
|
||||
"integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-roving-focus": "1.1.11",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
|
||||
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
|
||||
"integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-effect-event": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-previous": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
|
||||
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-size": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
|
||||
|
||||
Reference in New Issue
Block a user