mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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:
@@ -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,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user