mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
feat: implement backlog plan management and UI enhancements
- Added functionality to save, clear, and load backlog plans within the application. - Introduced a new API endpoint for clearing saved backlog plans. - Enhanced the backlog plan dialog to allow users to review and apply changes to their features. - Integrated dependency management features in the UI, allowing users to select parent and child dependencies for features. - Improved the graph view with options to manage plans and visualize dependencies effectively. - Updated the sidebar and settings to include provider visibility toggles for better user control over model selection. These changes aim to enhance the user experience by providing robust backlog management capabilities and improving the overall UI for feature planning.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Wand2, GitBranch } from 'lucide-react';
|
||||
import { Wand2, GitBranch, ClipboardCheck } from 'lucide-react';
|
||||
import { UsagePopover } from '@/components/usage-popover';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
@@ -25,6 +25,8 @@ interface BoardHeaderProps {
|
||||
isAutoModeRunning: boolean;
|
||||
onAutoModeToggle: (enabled: boolean) => void;
|
||||
onOpenPlanDialog: () => void;
|
||||
hasPendingPlan?: boolean;
|
||||
onOpenPendingPlan?: () => void;
|
||||
isMounted: boolean;
|
||||
// Search bar props
|
||||
searchQuery: string;
|
||||
@@ -50,6 +52,8 @@ export function BoardHeader({
|
||||
isAutoModeRunning,
|
||||
onAutoModeToggle,
|
||||
onOpenPlanDialog,
|
||||
hasPendingPlan,
|
||||
onOpenPendingPlan,
|
||||
isMounted,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
@@ -192,6 +196,15 @@ export function BoardHeader({
|
||||
{/* Plan Button with Settings - only show on desktop, mobile has it in the menu */}
|
||||
{isMounted && !isMobile && (
|
||||
<div className={controlContainerClass} data-testid="plan-button-container">
|
||||
{hasPendingPlan && (
|
||||
<button
|
||||
onClick={onOpenPendingPlan || onOpenPlanDialog}
|
||||
className="flex items-center gap-1.5 text-emerald-500 hover:text-emerald-400 transition-colors"
|
||||
data-testid="plan-review-button"
|
||||
>
|
||||
<ClipboardCheck className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onOpenPlanDialog}
|
||||
className="flex items-center gap-1.5 hover:text-foreground transition-colors"
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { CategoryAutocomplete } from '@/components/ui/category-autocomplete';
|
||||
import { DependencySelector } from '@/components/ui/dependency-selector';
|
||||
import {
|
||||
DescriptionImageDropZone,
|
||||
FeatureImagePath as DescriptionImagePath,
|
||||
@@ -99,6 +100,7 @@ type FeatureData = {
|
||||
planningMode: PlanningMode;
|
||||
requirePlanApproval: boolean;
|
||||
dependencies?: string[];
|
||||
childDependencies?: string[]; // Feature IDs that should depend on this feature
|
||||
workMode: WorkMode;
|
||||
};
|
||||
|
||||
@@ -188,6 +190,10 @@ export function AddFeatureDialog({
|
||||
const [ancestors, setAncestors] = useState<AncestorContext[]>([]);
|
||||
const [selectedAncestorIds, setSelectedAncestorIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Dependency selection state (not in spawn mode)
|
||||
const [parentDependencies, setParentDependencies] = useState<string[]>([]);
|
||||
const [childDependencies, setChildDependencies] = useState<string[]>([]);
|
||||
|
||||
// Get defaults from store
|
||||
const { defaultPlanningMode, defaultRequirePlanApproval, useWorktrees, defaultFeatureModel } =
|
||||
useAppStore();
|
||||
@@ -224,6 +230,10 @@ export function AddFeatureDialog({
|
||||
setAncestors([]);
|
||||
setSelectedAncestorIds(new Set());
|
||||
}
|
||||
|
||||
// Reset dependency selections
|
||||
setParentDependencies([]);
|
||||
setChildDependencies([]);
|
||||
}
|
||||
}, [
|
||||
open,
|
||||
@@ -291,6 +301,16 @@ export function AddFeatureDialog({
|
||||
}
|
||||
}
|
||||
|
||||
// Determine final dependencies
|
||||
// In spawn mode, use parent feature as dependency
|
||||
// Otherwise, use manually selected parent dependencies
|
||||
const finalDependencies =
|
||||
isSpawnMode && parentFeature
|
||||
? [parentFeature.id]
|
||||
: parentDependencies.length > 0
|
||||
? parentDependencies
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
title,
|
||||
category: finalCategory,
|
||||
@@ -306,7 +326,8 @@ export function AddFeatureDialog({
|
||||
priority,
|
||||
planningMode,
|
||||
requirePlanApproval,
|
||||
dependencies: isSpawnMode && parentFeature ? [parentFeature.id] : undefined,
|
||||
dependencies: finalDependencies,
|
||||
childDependencies: childDependencies.length > 0 ? childDependencies : undefined,
|
||||
workMode,
|
||||
};
|
||||
};
|
||||
@@ -331,6 +352,8 @@ export function AddFeatureDialog({
|
||||
setPreviewMap(new Map());
|
||||
setDescriptionError(false);
|
||||
setDescriptionHistory([]);
|
||||
setParentDependencies([]);
|
||||
setChildDependencies([]);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
@@ -641,6 +664,38 @@ export function AddFeatureDialog({
|
||||
testIdPrefix="feature-work-mode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Dependencies - only show when not in spawn mode */}
|
||||
{!isSpawnMode && allFeatures.length > 0 && (
|
||||
<div className="pt-2 space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Parent Dependencies (this feature depends on)
|
||||
</Label>
|
||||
<DependencySelector
|
||||
value={parentDependencies}
|
||||
onChange={setParentDependencies}
|
||||
features={allFeatures}
|
||||
type="parent"
|
||||
placeholder="Select features this depends on..."
|
||||
data-testid="add-feature-parent-deps"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Child Dependencies (features that depend on this)
|
||||
</Label>
|
||||
<DependencySelector
|
||||
value={childDependencies}
|
||||
onChange={setChildDependencies}
|
||||
features={allFeatures}
|
||||
type="child"
|
||||
placeholder="Select features that will depend on this..."
|
||||
data-testid="add-feature-child-deps"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ export function AgentOutputModal({
|
||||
onNumberKeyPress,
|
||||
projectPath: projectPathProp,
|
||||
}: AgentOutputModalProps) {
|
||||
const isBacklogPlan = featureId.startsWith('backlog-plan:');
|
||||
const [output, setOutput] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [viewMode, setViewMode] = useState<ViewMode | null>(null);
|
||||
@@ -83,6 +84,11 @@ export function AgentOutputModal({
|
||||
projectPathRef.current = resolvedProjectPath;
|
||||
setProjectPath(resolvedProjectPath);
|
||||
|
||||
if (isBacklogPlan) {
|
||||
setOutput('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use features API to get agent output
|
||||
if (api.features) {
|
||||
const result = await api.features.getAgentOutput(resolvedProjectPath, featureId);
|
||||
@@ -104,14 +110,14 @@ export function AgentOutputModal({
|
||||
};
|
||||
|
||||
loadOutput();
|
||||
}, [open, featureId, projectPathProp]);
|
||||
}, [open, featureId, projectPathProp, isBacklogPlan]);
|
||||
|
||||
// Listen to auto mode events and update output
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) return;
|
||||
if (!api?.autoMode || isBacklogPlan) return;
|
||||
|
||||
console.log('[AgentOutputModal] Subscribing to events for featureId:', featureId);
|
||||
|
||||
@@ -272,7 +278,43 @@ export function AgentOutputModal({
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [open, featureId]);
|
||||
}, [open, featureId, isBacklogPlan]);
|
||||
|
||||
// Listen to backlog plan events and update output
|
||||
useEffect(() => {
|
||||
if (!open || !isBacklogPlan) return;
|
||||
|
||||
const api = getElectronAPI();
|
||||
if (!api?.backlogPlan) return;
|
||||
|
||||
const unsubscribe = api.backlogPlan.onEvent((event: any) => {
|
||||
if (!event?.type) return;
|
||||
|
||||
let newContent = '';
|
||||
switch (event.type) {
|
||||
case 'backlog_plan_progress':
|
||||
newContent = `\n🧭 ${event.content || 'Backlog plan progress update'}\n`;
|
||||
break;
|
||||
case 'backlog_plan_error':
|
||||
newContent = `\n❌ Backlog plan error: ${event.error || 'Unknown error'}\n`;
|
||||
break;
|
||||
case 'backlog_plan_complete':
|
||||
newContent = `\n✅ Backlog plan completed\n`;
|
||||
break;
|
||||
default:
|
||||
newContent = `\nℹ️ ${event.type}\n`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (newContent) {
|
||||
setOutput((prev) => `${prev}${newContent}`);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [open, isBacklogPlan]);
|
||||
|
||||
// Handle scroll to detect if user scrolled up
|
||||
const handleScroll = () => {
|
||||
@@ -369,7 +411,7 @@ export function AgentOutputModal({
|
||||
</div>
|
||||
</div>
|
||||
<DialogDescription
|
||||
className="mt-1 max-h-24 overflow-y-auto break-words"
|
||||
className="mt-1 max-h-24 overflow-y-auto wrap-break-word"
|
||||
data-testid="agent-output-description"
|
||||
>
|
||||
{featureDescription}
|
||||
@@ -377,11 +419,13 @@ export function AgentOutputModal({
|
||||
</DialogHeader>
|
||||
|
||||
{/* Task Progress Panel - shows when tasks are being executed */}
|
||||
<TaskProgressPanel
|
||||
featureId={featureId}
|
||||
projectPath={projectPath}
|
||||
className="flex-shrink-0 mx-3 my-2"
|
||||
/>
|
||||
{!isBacklogPlan && (
|
||||
<TaskProgressPanel
|
||||
featureId={featureId}
|
||||
projectPath={projectPath}
|
||||
className="shrink-0 mx-3 my-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
{effectiveViewMode === 'changes' ? (
|
||||
<div className="flex-1 min-h-0 sm:min-h-[200px] sm:max-h-[60vh] overflow-y-auto scrollbar-visible">
|
||||
@@ -423,11 +467,11 @@ export function AgentOutputModal({
|
||||
) : effectiveViewMode === 'parsed' ? (
|
||||
<LogViewer output={output} />
|
||||
) : (
|
||||
<div className="whitespace-pre-wrap break-words text-zinc-300">{output}</div>
|
||||
<div className="whitespace-pre-wrap wrap-break-word text-zinc-300">{output}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground text-center flex-shrink-0">
|
||||
<div className="text-xs text-muted-foreground text-center shrink-0">
|
||||
{autoScrollRef.current
|
||||
? 'Auto-scrolling enabled'
|
||||
: 'Scroll to bottom to enable auto-scroll'}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -43,16 +44,6 @@ function normalizeEntry(entry: PhaseModelEntry | string): PhaseModelEntry {
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract model string from PhaseModelEntry or string
|
||||
*/
|
||||
function extractModel(entry: PhaseModelEntry | string): ModelAlias | CursorModelId {
|
||||
if (typeof entry === 'string') {
|
||||
return entry as ModelAlias | CursorModelId;
|
||||
}
|
||||
return entry.model;
|
||||
}
|
||||
|
||||
interface BacklogPlanDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -80,6 +71,7 @@ export function BacklogPlanDialog({
|
||||
setIsGeneratingPlan,
|
||||
currentBranch,
|
||||
}: BacklogPlanDialogProps) {
|
||||
const logger = createLogger('BacklogPlanDialog');
|
||||
const [mode, setMode] = useState<DialogMode>('input');
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [expandedChanges, setExpandedChanges] = useState<Set<number>>(new Set());
|
||||
@@ -110,11 +102,17 @@ export function BacklogPlanDialog({
|
||||
|
||||
const api = getElectronAPI();
|
||||
if (!api?.backlogPlan) {
|
||||
logger.warn('Backlog plan API not available');
|
||||
toast.error('API not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Start generation in background
|
||||
logger.debug('Starting backlog plan generation', {
|
||||
projectPath,
|
||||
promptLength: prompt.length,
|
||||
hasModelOverride: Boolean(modelOverride),
|
||||
});
|
||||
setIsGeneratingPlan(true);
|
||||
|
||||
// Use model override if set, otherwise use global default (extract model string from PhaseModelEntry)
|
||||
@@ -122,12 +120,20 @@ export function BacklogPlanDialog({
|
||||
const effectiveModel = effectiveModelEntry.model;
|
||||
const result = await api.backlogPlan.generate(projectPath, prompt, effectiveModel);
|
||||
if (!result.success) {
|
||||
logger.error('Backlog plan generation failed to start', {
|
||||
error: result.error,
|
||||
projectPath,
|
||||
});
|
||||
setIsGeneratingPlan(false);
|
||||
toast.error(result.error || 'Failed to start plan generation');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show toast and close dialog - generation runs in background
|
||||
logger.debug('Backlog plan generation started', {
|
||||
projectPath,
|
||||
model: effectiveModel,
|
||||
});
|
||||
toast.info('Generating plan... This will be ready soon!', {
|
||||
duration: 3000,
|
||||
});
|
||||
@@ -194,10 +200,15 @@ export function BacklogPlanDialog({
|
||||
currentBranch,
|
||||
]);
|
||||
|
||||
const handleDiscard = useCallback(() => {
|
||||
const handleDiscard = useCallback(async () => {
|
||||
setPendingPlanResult(null);
|
||||
setMode('input');
|
||||
}, [setPendingPlanResult]);
|
||||
|
||||
const api = getElectronAPI();
|
||||
if (api?.backlogPlan) {
|
||||
await api.backlogPlan.clear(projectPath);
|
||||
}
|
||||
}, [setPendingPlanResult, projectPath]);
|
||||
|
||||
const toggleChangeExpanded = (index: number) => {
|
||||
setExpandedChanges((prev) => {
|
||||
@@ -260,11 +271,11 @@ export function BacklogPlanDialog({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Describe the changes you want to make to your backlog. The AI will analyze your
|
||||
current features and propose additions, updates, or deletions.
|
||||
Describe the changes you want to make across your features. The AI will analyze your
|
||||
current feature list and propose additions, updates, deletions, or restructuring.
|
||||
</div>
|
||||
<Textarea
|
||||
placeholder="e.g., Add authentication features with login, signup, and password reset. Also add a dashboard feature that depends on authentication."
|
||||
placeholder="e.g., Refactor onboarding into smaller features, add a dashboard feature that depends on authentication, and remove the legacy tour task."
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
className="min-h-[150px] resize-none"
|
||||
@@ -283,7 +294,7 @@ export function BacklogPlanDialog({
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'review':
|
||||
case 'review': {
|
||||
if (!pendingPlanResult) return null;
|
||||
|
||||
const additions = pendingPlanResult.changes.filter((c) => c.type === 'add');
|
||||
@@ -389,6 +400,7 @@ export function BacklogPlanDialog({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'applying':
|
||||
return (
|
||||
@@ -402,7 +414,6 @@ export function BacklogPlanDialog({
|
||||
|
||||
// Get effective model entry (override or global default)
|
||||
const effectiveModelEntry = modelOverride || normalizeEntry(phaseModels.backlogPlanningModel);
|
||||
const effectiveModel = effectiveModelEntry.model;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
||||
@@ -410,12 +421,12 @@ export function BacklogPlanDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Wand2 className="w-5 h-5 text-primary" />
|
||||
{mode === 'review' ? 'Review Plan' : 'Plan Backlog Changes'}
|
||||
{mode === 'review' ? 'Review Plan' : 'Plan Feature Changes'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{mode === 'review'
|
||||
? 'Select which changes to apply to your backlog'
|
||||
: 'Use AI to add, update, or remove features from your backlog'}
|
||||
? 'Select which changes to apply to your features'
|
||||
: 'Use AI to add, update, remove, or restructure your features'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -447,7 +458,7 @@ export function BacklogPlanDialog({
|
||||
) : (
|
||||
<>
|
||||
<Wand2 className="w-4 h-4 mr-2" />
|
||||
Generate Plan
|
||||
Apply Changes
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { CategoryAutocomplete } from '@/components/ui/category-autocomplete';
|
||||
import { DependencySelector } from '@/components/ui/dependency-selector';
|
||||
import {
|
||||
DescriptionImageDropZone,
|
||||
FeatureImagePath as DescriptionImagePath,
|
||||
@@ -63,6 +64,8 @@ interface EditFeatureDialogProps {
|
||||
priority: number;
|
||||
planningMode: PlanningMode;
|
||||
requirePlanApproval: boolean;
|
||||
dependencies?: string[];
|
||||
childDependencies?: string[]; // Feature IDs that should depend on this feature
|
||||
},
|
||||
descriptionHistorySource?: 'enhance' | 'edit',
|
||||
enhancementMode?: EnhancementMode,
|
||||
@@ -127,6 +130,21 @@ export function EditFeatureDialog({
|
||||
feature?.descriptionHistory ?? []
|
||||
);
|
||||
|
||||
// Dependency state
|
||||
const [parentDependencies, setParentDependencies] = useState<string[]>(
|
||||
feature?.dependencies ?? []
|
||||
);
|
||||
// Child dependencies are features that have this feature in their dependencies
|
||||
const [childDependencies, setChildDependencies] = useState<string[]>(() => {
|
||||
if (!feature) return [];
|
||||
return allFeatures.filter((f) => f.dependencies?.includes(feature.id)).map((f) => f.id);
|
||||
});
|
||||
// Track original child dependencies to detect changes
|
||||
const [originalChildDependencies, setOriginalChildDependencies] = useState<string[]>(() => {
|
||||
if (!feature) return [];
|
||||
return allFeatures.filter((f) => f.dependencies?.includes(feature.id)).map((f) => f.id);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setEditingFeature(feature);
|
||||
if (feature) {
|
||||
@@ -145,13 +163,23 @@ export function EditFeatureDialog({
|
||||
thinkingLevel: feature.thinkingLevel || 'none',
|
||||
reasoningEffort: feature.reasoningEffort || 'none',
|
||||
});
|
||||
// Reset dependency state
|
||||
setParentDependencies(feature.dependencies ?? []);
|
||||
const childDeps = allFeatures
|
||||
.filter((f) => f.dependencies?.includes(feature.id))
|
||||
.map((f) => f.id);
|
||||
setChildDependencies(childDeps);
|
||||
setOriginalChildDependencies(childDeps);
|
||||
} else {
|
||||
setEditFeaturePreviewMap(new Map());
|
||||
setDescriptionChangeSource(null);
|
||||
setPreEnhancementDescription(null);
|
||||
setLocalHistory([]);
|
||||
setParentDependencies([]);
|
||||
setChildDependencies([]);
|
||||
setOriginalChildDependencies([]);
|
||||
}
|
||||
}, [feature]);
|
||||
}, [feature, allFeatures]);
|
||||
|
||||
const handleModelChange = (entry: PhaseModelEntry) => {
|
||||
setModelEntry(entry);
|
||||
@@ -180,6 +208,12 @@ export function EditFeatureDialog({
|
||||
// For 'custom' mode, use the specified branch name
|
||||
const finalBranchName = workMode === 'custom' ? editingFeature.branchName || '' : '';
|
||||
|
||||
// Check if child dependencies changed
|
||||
const childDepsChanged =
|
||||
childDependencies.length !== originalChildDependencies.length ||
|
||||
childDependencies.some((id) => !originalChildDependencies.includes(id)) ||
|
||||
originalChildDependencies.some((id) => !childDependencies.includes(id));
|
||||
|
||||
const updates = {
|
||||
title: editingFeature.title ?? '',
|
||||
category: editingFeature.category,
|
||||
@@ -195,6 +229,8 @@ export function EditFeatureDialog({
|
||||
planningMode,
|
||||
requirePlanApproval,
|
||||
workMode,
|
||||
dependencies: parentDependencies,
|
||||
childDependencies: childDepsChanged ? childDependencies : undefined,
|
||||
};
|
||||
|
||||
// Determine if description changed and what source to use
|
||||
@@ -547,6 +583,40 @@ export function EditFeatureDialog({
|
||||
testIdPrefix="edit-feature-work-mode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Dependencies */}
|
||||
{allFeatures.length > 1 && (
|
||||
<div className="pt-2 space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Parent Dependencies (this feature depends on)
|
||||
</Label>
|
||||
<DependencySelector
|
||||
currentFeatureId={editingFeature.id}
|
||||
value={parentDependencies}
|
||||
onChange={setParentDependencies}
|
||||
features={allFeatures}
|
||||
type="parent"
|
||||
placeholder="Select features this depends on..."
|
||||
data-testid="edit-feature-parent-deps"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Child Dependencies (features that depend on this)
|
||||
</Label>
|
||||
<DependencySelector
|
||||
currentFeatureId={editingFeature.id}
|
||||
value={childDependencies}
|
||||
onChange={setChildDependencies}
|
||||
features={allFeatures}
|
||||
type="child"
|
||||
placeholder="Select features that depend on this..."
|
||||
data-testid="edit-feature-child-deps"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ export function useBoardActions({
|
||||
planningMode: PlanningMode;
|
||||
requirePlanApproval: boolean;
|
||||
dependencies?: string[];
|
||||
childDependencies?: string[]; // Feature IDs that should depend on this feature
|
||||
workMode?: 'current' | 'auto' | 'custom';
|
||||
}) => {
|
||||
const workMode = featureData.workMode || 'current';
|
||||
@@ -189,6 +190,21 @@ export function useBoardActions({
|
||||
await persistFeatureCreate(createdFeature);
|
||||
saveCategory(featureData.category);
|
||||
|
||||
// Handle child dependencies - update other features to depend on this new feature
|
||||
if (featureData.childDependencies && featureData.childDependencies.length > 0) {
|
||||
for (const childId of featureData.childDependencies) {
|
||||
const childFeature = features.find((f) => f.id === childId);
|
||||
if (childFeature) {
|
||||
const childDeps = childFeature.dependencies || [];
|
||||
if (!childDeps.includes(createdFeature.id)) {
|
||||
const newDeps = [...childDeps, createdFeature.id];
|
||||
updateFeature(childId, { dependencies: newDeps });
|
||||
persistFeatureUpdate(childId, { dependencies: newDeps });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate title in the background if needed (non-blocking)
|
||||
if (needsTitleGeneration) {
|
||||
const api = getElectronAPI();
|
||||
@@ -230,6 +246,7 @@ export function useBoardActions({
|
||||
onWorktreeCreated,
|
||||
onWorktreeAutoSelect,
|
||||
currentWorktreeBranch,
|
||||
features,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -250,6 +267,8 @@ export function useBoardActions({
|
||||
planningMode?: PlanningMode;
|
||||
requirePlanApproval?: boolean;
|
||||
workMode?: 'current' | 'auto' | 'custom';
|
||||
dependencies?: string[];
|
||||
childDependencies?: string[]; // Feature IDs that should depend on this feature
|
||||
},
|
||||
descriptionHistorySource?: 'enhance' | 'edit',
|
||||
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer',
|
||||
@@ -303,8 +322,11 @@ export function useBoardActions({
|
||||
}
|
||||
}
|
||||
|
||||
// Separate child dependencies from the main updates (they affect other features)
|
||||
const { childDependencies, ...restUpdates } = updates;
|
||||
|
||||
const finalUpdates = {
|
||||
...updates,
|
||||
...restUpdates,
|
||||
title: updates.title,
|
||||
branchName: finalBranchName,
|
||||
};
|
||||
@@ -317,6 +339,45 @@ export function useBoardActions({
|
||||
enhancementMode,
|
||||
preEnhancementDescription
|
||||
);
|
||||
|
||||
// Handle child dependency changes
|
||||
// This updates other features' dependencies arrays
|
||||
if (childDependencies !== undefined) {
|
||||
// Find current child dependencies (features that have this feature in their dependencies)
|
||||
const currentChildDeps = features
|
||||
.filter((f) => f.dependencies?.includes(featureId))
|
||||
.map((f) => f.id);
|
||||
|
||||
// Find features to add this feature as a dependency (new child deps)
|
||||
const toAdd = childDependencies.filter((id) => !currentChildDeps.includes(id));
|
||||
// Find features to remove this feature as a dependency (removed child deps)
|
||||
const toRemove = currentChildDeps.filter((id) => !childDependencies.includes(id));
|
||||
|
||||
// Add this feature as a dependency to new child features
|
||||
for (const childId of toAdd) {
|
||||
const childFeature = features.find((f) => f.id === childId);
|
||||
if (childFeature) {
|
||||
const childDeps = childFeature.dependencies || [];
|
||||
if (!childDeps.includes(featureId)) {
|
||||
const newDeps = [...childDeps, featureId];
|
||||
updateFeature(childId, { dependencies: newDeps });
|
||||
persistFeatureUpdate(childId, { dependencies: newDeps });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove this feature as a dependency from removed child features
|
||||
for (const childId of toRemove) {
|
||||
const childFeature = features.find((f) => f.id === childId);
|
||||
if (childFeature) {
|
||||
const childDeps = childFeature.dependencies || [];
|
||||
const newDeps = childDeps.filter((depId) => depId !== featureId);
|
||||
updateFeature(childId, { dependencies: newDeps });
|
||||
persistFeatureUpdate(childId, { dependencies: newDeps });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updates.category) {
|
||||
saveCategory(updates.category);
|
||||
}
|
||||
@@ -330,6 +391,7 @@ export function useBoardActions({
|
||||
currentProject,
|
||||
onWorktreeCreated,
|
||||
currentWorktreeBranch,
|
||||
features,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Calling API features.update', { featureId, updates });
|
||||
const result = await api.features.update(
|
||||
currentProject.path,
|
||||
featureId,
|
||||
@@ -39,8 +40,14 @@ export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps
|
||||
enhancementMode,
|
||||
preEnhancementDescription
|
||||
);
|
||||
logger.info('API features.update result', {
|
||||
success: result.success,
|
||||
feature: result.feature,
|
||||
});
|
||||
if (result.success && result.feature) {
|
||||
updateFeature(result.feature.id, result.feature);
|
||||
} else if (!result.success) {
|
||||
logger.error('API features.update failed', result);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to persist feature update:', error);
|
||||
|
||||
@@ -31,6 +31,7 @@ export function ModelSelector({
|
||||
codexModelsLoading,
|
||||
codexModelsError,
|
||||
fetchCodexModels,
|
||||
disabledProviders,
|
||||
} = useAppStore();
|
||||
const { cursorCliStatus, codexCliStatus } = useSetupStore();
|
||||
|
||||
@@ -69,9 +70,8 @@ export function ModelSelector({
|
||||
|
||||
// Filter Cursor models based on enabled models from global settings
|
||||
const filteredCursorModels = CURSOR_MODELS.filter((model) => {
|
||||
// Extract the cursor model ID from the prefixed ID (e.g., "cursor-auto" -> "auto")
|
||||
const cursorModelId = stripProviderPrefix(model.id);
|
||||
return enabledCursorModels.includes(cursorModelId as any);
|
||||
// Compare model.id directly since both model.id and enabledCursorModels use full IDs with prefix
|
||||
return enabledCursorModels.includes(model.id as any);
|
||||
});
|
||||
|
||||
const handleProviderChange = (provider: ModelProvider) => {
|
||||
@@ -89,59 +89,79 @@ export function ModelSelector({
|
||||
}
|
||||
};
|
||||
|
||||
// Check which providers are disabled
|
||||
const isClaudeDisabled = disabledProviders.includes('claude');
|
||||
const isCursorDisabled = disabledProviders.includes('cursor');
|
||||
const isCodexDisabled = disabledProviders.includes('codex');
|
||||
|
||||
// Count available providers
|
||||
const availableProviders = [
|
||||
!isClaudeDisabled && 'claude',
|
||||
!isCursorDisabled && 'cursor',
|
||||
!isCodexDisabled && 'codex',
|
||||
].filter(Boolean) as ModelProvider[];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Provider Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>AI Provider</Label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleProviderChange('claude')}
|
||||
className={cn(
|
||||
'flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-center gap-2',
|
||||
selectedProvider === 'claude'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
{availableProviders.length > 1 && (
|
||||
<div className="space-y-2">
|
||||
<Label>AI Provider</Label>
|
||||
<div className="flex gap-2">
|
||||
{!isClaudeDisabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleProviderChange('claude')}
|
||||
className={cn(
|
||||
'flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-center gap-2',
|
||||
selectedProvider === 'claude'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-provider-claude`}
|
||||
>
|
||||
<AnthropicIcon className="w-4 h-4" />
|
||||
Claude
|
||||
</button>
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-provider-claude`}
|
||||
>
|
||||
<AnthropicIcon className="w-4 h-4" />
|
||||
Claude
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleProviderChange('cursor')}
|
||||
className={cn(
|
||||
'flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-center gap-2',
|
||||
selectedProvider === 'cursor'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
{!isCursorDisabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleProviderChange('cursor')}
|
||||
className={cn(
|
||||
'flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-center gap-2',
|
||||
selectedProvider === 'cursor'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-provider-cursor`}
|
||||
>
|
||||
<CursorIcon className="w-4 h-4" />
|
||||
Cursor CLI
|
||||
</button>
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-provider-cursor`}
|
||||
>
|
||||
<CursorIcon className="w-4 h-4" />
|
||||
Cursor CLI
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleProviderChange('codex')}
|
||||
className={cn(
|
||||
'flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-center gap-2',
|
||||
selectedProvider === 'codex'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
{!isCodexDisabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleProviderChange('codex')}
|
||||
className={cn(
|
||||
'flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-center gap-2',
|
||||
selectedProvider === 'codex'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-provider-codex`}
|
||||
>
|
||||
<OpenAIIcon className="w-4 h-4" />
|
||||
Codex CLI
|
||||
</button>
|
||||
)}
|
||||
data-testid={`${testIdPrefix}-provider-codex`}
|
||||
>
|
||||
<OpenAIIcon className="w-4 h-4" />
|
||||
Codex CLI
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Claude Models */}
|
||||
{selectedProvider === 'claude' && (
|
||||
{selectedProvider === 'claude' && !isClaudeDisabled && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="flex items-center gap-2">
|
||||
@@ -179,7 +199,7 @@ export function ModelSelector({
|
||||
)}
|
||||
|
||||
{/* Cursor Models */}
|
||||
{selectedProvider === 'cursor' && (
|
||||
{selectedProvider === 'cursor' && !isCursorDisabled && (
|
||||
<div className="space-y-3">
|
||||
{/* Warning when Cursor CLI is not available */}
|
||||
{!isCursorAvailable && (
|
||||
@@ -248,7 +268,7 @@ export function ModelSelector({
|
||||
)}
|
||||
|
||||
{/* Codex Models */}
|
||||
{selectedProvider === 'codex' && (
|
||||
{selectedProvider === 'codex' && !isCodexDisabled && (
|
||||
<div className="space-y-3">
|
||||
{/* Warning when Codex CLI is not available */}
|
||||
{!isCodexAvailable && (
|
||||
|
||||
Reference in New Issue
Block a user