mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +00:00
feat: add per-project default model override for new features (#640)
* feat: add per-project default model override for new features
- Add defaultFeatureModel to ProjectSettings type for project-level override
- Add defaultFeatureModel to Project interface for UI state
- Display Default Feature Model in Model Defaults section alongside phase models
- Include Default Feature Model in global Bulk Replace dialog
- Add Default Feature Model override section to Project Settings
- Add setProjectDefaultFeatureModel store action for project-level overrides
- Update clearAllProjectPhaseModelOverrides to also clear defaultFeatureModel
- Update add-feature-dialog to use project override when available
- Include Default Feature Model in Project Bulk Replace dialog
This allows projects with different complexity levels to use different
default models (e.g., Haiku for simple tasks, Opus for complex projects).
* fix: add server-side __CLEAR__ handler for defaultFeatureModel
- Add handler in settings-service.ts to properly delete defaultFeatureModel
when '__CLEAR__' marker is sent from the UI
- Fix bulk-replace-dialog.tsx to correctly return claude-opus when resetting
default feature model to Anthropic Direct (was incorrectly using
enhancementModel's settings which default to sonnet)
These fixes ensure:
1. Clearing project default model override properly removes the setting
instead of storing literal '__CLEAR__' string
2. Global bulk replace correctly resets default feature model to opus
* fix: include defaultFeatureModel in Reset to Defaults action
- Updated resetPhaseModels to also reset defaultFeatureModel to claude-opus
- Fixed initial state to use canonical 'claude-opus' instead of 'opus'
* refactor: use DEFAULT_GLOBAL_SETTINGS constant for defaultFeatureModel
Address PR review feedback:
- Replace hardcoded { model: 'claude-opus' } with DEFAULT_GLOBAL_SETTINGS.defaultFeatureModel
- Fix Prettier formatting for long destructuring lines
- Import DEFAULT_GLOBAL_SETTINGS from @automaker/types where needed
This improves maintainability by centralizing the default value.
This commit is contained in:
committed by
GitHub
parent
3ebd67f35f
commit
5ab53afd7f
@@ -827,6 +827,16 @@ export class SettingsService {
|
|||||||
delete updated.phaseModelOverrides;
|
delete updated.phaseModelOverrides;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle defaultFeatureModel special cases:
|
||||||
|
// - "__CLEAR__" marker means delete the key (use global setting)
|
||||||
|
// - object means project-specific override
|
||||||
|
if (
|
||||||
|
'defaultFeatureModel' in updates &&
|
||||||
|
(updates as Record<string, unknown>).defaultFeatureModel === '__CLEAR__'
|
||||||
|
) {
|
||||||
|
delete updated.defaultFeatureModel;
|
||||||
|
}
|
||||||
|
|
||||||
await writeSettingsJson(settingsPath, updated);
|
await writeSettingsJson(settingsPath, updated);
|
||||||
logger.info(`Project settings updated for ${projectPath}`);
|
logger.info(`Project settings updated for ${projectPath}`);
|
||||||
|
|
||||||
|
|||||||
@@ -195,8 +195,16 @@ export function AddFeatureDialog({
|
|||||||
const [childDependencies, setChildDependencies] = useState<string[]>([]);
|
const [childDependencies, setChildDependencies] = useState<string[]>([]);
|
||||||
|
|
||||||
// Get defaults from store
|
// Get defaults from store
|
||||||
const { defaultPlanningMode, defaultRequirePlanApproval, useWorktrees, defaultFeatureModel } =
|
const {
|
||||||
useAppStore();
|
defaultPlanningMode,
|
||||||
|
defaultRequirePlanApproval,
|
||||||
|
useWorktrees,
|
||||||
|
defaultFeatureModel,
|
||||||
|
currentProject,
|
||||||
|
} = useAppStore();
|
||||||
|
|
||||||
|
// Use project-level default feature model if set, otherwise fall back to global
|
||||||
|
const effectiveDefaultFeatureModel = currentProject?.defaultFeatureModel ?? defaultFeatureModel;
|
||||||
|
|
||||||
// Track previous open state to detect when dialog opens
|
// Track previous open state to detect when dialog opens
|
||||||
const wasOpenRef = useRef(false);
|
const wasOpenRef = useRef(false);
|
||||||
@@ -216,7 +224,7 @@ export function AddFeatureDialog({
|
|||||||
);
|
);
|
||||||
setPlanningMode(defaultPlanningMode);
|
setPlanningMode(defaultPlanningMode);
|
||||||
setRequirePlanApproval(defaultRequirePlanApproval);
|
setRequirePlanApproval(defaultRequirePlanApproval);
|
||||||
setModelEntry(defaultFeatureModel);
|
setModelEntry(effectiveDefaultFeatureModel);
|
||||||
|
|
||||||
// Initialize description history (empty for new feature)
|
// Initialize description history (empty for new feature)
|
||||||
setDescriptionHistory([]);
|
setDescriptionHistory([]);
|
||||||
@@ -241,7 +249,7 @@ export function AddFeatureDialog({
|
|||||||
defaultBranch,
|
defaultBranch,
|
||||||
defaultPlanningMode,
|
defaultPlanningMode,
|
||||||
defaultRequirePlanApproval,
|
defaultRequirePlanApproval,
|
||||||
defaultFeatureModel,
|
effectiveDefaultFeatureModel,
|
||||||
useWorktrees,
|
useWorktrees,
|
||||||
selectedNonMainWorktreeBranch,
|
selectedNonMainWorktreeBranch,
|
||||||
forceCurrentBranchMode,
|
forceCurrentBranchMode,
|
||||||
@@ -343,7 +351,7 @@ export function AddFeatureDialog({
|
|||||||
// When a non-main worktree is selected, use its branch name for custom mode
|
// When a non-main worktree is selected, use its branch name for custom mode
|
||||||
setBranchName(selectedNonMainWorktreeBranch || '');
|
setBranchName(selectedNonMainWorktreeBranch || '');
|
||||||
setPriority(2);
|
setPriority(2);
|
||||||
setModelEntry(defaultFeatureModel);
|
setModelEntry(effectiveDefaultFeatureModel);
|
||||||
setWorkMode(
|
setWorkMode(
|
||||||
getDefaultWorkMode(useWorktrees, selectedNonMainWorktreeBranch, forceCurrentBranchMode)
|
getDefaultWorkMode(useWorktrees, selectedNonMainWorktreeBranch, forceCurrentBranchMode)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import type {
|
|||||||
ClaudeCompatibleProvider,
|
ClaudeCompatibleProvider,
|
||||||
ClaudeModelAlias,
|
ClaudeModelAlias,
|
||||||
} from '@automaker/types';
|
} from '@automaker/types';
|
||||||
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
|
import { DEFAULT_PHASE_MODELS, DEFAULT_GLOBAL_SETTINGS } from '@automaker/types';
|
||||||
|
|
||||||
interface ProjectBulkReplaceDialogProps {
|
interface ProjectBulkReplaceDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -50,6 +50,10 @@ const PHASE_LABELS: Record<PhaseModelKey, string> = {
|
|||||||
|
|
||||||
const ALL_PHASES = Object.keys(PHASE_LABELS) as PhaseModelKey[];
|
const ALL_PHASES = Object.keys(PHASE_LABELS) as PhaseModelKey[];
|
||||||
|
|
||||||
|
// Special key for default feature model (not a phase but included in bulk replace)
|
||||||
|
const DEFAULT_FEATURE_MODEL_KEY = '__defaultFeatureModel__' as const;
|
||||||
|
type ExtendedPhaseKey = PhaseModelKey | typeof DEFAULT_FEATURE_MODEL_KEY;
|
||||||
|
|
||||||
// Claude model display names
|
// Claude model display names
|
||||||
const CLAUDE_MODEL_DISPLAY: Record<ClaudeModelAlias, string> = {
|
const CLAUDE_MODEL_DISPLAY: Record<ClaudeModelAlias, string> = {
|
||||||
haiku: 'Claude Haiku',
|
haiku: 'Claude Haiku',
|
||||||
@@ -62,11 +66,18 @@ export function ProjectBulkReplaceDialog({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
project,
|
project,
|
||||||
}: ProjectBulkReplaceDialogProps) {
|
}: ProjectBulkReplaceDialogProps) {
|
||||||
const { phaseModels, setProjectPhaseModelOverride, claudeCompatibleProviders } = useAppStore();
|
const {
|
||||||
|
phaseModels,
|
||||||
|
setProjectPhaseModelOverride,
|
||||||
|
claudeCompatibleProviders,
|
||||||
|
defaultFeatureModel,
|
||||||
|
setProjectDefaultFeatureModel,
|
||||||
|
} = useAppStore();
|
||||||
const [selectedProvider, setSelectedProvider] = useState<string>('anthropic');
|
const [selectedProvider, setSelectedProvider] = useState<string>('anthropic');
|
||||||
|
|
||||||
// Get project-level overrides
|
// Get project-level overrides
|
||||||
const projectOverrides = project.phaseModelOverrides || {};
|
const projectOverrides = project.phaseModelOverrides || {};
|
||||||
|
const projectDefaultFeatureModel = project.defaultFeatureModel;
|
||||||
|
|
||||||
// Get enabled providers
|
// Get enabled providers
|
||||||
const enabledProviders = useMemo(() => {
|
const enabledProviders = useMemo(() => {
|
||||||
@@ -122,11 +133,15 @@ export function ProjectBulkReplaceDialog({
|
|||||||
const findModelForClaudeAlias = (
|
const findModelForClaudeAlias = (
|
||||||
provider: ClaudeCompatibleProvider | null,
|
provider: ClaudeCompatibleProvider | null,
|
||||||
claudeAlias: ClaudeModelAlias,
|
claudeAlias: ClaudeModelAlias,
|
||||||
phase: PhaseModelKey
|
key: ExtendedPhaseKey
|
||||||
): PhaseModelEntry => {
|
): PhaseModelEntry => {
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
// Anthropic Direct - reset to default phase model (includes correct thinking levels)
|
// Anthropic Direct - reset to default phase model (includes correct thinking levels)
|
||||||
return DEFAULT_PHASE_MODELS[phase];
|
// For default feature model, use the default from global settings
|
||||||
|
if (key === DEFAULT_FEATURE_MODEL_KEY) {
|
||||||
|
return DEFAULT_GLOBAL_SETTINGS.defaultFeatureModel;
|
||||||
|
}
|
||||||
|
return DEFAULT_PHASE_MODELS[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find model that maps to this Claude alias
|
// Find model that maps to this Claude alias
|
||||||
@@ -146,60 +161,91 @@ export function ProjectBulkReplaceDialog({
|
|||||||
return { model: claudeAlias };
|
return { model: claudeAlias };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper to generate preview item for any entry
|
||||||
|
const generatePreviewItem = (
|
||||||
|
key: ExtendedPhaseKey,
|
||||||
|
label: string,
|
||||||
|
currentEntry: PhaseModelEntry
|
||||||
|
) => {
|
||||||
|
const claudeAlias = getClaudeModelAlias(currentEntry);
|
||||||
|
const newEntry = findModelForClaudeAlias(selectedProviderConfig, claudeAlias, key);
|
||||||
|
|
||||||
|
// Get display names
|
||||||
|
const getCurrentDisplay = (): string => {
|
||||||
|
if (currentEntry.providerId) {
|
||||||
|
const provider = enabledProviders.find((p) => p.id === currentEntry.providerId);
|
||||||
|
if (provider) {
|
||||||
|
const model = provider.models?.find((m) => m.id === currentEntry.model);
|
||||||
|
return model?.displayName || currentEntry.model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CLAUDE_MODEL_DISPLAY[claudeAlias] || currentEntry.model;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNewDisplay = (): string => {
|
||||||
|
if (newEntry.providerId && selectedProviderConfig) {
|
||||||
|
const model = selectedProviderConfig.models?.find((m) => m.id === newEntry.model);
|
||||||
|
return model?.displayName || newEntry.model;
|
||||||
|
}
|
||||||
|
return CLAUDE_MODEL_DISPLAY[newEntry.model as ClaudeModelAlias] || newEntry.model;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isChanged =
|
||||||
|
currentEntry.model !== newEntry.model ||
|
||||||
|
currentEntry.providerId !== newEntry.providerId ||
|
||||||
|
currentEntry.thinkingLevel !== newEntry.thinkingLevel;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
claudeAlias,
|
||||||
|
currentDisplay: getCurrentDisplay(),
|
||||||
|
newDisplay: getNewDisplay(),
|
||||||
|
newEntry,
|
||||||
|
isChanged,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Generate preview of changes
|
// Generate preview of changes
|
||||||
const preview = useMemo(() => {
|
const preview = useMemo(() => {
|
||||||
return ALL_PHASES.map((phase) => {
|
// Default feature model entry (first in the list)
|
||||||
// Current effective value (project override or global)
|
const globalDefaultFeature = defaultFeatureModel ?? DEFAULT_GLOBAL_SETTINGS.defaultFeatureModel;
|
||||||
|
const currentDefaultFeature = projectDefaultFeatureModel || globalDefaultFeature;
|
||||||
|
const defaultFeaturePreview = generatePreviewItem(
|
||||||
|
DEFAULT_FEATURE_MODEL_KEY,
|
||||||
|
'Default Feature Model',
|
||||||
|
currentDefaultFeature
|
||||||
|
);
|
||||||
|
|
||||||
|
// Phase model entries
|
||||||
|
const phasePreview = ALL_PHASES.map((phase) => {
|
||||||
const globalEntry = phaseModels[phase] ?? DEFAULT_PHASE_MODELS[phase];
|
const globalEntry = phaseModels[phase] ?? DEFAULT_PHASE_MODELS[phase];
|
||||||
const currentEntry = projectOverrides[phase] || globalEntry;
|
const currentEntry = projectOverrides[phase] || globalEntry;
|
||||||
const claudeAlias = getClaudeModelAlias(currentEntry);
|
return generatePreviewItem(phase, PHASE_LABELS[phase], currentEntry);
|
||||||
const newEntry = findModelForClaudeAlias(selectedProviderConfig, claudeAlias, phase);
|
|
||||||
|
|
||||||
// Get display names
|
|
||||||
const getCurrentDisplay = (): string => {
|
|
||||||
if (currentEntry.providerId) {
|
|
||||||
const provider = enabledProviders.find((p) => p.id === currentEntry.providerId);
|
|
||||||
if (provider) {
|
|
||||||
const model = provider.models?.find((m) => m.id === currentEntry.model);
|
|
||||||
return model?.displayName || currentEntry.model;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return CLAUDE_MODEL_DISPLAY[claudeAlias] || currentEntry.model;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNewDisplay = (): string => {
|
|
||||||
if (newEntry.providerId && selectedProviderConfig) {
|
|
||||||
const model = selectedProviderConfig.models?.find((m) => m.id === newEntry.model);
|
|
||||||
return model?.displayName || newEntry.model;
|
|
||||||
}
|
|
||||||
return CLAUDE_MODEL_DISPLAY[newEntry.model as ClaudeModelAlias] || newEntry.model;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isChanged =
|
|
||||||
currentEntry.model !== newEntry.model ||
|
|
||||||
currentEntry.providerId !== newEntry.providerId ||
|
|
||||||
currentEntry.thinkingLevel !== newEntry.thinkingLevel;
|
|
||||||
|
|
||||||
return {
|
|
||||||
phase,
|
|
||||||
label: PHASE_LABELS[phase],
|
|
||||||
claudeAlias,
|
|
||||||
currentDisplay: getCurrentDisplay(),
|
|
||||||
newDisplay: getNewDisplay(),
|
|
||||||
newEntry,
|
|
||||||
isChanged,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}, [phaseModels, projectOverrides, selectedProviderConfig, enabledProviders]);
|
|
||||||
|
return [defaultFeaturePreview, ...phasePreview];
|
||||||
|
}, [
|
||||||
|
phaseModels,
|
||||||
|
projectOverrides,
|
||||||
|
selectedProviderConfig,
|
||||||
|
enabledProviders,
|
||||||
|
defaultFeatureModel,
|
||||||
|
projectDefaultFeatureModel,
|
||||||
|
]);
|
||||||
|
|
||||||
// Count how many will change
|
// Count how many will change
|
||||||
const changeCount = preview.filter((p) => p.isChanged).length;
|
const changeCount = preview.filter((p) => p.isChanged).length;
|
||||||
|
|
||||||
// Apply the bulk replace as project overrides
|
// Apply the bulk replace as project overrides
|
||||||
const handleApply = () => {
|
const handleApply = () => {
|
||||||
preview.forEach(({ phase, newEntry, isChanged }) => {
|
preview.forEach(({ key, newEntry, isChanged }) => {
|
||||||
if (isChanged) {
|
if (isChanged) {
|
||||||
setProjectPhaseModelOverride(project.id, phase, newEntry);
|
if (key === DEFAULT_FEATURE_MODEL_KEY) {
|
||||||
|
setProjectDefaultFeatureModel(project.id, newEntry);
|
||||||
|
} else {
|
||||||
|
setProjectPhaseModelOverride(project.id, key as PhaseModelKey, newEntry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
@@ -295,7 +341,7 @@ export function ProjectBulkReplaceDialog({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm font-medium">Preview Changes</label>
|
<label className="text-sm font-medium">Preview Changes</label>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{changeCount} of {ALL_PHASES.length} will be overridden
|
{changeCount} of {preview.length} will be overridden
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="border rounded-lg overflow-hidden max-h-[300px] overflow-y-auto">
|
<div className="border rounded-lg overflow-hidden max-h-[300px] overflow-y-auto">
|
||||||
@@ -311,15 +357,23 @@ export function ProjectBulkReplaceDialog({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{preview.map(({ phase, label, currentDisplay, newDisplay, isChanged }) => (
|
{preview.map(({ key, label, currentDisplay, newDisplay, isChanged }) => (
|
||||||
<tr
|
<tr
|
||||||
key={phase}
|
key={key}
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-t border-border/50',
|
'border-t border-border/50',
|
||||||
isChanged ? 'bg-brand-500/5' : 'opacity-50'
|
isChanged ? 'bg-brand-500/5' : 'opacity-50',
|
||||||
|
key === DEFAULT_FEATURE_MODEL_KEY && 'bg-accent/30'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<td className="p-2 font-medium">{label}</td>
|
<td className="p-2 font-medium">
|
||||||
|
{label}
|
||||||
|
{key === DEFAULT_FEATURE_MODEL_KEY && (
|
||||||
|
<span className="ml-2 text-[10px] px-1.5 py-0.5 rounded bg-brand-500/20 text-brand-500">
|
||||||
|
Feature Default
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="p-2 text-muted-foreground">{currentDisplay}</td>
|
<td className="p-2 text-muted-foreground">{currentDisplay}</td>
|
||||||
<td className="p-2 text-center">
|
<td className="p-2 text-center">
|
||||||
{isChanged ? (
|
{isChanged ? (
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Workflow, RotateCcw, Globe, Check, Replace } from 'lucide-react';
|
import { Workflow, RotateCcw, Globe, Check, Replace, Sparkles } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { Project } from '@/lib/electron';
|
import type { Project } from '@/lib/electron';
|
||||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
||||||
import { ProjectBulkReplaceDialog } from './project-bulk-replace-dialog';
|
import { ProjectBulkReplaceDialog } from './project-bulk-replace-dialog';
|
||||||
import type { PhaseModelKey, PhaseModelEntry } from '@automaker/types';
|
import type { PhaseModelKey, PhaseModelEntry } from '@automaker/types';
|
||||||
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
|
import { DEFAULT_PHASE_MODELS, DEFAULT_GLOBAL_SETTINGS } from '@automaker/types';
|
||||||
|
|
||||||
interface ProjectModelsSectionProps {
|
interface ProjectModelsSectionProps {
|
||||||
project: Project;
|
project: Project;
|
||||||
@@ -88,6 +88,127 @@ const MEMORY_TASKS: PhaseConfig[] = [
|
|||||||
|
|
||||||
const ALL_PHASES = [...QUICK_TASKS, ...VALIDATION_TASKS, ...GENERATION_TASKS, ...MEMORY_TASKS];
|
const ALL_PHASES = [...QUICK_TASKS, ...VALIDATION_TASKS, ...GENERATION_TASKS, ...MEMORY_TASKS];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default feature model override section for per-project settings.
|
||||||
|
*/
|
||||||
|
function FeatureDefaultModelOverrideSection({ project }: { project: Project }) {
|
||||||
|
const {
|
||||||
|
defaultFeatureModel: globalDefaultFeatureModel,
|
||||||
|
setProjectDefaultFeatureModel,
|
||||||
|
claudeCompatibleProviders,
|
||||||
|
} = useAppStore();
|
||||||
|
|
||||||
|
const globalValue: PhaseModelEntry =
|
||||||
|
globalDefaultFeatureModel ?? DEFAULT_GLOBAL_SETTINGS.defaultFeatureModel;
|
||||||
|
const projectOverride = project.defaultFeatureModel;
|
||||||
|
const hasOverride = !!projectOverride;
|
||||||
|
const effectiveValue = projectOverride || globalValue;
|
||||||
|
|
||||||
|
// Get display name for a model
|
||||||
|
const getModelDisplayName = (entry: PhaseModelEntry): string => {
|
||||||
|
if (entry.providerId) {
|
||||||
|
const provider = (claudeCompatibleProviders || []).find((p) => p.id === entry.providerId);
|
||||||
|
if (provider) {
|
||||||
|
const model = provider.models?.find((m) => m.id === entry.model);
|
||||||
|
if (model) {
|
||||||
|
return `${model.displayName} (${provider.name})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default to model ID for built-in models (both short aliases and canonical IDs)
|
||||||
|
const modelMap: Record<string, string> = {
|
||||||
|
haiku: 'Claude Haiku',
|
||||||
|
sonnet: 'Claude Sonnet',
|
||||||
|
opus: 'Claude Opus',
|
||||||
|
'claude-haiku': 'Claude Haiku',
|
||||||
|
'claude-sonnet': 'Claude Sonnet',
|
||||||
|
'claude-opus': 'Claude Opus',
|
||||||
|
};
|
||||||
|
return modelMap[entry.model] || entry.model;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearOverride = () => {
|
||||||
|
setProjectDefaultFeatureModel(project.id, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetOverride = (entry: PhaseModelEntry) => {
|
||||||
|
setProjectDefaultFeatureModel(project.id, entry);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-foreground">Feature Defaults</h3>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Default model for new feature cards in this project
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-between p-4 rounded-xl',
|
||||||
|
'bg-accent/20 border',
|
||||||
|
hasOverride ? 'border-brand-500/30 bg-brand-500/5' : 'border-border/30',
|
||||||
|
'hover:bg-accent/30 transition-colors'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex-1 pr-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-brand-500/10 flex items-center justify-center">
|
||||||
|
<Sparkles className="w-4 h-4 text-brand-500" />
|
||||||
|
</div>
|
||||||
|
<h4 className="text-sm font-medium text-foreground">Default Feature Model</h4>
|
||||||
|
{hasOverride ? (
|
||||||
|
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-brand-500/20 text-brand-500">
|
||||||
|
Override
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-muted text-muted-foreground">
|
||||||
|
<Globe className="w-3 h-3" />
|
||||||
|
Global
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1 ml-10">
|
||||||
|
Model and thinking level used when creating new feature cards
|
||||||
|
</p>
|
||||||
|
{hasOverride && (
|
||||||
|
<p className="text-xs text-brand-500 mt-1 ml-10">
|
||||||
|
Using: {getModelDisplayName(effectiveValue)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{!hasOverride && (
|
||||||
|
<p className="text-xs text-muted-foreground/70 mt-1 ml-10">
|
||||||
|
Using global: {getModelDisplayName(globalValue)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{hasOverride && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleClearOverride}
|
||||||
|
className="h-8 px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-3.5 h-3.5 mr-1" />
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<PhaseModelSelector
|
||||||
|
compact
|
||||||
|
value={effectiveValue}
|
||||||
|
onChange={handleSetOverride}
|
||||||
|
align="end"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function PhaseOverrideItem({
|
function PhaseOverrideItem({
|
||||||
phase,
|
phase,
|
||||||
project,
|
project,
|
||||||
@@ -234,8 +355,10 @@ export function ProjectModelsSection({ project }: ProjectModelsSectionProps) {
|
|||||||
useAppStore();
|
useAppStore();
|
||||||
const [showBulkReplace, setShowBulkReplace] = useState(false);
|
const [showBulkReplace, setShowBulkReplace] = useState(false);
|
||||||
|
|
||||||
// Count how many overrides are set
|
// Count how many overrides are set (including defaultFeatureModel)
|
||||||
const overrideCount = Object.keys(project.phaseModelOverrides || {}).length;
|
const phaseOverrideCount = Object.keys(project.phaseModelOverrides || {}).length;
|
||||||
|
const hasDefaultFeatureModelOverride = !!project.defaultFeatureModel;
|
||||||
|
const overrideCount = phaseOverrideCount + (hasDefaultFeatureModelOverride ? 1 : 0);
|
||||||
|
|
||||||
// Check if Claude is available
|
// Check if Claude is available
|
||||||
const isClaudeDisabled = disabledProviders.includes('claude');
|
const isClaudeDisabled = disabledProviders.includes('claude');
|
||||||
@@ -328,6 +451,9 @@ export function ProjectModelsSection({ project }: ProjectModelsSectionProps) {
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-6 space-y-8">
|
<div className="p-6 space-y-8">
|
||||||
|
{/* Feature Defaults */}
|
||||||
|
<FeatureDefaultModelOverrideSection project={project} />
|
||||||
|
|
||||||
{/* Quick Tasks */}
|
{/* Quick Tasks */}
|
||||||
<PhaseGroup
|
<PhaseGroup
|
||||||
title="Quick Tasks"
|
title="Quick Tasks"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import type {
|
|||||||
ClaudeCompatibleProvider,
|
ClaudeCompatibleProvider,
|
||||||
ClaudeModelAlias,
|
ClaudeModelAlias,
|
||||||
} from '@automaker/types';
|
} from '@automaker/types';
|
||||||
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
|
import { DEFAULT_PHASE_MODELS, DEFAULT_GLOBAL_SETTINGS } from '@automaker/types';
|
||||||
|
|
||||||
interface BulkReplaceDialogProps {
|
interface BulkReplaceDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -48,6 +48,10 @@ const PHASE_LABELS: Record<PhaseModelKey, string> = {
|
|||||||
|
|
||||||
const ALL_PHASES = Object.keys(PHASE_LABELS) as PhaseModelKey[];
|
const ALL_PHASES = Object.keys(PHASE_LABELS) as PhaseModelKey[];
|
||||||
|
|
||||||
|
// Special key for default feature model (not a phase but included in bulk replace)
|
||||||
|
const DEFAULT_FEATURE_MODEL_KEY = '__defaultFeatureModel__' as const;
|
||||||
|
type ExtendedPhaseKey = PhaseModelKey | typeof DEFAULT_FEATURE_MODEL_KEY;
|
||||||
|
|
||||||
// Claude model display names
|
// Claude model display names
|
||||||
const CLAUDE_MODEL_DISPLAY: Record<ClaudeModelAlias, string> = {
|
const CLAUDE_MODEL_DISPLAY: Record<ClaudeModelAlias, string> = {
|
||||||
haiku: 'Claude Haiku',
|
haiku: 'Claude Haiku',
|
||||||
@@ -56,7 +60,13 @@ const CLAUDE_MODEL_DISPLAY: Record<ClaudeModelAlias, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function BulkReplaceDialog({ open, onOpenChange }: BulkReplaceDialogProps) {
|
export function BulkReplaceDialog({ open, onOpenChange }: BulkReplaceDialogProps) {
|
||||||
const { phaseModels, setPhaseModel, claudeCompatibleProviders } = useAppStore();
|
const {
|
||||||
|
phaseModels,
|
||||||
|
setPhaseModel,
|
||||||
|
claudeCompatibleProviders,
|
||||||
|
defaultFeatureModel,
|
||||||
|
setDefaultFeatureModel,
|
||||||
|
} = useAppStore();
|
||||||
const [selectedProvider, setSelectedProvider] = useState<string>('anthropic');
|
const [selectedProvider, setSelectedProvider] = useState<string>('anthropic');
|
||||||
|
|
||||||
// Get enabled providers
|
// Get enabled providers
|
||||||
@@ -113,11 +123,15 @@ export function BulkReplaceDialog({ open, onOpenChange }: BulkReplaceDialogProps
|
|||||||
const findModelForClaudeAlias = (
|
const findModelForClaudeAlias = (
|
||||||
provider: ClaudeCompatibleProvider | null,
|
provider: ClaudeCompatibleProvider | null,
|
||||||
claudeAlias: ClaudeModelAlias,
|
claudeAlias: ClaudeModelAlias,
|
||||||
phase: PhaseModelKey
|
key: ExtendedPhaseKey
|
||||||
): PhaseModelEntry => {
|
): PhaseModelEntry => {
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
// Anthropic Direct - reset to default phase model (includes correct thinking levels)
|
// Anthropic Direct - reset to default phase model (includes correct thinking levels)
|
||||||
return DEFAULT_PHASE_MODELS[phase];
|
// For default feature model, use the default from global settings
|
||||||
|
if (key === DEFAULT_FEATURE_MODEL_KEY) {
|
||||||
|
return DEFAULT_GLOBAL_SETTINGS.defaultFeatureModel;
|
||||||
|
}
|
||||||
|
return DEFAULT_PHASE_MODELS[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find model that maps to this Claude alias
|
// Find model that maps to this Claude alias
|
||||||
@@ -137,58 +151,83 @@ export function BulkReplaceDialog({ open, onOpenChange }: BulkReplaceDialogProps
|
|||||||
return { model: claudeAlias };
|
return { model: claudeAlias };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper to generate preview item for any entry
|
||||||
|
const generatePreviewItem = (
|
||||||
|
key: ExtendedPhaseKey,
|
||||||
|
label: string,
|
||||||
|
currentEntry: PhaseModelEntry
|
||||||
|
) => {
|
||||||
|
const claudeAlias = getClaudeModelAlias(currentEntry);
|
||||||
|
const newEntry = findModelForClaudeAlias(selectedProviderConfig, claudeAlias, key);
|
||||||
|
|
||||||
|
// Get display names
|
||||||
|
const getCurrentDisplay = (): string => {
|
||||||
|
if (currentEntry.providerId) {
|
||||||
|
const provider = enabledProviders.find((p) => p.id === currentEntry.providerId);
|
||||||
|
if (provider) {
|
||||||
|
const model = provider.models?.find((m) => m.id === currentEntry.model);
|
||||||
|
return model?.displayName || currentEntry.model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CLAUDE_MODEL_DISPLAY[claudeAlias] || currentEntry.model;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNewDisplay = (): string => {
|
||||||
|
if (newEntry.providerId && selectedProviderConfig) {
|
||||||
|
const model = selectedProviderConfig.models?.find((m) => m.id === newEntry.model);
|
||||||
|
return model?.displayName || newEntry.model;
|
||||||
|
}
|
||||||
|
return CLAUDE_MODEL_DISPLAY[newEntry.model as ClaudeModelAlias] || newEntry.model;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isChanged =
|
||||||
|
currentEntry.model !== newEntry.model ||
|
||||||
|
currentEntry.providerId !== newEntry.providerId ||
|
||||||
|
currentEntry.thinkingLevel !== newEntry.thinkingLevel;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
claudeAlias,
|
||||||
|
currentDisplay: getCurrentDisplay(),
|
||||||
|
newDisplay: getNewDisplay(),
|
||||||
|
newEntry,
|
||||||
|
isChanged,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Generate preview of changes
|
// Generate preview of changes
|
||||||
const preview = useMemo(() => {
|
const preview = useMemo(() => {
|
||||||
return ALL_PHASES.map((phase) => {
|
// Default feature model entry (first in the list)
|
||||||
|
const defaultFeatureModelEntry =
|
||||||
|
defaultFeatureModel ?? DEFAULT_GLOBAL_SETTINGS.defaultFeatureModel;
|
||||||
|
const defaultFeaturePreview = generatePreviewItem(
|
||||||
|
DEFAULT_FEATURE_MODEL_KEY,
|
||||||
|
'Default Feature Model',
|
||||||
|
defaultFeatureModelEntry
|
||||||
|
);
|
||||||
|
|
||||||
|
// Phase model entries
|
||||||
|
const phasePreview = ALL_PHASES.map((phase) => {
|
||||||
const currentEntry = phaseModels[phase] ?? DEFAULT_PHASE_MODELS[phase];
|
const currentEntry = phaseModels[phase] ?? DEFAULT_PHASE_MODELS[phase];
|
||||||
const claudeAlias = getClaudeModelAlias(currentEntry);
|
return generatePreviewItem(phase, PHASE_LABELS[phase], currentEntry);
|
||||||
const newEntry = findModelForClaudeAlias(selectedProviderConfig, claudeAlias, phase);
|
|
||||||
|
|
||||||
// Get display names
|
|
||||||
const getCurrentDisplay = (): string => {
|
|
||||||
if (currentEntry.providerId) {
|
|
||||||
const provider = enabledProviders.find((p) => p.id === currentEntry.providerId);
|
|
||||||
if (provider) {
|
|
||||||
const model = provider.models?.find((m) => m.id === currentEntry.model);
|
|
||||||
return model?.displayName || currentEntry.model;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return CLAUDE_MODEL_DISPLAY[claudeAlias] || currentEntry.model;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNewDisplay = (): string => {
|
|
||||||
if (newEntry.providerId && selectedProviderConfig) {
|
|
||||||
const model = selectedProviderConfig.models?.find((m) => m.id === newEntry.model);
|
|
||||||
return model?.displayName || newEntry.model;
|
|
||||||
}
|
|
||||||
return CLAUDE_MODEL_DISPLAY[newEntry.model as ClaudeModelAlias] || newEntry.model;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isChanged =
|
|
||||||
currentEntry.model !== newEntry.model ||
|
|
||||||
currentEntry.providerId !== newEntry.providerId ||
|
|
||||||
currentEntry.thinkingLevel !== newEntry.thinkingLevel;
|
|
||||||
|
|
||||||
return {
|
|
||||||
phase,
|
|
||||||
label: PHASE_LABELS[phase],
|
|
||||||
claudeAlias,
|
|
||||||
currentDisplay: getCurrentDisplay(),
|
|
||||||
newDisplay: getNewDisplay(),
|
|
||||||
newEntry,
|
|
||||||
isChanged,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}, [phaseModels, selectedProviderConfig, enabledProviders]);
|
|
||||||
|
return [defaultFeaturePreview, ...phasePreview];
|
||||||
|
}, [phaseModels, selectedProviderConfig, enabledProviders, defaultFeatureModel]);
|
||||||
|
|
||||||
// Count how many will change
|
// Count how many will change
|
||||||
const changeCount = preview.filter((p) => p.isChanged).length;
|
const changeCount = preview.filter((p) => p.isChanged).length;
|
||||||
|
|
||||||
// Apply the bulk replace
|
// Apply the bulk replace
|
||||||
const handleApply = () => {
|
const handleApply = () => {
|
||||||
preview.forEach(({ phase, newEntry, isChanged }) => {
|
preview.forEach(({ key, newEntry, isChanged }) => {
|
||||||
if (isChanged) {
|
if (isChanged) {
|
||||||
setPhaseModel(phase, newEntry);
|
if (key === DEFAULT_FEATURE_MODEL_KEY) {
|
||||||
|
setDefaultFeatureModel(newEntry);
|
||||||
|
} else {
|
||||||
|
setPhaseModel(key as PhaseModelKey, newEntry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
@@ -284,7 +323,7 @@ export function BulkReplaceDialog({ open, onOpenChange }: BulkReplaceDialogProps
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm font-medium">Preview Changes</label>
|
<label className="text-sm font-medium">Preview Changes</label>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{changeCount} of {ALL_PHASES.length} will change
|
{changeCount} of {preview.length} will change
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="border rounded-lg overflow-hidden max-h-[300px] overflow-y-auto">
|
<div className="border rounded-lg overflow-hidden max-h-[300px] overflow-y-auto">
|
||||||
@@ -298,15 +337,23 @@ export function BulkReplaceDialog({ open, onOpenChange }: BulkReplaceDialogProps
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{preview.map(({ phase, label, currentDisplay, newDisplay, isChanged }) => (
|
{preview.map(({ key, label, currentDisplay, newDisplay, isChanged }) => (
|
||||||
<tr
|
<tr
|
||||||
key={phase}
|
key={key}
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-t border-border/50',
|
'border-t border-border/50',
|
||||||
isChanged ? 'bg-brand-500/5' : 'opacity-50'
|
isChanged ? 'bg-brand-500/5' : 'opacity-50',
|
||||||
|
key === DEFAULT_FEATURE_MODEL_KEY && 'bg-accent/30'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<td className="p-2 font-medium">{label}</td>
|
<td className="p-2 font-medium">
|
||||||
|
{label}
|
||||||
|
{key === DEFAULT_FEATURE_MODEL_KEY && (
|
||||||
|
<span className="ml-2 text-[10px] px-1.5 py-0.5 rounded bg-brand-500/20 text-brand-500">
|
||||||
|
Feature Default
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="p-2 text-muted-foreground">{currentDisplay}</td>
|
<td className="p-2 text-muted-foreground">{currentDisplay}</td>
|
||||||
<td className="p-2 text-center">
|
<td className="p-2 text-center">
|
||||||
{isChanged ? (
|
{isChanged ? (
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Workflow, RotateCcw, Replace } from 'lucide-react';
|
import { Workflow, RotateCcw, Replace, Sparkles } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { PhaseModelSelector } from './phase-model-selector';
|
import { PhaseModelSelector } from './phase-model-selector';
|
||||||
import { BulkReplaceDialog } from './bulk-replace-dialog';
|
import { BulkReplaceDialog } from './bulk-replace-dialog';
|
||||||
import type { PhaseModelKey } from '@automaker/types';
|
import type { PhaseModelKey, PhaseModelEntry } from '@automaker/types';
|
||||||
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
|
import { DEFAULT_PHASE_MODELS, DEFAULT_GLOBAL_SETTINGS } from '@automaker/types';
|
||||||
|
|
||||||
interface PhaseConfig {
|
interface PhaseConfig {
|
||||||
key: PhaseModelKey;
|
key: PhaseModelKey;
|
||||||
@@ -113,6 +113,54 @@ function PhaseGroup({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default model for new feature cards section.
|
||||||
|
* This is separate from phase models but logically belongs with model configuration.
|
||||||
|
*/
|
||||||
|
function FeatureDefaultModelSection() {
|
||||||
|
const { defaultFeatureModel, setDefaultFeatureModel } = useAppStore();
|
||||||
|
const defaultValue: PhaseModelEntry =
|
||||||
|
defaultFeatureModel ?? DEFAULT_GLOBAL_SETTINGS.defaultFeatureModel;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-foreground">Feature Defaults</h3>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Default model for new feature cards when created
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-between p-4 rounded-xl',
|
||||||
|
'bg-accent/20 border border-border/30',
|
||||||
|
'hover:bg-accent/30 transition-colors'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 flex-1 pr-4">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-brand-500/10 flex items-center justify-center">
|
||||||
|
<Sparkles className="w-4 h-4 text-brand-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-foreground">Default Feature Model</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Model and thinking level used when creating new feature cards
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PhaseModelSelector
|
||||||
|
compact
|
||||||
|
value={defaultValue}
|
||||||
|
onChange={setDefaultFeatureModel}
|
||||||
|
align="end"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ModelDefaultsSection() {
|
export function ModelDefaultsSection() {
|
||||||
const { resetPhaseModels, claudeCompatibleProviders } = useAppStore();
|
const { resetPhaseModels, claudeCompatibleProviders } = useAppStore();
|
||||||
const [showBulkReplace, setShowBulkReplace] = useState(false);
|
const [showBulkReplace, setShowBulkReplace] = useState(false);
|
||||||
@@ -171,6 +219,9 @@ export function ModelDefaultsSection() {
|
|||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-6 space-y-8">
|
<div className="p-6 space-y-8">
|
||||||
|
{/* Feature Defaults */}
|
||||||
|
<FeatureDefaultModelSection />
|
||||||
|
|
||||||
{/* Quick Tasks */}
|
{/* Quick Tasks */}
|
||||||
<PhaseGroup
|
<PhaseGroup
|
||||||
title="Quick Tasks"
|
title="Quick Tasks"
|
||||||
|
|||||||
@@ -3412,6 +3412,11 @@ export interface Project {
|
|||||||
* If a phase is not present, the global setting is used.
|
* If a phase is not present, the global setting is used.
|
||||||
*/
|
*/
|
||||||
phaseModelOverrides?: Partial<import('@automaker/types').PhaseModelConfig>;
|
phaseModelOverrides?: Partial<import('@automaker/types').PhaseModelConfig>;
|
||||||
|
/**
|
||||||
|
* Override the default model for new feature cards in this project.
|
||||||
|
* If not specified, falls back to the global defaultFeatureModel setting.
|
||||||
|
*/
|
||||||
|
defaultFeatureModel?: import('@automaker/types').PhaseModelEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrashedProject extends Project {
|
export interface TrashedProject extends Project {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
DEFAULT_PHASE_MODELS,
|
DEFAULT_PHASE_MODELS,
|
||||||
DEFAULT_OPENCODE_MODEL,
|
DEFAULT_OPENCODE_MODEL,
|
||||||
DEFAULT_MAX_CONCURRENCY,
|
DEFAULT_MAX_CONCURRENCY,
|
||||||
|
DEFAULT_GLOBAL_SETTINGS,
|
||||||
} from '@automaker/types';
|
} from '@automaker/types';
|
||||||
|
|
||||||
const logger = createLogger('AppStore');
|
const logger = createLogger('AppStore');
|
||||||
@@ -1055,6 +1056,12 @@ export interface AppActions {
|
|||||||
) => void;
|
) => void;
|
||||||
clearAllProjectPhaseModelOverrides: (projectId: string) => void;
|
clearAllProjectPhaseModelOverrides: (projectId: string) => void;
|
||||||
|
|
||||||
|
// Project Default Feature Model Override
|
||||||
|
setProjectDefaultFeatureModel: (
|
||||||
|
projectId: string,
|
||||||
|
entry: import('@automaker/types').PhaseModelEntry | null // null = use global
|
||||||
|
) => void;
|
||||||
|
|
||||||
// Feature actions
|
// Feature actions
|
||||||
setFeatures: (features: Feature[]) => void;
|
setFeatures: (features: Feature[]) => void;
|
||||||
updateFeature: (id: string, updates: Partial<Feature>) => void;
|
updateFeature: (id: string, updates: Partial<Feature>) => void;
|
||||||
@@ -1527,7 +1534,7 @@ const initialState: AppState = {
|
|||||||
specCreatingForProject: null,
|
specCreatingForProject: null,
|
||||||
defaultPlanningMode: 'skip' as PlanningMode,
|
defaultPlanningMode: 'skip' as PlanningMode,
|
||||||
defaultRequirePlanApproval: false,
|
defaultRequirePlanApproval: false,
|
||||||
defaultFeatureModel: { model: 'opus' } as PhaseModelEntry,
|
defaultFeatureModel: DEFAULT_GLOBAL_SETTINGS.defaultFeatureModel,
|
||||||
pendingPlanApproval: null,
|
pendingPlanApproval: null,
|
||||||
claudeRefreshInterval: 60,
|
claudeRefreshInterval: 60,
|
||||||
claudeUsage: null,
|
claudeUsage: null,
|
||||||
@@ -2105,9 +2112,11 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear overrides from project
|
// Clear all model overrides from project (phaseModelOverrides + defaultFeatureModel)
|
||||||
const projects = get().projects.map((p) =>
|
const projects = get().projects.map((p) =>
|
||||||
p.id === projectId ? { ...p, phaseModelOverrides: undefined } : p
|
p.id === projectId
|
||||||
|
? { ...p, phaseModelOverrides: undefined, defaultFeatureModel: undefined }
|
||||||
|
: p
|
||||||
);
|
);
|
||||||
set({ projects });
|
set({ projects });
|
||||||
|
|
||||||
@@ -2118,6 +2127,49 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
currentProject: {
|
currentProject: {
|
||||||
...currentProject,
|
...currentProject,
|
||||||
phaseModelOverrides: undefined,
|
phaseModelOverrides: undefined,
|
||||||
|
defaultFeatureModel: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist to server (clear both)
|
||||||
|
const httpClient = getHttpApiClient();
|
||||||
|
httpClient.settings
|
||||||
|
.updateProject(project.path, {
|
||||||
|
phaseModelOverrides: '__CLEAR__',
|
||||||
|
defaultFeatureModel: '__CLEAR__',
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to clear model overrides:', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setProjectDefaultFeatureModel: (projectId, entry) => {
|
||||||
|
// Find the project to get its path for server sync
|
||||||
|
const project = get().projects.find((p) => p.id === projectId);
|
||||||
|
if (!project) {
|
||||||
|
console.error('Cannot set default feature model: project not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the project's defaultFeatureModel
|
||||||
|
const projects = get().projects.map((p) =>
|
||||||
|
p.id === projectId
|
||||||
|
? {
|
||||||
|
...p,
|
||||||
|
defaultFeatureModel: entry ?? undefined,
|
||||||
|
}
|
||||||
|
: p
|
||||||
|
);
|
||||||
|
set({ projects });
|
||||||
|
|
||||||
|
// Also update currentProject if it's the same project
|
||||||
|
const currentProject = get().currentProject;
|
||||||
|
if (currentProject?.id === projectId) {
|
||||||
|
set({
|
||||||
|
currentProject: {
|
||||||
|
...currentProject,
|
||||||
|
defaultFeatureModel: entry ?? undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2126,10 +2178,10 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
const httpClient = getHttpApiClient();
|
const httpClient = getHttpApiClient();
|
||||||
httpClient.settings
|
httpClient.settings
|
||||||
.updateProject(project.path, {
|
.updateProject(project.path, {
|
||||||
phaseModelOverrides: '__CLEAR__',
|
defaultFeatureModel: entry ?? '__CLEAR__',
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to clear phaseModelOverrides:', error);
|
console.error('Failed to persist defaultFeatureModel:', error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -2571,7 +2623,10 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
await syncSettingsToServer();
|
await syncSettingsToServer();
|
||||||
},
|
},
|
||||||
resetPhaseModels: async () => {
|
resetPhaseModels: async () => {
|
||||||
set({ phaseModels: DEFAULT_PHASE_MODELS });
|
set({
|
||||||
|
phaseModels: DEFAULT_PHASE_MODELS,
|
||||||
|
defaultFeatureModel: DEFAULT_GLOBAL_SETTINGS.defaultFeatureModel,
|
||||||
|
});
|
||||||
// Sync to server settings file
|
// Sync to server settings file
|
||||||
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
|
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
|
||||||
await syncSettingsToServer();
|
await syncSettingsToServer();
|
||||||
|
|||||||
@@ -1186,6 +1186,13 @@ export interface ProjectSettings {
|
|||||||
*/
|
*/
|
||||||
phaseModelOverrides?: Partial<PhaseModelConfig>;
|
phaseModelOverrides?: Partial<PhaseModelConfig>;
|
||||||
|
|
||||||
|
// Feature Defaults Override (per-project)
|
||||||
|
/**
|
||||||
|
* Override the default model for new feature cards in this project.
|
||||||
|
* If not specified, falls back to the global defaultFeatureModel setting.
|
||||||
|
*/
|
||||||
|
defaultFeatureModel?: PhaseModelEntry;
|
||||||
|
|
||||||
// Deprecated Claude API Profile Override
|
// Deprecated Claude API Profile Override
|
||||||
/**
|
/**
|
||||||
* @deprecated Use phaseModelOverrides instead.
|
* @deprecated Use phaseModelOverrides instead.
|
||||||
|
|||||||
Reference in New Issue
Block a user