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:
webdevcody
2026-01-15 22:21:46 -05:00
parent cb544e0011
commit 03436103d1
46 changed files with 1719 additions and 418 deletions

View File

@@ -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"

View File

@@ -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>

View File

@@ -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'}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,
]
);

View File

@@ -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);

View File

@@ -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 && (