Files
automaker/plan/cursor-cli-integration/phases/phase-8-profiles.md
Shirone de11908db1 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.
2025-12-28 01:32:55 +01:00

16 KiB

Phase 8: AI Profiles Integration

Status: completed Dependencies: Phase 1 (Types), Phase 7 (Settings) Estimated Effort: Medium (UI + types)


Objective

Extend the AI Profiles system to support Cursor as a provider, with proper handling of Cursor's embedded thinking mode (via model ID) vs Claude's separate thinking level.


Key Concept: Thinking Mode Handling

Claude Approach

  • Separate thinkingLevel property: 'none' | 'low' | 'medium' | 'high' | 'ultrathink'
  • Applied to any Claude model

Cursor Approach

  • Thinking is embedded in the model ID
  • Examples: claude-sonnet-4 (no thinking) vs claude-sonnet-4-thinking (with thinking)
  • No separate thinking level selector needed for Cursor profiles

Tasks

Task 8.1: Update AIProfile Type

Status: completed

File: libs/types/src/settings.ts

Update the AIProfile interface:

import { CursorModelId } from './cursor-models';

/**
 * AI Profile - saved configuration for different use cases
 */
export interface AIProfile {
  id: string;
  name: string;
  description: string;
  isBuiltIn: boolean;
  icon?: string;

  // Provider selection
  provider: ModelProvider; // 'claude' | 'cursor'

  // Claude-specific
  model?: AgentModel; // 'opus' | 'sonnet' | 'haiku'
  thinkingLevel?: ThinkingLevel; // 'none' | 'low' | 'medium' | 'high' | 'ultrathink'

  // Cursor-specific
  cursorModel?: CursorModelId; // 'auto' | 'claude-sonnet-4' | 'gpt-4o' | etc.
  // Note: For Cursor, thinking is in the model ID (e.g., 'claude-sonnet-4-thinking')
}

/**
 * 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';
    return model.includes('thinking') || model === 'o3-mini';
  }

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

Task 8.2: Update Profile Form Component

Status: completed

File: apps/ui/src/components/views/profiles-view/components/profile-form.tsx

Add Cursor-specific fields:

import React, { useState } from 'react';
import { Bot, Terminal } from 'lucide-react';
import { Button } from '@/components/ui/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 {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import {
  AIProfile,
  AgentModel,
  ModelProvider,
  ThinkingLevel,
  CursorModelId,
  CURSOR_MODEL_MAP,
  cursorModelHasThinking,
} from '@automaker/types';

interface ProfileFormProps {
  profile: AIProfile;
  onSave: (profile: AIProfile) => void;
  onCancel: () => void;
}

export function ProfileForm({ profile, onSave, onCancel }: ProfileFormProps) {
  const [formData, setFormData] = useState<AIProfile>(profile);

  const handleProviderChange = (provider: ModelProvider) => {
    setFormData((prev) => ({
      ...prev,
      provider,
      // Reset provider-specific fields
      model: provider === 'claude' ? 'sonnet' : undefined,
      thinkingLevel: provider === 'claude' ? 'none' : undefined,
      cursorModel: provider === 'cursor' ? 'auto' : undefined,
    }));
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      {/* Name & Description */}
      <div className="space-y-4">
        <div>
          <Label>Profile Name</Label>
          <Input
            value={formData.name}
            onChange={(e) => setFormData((p) => ({ ...p, name: e.target.value }))}
            placeholder="My Profile"
          />
        </div>
        <div>
          <Label>Description</Label>
          <Textarea
            value={formData.description}
            onChange={(e) => setFormData((p) => ({ ...p, description: e.target.value }))}
            placeholder="Describe when to use this profile..."
          />
        </div>
      </div>

      {/* Provider Selection */}
      <div className="space-y-2">
        <Label>AI Provider</Label>
        <Select value={formData.provider} onValueChange={handleProviderChange}>
          <SelectTrigger>
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="claude">
              <div className="flex items-center gap-2">
                <Bot className="w-4 h-4" />
                Claude (Anthropic)
              </div>
            </SelectItem>
            <SelectItem value="cursor">
              <div className="flex items-center gap-2">
                <Terminal className="w-4 h-4" />
                Cursor CLI
              </div>
            </SelectItem>
          </SelectContent>
        </Select>
      </div>

      {/* Claude-specific settings */}
      {formData.provider === 'claude' && (
        <>
          <div className="space-y-2">
            <Label>Model</Label>
            <Select
              value={formData.model || 'sonnet'}
              onValueChange={(v) => setFormData((p) => ({ ...p, model: v as AgentModel }))}
            >
              <SelectTrigger>
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="haiku">Haiku (Fast)</SelectItem>
                <SelectItem value="sonnet">Sonnet (Balanced)</SelectItem>
                <SelectItem value="opus">Opus (Powerful)</SelectItem>
              </SelectContent>
            </Select>
          </div>

          <div className="space-y-2">
            <Label>Thinking Level</Label>
            <Select
              value={formData.thinkingLevel || 'none'}
              onValueChange={(v) =>
                setFormData((p) => ({ ...p, thinkingLevel: v as ThinkingLevel }))
              }
            >
              <SelectTrigger>
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="none">None</SelectItem>
                <SelectItem value="low">Low</SelectItem>
                <SelectItem value="medium">Medium</SelectItem>
                <SelectItem value="high">High</SelectItem>
                <SelectItem value="ultrathink">Ultra</SelectItem>
              </SelectContent>
            </Select>
          </div>
        </>
      )}

      {/* Cursor-specific settings */}
      {formData.provider === 'cursor' && (
        <div className="space-y-2">
          <Label>Cursor Model</Label>
          <Select
            value={formData.cursorModel || 'auto'}
            onValueChange={(v) => setFormData((p) => ({ ...p, cursorModel: v as CursorModelId }))}
          >
            <SelectTrigger>
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {Object.entries(CURSOR_MODEL_MAP).map(([id, config]) => (
                <SelectItem key={id} value={id}>
                  <div className="flex items-center gap-2">
                    <span>{config.label}</span>
                    {config.hasThinking && (
                      <Badge variant="outline" className="text-xs">
                        Thinking
                      </Badge>
                    )}
                    <Badge
                      variant={config.tier === 'free' ? 'default' : 'secondary'}
                      className="text-xs"
                    >
                      {config.tier}
                    </Badge>
                  </div>
                </SelectItem>
              ))}
            </SelectContent>
          </Select>

          {/* Info about thinking models */}
          {formData.cursorModel && cursorModelHasThinking(formData.cursorModel) && (
            <p className="text-xs text-muted-foreground mt-2">
              This model has built-in extended thinking capabilities.
            </p>
          )}
        </div>
      )}

      {/* Form Actions */}
      <div className="flex justify-end gap-2 pt-4">
        <Button type="button" variant="outline" onClick={onCancel}>
          Cancel
        </Button>
        <Button type="submit">Save Profile</Button>
      </div>
    </form>
  );
}

