From d034b2290be9e15d891b51e58bdf8d8481c8834e Mon Sep 17 00:00:00 2001 From: Alejandro Tamayo Date: Fri, 17 Apr 2026 18:47:19 +0200 Subject: [PATCH] feat(agents): add Claude thinking effort selector Replaces the binary on/off toggle with a per-subChat Off/Low/Medium/High/XHigh/Max selector (filtered per model) using the SDK's new `effort` option; drops the deprecated `maxThinkingTokens`. A one-time migration seeds the new atom from the old boolean so existing preferences carry over. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/lib/trpc/routers/claude.ts | 8 +- .../settings-tabs/agents-preferences-tab.tsx | 42 ++++++--- src/renderer/features/agents/atoms/index.ts | 60 ++++++++++++ .../components/agent-model-selector.tsx | 93 ++++++++++--------- .../features/agents/lib/ipc-chat-transport.ts | 16 ++-- src/renderer/features/agents/lib/models.ts | 36 ++++++- .../features/agents/main/chat-input-area.tsx | 53 ++++++++++- .../features/agents/main/new-chat-form.tsx | 39 +++++++- src/renderer/lib/atoms/index.ts | 10 -- 9 files changed, 266 insertions(+), 91 deletions(-) diff --git a/src/main/lib/trpc/routers/claude.ts b/src/main/lib/trpc/routers/claude.ts index 9e5eadffe..a987b06eb 100644 --- a/src/main/lib/trpc/routers/claude.ts +++ b/src/main/lib/trpc/routers/claude.ts @@ -810,7 +810,9 @@ export const claudeRouter = router({ baseUrl: z.string().min(1), }) .optional(), - maxThinkingTokens: z.number().optional(), // Enable extended thinking + effort: z + .enum(["low", "medium", "high", "xhigh", "max"]) + .optional(), // Thinking/reasoning effort level images: z.array(imageAttachmentSchema).optional(), // Image attachments historyEnabled: z.boolean().optional(), offlineModeEnabled: z.boolean().optional(), // Whether offline mode (Ollama) is enabled in settings @@ -1994,9 +1996,7 @@ ${prompt} ...(!resumeSessionId && { continue: true }), ...(resolvedModel && { model: resolvedModel }), // fallbackModel: "claude-opus-4-5-20251101", - ...(input.maxThinkingTokens && { - maxThinkingTokens: input.maxThinkingTokens, - }), + ...(input.effort && { effort: input.effort }), }, } diff --git a/src/renderer/components/dialogs/settings-tabs/agents-preferences-tab.tsx b/src/renderer/components/dialogs/settings-tabs/agents-preferences-tab.tsx index 7d5af6e64..bd39e8ed8 100644 --- a/src/renderer/components/dialogs/settings-tabs/agents-preferences-tab.tsx +++ b/src/renderer/components/dialogs/settings-tabs/agents-preferences-tab.tsx @@ -6,7 +6,6 @@ import { ctrlTabTargetAtom, defaultAgentModeAtom, desktopNotificationsEnabledAtom, - extendedThinkingEnabledAtom, notifyWhenFocusedAtom, soundNotificationsEnabledAtom, preferredEditorAtom, @@ -14,6 +13,11 @@ import { type AutoAdvanceTarget, type CtrlTabTarget, } from "../../../lib/atoms" +import { lastSelectedClaudeThinkingAtom } from "../../../features/agents/atoms" +import { + formatClaudeThinkingLabel, + type ClaudeThinkingLevel, +} from "../../../features/agents/lib/models" import { APP_META, type ExternalApp } from "../../../../shared/external-apps" // Editor icon imports @@ -142,8 +146,8 @@ function useIsNarrowScreen(): boolean { } export function AgentsPreferencesTab() { - const [thinkingEnabled, setThinkingEnabled] = useAtom( - extendedThinkingEnabledAtom, + const [claudeThinking, setClaudeThinking] = useAtom( + lastSelectedClaudeThinkingAtom, ) const [soundEnabled, setSoundEnabled] = useAtom(soundNotificationsEnabledAtom) const [desktopNotificationsEnabled, setDesktopNotificationsEnabled] = useAtom(desktopNotificationsEnabledAtom) @@ -197,18 +201,34 @@ export function AgentsPreferencesTab() {
- Extended Thinking + Thinking Effort - Enable deeper reasoning with more thinking tokens (uses more - credits).{" "} - Disables response streaming. + Default effort level for Claude's reasoning. Higher levels think + longer and use more credits.
- +
diff --git a/src/renderer/features/agents/atoms/index.ts b/src/renderer/features/agents/atoms/index.ts index 666975a20..ada0526e3 100644 --- a/src/renderer/features/agents/atoms/index.ts +++ b/src/renderer/features/agents/atoms/index.ts @@ -234,6 +234,34 @@ export const lastSelectedCodexThinkingAtom = atomWithStorage( + "agents:lastSelectedClaudeThinking", + readInitialClaudeThinking(), + undefined, + { getOnInit: true }, +) + // Storage for per-subChat Claude model selection. // Falls back to lastSelectedModelIdAtom when sub-chat has no explicit selection yet. const subChatModelIdsStorageAtom = atomWithStorage>( @@ -323,6 +351,38 @@ export const subChatCodexThinkingAtomFamily = atomFamily((subChatId: string) => ), ) +// Storage for per-subChat Claude thinking level. +// Falls back to lastSelectedClaudeThinkingAtom when sub-chat has no explicit selection yet. +const subChatClaudeThinkingStorageAtom = atomWithStorage< + Record +>( + "agents:subChatClaudeThinking", + {}, + undefined, + { getOnInit: true }, +) + +export const subChatClaudeThinkingAtomFamily = atomFamily((subChatId: string) => + atom( + (get) => { + if (!subChatId) return get(lastSelectedClaudeThinkingAtom) + return ( + get(subChatClaudeThinkingStorageAtom)[subChatId] ?? + get(lastSelectedClaudeThinkingAtom) + ) + }, + (get, set, newThinking: ClaudeThinkingPreference) => { + if (!subChatId) { + set(lastSelectedClaudeThinkingAtom, newThinking) + return + } + const current = get(subChatClaudeThinkingStorageAtom) + if (current[subChatId] === newThinking) return + set(subChatClaudeThinkingStorageAtom, { ...current, [subChatId]: newThinking }) + }, + ), +) + // Storage for all sub-chat modes (persisted per subChatId) const subChatModesStorageAtom = atomWithStorage>( "agents:subChatModes", diff --git a/src/renderer/features/agents/components/agent-model-selector.tsx b/src/renderer/features/agents/components/agent-model-selector.tsx index 56cc4333c..035cfd53c 100644 --- a/src/renderer/features/agents/components/agent-model-selector.tsx +++ b/src/renderer/features/agents/components/agent-model-selector.tsx @@ -13,8 +13,7 @@ import { CommandList, CommandSeparator, } from "../../../components/ui/command" -import { CheckIcon, ClaudeCodeIcon, IconChevronDown, ThinkingIcon } from "../../../components/ui/icons" -import { Switch } from "../../../components/ui/switch" +import { CheckIcon, ClaudeCodeIcon, IconChevronDown } from "../../../components/ui/icons" import { Checkbox } from "../../../components/ui/checkbox" import { Button } from "../../../components/ui/button" import { @@ -23,8 +22,11 @@ import { PopoverTrigger, } from "../../../components/ui/popover" import { cn } from "../../../lib/utils" -import type { CodexThinkingLevel } from "../lib/models" -import { formatCodexThinkingLabel } from "../lib/models" +import type { ClaudeThinkingLevel, CodexThinkingLevel } from "../lib/models" +import { + formatClaudeThinkingLabel, + formatCodexThinkingLabel, +} from "../lib/models" const CROSS_PROVIDER_DIALOG_DISMISSED_KEY = "agent-model-selector:skip-cross-provider-dialog" @@ -40,6 +42,7 @@ type ClaudeModelOption = { id: string name: string version: string + thinkings: ClaudeThinkingLevel[] } type CodexModelOption = { @@ -70,8 +73,8 @@ interface AgentModelSelectorProps { recommendedOllamaModel?: string onSelectOllamaModel: (modelId: string) => void isConnected: boolean - thinkingEnabled: boolean - onThinkingChange: (enabled: boolean) => void + selectedThinking: ClaudeThinkingLevel + onSelectThinking: (thinking: ClaudeThinkingLevel) => void } codex: { models: CodexModelOption[] @@ -89,14 +92,16 @@ type FlatModelItem = | { type: "ollama"; modelName: string; isRecommended: boolean } | { type: "custom" } -function CodexThinkingSubMenu({ - thinkings, - selectedThinking, - onSelectThinking, +function ThinkingSubMenu({ + levels, + selected, + onSelect, + formatLabel, }: { - thinkings: CodexThinkingLevel[] - selectedThinking: CodexThinkingLevel - onSelectThinking: (thinking: CodexThinkingLevel) => void + levels: T[] + selected: T + onSelect: (level: T) => void + formatLabel: (level: T) => string }) { const triggerRef = useRef(null) const subMenuRef = useRef(null) @@ -167,9 +172,7 @@ function CodexThinkingSubMenu({ Thinking
- - {formatCodexThinkingLabel(selectedThinking)} - + {formatLabel(selected)}
@@ -183,15 +186,15 @@ function CodexThinkingSubMenu({ className="fixed z-50 min-w-[180px] overflow-auto rounded-[10px] border border-border bg-popover text-sm text-popover-foreground shadow-lg py-1 animate-in fade-in-0 zoom-in-95 slide-in-from-left-2" style={{ top: subPos.top, left: subPos.left }} > - {thinkings.map((thinking) => { - const isSelected = selectedThinking === thinking + {levels.map((level) => { + const isSelected = selected === level return (