From 67656aa367a1ba9243cd12bc61314e97798bb980 Mon Sep 17 00:00:00 2001 From: reverb256 Date: Fri, 8 May 2026 16:44:05 -0500 Subject: [PATCH] feat(settings): add configurable default effort level Adds a "Default effort level" setting that lets users choose a fixed reasoning effort (low/medium/high/xhigh/max) or "Last used" for new tasks. Mirrors the existing defaultInitialTaskMode pattern. Changes: - Add DefaultReasoningEffort type and defaultReasoningEffort state to settingsStore (persisted, default "last_used") - Update usePreviewConfig to respect defaultReasoningEffort when selecting the initial effort value for new sessions - Apply the same default in the model-change handler fallback - Add "Default effort level" selector in GeneralSettings > Input - Track setting changes via analytics Closes #1846 --- .../components/sections/GeneralSettings.tsx | 112 ++++++++++++------ .../features/settings/stores/settingsStore.ts | 41 ++++--- .../task-detail/hooks/usePreviewConfig.ts | 103 +++++++++------- 3 files changed, 161 insertions(+), 95 deletions(-) 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..a770d2c85 100644 --- a/apps/code/src/renderer/features/settings/components/sections/GeneralSettings.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/GeneralSettings.tsx @@ -1,10 +1,11 @@ import { useAuthStateValue } from "@features/auth/hooks/authQueries"; import { SettingRow } from "@features/settings/components/SettingRow"; import { - type AutoConvertLongText, - type CompletionSound, - type DefaultInitialTaskMode, - type DiffOpenMode, + type AutoConvertLongText, + type CompletionSound, + type DefaultInitialTaskMode, + type DefaultReasoningEffort, + type DiffOpenMode, type SendMessagesWith, useSettingsStore, } from "@features/settings/stores/settingsStore"; @@ -75,19 +76,21 @@ export function GeneralSettings() { dockBounceNotifications, completionSound, completionVolume, - autoConvertLongText, - defaultInitialTaskMode, - diffOpenMode, - sendMessagesWith, - hedgehogMode, - setDesktopNotifications, - setDockBadgeNotifications, - setDockBounceNotifications, - setCompletionSound, - setCompletionVolume, - setAutoConvertLongText, - setDefaultInitialTaskMode, - setDiffOpenMode, + autoConvertLongText, + defaultInitialTaskMode, + defaultReasoningEffort, + diffOpenMode, + sendMessagesWith, + hedgehogMode, + setDesktopNotifications, + setDockBadgeNotifications, + setDockBounceNotifications, + setCompletionSound, + setCompletionVolume, + setAutoConvertLongText, + setDefaultInitialTaskMode, + setDefaultReasoningEffort, + setDiffOpenMode, setSendMessagesWith, setHedgehogMode, } = useSettingsStore(); @@ -178,19 +181,31 @@ export function GeneralSettings() { [diffOpenMode, setDiffOpenMode], ); - const handleDefaultInitialTaskModeChange = useCallback( - (value: DefaultInitialTaskMode) => { - track(ANALYTICS_EVENTS.SETTING_CHANGED, { - setting_name: "default_initial_task_mode", - new_value: value, - old_value: defaultInitialTaskMode, - }); - setDefaultInitialTaskMode(value); - }, - [defaultInitialTaskMode, setDefaultInitialTaskMode], - ); - - const handleSendMessagesWithChange = useCallback( + const handleDefaultInitialTaskModeChange = useCallback( + (value: DefaultInitialTaskMode) => { + track(ANALYTICS_EVENTS.SETTING_CHANGED, { + setting_name: "default_initial_task_mode", + new_value: value, + old_value: defaultInitialTaskMode, + }); + setDefaultInitialTaskMode(value); + }, + [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, { setting_name: "send_messages_with", @@ -381,13 +396,36 @@ export function GeneralSettings() { Plan - Last used - - - - - Last used + + + + + + + handleDefaultReasoningEffortChange(value as DefaultReasoningEffort) + } + size="1" + > + + + Last used + Low + Medium + High + Extra High + Max + + + + + ; @@ -89,9 +91,10 @@ interface SettingsStore { setPreventSleepWhileRunning: (enabled: boolean) => void; setDebugLogsCloudRuns: (enabled: boolean) => void; setCustomInstructions: (instructions: string) => void; - setDefaultInitialTaskMode: (mode: DefaultInitialTaskMode) => void; - setLastUsedInitialTaskMode: (mode: ExecutionMode) => void; - setDiffOpenMode: (mode: DiffOpenMode) => void; + setDefaultInitialTaskMode: (mode: DefaultInitialTaskMode) => void; + setLastUsedInitialTaskMode: (mode: ExecutionMode) => void; + setDefaultReasoningEffort: (effort: DefaultReasoningEffort) => void; + setDiffOpenMode: (mode: DiffOpenMode) => void; setHedgehogMode: (enabled: boolean) => void; setMcpAppsDisabledServers: (servers: string[]) => void; } @@ -120,9 +123,10 @@ export const useSettingsStore = create()( preventSleepWhileRunning: false, debugLogsCloudRuns: false, customInstructions: "", - defaultInitialTaskMode: "plan", - lastUsedInitialTaskMode: "plan", - diffOpenMode: "auto", + defaultInitialTaskMode: "plan", + lastUsedInitialTaskMode: "plan", + defaultReasoningEffort: "last_used", + diffOpenMode: "auto", hedgehogMode: false, mcpAppsDisabledServers: [], hints: {}, @@ -194,11 +198,13 @@ export const useSettingsStore = create()( setDebugLogsCloudRuns: (enabled) => set({ debugLogsCloudRuns: enabled }), setCustomInstructions: (instructions) => set({ customInstructions: instructions }), - setDefaultInitialTaskMode: (mode) => - set({ defaultInitialTaskMode: mode }), - setLastUsedInitialTaskMode: (mode) => - set({ lastUsedInitialTaskMode: mode }), - setDiffOpenMode: (mode) => set({ diffOpenMode: mode }), + setDefaultInitialTaskMode: (mode) => + set({ defaultInitialTaskMode: mode }), + setLastUsedInitialTaskMode: (mode) => + set({ lastUsedInitialTaskMode: mode }), + setDefaultReasoningEffort: (effort) => + set({ defaultReasoningEffort: effort }), + setDiffOpenMode: (mode) => set({ diffOpenMode: mode }), setHedgehogMode: (enabled) => set({ hedgehogMode: enabled }), setMcpAppsDisabledServers: (servers) => set({ mcpAppsDisabledServers: servers }), @@ -228,9 +234,10 @@ export const useSettingsStore = create()( preventSleepWhileRunning: state.preventSleepWhileRunning, debugLogsCloudRuns: state.debugLogsCloudRuns, customInstructions: state.customInstructions, - defaultInitialTaskMode: state.defaultInitialTaskMode, - lastUsedInitialTaskMode: state.lastUsedInitialTaskMode, - diffOpenMode: state.diffOpenMode, + defaultInitialTaskMode: state.defaultInitialTaskMode, + lastUsedInitialTaskMode: state.lastUsedInitialTaskMode, + defaultReasoningEffort: state.defaultReasoningEffort, + diffOpenMode: state.diffOpenMode, hedgehogMode: state.hedgehogMode, hints: state.hints, mcpAppsDisabledServers: state.mcpAppsDisabledServers, 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..3a291e0af 100644 --- a/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts +++ b/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts @@ -67,11 +67,12 @@ export function usePreviewConfig( .then((options) => { if (abort.signal.aborted) return; - const { - defaultInitialTaskMode, - lastUsedInitialTaskMode, - lastUsedReasoningEffort, - } = useSettingsStore.getState(); + const { + defaultInitialTaskMode, + lastUsedInitialTaskMode, + defaultReasoningEffort, + lastUsedReasoningEffort, + } = useSettingsStore.getState(); // Use the mode option's existing currentValue (set by the server // based on the adapter) when the user hasn't chosen a preference, @@ -111,27 +112,40 @@ export function usePreviewConfig( : opt, ); - const withEffort = withMode.map((opt) => { - if (opt.category !== "thought_level" || opt.type !== "select") { - return opt; - } - const validValues = flattenValues( - opt.options as Array<{ - value?: string; - options?: Array<{ value: string }>; - }>, - ); - if ( - lastUsedReasoningEffort && - validValues.includes(lastUsedReasoningEffort) - ) { - return { - ...opt, - currentValue: lastUsedReasoningEffort, - } as SessionConfigOption; - } - return opt; - }); + const withEffort = withMode.map((opt) => { + if (opt.category !== "thought_level" || opt.type !== "select") { + return opt; + } + const validValues = flattenValues( + opt.options as Array<{ + value?: string; + options?: Array<{ value: string }>; + }>, + ); + // Mirror the defaultInitialTaskMode pattern: if defaultReasoningEffort + // is "last_used" and we have a valid last-used value, use it; + // otherwise, use the explicit default the user configured. + if (defaultReasoningEffort === "last_used") { + if ( + lastUsedReasoningEffort && + validValues.includes(lastUsedReasoningEffort) + ) { + return { + ...opt, + currentValue: lastUsedReasoningEffort, + } as SessionConfigOption; + } + return opt; + } + // User chose a fixed default — use it if valid for this model + if (validValues.includes(defaultReasoningEffort)) { + return { + ...opt, + currentValue: defaultReasoningEffort, + } as SessionConfigOption; + } + return opt; + }); setConfigOptions(withEffort); setIsLoading(false); @@ -168,26 +182,33 @@ export function usePreviewConfig( ? "reasoning_effort" : "effort"; - const { lastUsedReasoningEffort } = useSettingsStore.getState(); - const isValidEffort = (effort: unknown): effort is string => - typeof effort === "string" && - !!effortOpts?.some((e) => e.value === effort); - if (effortOpts && existingIdx >= 0) { - const currentEffort = updated[existingIdx].currentValue; - const nextEffort = isValidEffort(currentEffort) - ? currentEffort - : isValidEffort(lastUsedReasoningEffort) - ? lastUsedReasoningEffort - : "high"; + 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") { + return isValidEffort(defaultReasoningEffort) + ? defaultReasoningEffort + : "high"; + } + return isValidEffort(lastUsedReasoningEffort) + ? lastUsedReasoningEffort + : "high"; + }; + if (effortOpts && existingIdx >= 0) { + const currentEffort = updated[existingIdx].currentValue; + const nextEffort = isValidEffort(currentEffort) + ? currentEffort + : resolveEffortFallback(); updated[existingIdx] = { ...updated[existingIdx], currentValue: nextEffort, options: effortOpts, } as SessionConfigOption; - } else if (effortOpts && existingIdx === -1) { - const nextEffort = isValidEffort(lastUsedReasoningEffort) - ? lastUsedReasoningEffort - : "high"; + } else if (effortOpts && existingIdx === -1) { + const nextEffort = resolveEffortFallback(); updated = [ ...updated, {