Task 8.3: Update Profile Card Display

Status: completed

File: apps/ui/src/components/views/profiles-view/components/profile-card.tsx

Show provider-specific info:

import React from 'react';
import { Bot, Terminal } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from '@/components/ui/card';
import { AIProfile, CURSOR_MODEL_MAP, profileHasThinking } from '@automaker/types';

interface ProfileCardProps {
  profile: AIProfile;
  onEdit: (profile: AIProfile) => void;
  onDelete: (profile: AIProfile) => void;
}

export function ProfileCard({ profile, onEdit, onDelete }: ProfileCardProps) {
  const hasThinking = profileHasThinking(profile);

  return (
    <Card>
      <CardHeader className="pb-2">
        <div className="flex items-center justify-between">
          <CardTitle className="text-lg flex items-center gap-2">
            {profile.provider === 'cursor' ? (
              <Terminal className="w-4 h-4" />
            ) : (
              <Bot className="w-4 h-4" />
            )}
            {profile.name}
          </CardTitle>
          {profile.isBuiltIn && <Badge variant="secondary">Built-in</Badge>}
        </div>
        <CardDescription>{profile.description}</CardDescription>
      </CardHeader>

      <CardContent>
        <div className="flex flex-wrap gap-2">
          {/* Provider badge */}
          <Badge variant="outline" className="capitalize">
            {profile.provider}
          </Badge>

          {/* Model badge */}
          <Badge variant="outline">
            {profile.provider === 'cursor'
              ? CURSOR_MODEL_MAP[profile.cursorModel || 'auto']?.label || profile.cursorModel
              : profile.model}
          </Badge>

          {/* Thinking badge */}
          {hasThinking && <Badge variant="default">Thinking</Badge>}
        </div>
      </CardContent>

      {!profile.isBuiltIn && (
        <CardFooter className="pt-0">
          <div className="flex gap-2 ml-auto">
            <Button variant="ghost" size="sm" onClick={() => onEdit(profile)}>
              Edit
            </Button>
            <Button variant="ghost" size="sm" onClick={() => onDelete(profile)}>
              Delete
            </Button>
          </div>
        </CardFooter>
      )}
    </Card>
  );
}

Task 8.4: Add Default Cursor Profiles

Status: completed

File: apps/ui/src/components/views/profiles-view/constants.ts

Add built-in Cursor profiles:

import { AIProfile } from '@automaker/types';

