Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/main/lib/trpc/routers/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }),
},
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import {
ctrlTabTargetAtom,
defaultAgentModeAtom,
desktopNotificationsEnabledAtom,
extendedThinkingEnabledAtom,
notifyWhenFocusedAtom,
soundNotificationsEnabledAtom,
preferredEditorAtom,
type AgentMode,
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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -197,18 +201,34 @@ export function AgentsPreferencesTab() {
<div className="flex items-center justify-between p-4">
<div className="flex flex-col space-y-1">
<span className="text-sm font-medium text-foreground">
Extended Thinking
Thinking Effort
</span>
<span className="text-xs text-muted-foreground">
Enable deeper reasoning with more thinking tokens (uses more
credits).{" "}
<span className="text-foreground/70">Disables response streaming.</span>
Default effort level for Claude's reasoning. Higher levels think
longer and use more credits.
</span>
</div>
<Switch
checked={thinkingEnabled}
onCheckedChange={setThinkingEnabled}
/>
<Select
value={claudeThinking}
onValueChange={(value: ClaudeThinkingLevel) =>
setClaudeThinking(value)
}
>
<SelectTrigger className="w-auto px-2">
<span className="text-xs">
{formatClaudeThinkingLabel(claudeThinking)}
</span>
</SelectTrigger>
<SelectContent>
{(
["off", "low", "medium", "high", "xhigh", "max"] as const
).map((level) => (
<SelectItem key={level} value={level}>
{formatClaudeThinkingLabel(level)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between p-4 border-t border-border">
<div className="flex flex-col space-y-1">
Expand Down
60 changes: 60 additions & 0 deletions src/renderer/features/agents/atoms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,34 @@ export const lastSelectedCodexThinkingAtom = atomWithStorage<CodexThinkingPrefer
{ getOnInit: true },
)

export type ClaudeThinkingPreference =
| "off"
| "low"
| "medium"
| "high"
| "xhigh"
| "max"

// One-time migration from the legacy boolean toggle: true → "high", false → "off".
// Only consulted the first time the new key is read (atomWithStorage keeps the user's
// choice thereafter).
function readInitialClaudeThinking(): ClaudeThinkingPreference {
try {
const raw = localStorage.getItem("preferences:extended-thinking-enabled")
if (raw === null) return "high"
return JSON.parse(raw) === false ? "off" : "high"
} catch {
return "high"
}
}

export const lastSelectedClaudeThinkingAtom = atomWithStorage<ClaudeThinkingPreference>(
"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<Record<string, string>>(
Expand Down Expand Up @@ -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<string, ClaudeThinkingPreference>
>(
"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<Record<string, AgentMode>>(
"agents:subChatModes",
Expand Down
93 changes: 48 additions & 45 deletions src/renderer/features/agents/components/agent-model-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"

Expand All @@ -40,6 +42,7 @@ type ClaudeModelOption = {
id: string
name: string
version: string
thinkings: ClaudeThinkingLevel[]
}

type CodexModelOption = {
Expand Down Expand Up @@ -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[]
Expand All @@ -89,14 +92,16 @@ type FlatModelItem =
| { type: "ollama"; modelName: string; isRecommended: boolean }
| { type: "custom" }

function CodexThinkingSubMenu({
thinkings,
selectedThinking,
onSelectThinking,
function ThinkingSubMenu<T extends string>({
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<HTMLDivElement>(null)
const subMenuRef = useRef<HTMLDivElement>(null)
Expand Down Expand Up @@ -167,9 +172,7 @@ function CodexThinkingSubMenu({
<span>Thinking</span>
</div>
<div className="flex items-center gap-1 text-muted-foreground">
<span className="text-xs">
{formatCodexThinkingLabel(selectedThinking)}
</span>
<span className="text-xs">{formatLabel(selected)}</span>
<ChevronRight className="h-3.5 w-3.5 shrink-0" />
</div>
</div>
Expand All @@ -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 (
<button
key={thinking}
onClick={() => onSelectThinking(thinking)}
key={level}
onClick={() => onSelect(level)}
className="flex items-center justify-between gap-4 min-h-[32px] py-[5px] px-1.5 mx-1 w-[calc(100%-8px)] rounded-md text-sm cursor-default select-none outline-none dark:hover:bg-neutral-800 hover:text-foreground transition-colors"
>
<span>{formatCodexThinkingLabel(thinking)}</span>
<span>{formatLabel(level)}</span>
{isSelected && (
<CheckIcon className="h-3.5 w-3.5 shrink-0" />
)}
Expand Down Expand Up @@ -558,39 +561,39 @@ export function AgentModelSelector({
onValueChange={setSearch}
/>

{/* Claude thinking toggle */}
{/* Claude thinking effort selector with hover sub-menu */}
{selectedAgentId === "claude-code" &&
!claude.isOffline &&
!claude.hasCustomModelConfig && (
<>
<div
className="flex items-center justify-between min-h-[32px] py-[5px] px-1.5 mx-1"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-1.5">
<ThinkingIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<span className="text-sm">Thinking</span>
</div>
<Switch
checked={claude.thinkingEnabled}
onCheckedChange={claude.onThinkingChange}
className="scale-75"
/>
</div>
<CommandSeparator />
</>
)}
!claude.hasCustomModelConfig &&
(() => {
const selectedClaudeModel =
claude.models.find((m) => m.id === claude.selectedModelId) ||
claude.models[0]
if (!selectedClaudeModel) return null
return (
<>
<ThinkingSubMenu<ClaudeThinkingLevel>
levels={selectedClaudeModel.thinkings}
selected={claude.selectedThinking}
onSelect={claude.onSelectThinking}
formatLabel={formatClaudeThinkingLabel}
/>
<CommandSeparator />
</>
)
})()}

{/* Codex thinking level selector with hover sub-menu */}
{selectedAgentId === "codex" && (() => {
const selectedCodexModel = codex.models.find((m) => m.id === codex.selectedModelId) || codex.models[0]
if (!selectedCodexModel) return null
return (
<>
<CodexThinkingSubMenu
thinkings={selectedCodexModel.thinkings}
selectedThinking={codex.selectedThinking}
onSelectThinking={codex.onSelectThinking}
<ThinkingSubMenu<CodexThinkingLevel>
levels={selectedCodexModel.thinkings}
selected={codex.selectedThinking}
onSelect={codex.onSelectThinking}
formatLabel={formatCodexThinkingLabel}
/>
<CommandSeparator />
</>
Expand Down
16 changes: 8 additions & 8 deletions src/renderer/features/agents/lib/ipc-chat-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
type CustomClaudeConfig,
customClaudeConfigAtom,
enableTasksAtom,
extendedThinkingEnabledAtom,
historyEnabledAtom,
normalizeCustomClaudeConfig,
selectedOllamaModelAtom,
Expand All @@ -24,6 +23,7 @@ import {
MODEL_ID_MAP,
pendingAuthRetryMessageAtom,
pendingUserQuestionsAtom,
subChatClaudeThinkingAtomFamily,
subChatModelIdAtomFamily,
} from "../atoms"
import { useAgentSubChatStore } from "../stores/sub-chat-store"
Expand Down Expand Up @@ -160,12 +160,12 @@ export class IPCChatTransport implements ChatTransport<UIMessage> {
const metadata = lastAssistant?.metadata as AgentMessageMetadata | undefined
const sessionId = metadata?.sessionId

// Read extended thinking setting dynamically (so toggle applies to existing chats)
const thinkingEnabled = appStore.get(extendedThinkingEnabledAtom)
// Max thinking tokens for extended thinking mode
// SDK adds +1 internally, so 64000 becomes 64001 which exceeds Opus 4.5 limit
// Using 32000 to stay safely under the 64000 max output tokens limit
const maxThinkingTokens = thinkingEnabled ? 32_000 : undefined
// Read thinking effort per-subChat (mirrors the model selection)
const claudeThinkingLevel = appStore.get(
subChatClaudeThinkingAtomFamily(this.config.subChatId),
)
const effort =
claudeThinkingLevel === "off" ? undefined : claudeThinkingLevel
const historyEnabled = appStore.get(historyEnabledAtom)
const enableTasks = appStore.get(enableTasksAtom)

Expand Down Expand Up @@ -208,7 +208,7 @@ export class IPCChatTransport implements ChatTransport<UIMessage> {
projectPath: this.config.projectPath, // Original project path for MCP config lookup
mode: currentMode,
sessionId,
...(maxThinkingTokens && { maxThinkingTokens }),
...(effort && { effort }),
...(modelString && { model: modelString }),
...(customConfig && { customConfig }),
...(selectedOllamaModel && { selectedOllamaModel }),
Expand Down
Loading