feat: Enhance AutoModeService and UI for Cursor model support

- Updated AutoModeService to track model and provider for running features, improving logging and state management.
- Modified AddFeatureDialog to handle model selection for both Claude and Cursor, adjusting thinking level logic accordingly.
- Expanded ModelSelector to allow provider selection and dynamically display models based on the selected provider.
- Introduced new model constants for Cursor models, integrating them into the existing model management structure.
- Updated README and project plan to reflect the completion of task execution integration for Cursor models.
This commit is contained in:
Shirone
2025-12-28 01:43:57 +01:00
parent de11908db1
commit c90f12208f
6 changed files with 233 additions and 51 deletions

View File

@@ -316,6 +316,8 @@ interface RunningFeature {
abortController: AbortController; abortController: AbortController;
isAutoMode: boolean; isAutoMode: boolean;
startTime: number; startTime: number;
model?: string;
provider?: 'claude' | 'cursor';
} }
interface AutoLoopState { interface AutoLoopState {
@@ -604,9 +606,16 @@ export class AutoModeService {
typeof img === 'string' ? img : img.path typeof img === 'string' ? img : img.path
); );
// Get model from feature // Get model from feature and determine provider
const model = resolveModelString(feature.model, DEFAULT_MODELS.claude); const model = resolveModelString(feature.model, DEFAULT_MODELS.claude);
console.log(`[AutoMode] Executing feature ${featureId} with model: ${model} in ${workDir}`); const provider = ProviderFactory.getProviderNameForModel(model);
console.log(
`[AutoMode] Executing feature ${featureId} with model: ${model}, provider: ${provider} in ${workDir}`
);
// Store model and provider in running feature for tracking
tempRunningFeature.model = model;
tempRunningFeature.provider = provider;
// Run the agent with the feature's model and images // Run the agent with the feature's model and images
// Context files are passed as system prompt for higher priority // Context files are passed as system prompt for higher priority
@@ -640,6 +649,8 @@ export class AutoModeService {
(Date.now() - tempRunningFeature.startTime) / 1000 (Date.now() - tempRunningFeature.startTime) / 1000
)}s${finalStatus === 'verified' ? ' - auto-verified' : ''}`, )}s${finalStatus === 'verified' ? ' - auto-verified' : ''}`,
projectPath, projectPath,
model: tempRunningFeature.model,
provider: tempRunningFeature.provider,
}); });
} catch (error) { } catch (error) {
const errorInfo = classifyError(error); const errorInfo = classifyError(error);
@@ -805,6 +816,13 @@ ${prompt}
## Task ## Task
Address the follow-up instructions above. Review the previous work and make the requested changes or fixes.`; Address the follow-up instructions above. Review the previous work and make the requested changes or fixes.`;
// Get model from feature and determine provider early for tracking
const model = resolveModelString(feature?.model, DEFAULT_MODELS.claude);
const provider = ProviderFactory.getProviderNameForModel(model);
console.log(
`[AutoMode] Follow-up for feature ${featureId} using model: ${model}, provider: ${provider}`
);
this.runningFeatures.set(featureId, { this.runningFeatures.set(featureId, {
featureId, featureId,
projectPath, projectPath,
@@ -813,6 +831,8 @@ Address the follow-up instructions above. Review the previous work and make the
abortController, abortController,
isAutoMode: false, isAutoMode: false,
startTime: Date.now(), startTime: Date.now(),
model,
provider,
}); });
this.emitAutoModeEvent('auto_mode_feature_start', { this.emitAutoModeEvent('auto_mode_feature_start', {
@@ -823,13 +843,11 @@ Address the follow-up instructions above. Review the previous work and make the
title: 'Follow-up', title: 'Follow-up',
description: prompt.substring(0, 100), description: prompt.substring(0, 100),
}, },
model,
provider,
}); });
try { try {
// Get model from feature (already loaded above)
const model = resolveModelString(feature?.model, DEFAULT_MODELS.claude);
console.log(`[AutoMode] Follow-up for feature ${featureId} using model: ${model}`);
// Update feature status to in_progress // Update feature status to in_progress
await this.updateFeatureStatus(projectPath, featureId, 'in_progress'); await this.updateFeatureStatus(projectPath, featureId, 'in_progress');
@@ -925,6 +943,8 @@ Address the follow-up instructions above. Review the previous work and make the
passes: true, passes: true,
message: `Follow-up completed successfully${finalStatus === 'verified' ? ' - auto-verified' : ''}`, message: `Follow-up completed successfully${finalStatus === 'verified' ? ' - auto-verified' : ''}`,
projectPath, projectPath,
model,
provider,
}); });
} catch (error) { } catch (error) {
const errorInfo = classifyError(error); const errorInfo = classifyError(error);
@@ -1217,12 +1237,16 @@ Format your response as a structured markdown document.`;
projectPath: string; projectPath: string;
projectName: string; projectName: string;
isAutoMode: boolean; isAutoMode: boolean;
model?: string;
provider?: 'claude' | 'cursor';
}> { }> {
return Array.from(this.runningFeatures.values()).map((rf) => ({ return Array.from(this.runningFeatures.values()).map((rf) => ({
featureId: rf.featureId, featureId: rf.featureId,
projectPath: rf.projectPath, projectPath: rf.projectPath,
projectName: path.basename(rf.projectPath), projectName: path.basename(rf.projectPath),
isAutoMode: rf.isAutoMode, isAutoMode: rf.isAutoMode,
model: rf.model,
provider: rf.provider,
})); }));
} }

View File

@@ -312,11 +312,18 @@ export function AddFeatureDialog({
} }
}; };
const handleModelSelect = (model: AgentModel) => { 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-');
setNewFeature({ setNewFeature({
...newFeature, ...newFeature,
model, model: model as AgentModel,
thinkingLevel: modelSupportsThinking(model) ? newFeature.thinkingLevel : 'none', thinkingLevel: isCursorModel
? 'none'
: modelSupportsThinking(model)
? newFeature.thinkingLevel
: 'none',
}); });
}; };
@@ -328,7 +335,9 @@ export function AddFeatureDialog({
}); });
}; };
const newModelAllowsThinking = modelSupportsThinking(newFeature.model); // 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);
return ( return (
<Dialog open={open} onOpenChange={handleDialogClose}> <Dialog open={open} onOpenChange={handleDialogClose}>

View File

@@ -1,12 +1,16 @@
import type { AgentModel, ThinkingLevel } from '@/store/app-store'; import type { AgentModel, 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'; import { Brain, Zap, Scale, Cpu, Rocket, Sparkles } from 'lucide-react';
export type ModelOption = { export type ModelOption = {
id: AgentModel; id: string; // Claude models use AgentModel, Cursor models use "cursor-{id}"
label: string; label: string;
description: string; description: string;
badge?: string; badge?: string;
provider: 'claude'; provider: ModelProvider;
hasThinking?: boolean;
tier?: 'free' | 'pro';
}; };
export const CLAUDE_MODELS: ModelOption[] = [ export const CLAUDE_MODELS: ModelOption[] = [
@@ -33,6 +37,26 @@ export const CLAUDE_MODELS: ModelOption[] = [
}, },
]; ];
/**
* Cursor models derived from CURSOR_MODEL_MAP
* ID is prefixed with "cursor-" for ProviderFactory routing
*/
export const CURSOR_MODELS: ModelOption[] = Object.entries(CURSOR_MODEL_MAP).map(
([id, config]) => ({
id: `cursor-${id}`,
label: config.label,
description: config.description,
provider: 'cursor' as ModelProvider,
hasThinking: config.hasThinking,
tier: config.tier,
})
);
/**
* All available models (Claude + Cursor)
*/
export const ALL_MODELS: ModelOption[] = [...CLAUDE_MODELS, ...CURSOR_MODELS];
export const THINKING_LEVELS: ThinkingLevel[] = ['none', 'low', 'medium', 'high', 'ultrathink']; export const THINKING_LEVELS: ThinkingLevel[] = ['none', 'low', 'medium', 'high', 'ultrathink'];
export const THINKING_LEVEL_LABELS: Record<ThinkingLevel, string> = { export const THINKING_LEVEL_LABELS: Record<ThinkingLevel, string> = {

View File

@@ -1,54 +1,178 @@
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Brain } from 'lucide-react'; import { Badge } from '@/components/ui/badge';
import { Brain, Bot, Terminal } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { AgentModel } from '@/store/app-store'; import type { AgentModel } from '@/store/app-store';
import { CLAUDE_MODELS, ModelOption } from './model-constants'; import type { ModelProvider } from '@automaker/types';
import { CLAUDE_MODELS, CURSOR_MODELS, ModelOption } from './model-constants';
interface ModelSelectorProps { interface ModelSelectorProps {
selectedModel: AgentModel; selectedModel: string; // Can be AgentModel or "cursor-{id}"
onModelSelect: (model: AgentModel) => void; onModelSelect: (model: string) => void;
testIdPrefix?: string; 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({ export function ModelSelector({
selectedModel, selectedModel,
onModelSelect, onModelSelect,
testIdPrefix = 'model-select', testIdPrefix = 'model-select',
}: ModelSelectorProps) { }: ModelSelectorProps) {
const selectedProvider = getProviderFromModelString(selectedModel);
const handleProviderChange = (provider: ModelProvider) => {
if (provider === 'cursor' && selectedProvider !== 'cursor') {
// Switch to Cursor's default model
onModelSelect('cursor-auto');
} else if (provider === 'claude' && selectedProvider !== 'claude') {
// Switch to Claude's default model
onModelSelect('sonnet');
}
};
return ( return (
<div className="space-y-3"> <div className="space-y-4">
<div className="flex items-center justify-between"> {/* Provider Selection */}
<Label className="flex items-center gap-2"> <div className="space-y-2">
<Brain className="w-4 h-4 text-primary" /> <Label>AI Provider</Label>
Claude (SDK) <div className="flex gap-2">
</Label> <button
<span className="text-[11px] px-2 py-0.5 rounded-full border border-primary/40 text-primary"> type="button"
Native onClick={() => handleProviderChange('claude')}
</span> className={cn(
</div> 'flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-center gap-2',
<div className="flex gap-2 flex-wrap"> selectedProvider === 'claude'
{CLAUDE_MODELS.map((option) => { ? 'bg-primary text-primary-foreground border-primary'
const isSelected = selectedModel === option.id; : 'bg-background hover:bg-accent border-border'
const shortName = option.label.replace('Claude ', ''); )}
return ( data-testid={`${testIdPrefix}-provider-claude`}
<button >
key={option.id} <Bot className="w-4 h-4" />
type="button" Claude
onClick={() => onModelSelect(option.id)} </button>
title={option.description} <button
className={cn( type="button"
'flex-1 min-w-[80px] px-3 py-2 rounded-md border text-sm font-medium transition-colors', onClick={() => handleProviderChange('cursor')}
isSelected className={cn(
? 'bg-primary text-primary-foreground border-primary' 'flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-center gap-2',
: 'bg-background hover:bg-accent border-input' selectedProvider === 'cursor'
)} ? 'bg-primary text-primary-foreground border-primary'
data-testid={`${testIdPrefix}-${option.id}`} : 'bg-background hover:bg-accent border-border'
> )}
{shortName} data-testid={`${testIdPrefix}-provider-cursor`}
</button> >
); <Terminal className="w-4 h-4" />
})} Cursor CLI
</button>
</div>
</div> </div>
{/* Claude Models */}
{selectedProvider === 'claude' && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<Brain className="w-4 h-4 text-primary" />
Claude Model
</Label>
<span className="text-[11px] px-2 py-0.5 rounded-full border border-primary/40 text-primary">
Native SDK
</span>
</div>
<div className="flex gap-2 flex-wrap">
{CLAUDE_MODELS.map((option) => {
const isSelected = selectedModel === option.id;
const shortName = option.label.replace('Claude ', '');
return (
<button
key={option.id}
type="button"
onClick={() => onModelSelect(option.id)}
title={option.description}
className={cn(
'flex-1 min-w-[80px] px-3 py-2 rounded-md border text-sm font-medium transition-colors',
isSelected
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background hover:bg-accent border-input'
)}
data-testid={`${testIdPrefix}-${option.id}`}
>
{shortName}
</button>
);
})}
</div>
</div>
)}
{/* Cursor Models */}
{selectedProvider === 'cursor' && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<Terminal className="w-4 h-4 text-primary" />
Cursor Model
</Label>
<span className="text-[11px] px-2 py-0.5 rounded-full border border-amber-500/40 text-amber-600 dark:text-amber-400">
CLI
</span>
</div>
<div className="flex flex-col gap-2">
{CURSOR_MODELS.map((option) => {
const isSelected = selectedModel === option.id;
return (
<button
key={option.id}
type="button"
onClick={() => onModelSelect(option.id)}
title={option.description}
className={cn(
'w-full px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-between',
isSelected
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background hover:bg-accent border-border'
)}
data-testid={`${testIdPrefix}-${option.id}`}
>
<span>{option.label}</span>
<div className="flex gap-1">
{option.hasThinking && (
<Badge
variant="outline"
className={cn(
'text-xs',
isSelected
? 'border-primary-foreground/50 text-primary-foreground'
: 'border-amber-500/50 text-amber-600 dark:text-amber-400'
)}
>
Thinking
</Badge>
)}
{option.tier && (
<Badge
variant={option.tier === 'free' ? 'default' : 'secondary'}
className={cn('text-xs', isSelected && 'bg-primary-foreground/20')}
>
{option.tier}
</Badge>
)}
</div>
</button>
);
})}
</div>
</div>
)}
</div> </div>
); );
} }

View File

@@ -15,7 +15,7 @@
| 6 | [UI Setup Wizard](phases/phase-6-setup-wizard.md) | `completed` | ✅ | | 6 | [UI Setup Wizard](phases/phase-6-setup-wizard.md) | `completed` | ✅ |
| 7 | [Settings View Provider Tabs](phases/phase-7-settings.md) | `completed` | ✅ | | 7 | [Settings View Provider Tabs](phases/phase-7-settings.md) | `completed` | ✅ |
| 8 | [AI Profiles Integration](phases/phase-8-profiles.md) | `completed` | ✅ | | 8 | [AI Profiles Integration](phases/phase-8-profiles.md) | `completed` | ✅ |
| 9 | [Task Execution Integration](phases/phase-9-execution.md) | `pending` | - | | 9 | [Task Execution Integration](phases/phase-9-execution.md) | `completed` | |
| 10 | [Testing & Validation](phases/phase-10-testing.md) | `pending` | - | | 10 | [Testing & Validation](phases/phase-10-testing.md) | `pending` | - |
**Status Legend:** `pending` | `in_progress` | `completed` | `blocked` **Status Legend:** `pending` | `in_progress` | `completed` | `blocked`
@@ -393,3 +393,4 @@ Cursor models use their own `CURSOR_MODEL_MAP` in `@automaker/types`.
| 2025-12-27 | 1 | Added tasks 1.5-1.7: ModelOption, DEFAULT_MODELS, reuse InstallationStatus | | 2025-12-27 | 1 | Added tasks 1.5-1.7: ModelOption, DEFAULT_MODELS, reuse InstallationStatus |
| 2025-12-27 | 2 | Refactored to use `spawnJSONLProcess` and `isAbortError` from @automaker packages | | 2025-12-27 | 2 | Refactored to use `spawnJSONLProcess` and `isAbortError` from @automaker packages |
| 2025-12-27 | - | Added design decisions 4-5: @automaker packages usage, model-resolver note | | 2025-12-27 | - | Added design decisions 4-5: @automaker packages usage, model-resolver note |
| 2025-12-28 | 9 | Completed: ModelSelector with Cursor models, provider tracking in execution events |

View File

@@ -1,6 +1,6 @@
# Phase 9: Task Execution Integration # Phase 9: Task Execution Integration
**Status:** `pending` **Status:** `completed`
**Dependencies:** Phase 3 (Factory), Phase 8 (Profiles) **Dependencies:** Phase 3 (Factory), Phase 8 (Profiles)
**Estimated Effort:** Medium (service updates) **Estimated Effort:** Medium (service updates)