export const DEFAULT_PROFILES: AIProfile[] = [
  // Existing Claude profiles...
  {
    id: 'claude-default',
    name: 'Claude Default',
    description: 'Balanced Claude Sonnet model',
    provider: 'claude',
    model: 'sonnet',
    thinkingLevel: 'none',
    isBuiltIn: true,
    icon: 'bot',
  },
  // ... other Claude profiles

  // Cursor profiles
  {
    id: 'cursor-auto',
    name: 'Cursor Auto',
    description: 'Let Cursor choose the best model automatically',
    provider: 'cursor',
    cursorModel: 'auto',
    isBuiltIn: true,
    icon: 'terminal',
  },
  {
    id: 'cursor-fast',
    name: 'Cursor Fast',
    description: 'Quick responses with GPT-4o Mini',
    provider: 'cursor',
    cursorModel: 'gpt-4o-mini',
    isBuiltIn: true,
    icon: 'zap',
  },
  {
    id: 'cursor-thinking',
    name: 'Cursor Thinking',
    description: 'Claude Sonnet 4 with extended thinking for complex tasks',
    provider: 'cursor',
    cursorModel: 'claude-sonnet-4-thinking',
    isBuiltIn: true,
    icon: 'brain',
  },
];

Task 8.5: Update Profile Validation

Status: completed

Add validation for profile data:

import { AIProfile, CURSOR_MODEL_MAP } from '@automaker/types';

export function validateProfile(profile: AIProfile): { valid: boolean; errors: string[] } {
  const errors: string[] = [];

  if (!profile.name?.trim()) {
    errors.push('Profile name is required');
  }

  if (!['claude', 'cursor'].includes(profile.provider)) {
    errors.push('Invalid provider');
  }

  if (profile.provider === 'claude') {
    if (!profile.model) {
      errors.push('Claude model is required');
    }
  }

  if (profile.provider === 'cursor') {
    if (profile.cursorModel && !(profile.cursorModel in CURSOR_MODEL_MAP)) {
      errors.push('Invalid Cursor model');
    }
  }

  return {
    valid: errors.length === 0,
    errors,
  };
}

Verification

Test 1: Profile Creation with Cursor

  1. Navigate to Profiles view
  2. Click "Create Profile"
  3. Select "Cursor CLI" as provider
  4. Select a Cursor model
  5. Save the profile
  6. Verify it appears in the list with correct badges

Test 2: Thinking Mode Detection

import { profileHasThinking } from '@automaker/types';

// Claude with thinking
const claudeThinking: AIProfile = {
  id: '1',
  name: 'Test',
  description: '',
  provider: 'claude',
  model: 'sonnet',
  thinkingLevel: 'high',
  isBuiltIn: false,
};
console.assert(profileHasThinking(claudeThinking) === true);

// Claude without thinking
const claudeNoThinking: AIProfile = {
  id: '2',
  name: 'Test',
  description: '',
  provider: 'claude',
  model: 'sonnet',
  thinkingLevel: 'none',
  isBuiltIn: false,
};
console.assert(profileHasThinking(claudeNoThinking) === false);

// Cursor with thinking model
const cursorThinking: AIProfile = {
  id: '3',
  name: 'Test',
  description: '',
  provider: 'cursor',
  cursorModel: 'claude-sonnet-4-thinking',
  isBuiltIn: false,
};
console.assert(profileHasThinking(cursorThinking) === true);

// Cursor without thinking
const cursorNoThinking: AIProfile = {
  id: '4',
  name: 'Test',
  description: '',
  provider: 'cursor',
  cursorModel: 'gpt-4o',
  isBuiltIn: false,
};
console.assert(profileHasThinking(cursorNoThinking) === false);

console.log('All thinking detection tests passed!');

Test 3: Provider Switching

  1. Create a new profile
  2. Select Claude as provider
  3. Configure Claude options
  4. Switch to Cursor
  5. Verify Claude options are hidden
  6. Verify Cursor options are shown
  7. Previous selections should be cleared

Test 4: Built-in Profiles

  1. Navigate to Profiles view
  2. Verify Cursor built-in profiles appear
  3. Verify they cannot be edited/deleted
  4. Verify they show correct badges

Verification Checklist

Before marking this phase complete:

  • AIProfile type extended with Cursor fields
  • profileHasThinking() works for both providers
  • Profile form shows provider selector
  • Claude options shown only for Claude provider
  • Cursor options shown only for Cursor provider
  • Cursor models show thinking badge where applicable
  • Built-in Cursor profiles added
  • Profile cards display provider info
  • Profile validation works
  • Profiles persist correctly

Files Changed

File Action Description
libs/types/src/settings.ts Modify Add Cursor fields to AIProfile
apps/ui/src/components/views/profiles-view/components/profile-form.tsx Modify Add Cursor UI
apps/ui/src/components/views/profiles-view/components/profile-card.tsx Modify Show provider info
apps/ui/src/components/views/profiles-view/constants.ts Modify Add Cursor profiles

Design Notes

  • Provider selection is the first choice in profile form
  • Switching providers resets model-specific options
  • Cursor thinking is determined by model ID, not separate field
  • Built-in profiles provide good starting points
  • Profile cards show provider icon and model badges