fix(frontend): resolve agent profile before falling back to defaults (ACS-255) (#1068)
* fix(frontend): resolve agent profile before falling back to defaults Fixes ACS-255: MCP Server Overview was showing "Sonnet 4.5" instead of "Opus 4.5" when the "Auto (Optimized)" profile was selected. The bug occurred because AgentTools.tsx was falling back directly to DEFAULT_PHASE_MODELS (which is BALANCED_PHASE_MODELS = Sonnet) instead of first resolving the selected agent profile. Resolution order now: 1. Custom phase overrides (if user has customized) 2. Selected profile's phaseModels/phaseThinking 3. DEFAULT_PHASE_MODELS/DEFAULT_PHASE_THINKING (fallback) This matches the pattern used in AgentProfileSettings.tsx. Changes: - Added DEFAULT_AGENT_PROFILES import to AgentTools.tsx - Added selectedProfile resolution using useMemo - Added profilePhaseModels and profilePhaseThinking as intermediate step - Created comprehensive test suite for profile resolution logic Refs: ACS-255 * test: remove unused beforeEach import Addresses code scanning alert for unused import in AgentTools test file. Refs: PR #1068, ACS-255 * test: add feature-based and fixed settings resolution tests Addresses CodeRabbit AI review feedback to add test coverage for feature-based settings resolution in the resolveAgentSettings helper. Previously only phase-based resolution was tested. This commit adds: - Feature-based resolution tests (insights, ideation, roadmap, githubIssues, githubPrs, utility) - Fixed settings resolution test - Added DEFAULT_FEATURE_MODELS and DEFAULT_FEATURE_THINKING imports All 17 tests now pass, covering both phase and feature resolution paths. Refs: PR #1068, ACS-255 * refactor: extract agent settings resolution logic to utility Implements Gemini Code Assist review suggestion to improve separation of concerns and make the logic more reusable. Creates a new utility module `agent-settings-resolver.ts` that: - Centralizes agent profile resolution logic - Provides useResolvedAgentSettings hook for consistent resolution - Exports resolveAgentSettings function for agent-specific resolution - Includes comprehensive JSDoc documentation Benefits: - Single source of truth for agent settings resolution - Easier to test (utility functions vs component internals) - More reusable across other components - Better separation of concerns Changes: - Created src/renderer/lib/agent-settings-resolver.ts - Updated AgentTools.tsx to use the utility - Updated tests to use the utility functions All 17 tests pass, TypeScript compilation succeeds. Refs: PR #1068, ACS-255 * perf: memoize useResolvedAgentSettings return value Addresses CodeRabbit AI review feedback to memoize the return value of useResolvedAgentSettings hook using useMemo. This prevents unnecessary re-renders when the resolved settings haven't changed, improving performance for components that consume this hook. The memoization dependencies include: - selectedProfile (when profile changes) - settings.customPhaseModels (when custom models change) - settings.customPhaseThinking (when custom thinking changes) - settings.featureModels (when feature models change) - settings.featureThinking (when feature thinking changes) Refs: PR #1068, ACS-255 * refactor: address Auto Claude PR Review feedback (ACS-255) This commit addresses all 5 findings from the Auto Claude PR Review: - Remove unused imports (DEFAULT_PHASE_MODELS, DEFAULT_PHASE_THINKING, DEFAULT_FEATURE_MODELS, DEFAULT_FEATURE_THINKING) from AgentTools.tsx - Replace duplicate settingsSource type with imported AgentSettingsSource - Move React hook from lib/ to hooks/ directory (proper codebase pattern) - Simplify nested useMemo to single useMemo (better performance) - Update all import paths consistently All changes maintain backward compatibility and improve code organization. Test coverage remains comprehensive with 17/17 tests passing. Addresses review comments on PR #1068 Refs: ACS-255 * refactor: export useResolvedAgentSettings from hooks barrel file Address Auto Claude PR Review MEDIUM priority finding: - Add useResolvedAgentSettings exports to hooks/index.ts barrel file - Update AgentTools.tsx to use barrel file import - Update test imports to use barrel file import Follows established codebase pattern for consistent imports. All hooks are now exported through the barrel file. Ref: ACS-255, PR #1068 --------- Co-authored-by: StillKnotKnown <stillknotknown@users.noreply.github.com> Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
This commit is contained in:
@@ -52,13 +52,15 @@ import type { ProjectEnvConfig, AgentMcpOverrides, AgentMcpOverride, CustomMcpSe
|
||||
import { CustomMcpDialog } from './CustomMcpDialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
DEFAULT_PHASE_MODELS,
|
||||
DEFAULT_PHASE_THINKING,
|
||||
DEFAULT_FEATURE_MODELS,
|
||||
DEFAULT_FEATURE_THINKING,
|
||||
AVAILABLE_MODELS,
|
||||
THINKING_LEVELS
|
||||
THINKING_LEVELS,
|
||||
} from '../../shared/constants/models';
|
||||
import {
|
||||
useResolvedAgentSettings,
|
||||
resolveAgentSettings as resolveAgentModelConfig,
|
||||
type AgentSettingsSource,
|
||||
type ResolvedAgentSettings,
|
||||
} from '../hooks';
|
||||
import type { ModelTypeShort, ThinkingLevel } from '../../shared/types/settings';
|
||||
|
||||
// Agent configuration data - mirrors AGENT_CONFIGS from backend
|
||||
@@ -71,17 +73,7 @@ interface AgentConfig {
|
||||
mcp_servers: string[];
|
||||
mcp_optional?: string[];
|
||||
// Maps to settings source - either a phase or a feature
|
||||
settingsSource: {
|
||||
type: 'phase';
|
||||
phase: 'spec' | 'planning' | 'coding' | 'qa';
|
||||
} | {
|
||||
type: 'feature';
|
||||
feature: 'insights' | 'ideation' | 'roadmap' | 'githubIssues' | 'githubPrs' | 'utility';
|
||||
} | {
|
||||
type: 'fixed'; // For agents not yet configurable
|
||||
model: ModelTypeShort;
|
||||
thinking: ThinkingLevel;
|
||||
};
|
||||
settingsSource: AgentSettingsSource;
|
||||
}
|
||||
|
||||
// Helper to get model label from short name
|
||||
@@ -971,11 +963,9 @@ export function AgentTools() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Get phase and feature settings with defaults
|
||||
const phaseModels = settings.customPhaseModels || DEFAULT_PHASE_MODELS;
|
||||
const phaseThinking = settings.customPhaseThinking || DEFAULT_PHASE_THINKING;
|
||||
const featureModels = settings.featureModels || DEFAULT_FEATURE_MODELS;
|
||||
const featureThinking = settings.featureThinking || DEFAULT_FEATURE_THINKING;
|
||||
// Resolve agent settings using the centralized utility
|
||||
// Resolution order: custom overrides -> selected profile's config -> global defaults
|
||||
const { phaseModels, phaseThinking, featureModels, featureThinking } = useResolvedAgentSettings(settings);
|
||||
|
||||
// Get MCP server states for display
|
||||
const mcpServers = envConfig?.mcpServers || {};
|
||||
@@ -991,27 +981,9 @@ export function AgentTools() {
|
||||
].filter(Boolean).length;
|
||||
|
||||
// Resolve model and thinking for an agent based on its settings source
|
||||
const resolveAgentSettings = useMemo(() => {
|
||||
const getAgentModelConfig = useMemo(() => {
|
||||
return (config: AgentConfig): { model: ModelTypeShort; thinking: ThinkingLevel } => {
|
||||
const source = config.settingsSource;
|
||||
|
||||
if (source.type === 'phase') {
|
||||
return {
|
||||
model: phaseModels[source.phase],
|
||||
thinking: phaseThinking[source.phase],
|
||||
};
|
||||
} else if (source.type === 'feature') {
|
||||
return {
|
||||
model: featureModels[source.feature],
|
||||
thinking: featureThinking[source.feature],
|
||||
};
|
||||
} else {
|
||||
// Fixed settings
|
||||
return {
|
||||
model: source.model,
|
||||
thinking: source.thinking,
|
||||
};
|
||||
}
|
||||
return resolveAgentModelConfig(config.settingsSource, { phaseModels, phaseThinking, featureModels, featureThinking });
|
||||
};
|
||||
}, [phaseModels, phaseThinking, featureModels, featureThinking]);
|
||||
|
||||
@@ -1371,7 +1343,7 @@ export function AgentTools() {
|
||||
{isExpanded && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3 pl-6">
|
||||
{agents.map(({ id, config }) => {
|
||||
const { model, thinking } = resolveAgentSettings(config);
|
||||
const { model, thinking } = getAgentModelConfig(config);
|
||||
return (
|
||||
<AgentCard
|
||||
key={id}
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
/**
|
||||
* Tests for AgentTools component
|
||||
* Specifically tests agent profile resolution for phase-based and feature-based agents
|
||||
*/
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { DEFAULT_AGENT_PROFILES, DEFAULT_PHASE_MODELS, DEFAULT_FEATURE_MODELS, DEFAULT_FEATURE_THINKING } from '../../../shared/constants/models';
|
||||
import { resolveAgentSettings, type AgentSettingsSource } from '../../hooks';
|
||||
|
||||
// Mock electronAPI
|
||||
global.window.electronAPI = {
|
||||
getProjectEnv: vi.fn().mockResolvedValue({ success: true, data: null }),
|
||||
updateProjectEnv: vi.fn().mockResolvedValue({ success: true }),
|
||||
checkMcpHealth: vi.fn().mockResolvedValue({ success: true, data: null }),
|
||||
testMcpConnection: vi.fn().mockResolvedValue({ success: true, data: null }),
|
||||
} as any;
|
||||
|
||||
describe('AgentTools - Agent Profile Resolution', () => {
|
||||
describe('Profile Selection', () => {
|
||||
it('should find auto profile by ID', () => {
|
||||
const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === 'auto');
|
||||
expect(profile).toBeDefined();
|
||||
expect(profile?.id).toBe('auto');
|
||||
expect(profile?.name).toBe('Auto (Optimized)');
|
||||
expect(profile?.model).toBe('opus');
|
||||
});
|
||||
|
||||
it('should find complex profile by ID', () => {
|
||||
const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === 'complex');
|
||||
expect(profile).toBeDefined();
|
||||
expect(profile?.id).toBe('complex');
|
||||
expect(profile?.name).toBe('Complex Tasks');
|
||||
expect(profile?.model).toBe('opus');
|
||||
});
|
||||
|
||||
it('should find balanced profile by ID', () => {
|
||||
const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === 'balanced');
|
||||
expect(profile).toBeDefined();
|
||||
expect(profile?.id).toBe('balanced');
|
||||
expect(profile?.name).toBe('Balanced');
|
||||
expect(profile?.model).toBe('sonnet');
|
||||
});
|
||||
|
||||
it('should find quick profile by ID', () => {
|
||||
const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === 'quick');
|
||||
expect(profile).toBeDefined();
|
||||
expect(profile?.id).toBe('quick');
|
||||
expect(profile?.name).toBe('Quick Edits');
|
||||
expect(profile?.model).toBe('haiku');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Auto Profile Phase Configuration', () => {
|
||||
it('should have Opus for all phases in auto profile', () => {
|
||||
const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === 'auto');
|
||||
const phaseModels = profile?.phaseModels;
|
||||
|
||||
expect(phaseModels).toBeDefined();
|
||||
expect(phaseModels?.spec).toBe('opus');
|
||||
expect(phaseModels?.planning).toBe('opus');
|
||||
expect(phaseModels?.coding).toBe('opus');
|
||||
expect(phaseModels?.qa).toBe('opus');
|
||||
});
|
||||
|
||||
it('should have optimized thinking levels in auto profile', () => {
|
||||
const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === 'auto');
|
||||
const phaseThinking = profile?.phaseThinking;
|
||||
|
||||
expect(phaseThinking).toBeDefined();
|
||||
expect(phaseThinking?.spec).toBe('ultrathink');
|
||||
expect(phaseThinking?.planning).toBe('high');
|
||||
expect(phaseThinking?.coding).toBe('low');
|
||||
expect(phaseThinking?.qa).toBe('low');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Balanced Profile Phase Configuration', () => {
|
||||
it('should have Sonnet for all phases in balanced profile', () => {
|
||||
const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === 'balanced');
|
||||
const phaseModels = profile?.phaseModels;
|
||||
|
||||
expect(phaseModels).toBeDefined();
|
||||
expect(phaseModels?.spec).toBe('sonnet');
|
||||
expect(phaseModels?.planning).toBe('sonnet');
|
||||
expect(phaseModels?.coding).toBe('sonnet');
|
||||
expect(phaseModels?.qa).toBe('sonnet');
|
||||
});
|
||||
|
||||
it('should have medium thinking for all phases in balanced profile', () => {
|
||||
const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === 'balanced');
|
||||
const phaseThinking = profile?.phaseThinking;
|
||||
|
||||
expect(phaseThinking).toBeDefined();
|
||||
expect(phaseThinking?.spec).toBe('medium');
|
||||
expect(phaseThinking?.planning).toBe('medium');
|
||||
expect(phaseThinking?.coding).toBe('medium');
|
||||
expect(phaseThinking?.qa).toBe('medium');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Profile Resolution Logic', () => {
|
||||
it('should use profile phase models when no custom overrides exist', () => {
|
||||
// Simulate settings with selected profile but no custom overrides
|
||||
const selectedProfileId = 'auto';
|
||||
const customPhaseModels = undefined;
|
||||
|
||||
const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === selectedProfileId) || DEFAULT_AGENT_PROFILES[0];
|
||||
const profilePhaseModels = profile.phaseModels || DEFAULT_PHASE_MODELS;
|
||||
const phaseModels = customPhaseModels || profilePhaseModels;
|
||||
|
||||
// Should resolve to auto profile's opus models
|
||||
expect(phaseModels.spec).toBe('opus');
|
||||
expect(phaseModels.planning).toBe('opus');
|
||||
expect(phaseModels.coding).toBe('opus');
|
||||
expect(phaseModels.qa).toBe('opus');
|
||||
});
|
||||
|
||||
it('should use custom overrides when they exist', () => {
|
||||
// Simulate settings with custom overrides
|
||||
const selectedProfileId = 'auto';
|
||||
const customPhaseModels = {
|
||||
spec: 'sonnet' as const,
|
||||
planning: 'sonnet' as const,
|
||||
coding: 'sonnet' as const,
|
||||
qa: 'sonnet' as const,
|
||||
};
|
||||
|
||||
const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === selectedProfileId) || DEFAULT_AGENT_PROFILES[0];
|
||||
const profilePhaseModels = profile.phaseModels || DEFAULT_PHASE_MODELS;
|
||||
const phaseModels = customPhaseModels || profilePhaseModels;
|
||||
|
||||
// Should resolve to custom overrides (sonnet)
|
||||
expect(phaseModels.spec).toBe('sonnet');
|
||||
expect(phaseModels.planning).toBe('sonnet');
|
||||
expect(phaseModels.coding).toBe('sonnet');
|
||||
expect(phaseModels.qa).toBe('sonnet');
|
||||
});
|
||||
|
||||
it('should default to auto profile when selectedProfileId is undefined', () => {
|
||||
const selectedProfileId = undefined;
|
||||
const effectiveProfileId = selectedProfileId || 'auto';
|
||||
|
||||
const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === effectiveProfileId) || DEFAULT_AGENT_PROFILES[0];
|
||||
|
||||
expect(profile.id).toBe('auto');
|
||||
expect(profile.model).toBe('opus');
|
||||
});
|
||||
|
||||
it('should fall back to first profile when selected profile is not found', () => {
|
||||
const selectedProfileId = 'non-existent-profile';
|
||||
|
||||
const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === selectedProfileId) || DEFAULT_AGENT_PROFILES[0];
|
||||
|
||||
expect(profile.id).toBe('auto');
|
||||
expect(profile.model).toBe('opus');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Agent Settings Resolution (Utility)', () => {
|
||||
it('should resolve phase-based agent settings correctly', () => {
|
||||
const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === 'auto')!;
|
||||
const phaseModels = profile.phaseModels!;
|
||||
const phaseThinking = profile.phaseThinking!;
|
||||
const featureModels = DEFAULT_FEATURE_MODELS;
|
||||
const featureThinking = DEFAULT_FEATURE_THINKING;
|
||||
|
||||
const resolvedSettings = { phaseModels, phaseThinking, featureModels, featureThinking };
|
||||
|
||||
// Spec phase agent
|
||||
const specAgent = resolveAgentSettings(
|
||||
{ type: 'phase', phase: 'spec' },
|
||||
resolvedSettings
|
||||
);
|
||||
expect(specAgent.model).toBe('opus');
|
||||
expect(specAgent.thinking).toBe('ultrathink');
|
||||
|
||||
// Planning phase agent
|
||||
const planningAgent = resolveAgentSettings(
|
||||
{ type: 'phase', phase: 'planning' },
|
||||
resolvedSettings
|
||||
);
|
||||
expect(planningAgent.model).toBe('opus');
|
||||
expect(planningAgent.thinking).toBe('high');
|
||||
|
||||
// Coding phase agent
|
||||
const codingAgent = resolveAgentSettings(
|
||||
{ type: 'phase', phase: 'coding' },
|
||||
resolvedSettings
|
||||
);
|
||||
expect(codingAgent.model).toBe('opus');
|
||||
expect(codingAgent.thinking).toBe('low');
|
||||
|
||||
// QA phase agent
|
||||
const qaAgent = resolveAgentSettings(
|
||||
{ type: 'phase', phase: 'qa' },
|
||||
resolvedSettings
|
||||
);
|
||||
expect(qaAgent.model).toBe('opus');
|
||||
expect(qaAgent.thinking).toBe('low');
|
||||
});
|
||||
|
||||
it('should resolve feature-based agent settings correctly', () => {
|
||||
const phaseModels = DEFAULT_PHASE_MODELS;
|
||||
const phaseThinking = { spec: 'medium' as const, planning: 'medium' as const, coding: 'medium' as const, qa: 'medium' as const };
|
||||
const featureModels = DEFAULT_FEATURE_MODELS;
|
||||
const featureThinking = DEFAULT_FEATURE_THINKING;
|
||||
|
||||
const resolvedSettings = { phaseModels, phaseThinking, featureModels, featureThinking };
|
||||
|
||||
// Insights feature agent (defaults to sonnet)
|
||||
const insightsAgent = resolveAgentSettings(
|
||||
{ type: 'feature', feature: 'insights' },
|
||||
resolvedSettings
|
||||
);
|
||||
expect(insightsAgent.model).toBe('sonnet');
|
||||
expect(insightsAgent.thinking).toBe('medium');
|
||||
|
||||
// Ideation feature agent (defaults to opus)
|
||||
const ideationAgent = resolveAgentSettings(
|
||||
{ type: 'feature', feature: 'ideation' },
|
||||
resolvedSettings
|
||||
);
|
||||
expect(ideationAgent.model).toBe('opus');
|
||||
expect(ideationAgent.thinking).toBe('high');
|
||||
|
||||
// Roadmap feature agent (defaults to opus)
|
||||
const roadmapAgent = resolveAgentSettings(
|
||||
{ type: 'feature', feature: 'roadmap' },
|
||||
resolvedSettings
|
||||
);
|
||||
expect(roadmapAgent.model).toBe('opus');
|
||||
expect(roadmapAgent.thinking).toBe('high');
|
||||
|
||||
// GitHub Issues feature agent (defaults to opus)
|
||||
const githubIssuesAgent = resolveAgentSettings(
|
||||
{ type: 'feature', feature: 'githubIssues' },
|
||||
resolvedSettings
|
||||
);
|
||||
expect(githubIssuesAgent.model).toBe('opus');
|
||||
expect(githubIssuesAgent.thinking).toBe('medium');
|
||||
|
||||
// GitHub PRs feature agent (defaults to opus)
|
||||
const githubPrsAgent = resolveAgentSettings(
|
||||
{ type: 'feature', feature: 'githubPrs' },
|
||||
resolvedSettings
|
||||
);
|
||||
expect(githubPrsAgent.model).toBe('opus');
|
||||
expect(githubPrsAgent.thinking).toBe('medium');
|
||||
|
||||
// Utility feature agent (defaults to haiku)
|
||||
const utilityAgent = resolveAgentSettings(
|
||||
{ type: 'feature', feature: 'utility' },
|
||||
resolvedSettings
|
||||
);
|
||||
expect(utilityAgent.model).toBe('haiku');
|
||||
expect(utilityAgent.thinking).toBe('low');
|
||||
});
|
||||
|
||||
it('should resolve fixed settings correctly', () => {
|
||||
const phaseModels = DEFAULT_PHASE_MODELS;
|
||||
const phaseThinking = { spec: 'medium' as const, planning: 'medium' as const, coding: 'medium' as const, qa: 'medium' as const };
|
||||
const featureModels = DEFAULT_FEATURE_MODELS;
|
||||
const featureThinking = DEFAULT_FEATURE_THINKING;
|
||||
|
||||
const resolvedSettings = { phaseModels, phaseThinking, featureModels, featureThinking };
|
||||
|
||||
// Fixed settings agent
|
||||
const fixedAgent = resolveAgentSettings(
|
||||
{ type: 'fixed', model: 'opus', thinking: 'high' },
|
||||
resolvedSettings
|
||||
);
|
||||
expect(fixedAgent.model).toBe('opus');
|
||||
expect(fixedAgent.thinking).toBe('high');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bug Fix Regression Test (ACS-255)', () => {
|
||||
it('should resolve to opus when auto profile is selected (not sonnet from defaults)', () => {
|
||||
// This test verifies the fix for ACS-255:
|
||||
// MCP Server Overview was showing Sonnet instead of Opus when Auto profile was selected
|
||||
|
||||
const selectedProfileId = 'auto';
|
||||
const customPhaseModels = undefined; // No custom overrides
|
||||
|
||||
// The bug was using DEFAULT_PHASE_MODELS directly (which is BALANCED_PHASE_MODELS = sonnet)
|
||||
// The fix is to resolve the selected profile first
|
||||
const profile = DEFAULT_AGENT_PROFILES.find(p => p.id === selectedProfileId) || DEFAULT_AGENT_PROFILES[0];
|
||||
const profilePhaseModels = profile.phaseModels || DEFAULT_PHASE_MODELS;
|
||||
const phaseModels = customPhaseModels || profilePhaseModels;
|
||||
|
||||
// Should be opus (from auto profile), NOT sonnet (from DEFAULT_PHASE_MODELS)
|
||||
expect(phaseModels.spec).toBe('opus');
|
||||
expect(phaseModels.planning).toBe('opus');
|
||||
expect(phaseModels.coding).toBe('opus');
|
||||
expect(phaseModels.qa).toBe('opus');
|
||||
});
|
||||
|
||||
it('should ensure DEFAULT_PHASE_MODELS is balanced (sonnet)', () => {
|
||||
// This documents that DEFAULT_PHASE_MODELS is the balanced profile (sonnet)
|
||||
// The bug was that this was being used instead of resolving the selected profile
|
||||
|
||||
expect(DEFAULT_PHASE_MODELS.spec).toBe('sonnet');
|
||||
expect(DEFAULT_PHASE_MODELS.planning).toBe('sonnet');
|
||||
expect(DEFAULT_PHASE_MODELS.coding).toBe('sonnet');
|
||||
expect(DEFAULT_PHASE_MODELS.qa).toBe('sonnet');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,10 @@
|
||||
// Export all custom hooks
|
||||
export { useIpcListeners } from './useIpc';
|
||||
export { useVirtualizedTree } from './useVirtualizedTree';
|
||||
export { useClaudeLoginTerminal } from './useClaudeLoginTerminal';
|
||||
export { useIpcListeners } from './useIpc';
|
||||
export {
|
||||
useResolvedAgentSettings,
|
||||
resolveAgentSettings,
|
||||
type ResolvedAgentSettings,
|
||||
type AgentSettingsSource,
|
||||
} from './useResolvedAgentSettings';
|
||||
export { useVirtualizedTree } from './useVirtualizedTree';
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Agent Settings Resolution Hook
|
||||
*
|
||||
* Provides centralized logic for resolving agent model and thinking settings
|
||||
* based on the selected agent profile, custom overrides, and defaults.
|
||||
*
|
||||
* Resolution order for phase settings:
|
||||
* 1. Custom phase overrides (if user has customized)
|
||||
* 2. Selected profile's phaseModels/phaseThinking
|
||||
* 3. DEFAULT_PHASE_MODELS/DEFAULT_PHASE_THINKING (fallback)
|
||||
*
|
||||
* Feature settings are not tied to profiles and use:
|
||||
* 1. Custom feature overrides (if user has customized)
|
||||
* 2. DEFAULT_FEATURE_MODELS/DEFAULT_FEATURE_THINKING (fallback)
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
DEFAULT_AGENT_PROFILES,
|
||||
DEFAULT_PHASE_MODELS,
|
||||
DEFAULT_PHASE_THINKING,
|
||||
DEFAULT_FEATURE_MODELS,
|
||||
DEFAULT_FEATURE_THINKING,
|
||||
} from '../../shared/constants/models';
|
||||
import type {
|
||||
AppSettings,
|
||||
PhaseModelConfig,
|
||||
PhaseThinkingConfig,
|
||||
FeatureModelConfig,
|
||||
FeatureThinkingConfig,
|
||||
ModelTypeShort,
|
||||
ThinkingLevel,
|
||||
} from '../../shared/types/settings';
|
||||
|
||||
/**
|
||||
* Resolved agent settings configuration
|
||||
* Contains all the resolved model and thinking settings for agents
|
||||
*/
|
||||
export interface ResolvedAgentSettings {
|
||||
/** Phase model settings (spec, planning, coding, qa) */
|
||||
phaseModels: PhaseModelConfig;
|
||||
/** Phase thinking level settings */
|
||||
phaseThinking: PhaseThinkingConfig;
|
||||
/** Feature model settings (insights, ideation, roadmap, githubIssues, githubPrs, utility) */
|
||||
featureModels: FeatureModelConfig;
|
||||
/** Feature thinking level settings */
|
||||
featureThinking: FeatureThinkingConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent settings source configuration
|
||||
* Determines where an agent's model and thinking settings come from
|
||||
*/
|
||||
export type AgentSettingsSource =
|
||||
| { type: 'phase'; phase: 'spec' | 'planning' | 'coding' | 'qa' }
|
||||
| { type: 'feature'; feature: 'insights' | 'ideation' | 'roadmap' | 'githubIssues' | 'githubPrs' | 'utility' }
|
||||
| { type: 'fixed'; model: ModelTypeShort; thinking: ThinkingLevel };
|
||||
|
||||
/**
|
||||
* Resolved model and thinking for an agent
|
||||
*/
|
||||
export interface AgentModelConfig {
|
||||
model: ModelTypeShort;
|
||||
thinking: ThinkingLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to resolve agent settings based on the selected profile and custom overrides
|
||||
*
|
||||
* @param settings - The application settings containing selected profile and custom overrides
|
||||
* @returns Resolved agent settings with proper profile resolution
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { phaseModels, phaseThinking, featureModels, featureThinking } = useResolvedAgentSettings(settings);
|
||||
* ```
|
||||
*/
|
||||
export function useResolvedAgentSettings(settings: AppSettings): ResolvedAgentSettings {
|
||||
return useMemo(() => {
|
||||
// Get selected profile ID, default to 'auto'
|
||||
const selectedProfileId = settings.selectedAgentProfile || 'auto';
|
||||
|
||||
// Find the selected profile
|
||||
const selectedProfile = DEFAULT_AGENT_PROFILES.find((p) => p.id === selectedProfileId) || DEFAULT_AGENT_PROFILES[0];
|
||||
|
||||
// Profile defaults (used when no custom overrides exist)
|
||||
const profilePhaseModels = selectedProfile.phaseModels || DEFAULT_PHASE_MODELS;
|
||||
const profilePhaseThinking = selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING;
|
||||
|
||||
// Effective phase config: custom overrides take priority over profile defaults
|
||||
const phaseModels = settings.customPhaseModels || profilePhaseModels;
|
||||
const phaseThinking = settings.customPhaseThinking || profilePhaseThinking;
|
||||
|
||||
// Feature settings (not tied to profiles, use custom or defaults)
|
||||
const featureModels = settings.featureModels || DEFAULT_FEATURE_MODELS;
|
||||
const featureThinking = settings.featureThinking || DEFAULT_FEATURE_THINKING;
|
||||
|
||||
return {
|
||||
phaseModels,
|
||||
phaseThinking,
|
||||
featureModels,
|
||||
featureThinking,
|
||||
};
|
||||
}, [
|
||||
settings.selectedAgentProfile,
|
||||
settings.customPhaseModels,
|
||||
settings.customPhaseThinking,
|
||||
settings.featureModels,
|
||||
settings.featureThinking,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves model and thinking settings for a specific agent based on its settings source
|
||||
*
|
||||
* @param settingsSource - The agent's settings source (phase, feature, or fixed)
|
||||
* @param resolvedSettings - The resolved agent settings from useResolvedAgentSettings
|
||||
* @returns Model and thinking configuration for the agent
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const resolvedSettings = useResolvedAgentSettings(settings);
|
||||
* const { model, thinking } = resolveAgentSettings(agentConfig.settingsSource, resolvedSettings);
|
||||
* ```
|
||||
*/
|
||||
export function resolveAgentSettings(
|
||||
settingsSource: AgentSettingsSource,
|
||||
resolvedSettings: ResolvedAgentSettings
|
||||
): AgentModelConfig {
|
||||
if (settingsSource.type === 'phase') {
|
||||
return {
|
||||
model: resolvedSettings.phaseModels[settingsSource.phase],
|
||||
thinking: resolvedSettings.phaseThinking[settingsSource.phase],
|
||||
};
|
||||
} else if (settingsSource.type === 'feature') {
|
||||
return {
|
||||
model: resolvedSettings.featureModels[settingsSource.feature],
|
||||
thinking: resolvedSettings.featureThinking[settingsSource.feature],
|
||||
};
|
||||
} else {
|
||||
// Fixed settings
|
||||
return {
|
||||
model: settingsSource.model,
|
||||
thinking: settingsSource.thinking,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user