From de11908db1d17a35cd3efac2e960903211a5cd58 Mon Sep 17 00:00:00 2001 From: Shirone Date: Sun, 28 Dec 2025 01:32:55 +0100 Subject: [PATCH] feat: Integrate Cursor provider support in AI profiles - Updated AIProfile type to include support for Cursor provider, adding cursorModel and validation logic. - Enhanced ProfileForm component to handle provider selection and corresponding model configurations for both Claude and Cursor. - Implemented display functions for model and thinking configurations in ProfileQuickSelect. - Added default Cursor profiles to the application state. - Updated UI components to reflect provider-specific settings and validations. - Marked completion of the AI Profiles Integration phase in the project plan. --- .../shared/profile-quick-select.tsx | 42 +++- .../profiles-view/components/profile-form.tsx | 203 +++++++++++++++--- .../components/sortable-profile-card.tsx | 30 ++- .../components/views/profiles-view/utils.ts | 46 +++- apps/ui/src/store/app-store.ts | 29 +++ libs/types/src/index.ts | 2 + libs/types/src/settings.ts | 50 ++++- plan/cursor-cli-integration/README.md | 2 +- .../phases/phase-8-profiles.md | 12 +- 9 files changed, 355 insertions(+), 61 deletions(-) diff --git a/apps/ui/src/components/views/board-view/shared/profile-quick-select.tsx b/apps/ui/src/components/views/board-view/shared/profile-quick-select.tsx index 0b85c985..54bcc392 100644 --- a/apps/ui/src/components/views/board-view/shared/profile-quick-select.tsx +++ b/apps/ui/src/components/views/board-view/shared/profile-quick-select.tsx @@ -1,9 +1,35 @@ import { Label } from '@/components/ui/label'; import { Brain, UserCircle } from 'lucide-react'; import { cn } from '@/lib/utils'; -import { AgentModel, ThinkingLevel, AIProfile } from '@/store/app-store'; +import type { AgentModel, ThinkingLevel, AIProfile } from '@automaker/types'; +import { CURSOR_MODEL_MAP, profileHasThinking } from '@automaker/types'; import { PROFILE_ICONS } from './model-constants'; +/** + * Get display string for a profile's model configuration + */ +function getProfileModelDisplay(profile: AIProfile): string { + if (profile.provider === 'cursor') { + const cursorModel = profile.cursorModel || 'auto'; + const modelConfig = CURSOR_MODEL_MAP[cursorModel]; + return modelConfig?.label || cursorModel; + } + // Claude + return profile.model || 'sonnet'; +} + +/** + * Get display string for a profile's thinking configuration + */ +function getProfileThinkingDisplay(profile: AIProfile): string | null { + if (profile.provider === 'cursor') { + // For Cursor, thinking is embedded in the model + return profileHasThinking(profile) ? 'thinking' : null; + } + // Claude + return profile.thinkingLevel && profile.thinkingLevel !== 'none' ? profile.thinkingLevel : null; +} + interface ProfileQuickSelectProps { profiles: AIProfile[]; selectedModel: AgentModel; @@ -23,7 +49,11 @@ export function ProfileQuickSelect({ showManageLink = false, onManageLinkClick, }: ProfileQuickSelectProps) { - if (profiles.length === 0) { + // Filter to only Claude profiles for now - Cursor profiles will be supported + // when features support provider selection (Phase 9) + const claudeProfiles = profiles.filter((p) => p.provider === 'claude'); + + if (claudeProfiles.length === 0) { return null; } @@ -39,7 +69,7 @@ export function ProfileQuickSelect({
- {profiles.slice(0, 6).map((profile) => { + {claudeProfiles.slice(0, 6).map((profile) => { const IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : Brain; const isSelected = selectedModel === profile.model && selectedThinkingLevel === profile.thinkingLevel; @@ -47,7 +77,7 @@ export function ProfileQuickSelect({ diff --git a/apps/ui/src/components/views/profiles-view/components/profile-form.tsx b/apps/ui/src/components/views/profiles-view/components/profile-form.tsx index 5cc8c4b1..370e7cd2 100644 --- a/apps/ui/src/components/views/profiles-view/components/profile-form.tsx +++ b/apps/ui/src/components/views/profiles-view/components/profile-form.tsx @@ -4,13 +4,20 @@ import { HotkeyButton } from '@/components/ui/hotkey-button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; +import { Badge } from '@/components/ui/badge'; import { cn, modelSupportsThinking } from '@/lib/utils'; import { DialogFooter } from '@/components/ui/dialog'; -import { Brain } from 'lucide-react'; +import { Brain, Bot, Terminal } from 'lucide-react'; import { toast } from 'sonner'; -import type { AIProfile, AgentModel, ThinkingLevel } from '@/store/app-store'; +import type { + AIProfile, + AgentModel, + ThinkingLevel, + ModelProvider, + CursorModelId, +} from '@automaker/types'; +import { CURSOR_MODEL_MAP, cursorModelHasThinking } from '@automaker/types'; import { CLAUDE_MODELS, THINKING_LEVELS, ICON_OPTIONS } from '../constants'; -import { getProviderFromModel } from '../utils'; interface ProfileFormProps { profile: Partial; @@ -30,13 +37,27 @@ export function ProfileForm({ const [formData, setFormData] = useState({ name: profile.name || '', description: profile.description || '', - model: profile.model || ('opus' as AgentModel), + provider: (profile.provider || 'claude') as ModelProvider, + // Claude-specific + model: profile.model || ('sonnet' as AgentModel), thinkingLevel: profile.thinkingLevel || ('none' as ThinkingLevel), + // Cursor-specific + cursorModel: profile.cursorModel || ('auto' as CursorModelId), icon: profile.icon || 'Brain', }); - const provider = getProviderFromModel(formData.model); - const supportsThinking = modelSupportsThinking(formData.model); + const supportsThinking = formData.provider === 'claude' && modelSupportsThinking(formData.model); + + const handleProviderChange = (provider: ModelProvider) => { + setFormData({ + ...formData, + provider, + // Reset to defaults when switching providers + model: provider === 'claude' ? 'sonnet' : formData.model, + thinkingLevel: provider === 'claude' ? 'none' : formData.thinkingLevel, + cursorModel: provider === 'cursor' ? 'auto' : formData.cursorModel, + }); + }; const handleModelChange = (model: AgentModel) => { setFormData({ @@ -45,21 +66,39 @@ export function ProfileForm({ }); }; + const handleCursorModelChange = (cursorModel: CursorModelId) => { + setFormData({ + ...formData, + cursorModel, + }); + }; + const handleSubmit = () => { if (!formData.name.trim()) { toast.error('Please enter a profile name'); return; } - onSave({ + const baseProfile = { name: formData.name.trim(), description: formData.description.trim(), - model: formData.model, - thinkingLevel: supportsThinking ? formData.thinkingLevel : 'none', - provider, + provider: formData.provider, isBuiltIn: false, icon: formData.icon, - }); + }; + + if (formData.provider === 'cursor') { + onSave({ + ...baseProfile, + cursorModel: formData.cursorModel, + }); + } else { + onSave({ + ...baseProfile, + model: formData.model, + thinkingLevel: supportsThinking ? formData.thinkingLevel : 'none', + }); + } }; return ( @@ -113,34 +152,128 @@ export function ProfileForm({
- {/* Model Selection */} + {/* Provider Selection */}
- -
- {CLAUDE_MODELS.map(({ id, label }) => ( - - ))} + +
+ +
- {/* Thinking Level */} - {supportsThinking && ( + {/* Claude Model Selection */} + {formData.provider === 'claude' && ( +
+ +
+ {CLAUDE_MODELS.map(({ id, label }) => ( + + ))} +
+
+ )} + + {/* Cursor Model Selection */} + {formData.provider === 'cursor' && ( +
+ +
+ {Object.entries(CURSOR_MODEL_MAP).map(([id, config]) => ( + + ))} +
+ {formData.cursorModel && cursorModelHasThinking(formData.cursorModel) && ( +

+ This model has built-in extended thinking capabilities. +

+ )} +
+ )} + + {/* Claude Thinking Level */} + {formData.provider === 'claude' && supportsThinking && (

{profile.description}

- - {profile.model} + {/* Provider badge */} + + {profile.provider === 'cursor' ? ( + + ) : ( + + )} + {profile.provider === 'cursor' ? 'Cursor' : 'Claude'} - {profile.thinkingLevel !== 'none' && ( + + {/* Model badge */} + + {profile.provider === 'cursor' + ? CURSOR_MODEL_MAP[profile.cursorModel || 'auto']?.label || + profile.cursorModel || + 'auto' + : profile.model || 'sonnet'} + + + {/* Thinking badge - works for both providers */} + {profileHasThinking(profile) && ( - {profile.thinkingLevel} + {profile.provider === 'cursor' ? 'Thinking' : profile.thinkingLevel} )}
diff --git a/apps/ui/src/components/views/profiles-view/utils.ts b/apps/ui/src/components/views/profiles-view/utils.ts index d6a9ce3e..f464a8d0 100644 --- a/apps/ui/src/components/views/profiles-view/utils.ts +++ b/apps/ui/src/components/views/profiles-view/utils.ts @@ -1,6 +1,48 @@ -import type { AgentModel, ModelProvider } from '@/store/app-store'; +import type { AgentModel, ModelProvider, AIProfile } from '@automaker/types'; +import { CURSOR_MODEL_MAP } from '@automaker/types'; -// Helper to determine provider from model +// Helper to determine provider from model (legacy, always returns 'claude') export function getProviderFromModel(model: AgentModel): ModelProvider { return 'claude'; } + +/** + * Validate an AI profile for completeness and correctness + */ +export function validateProfile(profile: Partial): { + valid: boolean; + errors: string[]; +} { + const errors: string[] = []; + + // Name is required + if (!profile.name?.trim()) { + errors.push('Profile name is required'); + } + + // Provider must be valid + if (!profile.provider || !['claude', 'cursor'].includes(profile.provider)) { + errors.push('Invalid provider'); + } + + // Claude-specific validation + if (profile.provider === 'claude') { + if (!profile.model) { + errors.push('Claude model is required'); + } else if (!['haiku', 'sonnet', 'opus'].includes(profile.model)) { + errors.push('Invalid Claude model'); + } + } + + // Cursor-specific validation + if (profile.provider === 'cursor') { + if (profile.cursorModel && !(profile.cursorModel in CURSOR_MODEL_MAP)) { + errors.push('Invalid Cursor model'); + } + } + + return { + valid: errors.length === 0, + errors, + }; +} diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 874e1a6d..5bb5b3d9 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -859,6 +859,7 @@ export interface AppActions { // Default built-in AI profiles const DEFAULT_AI_PROFILES: AIProfile[] = [ + // Claude profiles { id: 'profile-heavy-task', name: 'Heavy Task', @@ -890,6 +891,34 @@ const DEFAULT_AI_PROFILES: AIProfile[] = [ isBuiltIn: true, icon: 'Zap', }, + // Cursor profiles + { + id: 'profile-cursor-auto', + name: 'Cursor Auto', + description: 'Let Cursor choose the best model automatically.', + provider: 'cursor', + cursorModel: 'auto', + isBuiltIn: true, + icon: 'Sparkles', + }, + { + id: 'profile-cursor-fast', + name: 'Cursor Fast', + description: 'Quick responses with GPT-4o Mini via Cursor.', + provider: 'cursor', + cursorModel: 'gpt-4o-mini', + isBuiltIn: true, + icon: 'Zap', + }, + { + id: 'profile-cursor-thinking', + name: 'Cursor Thinking', + description: 'Claude Sonnet 4 with extended thinking via Cursor for complex tasks.', + provider: 'cursor', + cursorModel: 'claude-sonnet-4-thinking', + isBuiltIn: true, + icon: 'Brain', + }, ]; const initialState: AppState = { diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index 5c97dfbc..20523071 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -71,6 +71,8 @@ export { SETTINGS_VERSION, CREDENTIALS_VERSION, PROJECT_SETTINGS_VERSION, + profileHasThinking, + getProfileModelString, } from './settings.js'; // Model display constants diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index 40d1e04a..ade96ac3 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -7,6 +7,8 @@ */ import type { AgentModel } from './model.js'; +import type { CursorModelId } from './cursor-models.js'; +import { CURSOR_MODEL_MAP } from './cursor-models.js'; // Re-export AgentModel for convenience export type { AgentModel }; @@ -151,16 +153,54 @@ export interface AIProfile { name: string; /** User-friendly description */ description: string; - /** Which Claude model to use (opus, sonnet, haiku) */ - model: AgentModel; - /** Extended thinking level for reasoning-based tasks */ - thinkingLevel: ThinkingLevel; - /** Provider (currently only "claude") */ + /** Provider selection: 'claude' or 'cursor' */ provider: ModelProvider; /** Whether this is a built-in default profile */ isBuiltIn: boolean; /** Optional icon identifier or emoji */ icon?: string; + + // Claude-specific settings + /** Which Claude model to use (opus, sonnet, haiku) - only for Claude provider */ + model?: AgentModel; + /** Extended thinking level for reasoning-based tasks - only for Claude provider */ + thinkingLevel?: ThinkingLevel; + + // Cursor-specific settings + /** Which Cursor model to use - only for Cursor provider + * Note: For Cursor, thinking is embedded in the model ID (e.g., 'claude-sonnet-4-thinking') + */ + cursorModel?: CursorModelId; +} + +/** + * Helper to determine if a profile uses thinking mode + */ +export function profileHasThinking(profile: AIProfile): boolean { + if (profile.provider === 'claude') { + return profile.thinkingLevel !== undefined && profile.thinkingLevel !== 'none'; + } + + if (profile.provider === 'cursor') { + const model = profile.cursorModel || 'auto'; + // Check using model map for hasThinking flag, or check for 'thinking' in name + const modelConfig = CURSOR_MODEL_MAP[model]; + return modelConfig?.hasThinking ?? false; + } + + return false; +} + +/** + * Get effective model string for execution + */ +export function getProfileModelString(profile: AIProfile): string { + if (profile.provider === 'cursor') { + return `cursor:${profile.cursorModel || 'auto'}`; + } + + // Claude + return profile.model || 'sonnet'; } /** diff --git a/plan/cursor-cli-integration/README.md b/plan/cursor-cli-integration/README.md index 4e1fd7a1..ebf2b51b 100644 --- a/plan/cursor-cli-integration/README.md +++ b/plan/cursor-cli-integration/README.md @@ -14,7 +14,7 @@ | 5 | [Log Parser Integration](phases/phase-5-log-parser.md) | `completed` | ✅ | | 6 | [UI Setup Wizard](phases/phase-6-setup-wizard.md) | `completed` | ✅ | | 7 | [Settings View Provider Tabs](phases/phase-7-settings.md) | `completed` | ✅ | -| 8 | [AI Profiles Integration](phases/phase-8-profiles.md) | `pending` | - | +| 8 | [AI Profiles Integration](phases/phase-8-profiles.md) | `completed` | ✅ | | 9 | [Task Execution Integration](phases/phase-9-execution.md) | `pending` | - | | 10 | [Testing & Validation](phases/phase-10-testing.md) | `pending` | - | diff --git a/plan/cursor-cli-integration/phases/phase-8-profiles.md b/plan/cursor-cli-integration/phases/phase-8-profiles.md index 22173301..69fa900b 100644 --- a/plan/cursor-cli-integration/phases/phase-8-profiles.md +++ b/plan/cursor-cli-integration/phases/phase-8-profiles.md @@ -1,6 +1,6 @@ # Phase 8: AI Profiles Integration -**Status:** `pending` +**Status:** `completed` **Dependencies:** Phase 1 (Types), Phase 7 (Settings) **Estimated Effort:** Medium (UI + types) @@ -31,7 +31,7 @@ Extend the AI Profiles system to support Cursor as a provider, with proper handl ### Task 8.1: Update AIProfile Type -**Status:** `pending` +**Status:** `completed` **File:** `libs/types/src/settings.ts` @@ -93,7 +93,7 @@ export function getProfileModelString(profile: AIProfile): string { ### Task 8.2: Update Profile Form Component -**Status:** `pending` +**Status:** `completed` **File:** `apps/ui/src/components/views/profiles-view/components/profile-form.tsx` @@ -289,7 +289,7 @@ export function ProfileForm({ profile, onSave, onCancel }: ProfileFormProps) { ### Task 8.3: Update Profile Card Display -**Status:** `pending` +**Status:** `completed` **File:** `apps/ui/src/components/views/profiles-view/components/profile-card.tsx` @@ -374,7 +374,7 @@ export function ProfileCard({ profile, onEdit, onDelete }: ProfileCardProps) { ### Task 8.4: Add Default Cursor Profiles -**Status:** `pending` +**Status:** `completed` **File:** `apps/ui/src/components/views/profiles-view/constants.ts` @@ -430,7 +430,7 @@ export const DEFAULT_PROFILES: AIProfile[] = [ ### Task 8.5: Update Profile Validation -**Status:** `pending` +**Status:** `completed` Add validation for profile data: