feat: add customizable phase configuration in app settings
Allow users to customize the model and thinking level for each phase (Spec Creation, Planning, Coding, QA Review) when using the Auto profile. These settings are persisted in app settings and used as defaults when creating new tasks. Changes: - Add customPhaseModels and customPhaseThinking to AppSettings type - Update AgentProfileSettings to show editable phase configuration when Auto profile is selected - Update TaskCreationWizard to initialize from custom settings - Phase config can still be overridden per-task in task creation wizard 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -94,7 +94,7 @@ export function registerSettingsHandlers(
|
|||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
IPC_CHANNELS.SETTINGS_GET,
|
IPC_CHANNELS.SETTINGS_GET,
|
||||||
async (): Promise<IPCResult<AppSettings>> => {
|
async (): Promise<IPCResult<AppSettings>> => {
|
||||||
let settings = { ...DEFAULT_APP_SETTINGS };
|
let settings: AppSettings = { ...DEFAULT_APP_SETTINGS };
|
||||||
let needsSave = false;
|
let needsSave = false;
|
||||||
|
|
||||||
if (existsSync(settingsPath)) {
|
if (existsSync(settingsPath)) {
|
||||||
@@ -106,12 +106,15 @@ export function registerSettingsHandlers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migration: Reset agent profile to 'auto' for existing users (one-time)
|
// Migration: Set agent profile to 'auto' for users who haven't made a selection (one-time)
|
||||||
// This ensures all users get the optimized 'auto' profile as the default
|
// This ensures new users get the optimized 'auto' profile as the default
|
||||||
const settingsAny = settings as Record<string, unknown>;
|
// while preserving existing user preferences
|
||||||
if (!settingsAny._migratedAgentProfileToAuto) {
|
if (!settings._migratedAgentProfileToAuto) {
|
||||||
settings.selectedAgentProfile = 'auto';
|
// Only set 'auto' if user hasn't made a selection yet
|
||||||
settingsAny._migratedAgentProfileToAuto = true;
|
if (!settings.selectedAgentProfile) {
|
||||||
|
settings.selectedAgentProfile = 'auto';
|
||||||
|
}
|
||||||
|
settings._migratedAgentProfileToAuto = true;
|
||||||
needsSave = true;
|
needsSave = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,8 +130,9 @@ export function registerSettingsHandlers(
|
|||||||
if (needsSave) {
|
if (needsSave) {
|
||||||
try {
|
try {
|
||||||
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
||||||
} catch {
|
} catch (error) {
|
||||||
// Ignore write errors during migration
|
console.error('[SETTINGS_GET] Failed to persist migration:', error);
|
||||||
|
// Continue anyway - settings will be migrated in-memory for this session
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,11 +102,12 @@ export function TaskCreationWizard({
|
|||||||
const [model, setModel] = useState<ModelType | ''>(selectedProfile.model);
|
const [model, setModel] = useState<ModelType | ''>(selectedProfile.model);
|
||||||
const [thinkingLevel, setThinkingLevel] = useState<ThinkingLevel | ''>(selectedProfile.thinkingLevel);
|
const [thinkingLevel, setThinkingLevel] = useState<ThinkingLevel | ''>(selectedProfile.thinkingLevel);
|
||||||
// Auto profile - per-phase configuration
|
// Auto profile - per-phase configuration
|
||||||
|
// Use custom settings from app settings if available, otherwise fall back to defaults
|
||||||
const [phaseModels, setPhaseModels] = useState<PhaseModelConfig | undefined>(
|
const [phaseModels, setPhaseModels] = useState<PhaseModelConfig | undefined>(
|
||||||
selectedProfile.phaseModels || DEFAULT_PHASE_MODELS
|
settings.customPhaseModels || selectedProfile.phaseModels || DEFAULT_PHASE_MODELS
|
||||||
);
|
);
|
||||||
const [phaseThinking, setPhaseThinking] = useState<PhaseThinkingConfig | undefined>(
|
const [phaseThinking, setPhaseThinking] = useState<PhaseThinkingConfig | undefined>(
|
||||||
selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING
|
settings.customPhaseThinking || selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING
|
||||||
);
|
);
|
||||||
|
|
||||||
// Image attachments
|
// Image attachments
|
||||||
@@ -143,8 +144,8 @@ export function TaskCreationWizard({
|
|||||||
setProfileId(draft.profileId || settings.selectedAgentProfile || 'auto');
|
setProfileId(draft.profileId || settings.selectedAgentProfile || 'auto');
|
||||||
setModel(draft.model || selectedProfile.model);
|
setModel(draft.model || selectedProfile.model);
|
||||||
setThinkingLevel(draft.thinkingLevel || selectedProfile.thinkingLevel);
|
setThinkingLevel(draft.thinkingLevel || selectedProfile.thinkingLevel);
|
||||||
setPhaseModels(draft.phaseModels || selectedProfile.phaseModels || DEFAULT_PHASE_MODELS);
|
setPhaseModels(draft.phaseModels || settings.customPhaseModels || selectedProfile.phaseModels || DEFAULT_PHASE_MODELS);
|
||||||
setPhaseThinking(draft.phaseThinking || selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING);
|
setPhaseThinking(draft.phaseThinking || settings.customPhaseThinking || selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING);
|
||||||
setImages(draft.images);
|
setImages(draft.images);
|
||||||
setReferencedFiles(draft.referencedFiles ?? []);
|
setReferencedFiles(draft.referencedFiles ?? []);
|
||||||
setRequireReviewBeforeCoding(draft.requireReviewBeforeCoding ?? false);
|
setRequireReviewBeforeCoding(draft.requireReviewBeforeCoding ?? false);
|
||||||
@@ -159,15 +160,15 @@ export function TaskCreationWizard({
|
|||||||
}
|
}
|
||||||
// Note: Referenced Files section is always visible, no need to expand
|
// Note: Referenced Files section is always visible, no need to expand
|
||||||
} else {
|
} else {
|
||||||
// No draft - initialize from selected profile
|
// No draft - initialize from selected profile and custom settings
|
||||||
setProfileId(settings.selectedAgentProfile || 'auto');
|
setProfileId(settings.selectedAgentProfile || 'auto');
|
||||||
setModel(selectedProfile.model);
|
setModel(selectedProfile.model);
|
||||||
setThinkingLevel(selectedProfile.thinkingLevel);
|
setThinkingLevel(selectedProfile.thinkingLevel);
|
||||||
setPhaseModels(selectedProfile.phaseModels || DEFAULT_PHASE_MODELS);
|
setPhaseModels(settings.customPhaseModels || selectedProfile.phaseModels || DEFAULT_PHASE_MODELS);
|
||||||
setPhaseThinking(selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING);
|
setPhaseThinking(settings.customPhaseThinking || selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [open, projectId, settings.selectedAgentProfile, selectedProfile.model, selectedProfile.thinkingLevel]);
|
}, [open, projectId, settings.selectedAgentProfile, settings.customPhaseModels, settings.customPhaseThinking, selectedProfile.model, selectedProfile.thinkingLevel]);
|
||||||
|
|
||||||
// Fetch branches and project default branch when dialog opens
|
// Fetch branches and project default branch when dialog opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -542,12 +543,12 @@ export function TaskCreationWizard({
|
|||||||
setPriority('');
|
setPriority('');
|
||||||
setComplexity('');
|
setComplexity('');
|
||||||
setImpact('');
|
setImpact('');
|
||||||
// Reset to selected profile defaults
|
// Reset to selected profile defaults and custom settings
|
||||||
setProfileId(settings.selectedAgentProfile || 'auto');
|
setProfileId(settings.selectedAgentProfile || 'auto');
|
||||||
setModel(selectedProfile.model);
|
setModel(selectedProfile.model);
|
||||||
setThinkingLevel(selectedProfile.thinkingLevel);
|
setThinkingLevel(selectedProfile.thinkingLevel);
|
||||||
setPhaseModels(selectedProfile.phaseModels || DEFAULT_PHASE_MODELS);
|
setPhaseModels(settings.customPhaseModels || selectedProfile.phaseModels || DEFAULT_PHASE_MODELS);
|
||||||
setPhaseThinking(selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING);
|
setPhaseThinking(settings.customPhaseThinking || selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING);
|
||||||
setImages([]);
|
setImages([]);
|
||||||
setReferencedFiles([]);
|
setReferencedFiles([]);
|
||||||
setRequireReviewBeforeCoding(false);
|
setRequireReviewBeforeCoding(false);
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
import { Brain, Scale, Zap, Check, Sparkles } from 'lucide-react';
|
import { useState } from 'react';
|
||||||
|
import { Brain, Scale, Zap, Check, Sparkles, ChevronDown, ChevronUp, RotateCcw } from 'lucide-react';
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
import { DEFAULT_AGENT_PROFILES, AVAILABLE_MODELS, THINKING_LEVELS } from '../../../shared/constants';
|
import {
|
||||||
|
DEFAULT_AGENT_PROFILES,
|
||||||
|
AVAILABLE_MODELS,
|
||||||
|
THINKING_LEVELS,
|
||||||
|
DEFAULT_PHASE_MODELS,
|
||||||
|
DEFAULT_PHASE_THINKING
|
||||||
|
} from '../../../shared/constants';
|
||||||
import { useSettingsStore, saveSettings } from '../../stores/settings-store';
|
import { useSettingsStore, saveSettings } from '../../stores/settings-store';
|
||||||
import { SettingsSection } from './SettingsSection';
|
import { SettingsSection } from './SettingsSection';
|
||||||
import type { AgentProfile } from '../../../shared/types/settings';
|
import { Label } from '../ui/label';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from '../ui/select';
|
||||||
|
import type { AgentProfile, PhaseModelConfig, PhaseThinkingConfig, ModelTypeShort, ThinkingLevel } from '../../../shared/types/settings';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Icon mapping for agent profile icons
|
* Icon mapping for agent profile icons
|
||||||
@@ -15,6 +31,13 @@ const iconMap: Record<string, React.ElementType> = {
|
|||||||
Sparkles
|
Sparkles
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PHASE_LABELS: Record<keyof PhaseModelConfig, { label: string; description: string }> = {
|
||||||
|
spec: { label: 'Spec Creation', description: 'Discovery, requirements, context gathering' },
|
||||||
|
planning: { label: 'Planning', description: 'Implementation planning and architecture' },
|
||||||
|
coding: { label: 'Coding', description: 'Actual code implementation' },
|
||||||
|
qa: { label: 'QA Review', description: 'Quality assurance and validation' }
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agent Profile Settings component
|
* Agent Profile Settings component
|
||||||
* Displays preset agent profiles for quick model/thinking level configuration
|
* Displays preset agent profiles for quick model/thinking level configuration
|
||||||
@@ -23,9 +46,40 @@ const iconMap: Record<string, React.ElementType> = {
|
|||||||
export function AgentProfileSettings() {
|
export function AgentProfileSettings() {
|
||||||
const settings = useSettingsStore((state) => state.settings);
|
const settings = useSettingsStore((state) => state.settings);
|
||||||
const selectedProfileId = settings.selectedAgentProfile || 'auto';
|
const selectedProfileId = settings.selectedAgentProfile || 'auto';
|
||||||
|
const [showPhaseConfig, setShowPhaseConfig] = useState(selectedProfileId === 'auto');
|
||||||
|
|
||||||
|
// Get current phase config from settings or defaults
|
||||||
|
const currentPhaseModels: PhaseModelConfig = settings.customPhaseModels || DEFAULT_PHASE_MODELS;
|
||||||
|
const currentPhaseThinking: PhaseThinkingConfig = settings.customPhaseThinking || DEFAULT_PHASE_THINKING;
|
||||||
|
|
||||||
const handleSelectProfile = async (profileId: string) => {
|
const handleSelectProfile = async (profileId: string) => {
|
||||||
await saveSettings({ selectedAgentProfile: profileId });
|
const success = await saveSettings({ selectedAgentProfile: profileId });
|
||||||
|
if (!success) {
|
||||||
|
// Log error for debugging - in future could show user toast notification
|
||||||
|
console.error('Failed to save agent profile selection');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Auto-expand phase config when Auto profile is selected
|
||||||
|
if (profileId === 'auto') {
|
||||||
|
setShowPhaseConfig(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhaseModelChange = async (phase: keyof PhaseModelConfig, value: ModelTypeShort) => {
|
||||||
|
const newPhaseModels = { ...currentPhaseModels, [phase]: value };
|
||||||
|
await saveSettings({ customPhaseModels: newPhaseModels });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhaseThinkingChange = async (phase: keyof PhaseThinkingConfig, value: ThinkingLevel) => {
|
||||||
|
const newPhaseThinking = { ...currentPhaseThinking, [phase]: value };
|
||||||
|
await saveSettings({ customPhaseThinking: newPhaseThinking });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetToDefaults = async () => {
|
||||||
|
await saveSettings({
|
||||||
|
customPhaseModels: DEFAULT_PHASE_MODELS,
|
||||||
|
customPhaseThinking: DEFAULT_PHASE_THINKING
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,6 +98,18 @@ export function AgentProfileSettings() {
|
|||||||
return level?.label || thinkingValue;
|
return level?.label || thinkingValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current config differs from defaults
|
||||||
|
*/
|
||||||
|
const hasCustomConfig = (): boolean => {
|
||||||
|
const phases: Array<keyof PhaseModelConfig> = ['spec', 'planning', 'coding', 'qa'];
|
||||||
|
return phases.some(
|
||||||
|
phase =>
|
||||||
|
currentPhaseModels[phase] !== DEFAULT_PHASE_MODELS[phase] ||
|
||||||
|
currentPhaseThinking[phase] !== DEFAULT_PHASE_THINKING[phase]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a single profile card
|
* Render a single profile card
|
||||||
*/
|
*/
|
||||||
@@ -126,6 +192,112 @@ export function AgentProfileSettings() {
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||||
{DEFAULT_AGENT_PROFILES.map(renderProfileCard)}
|
{DEFAULT_AGENT_PROFILES.map(renderProfileCard)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Phase Configuration (only for Auto profile) */}
|
||||||
|
{selectedProfileId === 'auto' && (
|
||||||
|
<div className="mt-6 rounded-lg border border-border bg-card">
|
||||||
|
{/* Header - Collapsible */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPhaseConfig(!showPhaseConfig)}
|
||||||
|
className="flex w-full items-center justify-between p-4 text-left hover:bg-muted/50 transition-colors rounded-t-lg"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-sm text-foreground">Phase Configuration</h4>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Customize model and thinking level for each phase
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{showPhaseConfig ? (
|
||||||
|
<ChevronUp className="h-4 w-4 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Phase Configuration Content */}
|
||||||
|
{showPhaseConfig && (
|
||||||
|
<div className="border-t border-border p-4 space-y-4">
|
||||||
|
{/* Reset button */}
|
||||||
|
{hasCustomConfig() && (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleResetToDefaults}
|
||||||
|
className="text-xs h-7"
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-3 w-3 mr-1.5" />
|
||||||
|
Reset to defaults
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Phase Configuration Grid */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{(Object.keys(PHASE_LABELS) as Array<keyof PhaseModelConfig>).map((phase) => (
|
||||||
|
<div key={phase} className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-sm font-medium text-foreground">
|
||||||
|
{PHASE_LABELS[phase].label}
|
||||||
|
</Label>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{PHASE_LABELS[phase].description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{/* Model Select */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-muted-foreground">Model</Label>
|
||||||
|
<Select
|
||||||
|
value={currentPhaseModels[phase]}
|
||||||
|
onValueChange={(value) => handlePhaseModelChange(phase, value as ModelTypeShort)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{AVAILABLE_MODELS.map((m) => (
|
||||||
|
<SelectItem key={m.value} value={m.value}>
|
||||||
|
{m.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{/* Thinking Level Select */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-muted-foreground">Thinking Level</Label>
|
||||||
|
<Select
|
||||||
|
value={currentPhaseThinking[phase]}
|
||||||
|
onValueChange={(value) => handlePhaseThinkingChange(phase, value as ThinkingLevel)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{THINKING_LEVELS.map((level) => (
|
||||||
|
<SelectItem key={level.value} value={level.value}>
|
||||||
|
{level.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info note */}
|
||||||
|
<p className="text-[10px] text-muted-foreground mt-4 pt-3 border-t border-border">
|
||||||
|
These settings will be used as defaults when creating new tasks with the Auto profile.
|
||||||
|
You can override them per-task in the task creation wizard.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -82,10 +82,15 @@ export interface AppSettings {
|
|||||||
onboardingCompleted?: boolean;
|
onboardingCompleted?: boolean;
|
||||||
// Selected agent profile for preset model/thinking configurations
|
// Selected agent profile for preset model/thinking configurations
|
||||||
selectedAgentProfile?: string;
|
selectedAgentProfile?: string;
|
||||||
|
// Custom phase configuration for Auto profile (overrides defaults)
|
||||||
|
customPhaseModels?: PhaseModelConfig;
|
||||||
|
customPhaseThinking?: PhaseThinkingConfig;
|
||||||
// Changelog preferences
|
// Changelog preferences
|
||||||
changelogFormat?: ChangelogFormat;
|
changelogFormat?: ChangelogFormat;
|
||||||
changelogAudience?: ChangelogAudience;
|
changelogAudience?: ChangelogAudience;
|
||||||
changelogEmojiLevel?: ChangelogEmojiLevel;
|
changelogEmojiLevel?: ChangelogEmojiLevel;
|
||||||
|
// Migration flags (internal use)
|
||||||
|
_migratedAgentProfileToAuto?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-Claude Source Environment Configuration (for auto-claude repo .env)
|
// Auto-Claude Source Environment Configuration (for auto-claude repo .env)
|
||||||
|
|||||||
Reference in New Issue
Block a user