feat: enhance settings view and feature defaults management

- Introduced default feature model settings in the settings view, allowing users to specify the default AI model for new feature cards.
- Updated navigation to include a direct link to model defaults in the settings menu.
- Enhanced the Add Feature dialog to utilize the default feature model from the app store.
- Implemented synchronization of the default feature model in settings migration and sync hooks.
- Improved UI components to reflect changes in default settings, ensuring a cohesive user experience.
This commit is contained in:
webdevcody
2026-01-13 09:30:15 -05:00
committed by DhanushSantosh
parent 8f1740c0f5
commit bb710ada1a
10 changed files with 124 additions and 17 deletions

View File

@@ -21,7 +21,8 @@ import {
FeatureTextFilePath as DescriptionTextFilePath, FeatureTextFilePath as DescriptionTextFilePath,
ImagePreviewMap, ImagePreviewMap,
} from '@/components/ui/description-image-dropzone'; } from '@/components/ui/description-image-dropzone';
import { Play, Cpu, FolderKanban } from 'lucide-react'; import { Play, Cpu, FolderKanban, Settings2 } from 'lucide-react';
import { useNavigate } from '@tanstack/react-router';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { modelSupportsThinking } from '@/lib/utils'; import { modelSupportsThinking } from '@/lib/utils';
@@ -33,7 +34,7 @@ import {
PlanningMode, PlanningMode,
Feature, Feature,
} from '@/store/app-store'; } from '@/store/app-store';
import type { ReasoningEffort, PhaseModelEntry } from '@automaker/types'; import type { ReasoningEffort, PhaseModelEntry, AgentModel } from '@automaker/types';
import { supportsReasoningEffort, isClaudeModel } from '@automaker/types'; import { supportsReasoningEffort, isClaudeModel } from '@automaker/types';
import { import {
TestingTabContent, TestingTabContent,
@@ -152,6 +153,7 @@ export function AddFeatureDialog({
forceCurrentBranchMode, forceCurrentBranchMode,
}: AddFeatureDialogProps) { }: AddFeatureDialogProps) {
const isSpawnMode = !!parentFeature; const isSpawnMode = !!parentFeature;
const navigate = useNavigate();
const [workMode, setWorkMode] = useState<WorkMode>('current'); const [workMode, setWorkMode] = useState<WorkMode>('current');
// Form state // Form state
@@ -187,7 +189,8 @@ export function AddFeatureDialog({
const [selectedAncestorIds, setSelectedAncestorIds] = useState<Set<string>>(new Set()); const [selectedAncestorIds, setSelectedAncestorIds] = useState<Set<string>>(new Set());
// Get defaults from store // Get defaults from store
const { defaultPlanningMode, defaultRequirePlanApproval, useWorktrees } = useAppStore(); const { defaultPlanningMode, defaultRequirePlanApproval, useWorktrees, defaultFeatureModel } =
useAppStore();
// 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);
@@ -207,7 +210,7 @@ export function AddFeatureDialog({
); );
setPlanningMode(defaultPlanningMode); setPlanningMode(defaultPlanningMode);
setRequirePlanApproval(defaultRequirePlanApproval); setRequirePlanApproval(defaultRequirePlanApproval);
setModelEntry({ model: 'opus' }); setModelEntry(defaultFeatureModel);
// Initialize description history (empty for new feature) // Initialize description history (empty for new feature)
setDescriptionHistory([]); setDescriptionHistory([]);
@@ -228,6 +231,7 @@ export function AddFeatureDialog({
defaultBranch, defaultBranch,
defaultPlanningMode, defaultPlanningMode,
defaultRequirePlanApproval, defaultRequirePlanApproval,
defaultFeatureModel,
useWorktrees, useWorktrees,
selectedNonMainWorktreeBranch, selectedNonMainWorktreeBranch,
forceCurrentBranchMode, forceCurrentBranchMode,
@@ -318,7 +322,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({ model: 'opus' }); setModelEntry(defaultFeatureModel);
setWorkMode( setWorkMode(
getDefaultWorkMode(useWorktrees, selectedNonMainWorktreeBranch, forceCurrentBranchMode) getDefaultWorkMode(useWorktrees, selectedNonMainWorktreeBranch, forceCurrentBranchMode)
); );
@@ -473,9 +477,31 @@ export function AddFeatureDialog({
{/* AI & Execution Section */} {/* AI & Execution Section */}
<div className={cardClass}> <div className={cardClass}>
<div className={sectionHeaderClass}> <div className="flex items-center justify-between">
<Cpu className="w-4 h-4 text-muted-foreground" /> <div className={sectionHeaderClass}>
<span>AI & Execution</span> <Cpu className="w-4 h-4 text-muted-foreground" />
<span>AI & Execution</span>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => {
onOpenChange(false);
navigate({ to: '/settings', search: { view: 'defaults' } });
}}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<Settings2 className="w-3.5 h-3.5" />
<span>Edit Defaults</span>
</button>
</TooltipTrigger>
<TooltipContent>
<p>Change default model and planning settings for new features</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">

View File

@@ -21,7 +21,8 @@ import {
FeatureTextFilePath as DescriptionTextFilePath, FeatureTextFilePath as DescriptionTextFilePath,
ImagePreviewMap, ImagePreviewMap,
} from '@/components/ui/description-image-dropzone'; } from '@/components/ui/description-image-dropzone';
import { GitBranch, Cpu, FolderKanban } from 'lucide-react'; import { GitBranch, Cpu, FolderKanban, Settings2 } from 'lucide-react';
import { useNavigate } from '@tanstack/react-router';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { cn, modelSupportsThinking } from '@/lib/utils'; import { cn, modelSupportsThinking } from '@/lib/utils';
import { Feature, ModelAlias, ThinkingLevel, useAppStore, PlanningMode } from '@/store/app-store'; import { Feature, ModelAlias, ThinkingLevel, useAppStore, PlanningMode } from '@/store/app-store';
@@ -86,6 +87,7 @@ export function EditFeatureDialog({
isMaximized, isMaximized,
allFeatures, allFeatures,
}: EditFeatureDialogProps) { }: EditFeatureDialogProps) {
const navigate = useNavigate();
const [editingFeature, setEditingFeature] = useState<Feature | null>(feature); const [editingFeature, setEditingFeature] = useState<Feature | null>(feature);
// Derive initial workMode from feature's branchName // Derive initial workMode from feature's branchName
const [workMode, setWorkMode] = useState<WorkMode>(() => { const [workMode, setWorkMode] = useState<WorkMode>(() => {
@@ -363,9 +365,31 @@ export function EditFeatureDialog({
{/* AI & Execution Section */} {/* AI & Execution Section */}
<div className={cardClass}> <div className={cardClass}>
<div className={sectionHeaderClass}> <div className="flex items-center justify-between">
<Cpu className="w-4 h-4 text-muted-foreground" /> <div className={sectionHeaderClass}>
<span>AI & Execution</span> <Cpu className="w-4 h-4 text-muted-foreground" />
<span>AI & Execution</span>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => {
onClose();
navigate({ to: '/settings', search: { view: 'defaults' } });
}}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<Settings2 className="w-3.5 h-3.5" />
<span>Edit Defaults</span>
</button>
</TooltipTrigger>
<TooltipContent>
<p>Change default model and planning settings for new features</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">

View File

@@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { useSearch } from '@tanstack/react-router';
import { useAppStore } from '@/store/app-store'; import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';
import { useSettingsView, type SettingsViewId } from './settings-view/hooks'; import { useSettingsView, type SettingsViewId } from './settings-view/hooks';
import { NAV_ITEMS } from './settings-view/config/navigation'; import { NAV_ITEMS } from './settings-view/config/navigation';
@@ -51,6 +51,8 @@ export function SettingsView() {
setDefaultPlanningMode, setDefaultPlanningMode,
defaultRequirePlanApproval, defaultRequirePlanApproval,
setDefaultRequirePlanApproval, setDefaultRequirePlanApproval,
defaultFeatureModel,
setDefaultFeatureModel,
autoLoadClaudeMd, autoLoadClaudeMd,
setAutoLoadClaudeMd, setAutoLoadClaudeMd,
promptCustomization, promptCustomization,
@@ -86,8 +88,11 @@ export function SettingsView() {
} }
}; };
// Get initial view from URL search params
const { view: initialView } = useSearch({ from: '/settings' });
// Use settings view navigation hook // Use settings view navigation hook
const { activeView, navigateTo } = useSettingsView(); const { activeView, navigateTo } = useSettingsView({ initialView });
// Handle navigation - if navigating to 'providers', default to 'claude-provider' // Handle navigation - if navigating to 'providers', default to 'claude-provider'
const handleNavigate = (viewId: SettingsViewId) => { const handleNavigate = (viewId: SettingsViewId) => {
@@ -152,11 +157,13 @@ export function SettingsView() {
skipVerificationInAutoMode={skipVerificationInAutoMode} skipVerificationInAutoMode={skipVerificationInAutoMode}
defaultPlanningMode={defaultPlanningMode} defaultPlanningMode={defaultPlanningMode}
defaultRequirePlanApproval={defaultRequirePlanApproval} defaultRequirePlanApproval={defaultRequirePlanApproval}
defaultFeatureModel={defaultFeatureModel}
onDefaultSkipTestsChange={setDefaultSkipTests} onDefaultSkipTestsChange={setDefaultSkipTests}
onEnableDependencyBlockingChange={setEnableDependencyBlocking} onEnableDependencyBlockingChange={setEnableDependencyBlocking}
onSkipVerificationInAutoModeChange={setSkipVerificationInAutoMode} onSkipVerificationInAutoModeChange={setSkipVerificationInAutoMode}
onDefaultPlanningModeChange={setDefaultPlanningMode} onDefaultPlanningModeChange={setDefaultPlanningMode}
onDefaultRequirePlanApprovalChange={setDefaultRequirePlanApproval} onDefaultRequirePlanApprovalChange={setDefaultRequirePlanApproval}
onDefaultFeatureModelChange={setDefaultFeatureModel}
/> />
); );
case 'worktrees': case 'worktrees':

View File

@@ -37,8 +37,8 @@ export const GLOBAL_NAV_GROUPS: NavigationGroup[] = [
{ {
label: 'Model & Prompts', label: 'Model & Prompts',
items: [ items: [
{ id: 'model-defaults', label: 'Model Defaults', icon: Workflow },
{ id: 'defaults', label: 'Feature Defaults', icon: FlaskConical }, { id: 'defaults', label: 'Feature Defaults', icon: FlaskConical },
{ id: 'model-defaults', label: 'Model Defaults', icon: Workflow },
{ id: 'worktrees', label: 'Worktrees', icon: GitBranch }, { id: 'worktrees', label: 'Worktrees', icon: GitBranch },
{ id: 'prompts', label: 'Prompt Customization', icon: MessageSquareText }, { id: 'prompts', label: 'Prompt Customization', icon: MessageSquareText },
{ id: 'api-keys', label: 'API Keys', icon: Key }, { id: 'api-keys', label: 'API Keys', icon: Key },

View File

@@ -10,6 +10,7 @@ import {
ScrollText, ScrollText,
ShieldCheck, ShieldCheck,
FastForward, FastForward,
Cpu,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import {
@@ -19,6 +20,8 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import type { PhaseModelEntry } from '@automaker/types';
import { PhaseModelSelector } from '../model-defaults/phase-model-selector';
type PlanningMode = 'skip' | 'lite' | 'spec' | 'full'; type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
@@ -28,11 +31,13 @@ interface FeatureDefaultsSectionProps {
skipVerificationInAutoMode: boolean; skipVerificationInAutoMode: boolean;
defaultPlanningMode: PlanningMode; defaultPlanningMode: PlanningMode;
defaultRequirePlanApproval: boolean; defaultRequirePlanApproval: boolean;
defaultFeatureModel: PhaseModelEntry;
onDefaultSkipTestsChange: (value: boolean) => void; onDefaultSkipTestsChange: (value: boolean) => void;
onEnableDependencyBlockingChange: (value: boolean) => void; onEnableDependencyBlockingChange: (value: boolean) => void;
onSkipVerificationInAutoModeChange: (value: boolean) => void; onSkipVerificationInAutoModeChange: (value: boolean) => void;
onDefaultPlanningModeChange: (value: PlanningMode) => void; onDefaultPlanningModeChange: (value: PlanningMode) => void;
onDefaultRequirePlanApprovalChange: (value: boolean) => void; onDefaultRequirePlanApprovalChange: (value: boolean) => void;
onDefaultFeatureModelChange: (value: PhaseModelEntry) => void;
} }
export function FeatureDefaultsSection({ export function FeatureDefaultsSection({
@@ -41,11 +46,13 @@ export function FeatureDefaultsSection({
skipVerificationInAutoMode, skipVerificationInAutoMode,
defaultPlanningMode, defaultPlanningMode,
defaultRequirePlanApproval, defaultRequirePlanApproval,
defaultFeatureModel,
onDefaultSkipTestsChange, onDefaultSkipTestsChange,
onEnableDependencyBlockingChange, onEnableDependencyBlockingChange,
onSkipVerificationInAutoModeChange, onSkipVerificationInAutoModeChange,
onDefaultPlanningModeChange, onDefaultPlanningModeChange,
onDefaultRequirePlanApprovalChange, onDefaultRequirePlanApprovalChange,
onDefaultFeatureModelChange,
}: FeatureDefaultsSectionProps) { }: FeatureDefaultsSectionProps) {
return ( return (
<div <div
@@ -68,6 +75,30 @@ export function FeatureDefaultsSection({
</p> </p>
</div> </div>
<div className="p-6 space-y-5"> <div className="p-6 space-y-5">
{/* Default Feature Model Setting */}
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<div className="w-10 h-10 mt-0.5 rounded-xl flex items-center justify-center shrink-0 bg-brand-500/10">
<Cpu className="w-5 h-5 text-brand-500" />
</div>
<div className="flex-1 space-y-2">
<div className="flex items-center justify-between">
<Label className="text-foreground font-medium">Default Model</Label>
<PhaseModelSelector
value={defaultFeatureModel}
onChange={onDefaultFeatureModelChange}
compact
align="end"
/>
</div>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
The default AI model and thinking level used when creating new feature cards.
</p>
</div>
</div>
{/* Separator */}
<div className="border-t border-border/30" />
{/* Planning Mode Default */} {/* Planning Mode Default */}
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3"> <div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<div <div
@@ -165,12 +196,11 @@ export function FeatureDefaultsSection({
</p> </p>
</div> </div>
</div> </div>
<div className="border-t border-border/30" />
</> </>
)} )}
{/* Separator */} {/* Separator */}
{defaultPlanningMode === 'skip' && <div className="border-t border-border/30" />} <div className="border-t border-border/30" />
{/* Automated Testing Setting */} {/* Automated Testing Setting */}
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3"> <div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">

View File

@@ -562,6 +562,7 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
useWorktrees: settings.useWorktrees ?? true, useWorktrees: settings.useWorktrees ?? true,
defaultPlanningMode: settings.defaultPlanningMode ?? 'skip', defaultPlanningMode: settings.defaultPlanningMode ?? 'skip',
defaultRequirePlanApproval: settings.defaultRequirePlanApproval ?? false, defaultRequirePlanApproval: settings.defaultRequirePlanApproval ?? false,
defaultFeatureModel: settings.defaultFeatureModel ?? { model: 'opus' },
muteDoneSound: settings.muteDoneSound ?? false, muteDoneSound: settings.muteDoneSound ?? false,
enhancementModel: settings.enhancementModel ?? 'sonnet', enhancementModel: settings.enhancementModel ?? 'sonnet',
validationModel: settings.validationModel ?? 'opus', validationModel: settings.validationModel ?? 'opus',

View File

@@ -42,6 +42,7 @@ const SETTINGS_FIELDS_TO_SYNC = [
'useWorktrees', 'useWorktrees',
'defaultPlanningMode', 'defaultPlanningMode',
'defaultRequirePlanApproval', 'defaultRequirePlanApproval',
'defaultFeatureModel',
'muteDoneSound', 'muteDoneSound',
'enhancementModel', 'enhancementModel',
'validationModel', 'validationModel',
@@ -466,6 +467,7 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
useWorktrees: serverSettings.useWorktrees, useWorktrees: serverSettings.useWorktrees,
defaultPlanningMode: serverSettings.defaultPlanningMode, defaultPlanningMode: serverSettings.defaultPlanningMode,
defaultRequirePlanApproval: serverSettings.defaultRequirePlanApproval, defaultRequirePlanApproval: serverSettings.defaultRequirePlanApproval,
defaultFeatureModel: serverSettings.defaultFeatureModel ?? { model: 'opus' },
muteDoneSound: serverSettings.muteDoneSound, muteDoneSound: serverSettings.muteDoneSound,
enhancementModel: serverSettings.enhancementModel, enhancementModel: serverSettings.enhancementModel,
validationModel: serverSettings.validationModel, validationModel: serverSettings.validationModel,

View File

@@ -1,6 +1,16 @@
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router';
import { SettingsView } from '@/components/views/settings-view'; import { SettingsView } from '@/components/views/settings-view';
import type { SettingsViewId } from '@/components/views/settings-view/hooks';
interface SettingsSearchParams {
view?: SettingsViewId;
}
export const Route = createFileRoute('/settings')({ export const Route = createFileRoute('/settings')({
component: SettingsView, component: SettingsView,
validateSearch: (search: Record<string, unknown>): SettingsSearchParams => {
return {
view: search.view as SettingsViewId | undefined,
};
},
}); });

View File

@@ -657,6 +657,7 @@ export interface AppState {
defaultPlanningMode: PlanningMode; defaultPlanningMode: PlanningMode;
defaultRequirePlanApproval: boolean; defaultRequirePlanApproval: boolean;
defaultFeatureModel: PhaseModelEntry;
// Plan Approval State // Plan Approval State
// When a plan requires user approval, this holds the pending approval details // When a plan requires user approval, this holds the pending approval details
@@ -1104,6 +1105,7 @@ export interface AppActions {
setDefaultPlanningMode: (mode: PlanningMode) => void; setDefaultPlanningMode: (mode: PlanningMode) => void;
setDefaultRequirePlanApproval: (require: boolean) => void; setDefaultRequirePlanApproval: (require: boolean) => void;
setDefaultFeatureModel: (entry: PhaseModelEntry) => void;
// Plan Approval actions // Plan Approval actions
setPendingPlanApproval: ( setPendingPlanApproval: (
@@ -1277,6 +1279,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,
pendingPlanApproval: null, pendingPlanApproval: null,
claudeRefreshInterval: 60, claudeRefreshInterval: 60,
claudeUsage: null, claudeUsage: null,
@@ -3093,6 +3096,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
setDefaultPlanningMode: (mode) => set({ defaultPlanningMode: mode }), setDefaultPlanningMode: (mode) => set({ defaultPlanningMode: mode }),
setDefaultRequirePlanApproval: (require) => set({ defaultRequirePlanApproval: require }), setDefaultRequirePlanApproval: (require) => set({ defaultRequirePlanApproval: require }),
setDefaultFeatureModel: (entry) => set({ defaultFeatureModel: entry }),
// Plan Approval actions // Plan Approval actions
setPendingPlanApproval: (approval) => set({ pendingPlanApproval: approval }), setPendingPlanApproval: (approval) => set({ pendingPlanApproval: approval }),

View File

@@ -375,6 +375,8 @@ export interface GlobalSettings {
defaultPlanningMode: PlanningMode; defaultPlanningMode: PlanningMode;
/** Default: require manual approval before generating */ /** Default: require manual approval before generating */
defaultRequirePlanApproval: boolean; defaultRequirePlanApproval: boolean;
/** Default model and thinking level for new feature cards */
defaultFeatureModel: PhaseModelEntry;
// Audio Preferences // Audio Preferences
/** Mute completion notification sound */ /** Mute completion notification sound */
@@ -698,6 +700,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
useWorktrees: true, useWorktrees: true,
defaultPlanningMode: 'skip', defaultPlanningMode: 'skip',
defaultRequirePlanApproval: false, defaultRequirePlanApproval: false,
defaultFeatureModel: { model: 'opus' },
muteDoneSound: false, muteDoneSound: false,
phaseModels: DEFAULT_PHASE_MODELS, phaseModels: DEFAULT_PHASE_MODELS,
enhancementModel: 'sonnet', enhancementModel: 'sonnet',