mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
feat(ui): Enhance AI model handling with Cursor support
- Refactor model handling to support both Claude and Cursor models across various components. - Introduce `stripProviderPrefix` utility for consistent model ID processing. - Update `CursorProvider` to utilize `isCursorModel` for model validation. - Implement model override functionality in GitHub issue validation and enhancement routes. - Add `useCursorStatusInit` hook to initialize Cursor CLI status on app startup. - Update UI components to reflect changes in model selection and validation processes. This update improves the flexibility of AI model usage and enhances user experience by allowing quick model overrides.
This commit is contained in:
@@ -25,7 +25,7 @@ import { getElectronAPI } from '@/lib/electron';
|
||||
import { modelSupportsThinking } from '@/lib/utils';
|
||||
import {
|
||||
useAppStore,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
ThinkingLevel,
|
||||
FeatureImage,
|
||||
AIProfile,
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
PlanningModeSelector,
|
||||
AncestorContextSection,
|
||||
} from '../shared';
|
||||
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -54,6 +55,7 @@ import {
|
||||
formatAncestorContextForPrompt,
|
||||
type AncestorContext,
|
||||
} from '@automaker/dependency-resolver';
|
||||
import { isCursorModel, PROVIDER_PREFIXES } from '@automaker/types';
|
||||
|
||||
interface AddFeatureDialogProps {
|
||||
open: boolean;
|
||||
@@ -66,7 +68,7 @@ interface AddFeatureDialogProps {
|
||||
imagePaths: DescriptionImagePath[];
|
||||
textFilePaths: DescriptionTextFilePath[];
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
branchName: string; // Can be empty string to use current branch
|
||||
priority: number;
|
||||
@@ -115,7 +117,7 @@ export function AddFeatureDialog({
|
||||
imagePaths: [] as DescriptionImagePath[],
|
||||
textFilePaths: [] as DescriptionTextFilePath[],
|
||||
skipTests: false,
|
||||
model: 'opus' as AgentModel,
|
||||
model: 'opus' as ModelAlias,
|
||||
thinkingLevel: 'none' as ThinkingLevel,
|
||||
branchName: '',
|
||||
priority: 2 as number, // Default to medium priority
|
||||
@@ -136,14 +138,12 @@ export function AddFeatureDialog({
|
||||
const [ancestors, setAncestors] = useState<AncestorContext[]>([]);
|
||||
const [selectedAncestorIds, setSelectedAncestorIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Get enhancement model, planning mode defaults, and worktrees setting from store
|
||||
const {
|
||||
enhancementModel,
|
||||
defaultPlanningMode,
|
||||
defaultRequirePlanApproval,
|
||||
defaultAIProfileId,
|
||||
useWorktrees,
|
||||
} = useAppStore();
|
||||
// Get planning mode defaults and worktrees setting from store
|
||||
const { defaultPlanningMode, defaultRequirePlanApproval, defaultAIProfileId, useWorktrees } =
|
||||
useAppStore();
|
||||
|
||||
// Enhancement model override
|
||||
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
|
||||
|
||||
// Sync defaults when dialog opens
|
||||
useEffect(() => {
|
||||
@@ -294,7 +294,7 @@ export function AddFeatureDialog({
|
||||
const result = await api.enhancePrompt?.enhance(
|
||||
newFeature.description,
|
||||
enhancementMode,
|
||||
enhancementModel
|
||||
enhancementOverride.effectiveModel
|
||||
);
|
||||
|
||||
if (result?.success && result.enhancedText) {
|
||||
@@ -315,11 +315,11 @@ export function AddFeatureDialog({
|
||||
const handleModelSelect = (model: string) => {
|
||||
// For Cursor models, thinking is handled by the model itself
|
||||
// For Claude models, check if it supports extended thinking
|
||||
const isCursorModel = model.startsWith('cursor-');
|
||||
const isCursor = isCursorModel(model);
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
model: model as AgentModel,
|
||||
thinkingLevel: isCursorModel
|
||||
model: model as ModelAlias,
|
||||
thinkingLevel: isCursor
|
||||
? 'none'
|
||||
: modelSupportsThinking(model)
|
||||
? newFeature.thinkingLevel
|
||||
@@ -330,10 +330,10 @@ export function AddFeatureDialog({
|
||||
const handleProfileSelect = (profile: AIProfile) => {
|
||||
if (profile.provider === 'cursor') {
|
||||
// Cursor profile - set cursor model
|
||||
const cursorModel = `cursor-${profile.cursorModel || 'auto'}`;
|
||||
const cursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`;
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
model: cursorModel as AgentModel,
|
||||
model: cursorModel as ModelAlias,
|
||||
thinkingLevel: 'none', // Cursor handles thinking internally
|
||||
});
|
||||
} else {
|
||||
@@ -347,8 +347,9 @@ export function AddFeatureDialog({
|
||||
};
|
||||
|
||||
// Cursor models handle thinking internally, so only show thinking selector for Claude models
|
||||
const isCursorModel = newFeature.model.startsWith('cursor-');
|
||||
const newModelAllowsThinking = !isCursorModel && modelSupportsThinking(newFeature.model);
|
||||
const isCurrentModelCursor = isCursorModel(newFeature.model);
|
||||
const newModelAllowsThinking =
|
||||
!isCurrentModelCursor && modelSupportsThinking(newFeature.model || 'sonnet');
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleDialogClose}>
|
||||
@@ -480,6 +481,15 @@ export function AddFeatureDialog({
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Enhance with AI
|
||||
</Button>
|
||||
|
||||
<ModelOverrideTrigger
|
||||
currentModel={enhancementOverride.effectiveModel}
|
||||
onModelChange={enhancementOverride.setOverride}
|
||||
phase="enhancementModel"
|
||||
isOverridden={enhancementOverride.isOverridden}
|
||||
size="sm"
|
||||
variant="icon"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category (optional)</Label>
|
||||
@@ -540,7 +550,7 @@ export function AddFeatureDialog({
|
||||
profiles={aiProfiles}
|
||||
selectedModel={newFeature.model}
|
||||
selectedThinkingLevel={newFeature.thinkingLevel}
|
||||
selectedCursorModel={isCursorModel ? newFeature.model : undefined}
|
||||
selectedCursorModel={isCurrentModelCursor ? newFeature.model : undefined}
|
||||
onSelect={handleProfileSelect}
|
||||
showManageLink
|
||||
onManageLinkClick={() => {
|
||||
|
||||
@@ -32,7 +32,7 @@ import { getElectronAPI } from '@/lib/electron';
|
||||
import { modelSupportsThinking } from '@/lib/utils';
|
||||
import {
|
||||
Feature,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
ThinkingLevel,
|
||||
AIProfile,
|
||||
useAppStore,
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
BranchSelector,
|
||||
PlanningModeSelector,
|
||||
} from '../shared';
|
||||
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -54,6 +55,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { DependencyTreeDialog } from './dependency-tree-dialog';
|
||||
import { isCursorModel, PROVIDER_PREFIXES } from '@automaker/types';
|
||||
|
||||
interface EditFeatureDialogProps {
|
||||
feature: Feature | null;
|
||||
@@ -65,7 +67,7 @@ interface EditFeatureDialogProps {
|
||||
category: string;
|
||||
description: string;
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
imagePaths: DescriptionImagePath[];
|
||||
textFilePaths: DescriptionTextFilePath[];
|
||||
@@ -117,8 +119,11 @@ export function EditFeatureDialog({
|
||||
feature?.requirePlanApproval ?? false
|
||||
);
|
||||
|
||||
// Get enhancement model and worktrees setting from store
|
||||
const { enhancementModel, useWorktrees } = useAppStore();
|
||||
// Get worktrees setting from store
|
||||
const { useWorktrees } = useAppStore();
|
||||
|
||||
// Enhancement model override
|
||||
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
|
||||
|
||||
useEffect(() => {
|
||||
setEditingFeature(feature);
|
||||
@@ -148,7 +153,7 @@ export function EditFeatureDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedModel = (editingFeature.model ?? 'opus') as AgentModel;
|
||||
const selectedModel = (editingFeature.model ?? 'opus') as ModelAlias;
|
||||
const normalizedThinking: ThinkingLevel = modelSupportsThinking(selectedModel)
|
||||
? (editingFeature.thinkingLevel ?? 'none')
|
||||
: 'none';
|
||||
@@ -187,22 +192,40 @@ export function EditFeatureDialog({
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelSelect = (model: AgentModel) => {
|
||||
const handleModelSelect = (model: string) => {
|
||||
if (!editingFeature) return;
|
||||
// For Cursor models, thinking is handled by the model itself
|
||||
// For Claude models, check if it supports extended thinking
|
||||
const isCursor = isCursorModel(model);
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
model,
|
||||
thinkingLevel: modelSupportsThinking(model) ? editingFeature.thinkingLevel : 'none',
|
||||
model: model as ModelAlias,
|
||||
thinkingLevel: isCursor
|
||||
? 'none'
|
||||
: modelSupportsThinking(model)
|
||||
? editingFeature.thinkingLevel
|
||||
: 'none',
|
||||
});
|
||||
};
|
||||
|
||||
const handleProfileSelect = (model: AgentModel, thinkingLevel: ThinkingLevel) => {
|
||||
const handleProfileSelect = (profile: AIProfile) => {
|
||||
if (!editingFeature) return;
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
model,
|
||||
thinkingLevel,
|
||||
});
|
||||
if (profile.provider === 'cursor') {
|
||||
// Cursor profile - set cursor model
|
||||
const cursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`;
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
model: cursorModel as ModelAlias,
|
||||
thinkingLevel: 'none', // Cursor handles thinking internally
|
||||
});
|
||||
} else {
|
||||
// Claude profile
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
model: profile.model || 'sonnet',
|
||||
thinkingLevel: profile.thinkingLevel || 'none',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnhanceDescription = async () => {
|
||||
@@ -214,7 +237,7 @@ export function EditFeatureDialog({
|
||||
const result = await api.enhancePrompt?.enhance(
|
||||
editingFeature.description,
|
||||
enhancementMode,
|
||||
enhancementModel
|
||||
enhancementOverride.effectiveModel
|
||||
);
|
||||
|
||||
if (result?.success && result.enhancedText) {
|
||||
@@ -232,7 +255,10 @@ export function EditFeatureDialog({
|
||||
}
|
||||
};
|
||||
|
||||
const editModelAllowsThinking = modelSupportsThinking(editingFeature?.model);
|
||||
// Cursor models handle thinking internally, so only show thinking selector for Claude models
|
||||
const isCurrentModelCursor = isCursorModel(editingFeature?.model as string);
|
||||
const editModelAllowsThinking =
|
||||
!isCurrentModelCursor && modelSupportsThinking(editingFeature?.model);
|
||||
|
||||
if (!editingFeature) {
|
||||
return null;
|
||||
@@ -361,6 +387,15 @@ export function EditFeatureDialog({
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Enhance with AI
|
||||
</Button>
|
||||
|
||||
<ModelOverrideTrigger
|
||||
currentModel={enhancementOverride.effectiveModel}
|
||||
onModelChange={enhancementOverride.setOverride}
|
||||
phase="enhancementModel"
|
||||
isOverridden={enhancementOverride.isOverridden}
|
||||
size="sm"
|
||||
variant="icon"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-category">Category (optional)</Label>
|
||||
@@ -437,6 +472,9 @@ export function EditFeatureDialog({
|
||||
profiles={aiProfiles}
|
||||
selectedModel={editingFeature.model ?? 'opus'}
|
||||
selectedThinkingLevel={editingFeature.thinkingLevel ?? 'none'}
|
||||
selectedCursorModel={
|
||||
isCurrentModelCursor ? (editingFeature.model as string) : undefined
|
||||
}
|
||||
onSelect={handleProfileSelect}
|
||||
testIdPrefix="edit-profile-quick-select"
|
||||
/>
|
||||
@@ -450,7 +488,7 @@ export function EditFeatureDialog({
|
||||
{(!showProfilesOnly || showEditAdvancedOptions) && (
|
||||
<>
|
||||
<ModelSelector
|
||||
selectedModel={(editingFeature.model ?? 'opus') as AgentModel}
|
||||
selectedModel={(editingFeature.model ?? 'opus') as ModelAlias}
|
||||
onModelSelect={handleModelSelect}
|
||||
testIdPrefix="edit-model-select"
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
|
||||
import {
|
||||
Feature,
|
||||
FeatureImage,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
ThinkingLevel,
|
||||
PlanningMode,
|
||||
useAppStore,
|
||||
@@ -92,7 +92,7 @@ export function useBoardActions({
|
||||
images: FeatureImage[];
|
||||
imagePaths: DescriptionImagePath[];
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
branchName: string;
|
||||
priority: number;
|
||||
@@ -210,7 +210,7 @@ export function useBoardActions({
|
||||
category: string;
|
||||
description: string;
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
imagePaths: DescriptionImagePath[];
|
||||
branchName: string;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { AgentModel, ThinkingLevel } from '@/store/app-store';
|
||||
import type { ModelAlias, ThinkingLevel } from '@/store/app-store';
|
||||
import type { ModelProvider } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP } from '@automaker/types';
|
||||
import { Brain, Zap, Scale, Cpu, Rocket, Sparkles } from 'lucide-react';
|
||||
|
||||
export type ModelOption = {
|
||||
id: string; // Claude models use AgentModel, Cursor models use "cursor-{id}"
|
||||
id: string; // Claude models use ModelAlias, Cursor models use "cursor-{id}"
|
||||
label: string;
|
||||
description: string;
|
||||
badge?: string;
|
||||
|
||||
@@ -2,28 +2,19 @@ import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Brain, Bot, Terminal, AlertTriangle } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { AgentModel } from '@/store/app-store';
|
||||
import type { ModelAlias } from '@/store/app-store';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
import { getModelProvider, PROVIDER_PREFIXES, stripProviderPrefix } from '@automaker/types';
|
||||
import type { ModelProvider } from '@automaker/types';
|
||||
import { CLAUDE_MODELS, CURSOR_MODELS, ModelOption } from './model-constants';
|
||||
|
||||
interface ModelSelectorProps {
|
||||
selectedModel: string; // Can be AgentModel or "cursor-{id}"
|
||||
selectedModel: string; // Can be ModelAlias or "cursor-{id}"
|
||||
onModelSelect: (model: string) => void;
|
||||
testIdPrefix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the provider from a model string
|
||||
*/
|
||||
function getProviderFromModelString(model: string): ModelProvider {
|
||||
if (model.startsWith('cursor-')) {
|
||||
return 'cursor';
|
||||
}
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
export function ModelSelector({
|
||||
selectedModel,
|
||||
onModelSelect,
|
||||
@@ -32,7 +23,7 @@ export function ModelSelector({
|
||||
const { enabledCursorModels, cursorDefaultModel } = useAppStore();
|
||||
const { cursorCliStatus } = useSetupStore();
|
||||
|
||||
const selectedProvider = getProviderFromModelString(selectedModel);
|
||||
const selectedProvider = getModelProvider(selectedModel);
|
||||
|
||||
// Check if Cursor CLI is available
|
||||
const isCursorAvailable = cursorCliStatus?.installed && cursorCliStatus?.auth?.authenticated;
|
||||
@@ -40,14 +31,14 @@ 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 = model.id.replace('cursor-', '');
|
||||
const cursorModelId = stripProviderPrefix(model.id);
|
||||
return enabledCursorModels.includes(cursorModelId as any);
|
||||
});
|
||||
|
||||
const handleProviderChange = (provider: ModelProvider) => {
|
||||
if (provider === 'cursor' && selectedProvider !== 'cursor') {
|
||||
// Switch to Cursor's default model (from global settings)
|
||||
onModelSelect(`cursor-${cursorDefaultModel}`);
|
||||
onModelSelect(`${PROVIDER_PREFIXES.cursor}${cursorDefaultModel}`);
|
||||
} else if (provider === 'claude' && selectedProvider !== 'claude') {
|
||||
// Switch to Claude's default model
|
||||
onModelSelect('sonnet');
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Brain, UserCircle, Terminal } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { AgentModel, ThinkingLevel, AIProfile } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP, profileHasThinking } from '@automaker/types';
|
||||
import type { ModelAlias, ThinkingLevel, AIProfile, CursorModelId } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP, profileHasThinking, PROVIDER_PREFIXES } from '@automaker/types';
|
||||
import { PROFILE_ICONS } from './model-constants';
|
||||
|
||||
/**
|
||||
@@ -32,7 +32,7 @@ function getProfileThinkingDisplay(profile: AIProfile): string | null {
|
||||
|
||||
interface ProfileQuickSelectProps {
|
||||
profiles: AIProfile[];
|
||||
selectedModel: AgentModel;
|
||||
selectedModel: ModelAlias | CursorModelId;
|
||||
selectedThinkingLevel: ThinkingLevel;
|
||||
selectedCursorModel?: string; // For detecting cursor profile selection
|
||||
onSelect: (profile: AIProfile) => void; // Changed to pass full profile
|
||||
@@ -62,7 +62,7 @@ export function ProfileQuickSelect({
|
||||
const isProfileSelected = (profile: AIProfile): boolean => {
|
||||
if (profile.provider === 'cursor') {
|
||||
// For cursor profiles, check if cursor model matches
|
||||
const profileCursorModel = `cursor-${profile.cursorModel || 'auto'}`;
|
||||
const profileCursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`;
|
||||
return selectedCursorModel === profileCursorModel;
|
||||
}
|
||||
// For Claude profiles
|
||||
|
||||
Reference in New Issue
Block a user