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:
Kacper
2025-12-30 04:01:56 +01:00
parent 3d655c3298
commit 39f2c8c9ff
38 changed files with 713 additions and 258 deletions

View File

@@ -1,5 +1,6 @@
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import { useAppStore, type AgentModel } from '@/store/app-store';
import { useAppStore, type ModelAlias } from '@/store/app-store';
import type { CursorModelId } from '@automaker/types';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ImageDropZone } from '@/components/ui/image-drop-zone';
@@ -63,7 +64,7 @@ export function AgentView() {
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
const [showSessionManager, setShowSessionManager] = useState(true);
const [isDragOver, setIsDragOver] = useState(false);
const [selectedModel, setSelectedModel] = useState<AgentModel>('sonnet');
const [selectedModel, setSelectedModel] = useState<ModelAlias | CursorModelId>('sonnet');
// Track if initial session has been loaded
const initialSessionLoadedRef = useRef(false);

View File

@@ -9,7 +9,7 @@ import {
import { useAppStore, Feature } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron';
import type { AutoModeEvent } from '@/types/electron';
import type { BacklogPlanResult } from '@automaker/types';
import type { ModelAlias, CursorModelId, BacklogPlanResult } from '@automaker/types';
import { pathsEqual } from '@/lib/utils';
import { toast } from 'sonner';
import { getBlockingDependencies } from '@automaker/dependency-resolver';

View File

@@ -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={() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ import { useGithubIssues, useIssueValidation } from './github-issues-view/hooks'
import { IssueRow, IssueDetailPanel, IssuesListHeader } from './github-issues-view/components';
import { ValidationDialog } from './github-issues-view/dialogs';
import { formatDate, getFeaturePriority } from './github-issues-view/utils';
import { useModelOverride } from '@/components/shared';
export function GitHubIssuesView() {
const [selectedIssue, setSelectedIssue] = useState<GitHubIssue | null>(null);
@@ -21,6 +22,9 @@ export function GitHubIssuesView() {
const { currentProject, defaultAIProfileId, aiProfiles, getCurrentWorktree, worktreesByProject } =
useAppStore();
// Model override for validation
const validationModelOverride = useModelOverride({ phase: 'validationModel' });
const { openIssues, closedIssues, loading, refreshing, error, refresh } = useGithubIssues();
const { validatingIssues, cachedValidations, handleValidateIssue, handleViewCachedValidation } =
@@ -85,6 +89,9 @@ export function GitHubIssuesView() {
.filter(Boolean)
.join('\n');
// Use profile default model
const featureModel = defaultProfile?.model ?? 'opus';
const feature = {
id: `issue-${issue.number}-${crypto.randomUUID()}`,
title: issue.title,
@@ -93,7 +100,7 @@ export function GitHubIssuesView() {
status: 'backlog' as const,
passes: false,
priority: getFeaturePriority(validation.estimatedComplexity),
model: defaultProfile?.model ?? 'opus',
model: featureModel,
thinkingLevel: defaultProfile?.thinkingLevel ?? 'none',
branchName: currentBranch,
createdAt: new Date().toISOString(),
@@ -205,6 +212,7 @@ export function GitHubIssuesView() {
onClose={() => setSelectedIssue(null)}
onShowRevalidateConfirm={() => setShowRevalidateConfirm(true)}
formatDate={formatDate}
modelOverride={validationModelOverride}
/>
)}
@@ -228,7 +236,10 @@ export function GitHubIssuesView() {
confirmText="Re-validate"
onConfirm={() => {
if (selectedIssue) {
handleValidateIssue(selectedIssue, { forceRevalidate: true });
handleValidateIssue(selectedIssue, {
forceRevalidate: true,
model: validationModelOverride.effectiveModel,
});
}
}}
/>

View File

@@ -16,6 +16,7 @@ import { Markdown } from '@/components/ui/markdown';
import { cn } from '@/lib/utils';
import type { IssueDetailPanelProps } from '../types';
import { isValidationStale } from '../utils';
import { ModelOverrideTrigger } from '@/components/shared';
export function IssueDetailPanel({
issue,
@@ -27,6 +28,7 @@ export function IssueDetailPanel({
onClose,
onShowRevalidateConfirm,
formatDate,
modelOverride,
}: IssueDetailPanelProps) {
const isValidating = validatingIssues.has(issue.number);
const cached = cachedValidations.get(issue.number);
@@ -83,10 +85,23 @@ export function IssueDetailPanel({
<Clock className="h-4 w-4 mr-1 text-yellow-500" />
View (stale)
</Button>
<ModelOverrideTrigger
currentModel={modelOverride.effectiveModel}
onModelChange={modelOverride.setOverride}
phase="validationModel"
isOverridden={modelOverride.isOverridden}
size="sm"
variant="icon"
/>
<Button
variant="default"
size="sm"
onClick={() => onValidateIssue(issue, { forceRevalidate: true })}
onClick={() =>
onValidateIssue(issue, {
forceRevalidate: true,
model: modelOverride.effectiveModel,
})
}
>
<Wand2 className="h-4 w-4 mr-1" />
Re-validate
@@ -96,10 +111,24 @@ export function IssueDetailPanel({
}
return (
<Button variant="default" size="sm" onClick={() => onValidateIssue(issue)}>
<Wand2 className="h-4 w-4 mr-1" />
Validate with AI
</Button>
<>
<ModelOverrideTrigger
currentModel={modelOverride.effectiveModel}
onModelChange={modelOverride.setOverride}
phase="validationModel"
isOverridden={modelOverride.isOverridden}
size="sm"
variant="icon"
/>
<Button
variant="default"
size="sm"
onClick={() => onValidateIssue(issue, { model: modelOverride.effectiveModel })}
>
<Wand2 className="h-4 w-4 mr-1" />
Validate with AI
</Button>
</>
);
})()}
<Button variant="outline" size="sm" onClick={() => onOpenInGitHub(issue.url)}>

View File

@@ -205,8 +205,8 @@ export function useIssueValidation({
}, []);
const handleValidateIssue = useCallback(
async (issue: GitHubIssue, options: { forceRevalidate?: boolean } = {}) => {
const { forceRevalidate = false } = options;
async (issue: GitHubIssue, options: { forceRevalidate?: boolean; model?: string } = {}) => {
const { forceRevalidate = false, model } = options;
if (!currentProject?.path) {
toast.error('No project selected');
@@ -233,6 +233,9 @@ export function useIssueValidation({
description: 'You will be notified when the analysis is complete',
});
// Use provided model override or fall back to global validationModel
const modelToUse = model || validationModel;
try {
const api = getElectronAPI();
if (api.github?.validateIssue) {
@@ -244,7 +247,7 @@ export function useIssueValidation({
issueBody: issue.body || '',
issueLabels: issue.labels.map((l) => l.name),
},
validationModel
modelToUse
);
if (!result.success) {

View File

@@ -1,4 +1,5 @@
import type { GitHubIssue, StoredValidation } from '@/lib/electron';
import type { ModelAlias, CursorModelId } from '@automaker/types';
export interface IssueRowProps {
issue: GitHubIssue;
@@ -18,11 +19,21 @@ export interface IssueDetailPanelProps {
cachedValidations: Map<number, StoredValidation>;
onValidateIssue: (
issue: GitHubIssue,
options?: { showDialog?: boolean; forceRevalidate?: boolean }
options?: {
showDialog?: boolean;
forceRevalidate?: boolean;
model?: ModelAlias | CursorModelId;
}
) => Promise<void>;
onViewCachedValidation: (issue: GitHubIssue) => Promise<void>;
onOpenInGitHub: (url: string) => void;
onClose: () => void;
onShowRevalidateConfirm: () => void;
formatDate: (date: string) => string;
/** Model override state */
modelOverride: {
effectiveModel: ModelAlias | CursorModelId;
isOverridden: boolean;
setOverride: (model: ModelAlias | CursorModelId | null) => void;
};
}

View File

@@ -11,7 +11,7 @@ import { Brain, Bot, Terminal } from 'lucide-react';
import { toast } from 'sonner';
import type {
AIProfile,
AgentModel,
ModelAlias,
ThinkingLevel,
ModelProvider,
CursorModelId,
@@ -42,7 +42,7 @@ export function ProfileForm({
description: profile.description || '',
provider: (profile.provider || 'claude') as ModelProvider,
// Claude-specific
model: profile.model || ('sonnet' as AgentModel),
model: profile.model || ('sonnet' as ModelAlias),
thinkingLevel: profile.thinkingLevel || ('none' as ThinkingLevel),
// Cursor-specific
cursorModel: profile.cursorModel || ('auto' as CursorModelId),
@@ -62,7 +62,7 @@ export function ProfileForm({
});
};
const handleModelChange = (model: AgentModel) => {
const handleModelChange = (model: ModelAlias) => {
setFormData({
...formData,
model,

View File

@@ -1,5 +1,5 @@
import { Brain, Zap, Scale, Cpu, Rocket, Sparkles } from 'lucide-react';
import type { AgentModel, ThinkingLevel } from '@/store/app-store';
import type { ModelAlias, ThinkingLevel } from '@/store/app-store';
// Icon mapping for profiles
export const PROFILE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
@@ -22,7 +22,7 @@ export const ICON_OPTIONS = [
];
// Model options for the form
export const CLAUDE_MODELS: { id: AgentModel; label: string }[] = [
export const CLAUDE_MODELS: { id: ModelAlias; label: string }[] = [
{ id: 'haiku', label: 'Claude Haiku' },
{ id: 'sonnet', label: 'Claude Sonnet' },
{ id: 'opus', label: 'Claude Opus' },

View File

@@ -1,8 +1,8 @@
import type { AgentModel, ModelProvider, AIProfile } from '@automaker/types';
import type { ModelAlias, ModelProvider, AIProfile } from '@automaker/types';
import { CURSOR_MODEL_MAP } from '@automaker/types';
// Helper to determine provider from model (legacy, always returns 'claude')
export function getProviderFromModel(model: AgentModel): ModelProvider {
export function getProviderFromModel(model: ModelAlias): ModelProvider {
return 'claude';
}

View File

@@ -23,7 +23,7 @@ import {
SelectValue,
} from '@/components/ui/select';
import type { AIProfile } from '@/store/app-store';
import type { AgentModel } from '@automaker/types';
import type { ModelAlias } from '@automaker/types';
type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
@@ -36,7 +36,7 @@ interface FeatureDefaultsSectionProps {
defaultRequirePlanApproval: boolean;
defaultAIProfileId: string | null;
aiProfiles: AIProfile[];
validationModel: AgentModel;
validationModel: ModelAlias;
onShowProfilesOnlyChange: (value: boolean) => void;
onDefaultSkipTestsChange: (value: boolean) => void;
onEnableDependencyBlockingChange: (value: boolean) => void;
@@ -44,7 +44,7 @@ interface FeatureDefaultsSectionProps {
onDefaultPlanningModeChange: (value: PlanningMode) => void;
onDefaultRequirePlanApprovalChange: (value: boolean) => void;
onDefaultAIProfileIdChange: (value: string | null) => void;
onValidationModelChange: (value: AgentModel) => void;
onValidationModelChange: (value: ModelAlias) => void;
}
export function FeatureDefaultsSection({
@@ -243,7 +243,7 @@ export function FeatureDefaultsSection({
<Label className="text-foreground font-medium">Issue Validation Model</Label>
<Select
value={validationModel}
onValueChange={(v: string) => onValidationModelChange(v as AgentModel)}
onValueChange={(v: string) => onValidationModelChange(v as ModelAlias)}
>
<SelectTrigger className="w-[140px] h-8" data-testid="validation-model-select">
<SelectValue />

View File

@@ -1,13 +1,14 @@
import { cn } from '@/lib/utils';
import { useAppStore } from '@/store/app-store';
import type { AgentModel, CursorModelId } from '@automaker/types';
import type { ModelAlias, CursorModelId } from '@automaker/types';
import { stripProviderPrefix } from '@automaker/types';
import { CLAUDE_MODELS, CURSOR_MODELS } from '@/components/views/board-view/shared/model-constants';
interface PhaseModelSelectorProps {
label: string;
description: string;
value: AgentModel | CursorModelId;
onChange: (model: AgentModel | CursorModelId) => void;
value: ModelAlias | CursorModelId;
onChange: (model: ModelAlias | CursorModelId) => void;
}
export function PhaseModelSelector({
@@ -20,13 +21,10 @@ export function PhaseModelSelector({
// Filter Cursor models to only show enabled ones
const availableCursorModels = CURSOR_MODELS.filter((model) => {
const cursorId = model.id.replace('cursor-', '') as CursorModelId;
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
return enabledCursorModels.includes(cursorId);
});
// Check if current value is a Claude model or Cursor model
const isClaudeModel = (v: string) => ['haiku', 'sonnet', 'opus'].includes(v);
return (
<div
className={cn(
@@ -50,7 +48,7 @@ export function PhaseModelSelector({
return (
<button
key={model.id}
onClick={() => onChange(model.id as AgentModel)}
onClick={() => onChange(model.id as ModelAlias)}
className={cn(
'px-3 py-1.5 rounded-lg text-xs font-medium',
'transition-all duration-150',
@@ -75,7 +73,7 @@ export function PhaseModelSelector({
{/* Cursor Models */}
{availableCursorModels.map((model) => {
const cursorId = model.id.replace('cursor-', '') as CursorModelId;
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
const isActive = value === cursorId;
return (
<button

View File

@@ -13,6 +13,7 @@ import { Terminal, Info } from 'lucide-react';
import { toast } from 'sonner';
import { getHttpApiClient } from '@/lib/http-api-client';
import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';
import { cn } from '@/lib/utils';
import type { CursorModelId, CursorModelConfig } from '@automaker/types';
import { CURSOR_MODEL_MAP } from '@automaker/types';
@@ -33,6 +34,7 @@ export function CursorSettingsTab() {
// Global settings from store
const { enabledCursorModels, cursorDefaultModel, setCursorDefaultModel, toggleCursorModel } =
useAppStore();
const { setCursorCliStatus } = useSetupStore();
const [status, setStatus] = useState<CursorStatus | null>(null);
const [isLoading, setIsLoading] = useState(true);
@@ -48,11 +50,24 @@ export function CursorSettingsTab() {
const statusResult = await api.setup.getCursorStatus();
if (statusResult.success) {
setStatus({
const newStatus = {
installed: statusResult.installed ?? false,
version: statusResult.version ?? undefined,
authenticated: statusResult.auth?.authenticated ?? false,
method: statusResult.auth?.method,
};
setStatus(newStatus);
// Also update the global setup store so other components can access the status
setCursorCliStatus({
installed: newStatus.installed,
version: newStatus.version,
auth: newStatus.authenticated
? {
authenticated: true,
method: newStatus.method || 'unknown',
}
: undefined,
});
}
} catch (error) {
@@ -61,7 +76,7 @@ export function CursorSettingsTab() {
} finally {
setIsLoading(false);
}
}, []);
}, [setCursorCliStatus]);
useEffect(() => {
loadData();