diff --git a/apps/code/src/renderer/features/settings/components/sections/GeneralSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/GeneralSettings.tsx index c9d35b912..c64480e83 100644 --- a/apps/code/src/renderer/features/settings/components/sections/GeneralSettings.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/GeneralSettings.tsx @@ -4,6 +4,7 @@ import { type AutoConvertLongText, type CompletionSound, type DefaultInitialTaskMode, + type DefaultReasoningEffort, type DiffOpenMode, type SendMessagesWith, useSettingsStore, @@ -77,6 +78,7 @@ export function GeneralSettings() { completionVolume, autoConvertLongText, defaultInitialTaskMode, + defaultReasoningEffort, diffOpenMode, sendMessagesWith, hedgehogMode, @@ -87,6 +89,7 @@ export function GeneralSettings() { setCompletionVolume, setAutoConvertLongText, setDefaultInitialTaskMode, + setDefaultReasoningEffort, setDiffOpenMode, setSendMessagesWith, setHedgehogMode, @@ -190,6 +193,18 @@ export function GeneralSettings() { [defaultInitialTaskMode, setDefaultInitialTaskMode], ); + const handleDefaultReasoningEffortChange = useCallback( + (value: DefaultReasoningEffort) => { + track(ANALYTICS_EVENTS.SETTING_CHANGED, { + setting_name: "default_reasoning_effort", + new_value: value, + old_value: defaultReasoningEffort, + }); + setDefaultReasoningEffort(value); + }, + [defaultReasoningEffort, setDefaultReasoningEffort], + ); + const handleSendMessagesWithChange = useCallback( (value: SendMessagesWith) => { track(ANALYTICS_EVENTS.SETTING_CHANGED, { @@ -386,6 +401,29 @@ export function GeneralSettings() { + + + handleDefaultReasoningEffortChange(value as DefaultReasoningEffort) + } + size="1" + > + + + Last used + Low + Medium + High + Extra High + Max + + + + ; defaultInitialTaskMode: DefaultInitialTaskMode; lastUsedInitialTaskMode: ExecutionMode; + defaultReasoningEffort: DefaultReasoningEffort; setDefaultRunMode: (mode: DefaultRunMode) => void; setLastUsedRunMode: (mode: "local" | "cloud") => void; setLastUsedLocalWorkspaceMode: (mode: LocalWorkspaceMode) => void; @@ -71,6 +79,7 @@ interface SettingsStore { getLastUsedEnvironment: (repoPath: string) => string | null; setDefaultInitialTaskMode: (mode: DefaultInitialTaskMode) => void; setLastUsedInitialTaskMode: (mode: ExecutionMode) => void; + setDefaultReasoningEffort: (effort: DefaultReasoningEffort) => void; // Notifications desktopNotifications: boolean; @@ -140,6 +149,7 @@ export const useSettingsStore = create()( lastUsedEnvironments: {}, defaultInitialTaskMode: "plan", lastUsedInitialTaskMode: "plan", + defaultReasoningEffort: "last_used", setDefaultRunMode: (mode) => set({ defaultRunMode: mode }), setLastUsedRunMode: (mode) => set({ lastUsedRunMode: mode }), setLastUsedLocalWorkspaceMode: (mode) => @@ -167,6 +177,8 @@ export const useSettingsStore = create()( set({ defaultInitialTaskMode: mode }), setLastUsedInitialTaskMode: (mode) => set({ lastUsedInitialTaskMode: mode }), + setDefaultReasoningEffort: (effort) => + set({ defaultReasoningEffort: effort }), // Notifications desktopNotifications: true, @@ -264,6 +276,7 @@ export const useSettingsStore = create()( lastUsedEnvironments: state.lastUsedEnvironments, defaultInitialTaskMode: state.defaultInitialTaskMode, lastUsedInitialTaskMode: state.lastUsedInitialTaskMode, + defaultReasoningEffort: state.defaultReasoningEffort, // Notifications desktopNotifications: state.desktopNotifications, diff --git a/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts b/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts index 02a0aa2d0..94a6dfbe7 100644 --- a/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts +++ b/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts @@ -35,6 +35,45 @@ function flattenValues( ); } +const EFFORT_RANK: Record = { + low: 0, + medium: 1, + high: 2, + xhigh: 3, + max: 4, +}; + +/** + * Clamp a desired effort to the nearest level the current model supports. + * Falls back to the highest supported level when the desired level has no + * known rank (e.g. unrecognized value from older settings). + */ +function clampEffortToAvailable( + desired: string, + available: string[], +): string | null { + if (available.length === 0) return null; + if (available.includes(desired)) return desired; + + const desiredRank = EFFORT_RANK[desired]; + if (desiredRank === undefined) { + return available[available.length - 1]; + } + + const ranked = available + .map((value) => ({ value, rank: EFFORT_RANK[value] })) + .filter((entry): entry is { value: string; rank: number } => + Number.isFinite(entry.rank), + ); + if (ranked.length === 0) return available[0]; + + return ranked.reduce((closest, entry) => + Math.abs(entry.rank - desiredRank) < Math.abs(closest.rank - desiredRank) + ? entry + : closest, + ).value; +} + /** * Fetches config options (models, modes, effort levels) for the task input * page via a lightweight tRPC query. No agent session is created. @@ -70,6 +109,7 @@ export function usePreviewConfig( const { defaultInitialTaskMode, lastUsedInitialTaskMode, + defaultReasoningEffort, lastUsedReasoningEffort, } = useSettingsStore.getState(); @@ -121,13 +161,26 @@ export function usePreviewConfig( options?: Array<{ value: string }>; }>, ); - if ( - lastUsedReasoningEffort && - validValues.includes(lastUsedReasoningEffort) - ) { + if (defaultReasoningEffort === "last_used") { + if ( + lastUsedReasoningEffort && + validValues.includes(lastUsedReasoningEffort) + ) { + return { + ...opt, + currentValue: lastUsedReasoningEffort, + } as SessionConfigOption; + } + return opt; + } + const clamped = clampEffortToAvailable( + defaultReasoningEffort, + validValues, + ); + if (clamped) { return { ...opt, - currentValue: lastUsedReasoningEffort, + currentValue: clamped, } as SessionConfigOption; } return opt; @@ -168,26 +221,34 @@ export function usePreviewConfig( ? "reasoning_effort" : "effort"; - const { lastUsedReasoningEffort } = useSettingsStore.getState(); + const { lastUsedReasoningEffort, defaultReasoningEffort } = + useSettingsStore.getState(); const isValidEffort = (effort: unknown): effort is string => typeof effort === "string" && !!effortOpts?.some((e) => e.value === effort); + const resolveEffortFallback = (): string => { + if ( + defaultReasoningEffort !== "last_used" && + isValidEffort(defaultReasoningEffort) + ) { + return defaultReasoningEffort; + } + return isValidEffort(lastUsedReasoningEffort) + ? lastUsedReasoningEffort + : "high"; + }; if (effortOpts && existingIdx >= 0) { const currentEffort = updated[existingIdx].currentValue; const nextEffort = isValidEffort(currentEffort) ? currentEffort - : isValidEffort(lastUsedReasoningEffort) - ? lastUsedReasoningEffort - : "high"; + : resolveEffortFallback(); updated[existingIdx] = { ...updated[existingIdx], currentValue: nextEffort, options: effortOpts, } as SessionConfigOption; } else if (effortOpts && existingIdx === -1) { - const nextEffort = isValidEffort(lastUsedReasoningEffort) - ? lastUsedReasoningEffort - : "high"; + const nextEffort = resolveEffortFallback(); updated = [ ...updated, {