From 132f565aeae85cd0e16247c0121deab62ef9a80b Mon Sep 17 00:00:00 2001 From: reverb256 Date: Fri, 8 May 2026 16:44:05 -0500 Subject: [PATCH 1/3] 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 | 38 +++++++++++++++++ .../features/settings/stores/settingsStore.ts | 13 ++++++ .../task-detail/hooks/usePreviewConfig.ts | 41 +++++++++++++------ 3 files changed, 80 insertions(+), 12 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..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..739ddc047 100644 --- a/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts +++ b/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts @@ -70,6 +70,7 @@ export function usePreviewConfig( const { defaultInitialTaskMode, lastUsedInitialTaskMode, + defaultReasoningEffort, lastUsedReasoningEffort, } = useSettingsStore.getState(); @@ -121,13 +122,22 @@ 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; + } + if (validValues.includes(defaultReasoningEffort)) { return { ...opt, - currentValue: lastUsedReasoningEffort, + currentValue: defaultReasoningEffort, } as SessionConfigOption; } return opt; @@ -168,26 +178,33 @@ 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") { + return isValidEffort(defaultReasoningEffort) + ? defaultReasoningEffort + : "high"; + } + 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, { From f6b8b492f8fbbaa434e2a6abee06ff48a410990e Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Tue, 19 May 2026 17:44:49 +0200 Subject: [PATCH 2/3] feat(settings): clamp default effort to nearest supported level If the user's defaultReasoningEffort isn't supported by the current model, fall to the nearest available level by rank (low < medium < high < xhigh < max) instead of silently no-opping. So 'max' on a model that caps at 'high' becomes 'high'; 'low' on a model that only supports 'medium+' becomes 'medium'. Generated-By: PostHog Code Task-Id: c5674c04-c95c-4bfe-bfcf-248a921b4aae --- .../task-detail/hooks/usePreviewConfig.ts | 70 +++++++++++++++---- 1 file changed, 58 insertions(+), 12 deletions(-) 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 739ddc047..37987f06a 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. @@ -134,10 +173,14 @@ export function usePreviewConfig( } return opt; } - if (validValues.includes(defaultReasoningEffort)) { + const clamped = clampEffortToAvailable( + defaultReasoningEffort, + validValues, + ); + if (clamped) { return { ...opt, - currentValue: defaultReasoningEffort, + currentValue: clamped, } as SessionConfigOption; } return opt; @@ -180,18 +223,21 @@ export function usePreviewConfig( const { lastUsedReasoningEffort, defaultReasoningEffort } = useSettingsStore.getState(); + const availableValues: string[] = + effortOpts?.map((e) => e.value) ?? []; const isValidEffort = (effort: unknown): effort is string => - typeof effort === "string" && - !!effortOpts?.some((e) => e.value === effort); + typeof effort === "string" && availableValues.includes(effort); const resolveEffortFallback = (): string => { - if (defaultReasoningEffort !== "last_used") { - return isValidEffort(defaultReasoningEffort) - ? defaultReasoningEffort - : "high"; - } - return isValidEffort(lastUsedReasoningEffort) - ? lastUsedReasoningEffort - : "high"; + const desired = + defaultReasoningEffort === "last_used" + ? lastUsedReasoningEffort + : defaultReasoningEffort; + if (isValidEffort(desired)) return desired; + const clamped = + typeof desired === "string" + ? clampEffortToAvailable(desired, availableValues) + : null; + return clamped ?? availableValues[0] ?? "high"; }; if (effortOpts && existingIdx >= 0) { const currentEffort = updated[existingIdx].currentValue; From af2c1b349b0d4e271bbe1c7e3fdf58172a215aec Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Tue, 19 May 2026 17:53:59 +0200 Subject: [PATCH 3/3] revert: keep "high" fallback on cross-model effort switch When a user switches to a model that doesn't support their last-used effort level, fall back to "high" rather than clamping. Matches the pre-feature behavior. Clamping is still applied for the new explicit default-effort setting on initial load, where there's no prior behavior to preserve. Generated-By: PostHog Code Task-Id: c5674c04-c95c-4bfe-bfcf-248a921b4aae --- .../task-detail/hooks/usePreviewConfig.ts | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) 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 37987f06a..94a6dfbe7 100644 --- a/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts +++ b/apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts @@ -223,21 +223,19 @@ export function usePreviewConfig( const { lastUsedReasoningEffort, defaultReasoningEffort } = useSettingsStore.getState(); - const availableValues: string[] = - effortOpts?.map((e) => e.value) ?? []; const isValidEffort = (effort: unknown): effort is string => - typeof effort === "string" && availableValues.includes(effort); + typeof effort === "string" && + !!effortOpts?.some((e) => e.value === effort); const resolveEffortFallback = (): string => { - const desired = - defaultReasoningEffort === "last_used" - ? lastUsedReasoningEffort - : defaultReasoningEffort; - if (isValidEffort(desired)) return desired; - const clamped = - typeof desired === "string" - ? clampEffortToAvailable(desired, availableValues) - : null; - return clamped ?? availableValues[0] ?? "high"; + if ( + defaultReasoningEffort !== "last_used" && + isValidEffort(defaultReasoningEffort) + ) { + return defaultReasoningEffort; + } + return isValidEffort(lastUsedReasoningEffort) + ? lastUsedReasoningEffort + : "high"; }; if (effortOpts && existingIdx >= 0) { const currentEffort = updated[existingIdx].currentValue;