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;
isAutoMode: boolean;
startTime: number;
model?: string;
provider?: 'claude' | 'cursor';
}
interface AutoLoopState {
@@ -604,9 +606,16 @@ export class AutoModeService {
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);
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
// Context files are passed as system prompt for higher priority
@@ -640,6 +649,8 @@ export class AutoModeService {
(Date.now() - tempRunningFeature.startTime) / 1000
)}s${finalStatus === 'verified' ? ' - auto-verified' : ''}`,
projectPath,
model: tempRunningFeature.model,
provider: tempRunningFeature.provider,
});
} catch (error) {
const errorInfo = classifyError(error);
@@ -805,6 +816,13 @@ ${prompt}
## Task
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, {
featureId,
projectPath,
@@ -813,6 +831,8 @@ Address the follow-up instructions above. Review the previous work and make the
abortController,
isAutoMode: false,
startTime: Date.now(),
model,
provider,
});
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',
description: prompt.substring(0, 100),
},
model,
provider,
});
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
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,
message: `Follow-up completed successfully${finalStatus === 'verified' ? ' - auto-verified' : ''}`,
projectPath,
model,
provider,
});
} catch (error) {
const errorInfo = classifyError(error);
@@ -1217,12 +1237,16 @@ Format your response as a structured markdown document.`;
projectPath: string;
projectName: string;
isAutoMode: boolean;
model?: string;
provider?: 'claude' | 'cursor';
}> {
return Array.from(this.runningFeatures.values()).map((rf) => ({
featureId: rf.featureId,
projectPath: rf.projectPath,
projectName: path.basename(rf.projectPath),
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({
...newFeature,
model,
thinkingLevel: modelSupportsThinking(model) ? newFeature.thinkingLevel : 'none',
model: model as AgentModel,
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 (
<Dialog open={open} onOpenChange={handleDialogClose}>

View File

@@ -1,12 +1,16 @@
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';
export type ModelOption = {
id: AgentModel;
id: string; // Claude models use AgentModel, Cursor models use "cursor-{id}"
label: string;
description: string;
badge?: string;
provider: 'claude';
provider: ModelProvider;
hasThinking?: boolean;
tier?: 'free' | 'pro';
};
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_LEVEL_LABELS: Record<ThinkingLevel, string> = {

View File

@@ -1,54 +1,178 @@
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 { AgentModel } from '@/store/app-store';
import { CLAUDE_MODELS, ModelOption } from './model-constants';
import type { AgentModel } from '@/store/app-store';
import type { ModelProvider } from '@automaker/types';
import { CLAUDE_MODELS, CURSOR_MODELS, ModelOption } from './model-constants';
interface ModelSelectorProps {
selectedModel: AgentModel;
onModelSelect: (model: AgentModel) => void;
selectedModel: string; // Can be AgentModel 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,
testIdPrefix = 'model-select',
}: 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 (
<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 (SDK)
</Label>
<span className="text-[11px] px-2 py-0.5 rounded-full border border-primary/40 text-primary">
Native
</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 className="space-y-4">
{/* Provider Selection */}
<div className="space-y-2">
<Label>AI Provider</Label>
<div className="flex gap-2">
<button
type="button"
onClick={() => handleProviderChange('claude')}
className={cn(
'flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-center gap-2',
selectedProvider === 'claude'
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background hover:bg-accent border-border'
)}
data-testid={`${testIdPrefix}-provider-claude`}
>
<Bot className="w-4 h-4" />
Claude
</button>
<button
type="button"
onClick={() => handleProviderChange('cursor')}
className={cn(
'flex-1 px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-center gap-2',
selectedProvider === 'cursor'
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background hover:bg-accent border-border'
)}
data-testid={`${testIdPrefix}-provider-cursor`}
>
<Terminal className="w-4 h-4" />
Cursor CLI
</button>
</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>
);
}

View File

@@ -15,7 +15,7 @@
| 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) | `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` | - |
**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 | 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-28 | 9 | Completed: ModelSelector with Cursor models, provider tracking in execution events |

View File

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