feat: enhance suggestion generation with model and thinking level overrides

- Updated the generateSuggestions function to accept model and thinking level overrides, allowing for more flexible suggestion generation.
- Modified the API client and UI components to support passing these new parameters, improving user control over the suggestion process.
- Introduced a new phase model for AI Suggestions in settings, enhancing the overall functionality and user experience.
This commit is contained in:
Shirone
2026-01-03 02:56:08 +01:00
parent 6d4f28575f
commit d13a16111c
6 changed files with 93 additions and 17 deletions

View File

@@ -1,14 +1,14 @@
/** /**
* Business logic for generating suggestions * Business logic for generating suggestions
* *
* Model is configurable via phaseModels.enhancementModel in settings * Model is configurable via phaseModels.suggestionsModel in settings
* (Feature Enhancement in the UI). Supports both Claude and Cursor models. * (AI Suggestions in the UI). Supports both Claude and Cursor models.
*/ */
import { query } from '@anthropic-ai/claude-agent-sdk'; import { query } from '@anthropic-ai/claude-agent-sdk';
import type { EventEmitter } from '../../lib/events.js'; import type { EventEmitter } from '../../lib/events.js';
import { createLogger } from '@automaker/utils'; import { createLogger } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types'; import { DEFAULT_PHASE_MODELS, isCursorModel, type ThinkingLevel } from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver'; import { resolvePhaseModel } from '@automaker/model-resolver';
import { createSuggestionsOptions } from '../../lib/sdk-options.js'; import { createSuggestionsOptions } from '../../lib/sdk-options.js';
import { extractJsonWithArray } from '../../lib/json-extractor.js'; import { extractJsonWithArray } from '../../lib/json-extractor.js';
@@ -135,7 +135,9 @@ export async function generateSuggestions(
suggestionType: string, suggestionType: string,
events: EventEmitter, events: EventEmitter,
abortController: AbortController, abortController: AbortController,
settingsService?: SettingsService settingsService?: SettingsService,
modelOverride?: string,
thinkingLevelOverride?: ThinkingLevel
): Promise<void> { ): Promise<void> {
const typePrompts: Record<string, string> = { const typePrompts: Record<string, string> = {
features: 'Analyze this project and suggest new features that would add value.', features: 'Analyze this project and suggest new features that would add value.',
@@ -171,11 +173,28 @@ The response will be automatically formatted as structured JSON.`;
'[Suggestions]' '[Suggestions]'
); );
// Get model from phase settings (Feature Enhancement = enhancementModel) // Get model from phase settings (AI Suggestions = suggestionsModel)
// Use override if provided, otherwise fall back to settings
const settings = await settingsService?.getGlobalSettings(); const settings = await settingsService?.getGlobalSettings();
const phaseModelEntry = let model: string;
settings?.phaseModels?.enhancementModel || DEFAULT_PHASE_MODELS.enhancementModel; let thinkingLevel: ThinkingLevel | undefined;
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
if (modelOverride) {
// Use explicit override - resolve the model string
const resolved = resolvePhaseModel({
model: modelOverride,
thinkingLevel: thinkingLevelOverride,
});
model = resolved.model;
thinkingLevel = resolved.thinkingLevel;
} else {
// Use settings-based model
const phaseModelEntry =
settings?.phaseModels?.suggestionsModel || DEFAULT_PHASE_MODELS.suggestionsModel;
const resolved = resolvePhaseModel(phaseModelEntry);
model = resolved.model;
thinkingLevel = resolved.thinkingLevel;
}
logger.info('[Suggestions] Using model:', model); logger.info('[Suggestions] Using model:', model);

View File

@@ -5,6 +5,7 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js'; import type { EventEmitter } from '../../../lib/events.js';
import { createLogger } from '@automaker/utils'; import { createLogger } from '@automaker/utils';
import type { ThinkingLevel } from '@automaker/types';
import { getSuggestionsStatus, setRunningState, getErrorMessage, logError } from '../common.js'; import { getSuggestionsStatus, setRunningState, getErrorMessage, logError } from '../common.js';
import { generateSuggestions } from '../generate-suggestions.js'; import { generateSuggestions } from '../generate-suggestions.js';
import type { SettingsService } from '../../../services/settings-service.js'; import type { SettingsService } from '../../../services/settings-service.js';
@@ -14,9 +15,16 @@ const logger = createLogger('Suggestions');
export function createGenerateHandler(events: EventEmitter, settingsService?: SettingsService) { export function createGenerateHandler(events: EventEmitter, settingsService?: SettingsService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const { projectPath, suggestionType = 'features' } = req.body as { const {
projectPath,
suggestionType = 'features',
model,
thinkingLevel,
} = req.body as {
projectPath: string; projectPath: string;
suggestionType?: string; suggestionType?: string;
model?: string;
thinkingLevel?: ThinkingLevel;
}; };
if (!projectPath) { if (!projectPath) {
@@ -38,7 +46,15 @@ export function createGenerateHandler(events: EventEmitter, settingsService?: Se
setRunningState(true, abortController); setRunningState(true, abortController);
// Start generation in background // Start generation in background
generateSuggestions(projectPath, suggestionType, events, abortController, settingsService) generateSuggestions(
projectPath,
suggestionType,
events,
abortController,
settingsService,
model,
thinkingLevel
)
.catch((error) => { .catch((error) => {
logError(error, 'Generate suggestions failed (background)'); logError(error, 'Generate suggestions failed (background)');
events.emit('suggestions:event', { events.emit('suggestions:event', {

View File

@@ -34,6 +34,8 @@ import {
import { useAppStore, Feature } from '@/store/app-store'; import { useAppStore, Feature } from '@/store/app-store';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { LogViewer } from '@/components/ui/log-viewer'; import { LogViewer } from '@/components/ui/log-viewer';
import { useModelOverride } from '@/components/shared/use-model-override';
import { ModelOverrideTrigger } from '@/components/shared/model-override-trigger';
const logger = createLogger('FeatureSuggestions'); const logger = createLogger('FeatureSuggestions');
@@ -104,6 +106,11 @@ export function FeatureSuggestionsDialog({
const { features, setFeatures } = useAppStore(); const { features, setFeatures } = useAppStore();
// Model override for suggestions
const { effectiveModelEntry, isOverridden, setOverride } = useModelOverride({
phase: 'suggestionsModel',
});
// Initialize selectedIds when suggestions change // Initialize selectedIds when suggestions change
useEffect(() => { useEffect(() => {
if (suggestions.length > 0 && selectedIds.size === 0) { if (suggestions.length > 0 && selectedIds.size === 0) {
@@ -173,7 +180,13 @@ export function FeatureSuggestionsDialog({
setCurrentSuggestionType(suggestionType); setCurrentSuggestionType(suggestionType);
try { try {
const result = await api.suggestions.generate(projectPath, suggestionType); // Pass model and thinkingLevel from the effective model entry
const result = await api.suggestions.generate(
projectPath,
suggestionType,
effectiveModelEntry.model,
effectiveModelEntry.thinkingLevel
);
if (!result.success) { if (!result.success) {
toast.error(result.error || 'Failed to start generation'); toast.error(result.error || 'Failed to start generation');
setIsGenerating(false); setIsGenerating(false);
@@ -184,7 +197,7 @@ export function FeatureSuggestionsDialog({
setIsGenerating(false); setIsGenerating(false);
} }
}, },
[projectPath, setIsGenerating, setSuggestions] [projectPath, setIsGenerating, setSuggestions, effectiveModelEntry]
); );
// Stop generating // Stop generating
@@ -330,6 +343,14 @@ export function FeatureSuggestionsDialog({
AI Suggestions AI Suggestions
</> </>
)} )}
<ModelOverrideTrigger
currentModelEntry={effectiveModelEntry}
onModelChange={setOverride}
phase="suggestionsModel"
isOverridden={isOverridden}
size="sm"
variant="icon"
/>
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
{currentConfig {currentConfig

View File

@@ -14,6 +14,7 @@ import type {
ModelAlias, ModelAlias,
GitHubComment, GitHubComment,
IssueCommentsResult, IssueCommentsResult,
ThinkingLevel,
} from '@automaker/types'; } from '@automaker/types';
import { getJSON, setJSON, removeItem } from './storage'; import { getJSON, setJSON, removeItem } from './storage';
@@ -282,7 +283,9 @@ export type SuggestionType = 'features' | 'refactoring' | 'security' | 'performa
export interface SuggestionsAPI { export interface SuggestionsAPI {
generate: ( generate: (
projectPath: string, projectPath: string,
suggestionType?: SuggestionType suggestionType?: SuggestionType,
model?: string,
thinkingLevel?: ThinkingLevel
) => Promise<{ success: boolean; error?: string }>; ) => Promise<{ success: boolean; error?: string }>;
stop: () => Promise<{ success: boolean; error?: string }>; stop: () => Promise<{ success: boolean; error?: string }>;
status: () => Promise<{ status: () => Promise<{
@@ -2065,7 +2068,12 @@ let mockSuggestionsTimeout: NodeJS.Timeout | null = null;
function createMockSuggestionsAPI(): SuggestionsAPI { function createMockSuggestionsAPI(): SuggestionsAPI {
return { return {
generate: async (projectPath: string, suggestionType: SuggestionType = 'features') => { generate: async (
projectPath: string,
suggestionType: SuggestionType = 'features',
model?: string,
thinkingLevel?: ThinkingLevel
) => {
if (mockSuggestionsRunning) { if (mockSuggestionsRunning) {
return { return {
success: false, success: false,
@@ -2074,7 +2082,11 @@ function createMockSuggestionsAPI(): SuggestionsAPI {
} }
mockSuggestionsRunning = true; mockSuggestionsRunning = true;
logger.info(`Mock generating ${suggestionType} suggestions for: ${projectPath}`); logger.info(
`Mock generating ${suggestionType} suggestions for: ${projectPath}` +
(model ? ` with model: ${model}` : '') +
(thinkingLevel ? ` thinkingLevel: ${thinkingLevel}` : '')
);
// Simulate async suggestion generation // Simulate async suggestion generation
simulateSuggestionsGeneration(suggestionType); simulateSuggestionsGeneration(suggestionType);

View File

@@ -1305,8 +1305,13 @@ export class HttpApiClient implements ElectronAPI {
// Suggestions API // Suggestions API
suggestions: SuggestionsAPI = { suggestions: SuggestionsAPI = {
generate: (projectPath: string, suggestionType?: SuggestionType) => generate: (
this.post('/api/suggestions/generate', { projectPath, suggestionType }), projectPath: string,
suggestionType?: SuggestionType,
model?: string,
thinkingLevel?: string
) =>
this.post('/api/suggestions/generate', { projectPath, suggestionType, model, thinkingLevel }),
stop: () => this.post('/api/suggestions/stop'), stop: () => this.post('/api/suggestions/stop'),
status: () => this.get('/api/suggestions/status'), status: () => this.get('/api/suggestions/status'),
onEvent: (callback: (event: SuggestionsEvent) => void) => { onEvent: (callback: (event: SuggestionsEvent) => void) => {

View File

@@ -139,6 +139,8 @@ export interface PhaseModelConfig {
backlogPlanningModel: PhaseModelEntry; backlogPlanningModel: PhaseModelEntry;
/** Model for analyzing project structure */ /** Model for analyzing project structure */
projectAnalysisModel: PhaseModelEntry; projectAnalysisModel: PhaseModelEntry;
/** Model for AI suggestions (feature, refactoring, security, performance) */
suggestionsModel: PhaseModelEntry;
} }
/** Keys of PhaseModelConfig for type-safe access */ /** Keys of PhaseModelConfig for type-safe access */
@@ -608,6 +610,7 @@ export const DEFAULT_PHASE_MODELS: PhaseModelConfig = {
featureGenerationModel: { model: 'sonnet' }, featureGenerationModel: { model: 'sonnet' },
backlogPlanningModel: { model: 'sonnet' }, backlogPlanningModel: { model: 'sonnet' },
projectAnalysisModel: { model: 'sonnet' }, projectAnalysisModel: { model: 'sonnet' },
suggestionsModel: { model: 'sonnet' },
}; };
/** Current version of the global settings schema */ /** Current version of the global settings schema */