mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
feat(ui): Add Phase Models settings tab
- Add PhaseModelsSection with grouped phase configuration: - Quick Tasks: enhancement, file/image description - Validation Tasks: GitHub issue validation - Generation Tasks: spec, features, backlog, analysis - Add PhaseModelSelector component showing Claude + Cursor models - Add phaseModels state and actions to app-store - Add 'phase-models' navigation item with Workflow icon 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { DeleteProjectDialog } from './settings-view/components/delete-project-d
|
|||||||
import { SettingsNavigation } from './settings-view/components/settings-navigation';
|
import { SettingsNavigation } from './settings-view/components/settings-navigation';
|
||||||
import { ApiKeysSection } from './settings-view/api-keys/api-keys-section';
|
import { ApiKeysSection } from './settings-view/api-keys/api-keys-section';
|
||||||
import { AIEnhancementSection } from './settings-view/ai-enhancement';
|
import { AIEnhancementSection } from './settings-view/ai-enhancement';
|
||||||
|
import { PhaseModelsSection } from './settings-view/phase-models';
|
||||||
import { AppearanceSection } from './settings-view/appearance/appearance-section';
|
import { AppearanceSection } from './settings-view/appearance/appearance-section';
|
||||||
import { TerminalSection } from './settings-view/terminal/terminal-section';
|
import { TerminalSection } from './settings-view/terminal/terminal-section';
|
||||||
import { AudioSection } from './settings-view/audio/audio-section';
|
import { AudioSection } from './settings-view/audio/audio-section';
|
||||||
@@ -88,6 +89,8 @@ export function SettingsView() {
|
|||||||
return <ProviderTabs defaultTab={activeView === 'claude' ? 'claude' : undefined} />;
|
return <ProviderTabs defaultTab={activeView === 'claude' ? 'claude' : undefined} />;
|
||||||
case 'ai-enhancement':
|
case 'ai-enhancement':
|
||||||
return <AIEnhancementSection />;
|
return <AIEnhancementSection />;
|
||||||
|
case 'phase-models':
|
||||||
|
return <PhaseModelsSection />;
|
||||||
case 'appearance':
|
case 'appearance':
|
||||||
return (
|
return (
|
||||||
<AppearanceSection
|
<AppearanceSection
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
FlaskConical,
|
FlaskConical,
|
||||||
Trash2,
|
Trash2,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
|
Workflow,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { SettingsViewId } from '../hooks/use-settings-view';
|
import type { SettingsViewId } from '../hooks/use-settings-view';
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ export const NAV_ITEMS: NavigationItem[] = [
|
|||||||
{ id: 'api-keys', label: 'API Keys', icon: Key },
|
{ id: 'api-keys', label: 'API Keys', icon: Key },
|
||||||
{ id: 'providers', label: 'AI Providers', icon: Bot },
|
{ id: 'providers', label: 'AI Providers', icon: Bot },
|
||||||
{ id: 'ai-enhancement', label: 'AI Enhancement', icon: Sparkles },
|
{ id: 'ai-enhancement', label: 'AI Enhancement', icon: Sparkles },
|
||||||
|
{ id: 'phase-models', label: 'Phase Models', icon: Workflow },
|
||||||
{ id: 'appearance', label: 'Appearance', icon: Palette },
|
{ id: 'appearance', label: 'Appearance', icon: Palette },
|
||||||
{ id: 'terminal', label: 'Terminal', icon: SquareTerminal },
|
{ id: 'terminal', label: 'Terminal', icon: SquareTerminal },
|
||||||
{ id: 'keyboard', label: 'Keyboard Shortcuts', icon: Settings2 },
|
{ id: 'keyboard', label: 'Keyboard Shortcuts', icon: Settings2 },
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export type SettingsViewId =
|
|||||||
| 'claude'
|
| 'claude'
|
||||||
| 'providers'
|
| 'providers'
|
||||||
| 'ai-enhancement'
|
| 'ai-enhancement'
|
||||||
|
| 'phase-models'
|
||||||
| 'appearance'
|
| 'appearance'
|
||||||
| 'terminal'
|
| 'terminal'
|
||||||
| 'keyboard'
|
| 'keyboard'
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { PhaseModelsSection } from './phase-models-section';
|
||||||
|
export { PhaseModelSelector } from './phase-model-selector';
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import type { AgentModel, CursorModelId } from '@automaker/types';
|
||||||
|
import { CLAUDE_MODELS, CURSOR_MODELS } from '@/components/views/board-view/shared/model-constants';
|
||||||
|
|
||||||
|
interface PhaseModelSelectorProps {
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
value: AgentModel | CursorModelId;
|
||||||
|
onChange: (model: AgentModel | CursorModelId) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PhaseModelSelector({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: PhaseModelSelectorProps) {
|
||||||
|
const { enabledCursorModels } = useAppStore();
|
||||||
|
|
||||||
|
// Filter Cursor models to only show enabled ones
|
||||||
|
const availableCursorModels = CURSOR_MODELS.filter((model) => {
|
||||||
|
const cursorId = model.id.replace('cursor-', '') as CursorModelId;
|
||||||
|
return enabledCursorModels.includes(cursorId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if current value is a Claude model or Cursor model
|
||||||
|
const isClaudeModel = (v: string) => ['haiku', 'sonnet', 'opus'].includes(v);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'p-4 rounded-xl',
|
||||||
|
'bg-accent/20 border border-border/30',
|
||||||
|
'hover:bg-accent/30 transition-colors'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{/* Label and Description */}
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-foreground">{label}</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">{description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Model Selection */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{/* Claude Models */}
|
||||||
|
{CLAUDE_MODELS.map((model) => {
|
||||||
|
const isActive = value === model.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={model.id}
|
||||||
|
onClick={() => onChange(model.id as AgentModel)}
|
||||||
|
className={cn(
|
||||||
|
'px-3 py-1.5 rounded-lg text-xs font-medium',
|
||||||
|
'transition-all duration-150',
|
||||||
|
isActive
|
||||||
|
? ['bg-brand-500/20 text-brand-500', 'border border-brand-500/40', 'shadow-sm']
|
||||||
|
: [
|
||||||
|
'bg-accent/50 text-muted-foreground',
|
||||||
|
'border border-transparent',
|
||||||
|
'hover:bg-accent hover:text-foreground',
|
||||||
|
]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{model.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Divider if there are Cursor models */}
|
||||||
|
{availableCursorModels.length > 0 && (
|
||||||
|
<div className="w-px h-6 bg-border/50 mx-1 self-center" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cursor Models */}
|
||||||
|
{availableCursorModels.map((model) => {
|
||||||
|
const cursorId = model.id.replace('cursor-', '') as CursorModelId;
|
||||||
|
const isActive = value === cursorId;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={model.id}
|
||||||
|
onClick={() => onChange(cursorId)}
|
||||||
|
className={cn(
|
||||||
|
'px-3 py-1.5 rounded-lg text-xs font-medium',
|
||||||
|
'transition-all duration-150',
|
||||||
|
isActive
|
||||||
|
? [
|
||||||
|
'bg-purple-500/20 text-purple-400',
|
||||||
|
'border border-purple-500/40',
|
||||||
|
'shadow-sm',
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'bg-accent/50 text-muted-foreground',
|
||||||
|
'border border-transparent',
|
||||||
|
'hover:bg-accent hover:text-foreground',
|
||||||
|
]
|
||||||
|
)}
|
||||||
|
title={model.description}
|
||||||
|
>
|
||||||
|
{model.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { Workflow, RotateCcw } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { PhaseModelSelector } from './phase-model-selector';
|
||||||
|
import type { PhaseModelKey } from '@automaker/types';
|
||||||
|
|
||||||
|
interface PhaseConfig {
|
||||||
|
key: PhaseModelKey;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QUICK_TASKS: PhaseConfig[] = [
|
||||||
|
{
|
||||||
|
key: 'enhancementModel',
|
||||||
|
label: 'Feature Enhancement',
|
||||||
|
description: 'Improves feature names and descriptions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'fileDescriptionModel',
|
||||||
|
label: 'File Descriptions',
|
||||||
|
description: 'Generates descriptions for context files',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'imageDescriptionModel',
|
||||||
|
label: 'Image Descriptions',
|
||||||
|
description: 'Analyzes and describes context images',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const VALIDATION_TASKS: PhaseConfig[] = [
|
||||||
|
{
|
||||||
|
key: 'validationModel',
|
||||||
|
label: 'GitHub Issue Validation',
|
||||||
|
description: 'Validates and improves GitHub issues',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const GENERATION_TASKS: PhaseConfig[] = [
|
||||||
|
{
|
||||||
|
key: 'specGenerationModel',
|
||||||
|
label: 'App Specification',
|
||||||
|
description: 'Generates full application specifications',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'featureGenerationModel',
|
||||||
|
label: 'Feature Generation',
|
||||||
|
description: 'Creates features from specifications',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'backlogPlanningModel',
|
||||||
|
label: 'Backlog Planning',
|
||||||
|
description: 'Reorganizes and prioritizes backlog',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'projectAnalysisModel',
|
||||||
|
label: 'Project Analysis',
|
||||||
|
description: 'Analyzes project structure for suggestions',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function PhaseGroup({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
phases,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
phases: PhaseConfig[];
|
||||||
|
}) {
|
||||||
|
const { phaseModels, setPhaseModel } = useAppStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-foreground">{title}</h3>
|
||||||
|
<p className="text-xs text-muted-foreground">{subtitle}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{phases.map((phase) => (
|
||||||
|
<PhaseModelSelector
|
||||||
|
key={phase.key}
|
||||||
|
label={phase.label}
|
||||||
|
description={phase.description}
|
||||||
|
value={phaseModels[phase.key]}
|
||||||
|
onChange={(model) => setPhaseModel(phase.key, model)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PhaseModelsSection() {
|
||||||
|
const { resetPhaseModels } = useAppStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-2xl overflow-hidden',
|
||||||
|
'border border-border/50',
|
||||||
|
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||||
|
'shadow-sm shadow-black/5'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||||
|
<Workflow className="w-5 h-5 text-brand-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground tracking-tight">Phase Models</h2>
|
||||||
|
<p className="text-sm text-muted-foreground/80">
|
||||||
|
Configure which AI model to use for each application task
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" onClick={resetPhaseModels} className="gap-2">
|
||||||
|
<RotateCcw className="w-3.5 h-3.5" />
|
||||||
|
Reset to Defaults
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6 space-y-8">
|
||||||
|
{/* Quick Tasks */}
|
||||||
|
<PhaseGroup
|
||||||
|
title="Quick Tasks"
|
||||||
|
subtitle="Fast models recommended for speed and cost savings"
|
||||||
|
phases={QUICK_TASKS}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Validation Tasks */}
|
||||||
|
<PhaseGroup
|
||||||
|
title="Validation Tasks"
|
||||||
|
subtitle="Smart models recommended for accuracy"
|
||||||
|
phases={VALIDATION_TASKS}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Generation Tasks */}
|
||||||
|
<PhaseGroup
|
||||||
|
title="Generation Tasks"
|
||||||
|
subtitle="Powerful models recommended for quality output"
|
||||||
|
phases={GENERATION_TASKS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,8 +8,10 @@ import type {
|
|||||||
PlanningMode,
|
PlanningMode,
|
||||||
AIProfile,
|
AIProfile,
|
||||||
CursorModelId,
|
CursorModelId,
|
||||||
|
PhaseModelConfig,
|
||||||
|
PhaseModelKey,
|
||||||
} from '@automaker/types';
|
} from '@automaker/types';
|
||||||
import { getAllCursorModelIds } from '@automaker/types';
|
import { getAllCursorModelIds, DEFAULT_PHASE_MODELS } from '@automaker/types';
|
||||||
|
|
||||||
// Re-export ThemeMode for convenience
|
// Re-export ThemeMode for convenience
|
||||||
export type { ThemeMode };
|
export type { ThemeMode };
|
||||||
@@ -480,6 +482,9 @@ export interface AppState {
|
|||||||
// Validation Model Settings
|
// Validation Model Settings
|
||||||
validationModel: AgentModel; // Model used for GitHub issue validation (default: opus)
|
validationModel: AgentModel; // Model used for GitHub issue validation (default: opus)
|
||||||
|
|
||||||
|
// Phase Model Settings - per-phase AI model configuration
|
||||||
|
phaseModels: PhaseModelConfig;
|
||||||
|
|
||||||
// Cursor CLI Settings (global)
|
// Cursor CLI Settings (global)
|
||||||
enabledCursorModels: CursorModelId[]; // Which Cursor models are available in feature modal
|
enabledCursorModels: CursorModelId[]; // Which Cursor models are available in feature modal
|
||||||
cursorDefaultModel: CursorModelId; // Default Cursor model selection
|
cursorDefaultModel: CursorModelId; // Default Cursor model selection
|
||||||
@@ -760,6 +765,11 @@ export interface AppActions {
|
|||||||
// Validation Model actions
|
// Validation Model actions
|
||||||
setValidationModel: (model: AgentModel) => void;
|
setValidationModel: (model: AgentModel) => void;
|
||||||
|
|
||||||
|
// Phase Model actions
|
||||||
|
setPhaseModel: (phase: PhaseModelKey, model: AgentModel | CursorModelId) => void;
|
||||||
|
setPhaseModels: (models: Partial<PhaseModelConfig>) => void;
|
||||||
|
resetPhaseModels: () => void;
|
||||||
|
|
||||||
// Cursor CLI Settings actions
|
// Cursor CLI Settings actions
|
||||||
setEnabledCursorModels: (models: CursorModelId[]) => void;
|
setEnabledCursorModels: (models: CursorModelId[]) => void;
|
||||||
setCursorDefaultModel: (model: CursorModelId) => void;
|
setCursorDefaultModel: (model: CursorModelId) => void;
|
||||||
@@ -904,32 +914,14 @@ const DEFAULT_AI_PROFILES: AIProfile[] = [
|
|||||||
},
|
},
|
||||||
// Cursor profiles
|
// Cursor profiles
|
||||||
{
|
{
|
||||||
id: 'profile-cursor-auto',
|
id: 'profile-cursor-refactoring',
|
||||||
name: 'Cursor Auto',
|
name: 'Cursor Refactoring',
|
||||||
description: 'Let Cursor choose the best model automatically.',
|
description: 'Cursor Composer 1 for refactoring tasks.',
|
||||||
provider: 'cursor',
|
provider: 'cursor',
|
||||||
cursorModel: 'auto',
|
cursorModel: 'composer-1',
|
||||||
isBuiltIn: true,
|
isBuiltIn: true,
|
||||||
icon: 'Sparkles',
|
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 = {
|
const initialState: AppState = {
|
||||||
@@ -968,6 +960,7 @@ const initialState: AppState = {
|
|||||||
muteDoneSound: false, // Default to sound enabled (not muted)
|
muteDoneSound: false, // Default to sound enabled (not muted)
|
||||||
enhancementModel: 'sonnet', // Default to sonnet for feature enhancement
|
enhancementModel: 'sonnet', // Default to sonnet for feature enhancement
|
||||||
validationModel: 'opus', // Default to opus for GitHub issue validation
|
validationModel: 'opus', // Default to opus for GitHub issue validation
|
||||||
|
phaseModels: DEFAULT_PHASE_MODELS, // Phase-specific model configuration
|
||||||
enabledCursorModels: getAllCursorModelIds(), // All Cursor models enabled by default
|
enabledCursorModels: getAllCursorModelIds(), // All Cursor models enabled by default
|
||||||
cursorDefaultModel: 'auto', // Default to auto selection
|
cursorDefaultModel: 'auto', // Default to auto selection
|
||||||
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
|
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
|
||||||
@@ -1596,6 +1589,23 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
// Validation Model actions
|
// Validation Model actions
|
||||||
setValidationModel: (model) => set({ validationModel: model }),
|
setValidationModel: (model) => set({ validationModel: model }),
|
||||||
|
|
||||||
|
// Phase Model actions
|
||||||
|
setPhaseModel: (phase, model) =>
|
||||||
|
set((state) => ({
|
||||||
|
phaseModels: {
|
||||||
|
...state.phaseModels,
|
||||||
|
[phase]: model,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
setPhaseModels: (models) =>
|
||||||
|
set((state) => ({
|
||||||
|
phaseModels: {
|
||||||
|
...state.phaseModels,
|
||||||
|
...models,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
resetPhaseModels: () => set({ phaseModels: DEFAULT_PHASE_MODELS }),
|
||||||
|
|
||||||
// Cursor CLI Settings actions
|
// Cursor CLI Settings actions
|
||||||
setEnabledCursorModels: (models) => set({ enabledCursorModels: models }),
|
setEnabledCursorModels: (models) => set({ enabledCursorModels: models }),
|
||||||
setCursorDefaultModel: (model) => set({ cursorDefaultModel: model }),
|
setCursorDefaultModel: (model) => set({ cursorDefaultModel: model }),
|
||||||
|
|||||||
Reference in New Issue
Block a user