From e44efa97d6d1359786d1de89132582862f272cc6 Mon Sep 17 00:00:00 2001 From: bashrusakh <127580858+bashrusakh@users.noreply.github.com> Date: Wed, 24 Jun 2026 07:22:59 +1100 Subject: [PATCH] feat(agents): expose thinking variant configuration in agent settings (#1715) * feat(agents): expose thinking variant configuration in agent settings Fix #1425: add variant field to agent config UI so users can configure thinking/reasoning depth per agent without editing opencode.json. Changes: - Added variant to AgentConfig and AgentDraft types in useAgentsStore - Pass variant in createAgent and updateAgent API calls - Support null for temperature, top_p, and variant to clear overrides - Added variant input field in AgentsPage 'Model & Parameters' section - Added variant to settings search registry - Added i18n strings for variant field in all 8 non-English locales The variant field maps to provider-specific parameters (e.g. Anthropic high/max variant, OpenAI reasoning effort). Users can enter any string value; the SDK passes it through to the model provider. Clearing temperature/topP/variant now sends null to the server instead of omitting the field, which properly removes the override in opencode.json. * fix(sync): preserve tool state.time in materialization merge * chore: trigger re-review * fix(agents): use thinking variant selector in settings * fix(agents): preserve thinking variant values --------- Co-authored-by: Leonid Skorobogatyy --- .../components/sections/agents/AgentsPage.tsx | 108 +++++++++++++++++- .../sections/agents/AgentsSidebar.tsx | 2 + .../ui/src/lib/i18n/messages/en.settings.ts | 4 + .../ui/src/lib/i18n/messages/es.settings.ts | 4 + .../ui/src/lib/i18n/messages/fr.settings.ts | 4 + .../ui/src/lib/i18n/messages/ko.settings.ts | 4 + .../ui/src/lib/i18n/messages/pl.settings.ts | 4 + .../src/lib/i18n/messages/pt-BR.settings.ts | 4 + .../ui/src/lib/i18n/messages/uk.settings.ts | 4 + .../src/lib/i18n/messages/zh-CN.settings.ts | 4 + .../src/lib/i18n/messages/zh-TW.settings.ts | 4 + packages/ui/src/lib/settings/search.ts | 7 ++ packages/ui/src/stores/useAgentsStore.ts | 4 + 13 files changed, 155 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/sections/agents/AgentsPage.tsx b/packages/ui/src/components/sections/agents/AgentsPage.tsx index 894e853c..5686a5d5 100644 --- a/packages/ui/src/components/sections/agents/AgentsPage.tsx +++ b/packages/ui/src/components/sections/agents/AgentsPage.tsx @@ -16,6 +16,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { useI18n } from '@/lib/i18n'; import { parseModelIdentifier } from '@/lib/modelIdentifier'; +import { useConfigStore } from '@/stores/useConfigStore'; import { Select, SelectContent, @@ -23,7 +24,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { Icon } from "@/components/icon/Icon"; +import { Icon } from '@/components/icon/Icon'; type PermissionAction = 'allow' | 'ask' | 'deny'; type PermissionRule = { permission: string; pattern: string; action: PermissionAction }; @@ -192,9 +193,31 @@ const buildPermissionConfigWithGlobal = ( return result as AgentConfig['permission']; }; +type AgentVariantProvider = { + id: string; + models?: Array<{ + id?: string; + variants?: Record; + }>; +}; + +const getVariantOptionsForModel = ( + providers: AgentVariantProvider[], + modelValue: string, +): string[] => { + const parsedModel = parseModelIdentifier(modelValue); + if (!parsedModel) { + return []; + } + + const provider = providers.find((item) => item.id === parsedModel.providerId); + const model = provider?.models?.find((item) => item.id === parsedModel.modelId); + return model?.variants ? Object.keys(model.variants) : []; +}; export const AgentsPage: React.FC = () => { const { t } = useI18n(); const { isMobile } = useDeviceInfo(); + const providers = useConfigStore((state) => state.providers) as AgentVariantProvider[]; const { selectedAgentName, getAgentByName, @@ -221,6 +244,7 @@ export const AgentsPage: React.FC = () => { const [description, setDescription] = React.useState(''); const [mode, setMode] = React.useState<'primary' | 'subagent' | 'all'>('subagent'); const [model, setModel] = React.useState(''); + const [variant, setVariant] = React.useState(''); const [temperature, setTemperature] = React.useState(undefined); const [topP, setTopP] = React.useState(undefined); const [prompt, setPrompt] = React.useState(''); @@ -237,6 +261,7 @@ export const AgentsPage: React.FC = () => { description: string; mode: 'primary' | 'subagent' | 'all'; model: string; + variant: string; temperature: number | undefined; topP: number | undefined; prompt: string; @@ -246,6 +271,15 @@ export const AgentsPage: React.FC = () => { const currentDirectory = useDirectoryStore((state) => state.currentDirectory ?? null); const [toolIds, setToolIds] = React.useState([]); + const variantOptions = React.useMemo(() => getVariantOptionsForModel(providers, model), [model, providers]); + const hasVariantOptions = variantOptions.length > 0; + const selectedVariantValue = React.useMemo(() => { + if (!variant || !variantOptions.includes(variant)) { + return '__default'; + } + return variant; + }, [variant, variantOptions]); + const shouldUseVariantSelect = hasVariantOptions && (!variant || variantOptions.includes(variant)); const permissionsBySession = useDirectorySync((state) => state.permission); @@ -469,6 +503,7 @@ export const AgentsPage: React.FC = () => { const descriptionValue = agentDraft.description || ''; const modeValue = agentDraft.mode || 'subagent'; const modelValue = agentDraft.model || ''; + const variantValue = agentDraft.variant || ''; const temperatureValue = agentDraft.temperature; const topPValue = agentDraft.top_p; const promptValue = agentDraft.prompt || ''; @@ -478,6 +513,7 @@ export const AgentsPage: React.FC = () => { setDescription(descriptionValue); setMode(modeValue); setModel(modelValue); + setVariant(variantValue); setTemperature(temperatureValue); setTopP(topPValue); setPrompt(promptValue); @@ -491,6 +527,7 @@ export const AgentsPage: React.FC = () => { description: descriptionValue, mode: modeValue, model: modelValue, + variant: variantValue, temperature: temperatureValue, topP: topPValue, prompt: promptValue, @@ -506,6 +543,7 @@ export const AgentsPage: React.FC = () => { const modelValue = selectedAgent.model?.providerID && selectedAgent.model?.modelID ? `${selectedAgent.model.providerID}/${selectedAgent.model.modelID}` : ''; + const variantValue = selectedAgent.variant || ''; const temperatureValue = selectedAgent.temperature; const topPValue = selectedAgent.topP; const promptValue = selectedAgent.prompt || ''; @@ -514,6 +552,7 @@ export const AgentsPage: React.FC = () => { setMode(modeValue); setModel(modelValue); + setVariant(variantValue); setTemperature(temperatureValue); setTopP(topPValue); setPrompt(promptValue); @@ -528,6 +567,7 @@ export const AgentsPage: React.FC = () => { description: descriptionValue, mode: modeValue, model: modelValue, + variant: variantValue, temperature: temperatureValue, topP: topPValue, prompt: promptValue, @@ -551,6 +591,7 @@ export const AgentsPage: React.FC = () => { if (description !== initial.description) return true; if (mode !== initial.mode) return true; if (model !== initial.model) return true; + if (variant !== initial.variant) return true; if (temperature !== initial.temperature) return true; if (topP !== initial.topP) return true; if (prompt !== initial.prompt) return true; @@ -558,7 +599,7 @@ export const AgentsPage: React.FC = () => { if (!areRulesEqual(permissionRules, initial.permissionRules)) return true; return false; - }, [description, draftName, draftScope, globalPermission, isNewAgent, mode, model, permissionRules, prompt, temperature, topP]); + }, [description, draftName, draftScope, globalPermission, isNewAgent, mode, model, permissionRules, prompt, temperature, topP, variant]); const handleSave = async () => { const agentName = isNewAgent ? draftName.trim().replace(/\s+/g, '-') : selectedAgentName?.trim(); @@ -578,6 +619,7 @@ export const AgentsPage: React.FC = () => { try { const trimmedModel = model.trim(); + const trimmedVariant = variant.trim(); const trimmedPrompt = prompt.trim(); const permissionConfig = buildPermissionConfigWithGlobal(globalPermission, permissionRules); const config: AgentConfig = { @@ -585,6 +627,7 @@ export const AgentsPage: React.FC = () => { description: description.trim() || undefined, mode, model: trimmedModel === '' ? null : trimmedModel, + variant: trimmedVariant === '' ? null : trimmedVariant || undefined, temperature, top_p: topP, prompt: trimmedPrompt || (isNewAgent ? undefined : null), @@ -777,11 +820,72 @@ export const AgentsPage: React.FC = () => { } else { setModel(''); } + setVariant(''); }} /> +
+
+
+ {t('settings.agents.page.field.variant')} + + + + + + {t('settings.agents.page.field.variantTooltip')} + + +
+ {t('settings.agents.page.field.variantHint')} +
+
+ {shouldUseVariantSelect ? ( + + ) : ( + <> + setVariant(event.target.value)} + placeholder={t('settings.agents.page.field.variantPlaceholder')} + disabled={!model && !variant} + className={cn('h-7 w-40', isMobile && 'w-full')} + /> + {variant && ( + + )} + + )} +
+
+
diff --git a/packages/ui/src/components/sections/agents/AgentsSidebar.tsx b/packages/ui/src/components/sections/agents/AgentsSidebar.tsx index 5d5cec20..c3622529 100644 --- a/packages/ui/src/components/sections/agents/AgentsSidebar.tsx +++ b/packages/ui/src/components/sections/agents/AgentsSidebar.tsx @@ -232,6 +232,7 @@ export const AgentsSidebar: React.FC = ({ onItemSelect }) => scope: extAgent.scope || 'user', description: agent.description, model: modelStr, + variant: agent.variant, temperature: agent.temperature, top_p: agent.topP, prompt: agent.prompt, @@ -277,6 +278,7 @@ export const AgentsSidebar: React.FC = ({ onItemSelect }) => name: sanitizedName, description: renameDialogAgent.description, model: renameModelStr, + variant: renameDialogAgent.variant, temperature: renameDialogAgent.temperature, top_p: renameDialogAgent.topP, prompt: renameDialogAgent.prompt, diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index 83e9e0a1..7ea4b78d 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -428,6 +428,10 @@ export const settingsDict = { 'settings.agents.page.field.topPTooltip': 'Nucleus sampling diversity. Lower = likely tokens only.', 'settings.agents.page.field.topPRange': '0.0 to 1.0', 'settings.agents.page.field.clearTopPAria': 'Clear top p override', + 'settings.agents.page.field.variant': 'Thinking Variant', + 'settings.agents.page.field.variantTooltip': 'Controls the thinking/reasoning depth for this agent. Maps to provider-specific parameters (e.g. Anthropic variant, OpenAI reasoning effort).', + 'settings.agents.page.field.variantHint': 'e.g. high, max, low', + 'settings.agents.page.field.variantPlaceholder': 'default', 'settings.agents.page.field.systemPromptPlaceholder': 'You are an expert coding assistant...', 'settings.agents.page.mode.primary': 'Primary', 'settings.agents.page.mode.subagent': 'Subagent', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index c78bb4d5..e67423ea 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -387,6 +387,10 @@ export const settingsDict = { "settings.agents.page.field.mode": "Modo", "settings.agents.page.field.modeTooltip": "Visibilidad principal vs subagente", "settings.agents.page.field.overrideModel": "Sobrescribir modelo", + "settings.agents.page.field.variant": "Variante de pensamiento", + "settings.agents.page.field.variantTooltip": "Opcional. Si el modelo admite razonamiento, la variante controla la profundidad del razonamiento.", + "settings.agents.page.field.variantHint": "Específico del modelo (p. ej. variante de Anthropic)", + "settings.agents.page.field.variantPlaceholder": "p. ej. high, max, low, none", "settings.agents.page.field.temperature": "Temperatura", "settings.agents.page.field.temperatureTooltip": "Controla la aleatoriedad. Mayor = más creativo, menor = más enfocado.", "settings.agents.page.field.temperatureRange": "0.0 a 2.0", diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts index 89fc09e7..51a60d62 100644 --- a/packages/ui/src/lib/i18n/messages/fr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts @@ -376,6 +376,10 @@ export const settingsDict = { 'settings.agents.page.field.mode': 'Mode', 'settings.agents.page.field.modeTooltip': 'Visibilité principale et sous-agent', 'settings.agents.page.field.overrideModel': 'Remplacer le modèle', + 'settings.agents.page.field.variant': 'Variante de réflexion', + 'settings.agents.page.field.variantTooltip': 'Optionnel. Si le modèle prend en charge le raisonnement, la variante contrôle la profondeur du raisonnement.', + 'settings.agents.page.field.variantHint': 'Spécifique au modèle (par ex. variante Anthropic)', + 'settings.agents.page.field.variantPlaceholder': 'par ex. high, max, low, none', 'settings.agents.page.field.temperature': 'Température', 'settings.agents.page.field.temperatureTooltip': 'Contrôle le caractère aléatoire. Plus élevé = créatif, Inférieur = concentré.', 'settings.agents.page.field.temperatureRange': '0.0 à 2.0', diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index 2e4752db..aca29809 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -387,6 +387,10 @@ export const settingsDict = { 'settings.agents.page.field.mode': '모드', 'settings.agents.page.field.modeTooltip': '주 에이전트/보조 에이전트로 표시할지 선택합니다', 'settings.agents.page.field.overrideModel': '모델 오버라이드', + 'settings.agents.page.field.variant': '사고 변형', + 'settings.agents.page.field.variantTooltip': '선택 사항. 모델이 추론을 지원하는 경우, 변형은 추론 깊이를 제어합니다.', + 'settings.agents.page.field.variantHint': '모델별 (예: Anthropic variant)', + 'settings.agents.page.field.variantPlaceholder': '예: high, max, low, none', 'settings.agents.page.field.temperature': '온도', 'settings.agents.page.field.temperatureTooltip': '무작위성을 제어합니다. 높을수록 창의적이고 낮을수록 집중적입니다.', 'settings.agents.page.field.temperatureRange': '0.0 ~ 2.0', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index a469c319..03600ead 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -28,6 +28,10 @@ export const settingsDict = { 'settings.agents.page.field.overrideModel': 'Nadpisz model', 'settings.agents.page.field.scopePlaceholder': 'Zakres', 'settings.agents.page.field.systemPromptPlaceholder': 'Jesteś ekspertem w asystowaniu przy kodowaniu...', + 'settings.agents.page.field.variant': 'Wariant myślenia', + 'settings.agents.page.field.variantTooltip': 'Opcjonalne. Jeśli model obsługuje rozumowanie, wariant kontroluje głębokość rozumowania.', + 'settings.agents.page.field.variantHint': 'Zależne od modelu (np. wariant Anthropic)', + 'settings.agents.page.field.variantPlaceholder': 'np. high, max, low, none', 'settings.agents.page.field.temperature': 'Temperatura', 'settings.agents.page.field.temperatureRange': 'od 0.0 do 2.0', 'settings.agents.page.field.temperatureTooltip': 'Kontroluje losowość. Wyższa = bardziej kreatywny, niższa = bardziej skupiony.', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index da72559a..98daddb1 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -387,6 +387,10 @@ export const settingsDict = { "settings.agents.page.field.mode": "Modo", "settings.agents.page.field.modeTooltip": "Visibilidade principal vs subagente", "settings.agents.page.field.overrideModel": "Sobrescrever modelo", + "settings.agents.page.field.variant": "Variante de pensamento", + "settings.agents.page.field.variantTooltip": "Opcional. Se o modelo suportar raciocínio, a variante controla a profundidade do raciocínio.", + "settings.agents.page.field.variantHint": "Específico do modelo (ex.: variante Anthropic)", + "settings.agents.page.field.variantPlaceholder": "ex.: high, max, low, none", "settings.agents.page.field.temperature": "Temperatura", "settings.agents.page.field.temperatureTooltip": "Controla a aleatoriedade. Maior = mais criativo, menor = mais focado.", "settings.agents.page.field.temperatureRange": "0.0 a 2.0", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index 41dff574..a796c7ca 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -387,6 +387,10 @@ export const settingsDict = { "settings.agents.page.field.mode": "Режим", "settings.agents.page.field.modeTooltip": "Видимість основного або субагента", "settings.agents.page.field.overrideModel": "Перевизначити модель", + "settings.agents.page.field.variant": "Варіант мислення", + "settings.agents.page.field.variantTooltip": "Необов'язково. Якщо модель підтримує міркування, варіант контролює глибину міркування.", + "settings.agents.page.field.variantHint": "Специфічно для моделі (напр. варіант Anthropic)", + "settings.agents.page.field.variantPlaceholder": "напр. high, max, low, none", "settings.agents.page.field.temperature": "Температура", "settings.agents.page.field.temperatureTooltip": "Контролює випадковість. Вищий = творчий, нижчий = зосереджений.", "settings.agents.page.field.temperatureRange": "0,0 до 2,0", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index 9604ee48..9982c676 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -387,6 +387,10 @@ export const settingsDict = { 'settings.agents.page.field.mode': '模式', 'settings.agents.page.field.modeTooltip': '主智能体与子智能体可见性', 'settings.agents.page.field.overrideModel': '覆盖模型', + 'settings.agents.page.field.variant': '思考变体', + 'settings.agents.page.field.variantTooltip': '可选。如果模型支持推理,变体控制推理深度。', + 'settings.agents.page.field.variantHint': '模型特定(如 Anthropic variant)', + 'settings.agents.page.field.variantPlaceholder': '例如 high, max, low, none', 'settings.agents.page.field.temperature': 'Temperature', 'settings.agents.page.field.temperatureTooltip': '控制随机性。越高越有创造性,越低越聚焦。', 'settings.agents.page.field.temperatureRange': '0.0 到 2.0', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts index 8a380184..7a9439db 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -384,6 +384,10 @@ 'settings.agents.page.field.mode': '模式', 'settings.agents.page.field.modeTooltip': 'Primary 與 Subagent 可見性', 'settings.agents.page.field.overrideModel': '覆寫模型', + 'settings.agents.page.field.variant': '思考變體', + 'settings.agents.page.field.variantTooltip': '選填。如果模型支援推理,變體控制推理深度。', + 'settings.agents.page.field.variantHint': '模型特定(如 Anthropic variant)', + 'settings.agents.page.field.variantPlaceholder': '例如 high, max, low, none', 'settings.agents.page.field.temperature': 'Temperature', 'settings.agents.page.field.temperatureTooltip': '控制隨機性。越高越有創造力,越低越聚焦。', 'settings.agents.page.field.temperatureRange': '0.0 到 2.0', diff --git a/packages/ui/src/lib/settings/search.ts b/packages/ui/src/lib/settings/search.ts index b33c20f7..8a849308 100644 --- a/packages/ui/src/lib/settings/search.ts +++ b/packages/ui/src/lib/settings/search.ts @@ -428,6 +428,13 @@ export const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [ titleKey: 'settings.agents.page.field.overrideModel', keywords: ['model', 'provider'], }, + { + id: 'agents.variant', + page: 'agents', + titleKey: 'settings.agents.page.field.variant', + descriptionKey: 'settings.agents.page.field.variantTooltip', + keywords: ['thinking', 'reasoning', 'variant', 'depth'], + }, { id: 'agents.temperature', page: 'agents', diff --git a/packages/ui/src/stores/useAgentsStore.ts b/packages/ui/src/stores/useAgentsStore.ts index 3e07b409..ef31b65c 100644 --- a/packages/ui/src/stores/useAgentsStore.ts +++ b/packages/ui/src/stores/useAgentsStore.ts @@ -104,6 +104,7 @@ export interface AgentConfig { name: string; description?: string; model?: string | null; + variant?: string | null; temperature?: number; top_p?: number; prompt?: string | null; @@ -170,6 +171,7 @@ export interface AgentDraft { scope: AgentScope; description?: string; model?: string | null; + variant?: string; temperature?: number; top_p?: number; prompt?: string; @@ -331,6 +333,7 @@ export const useAgentsStore = create()( if (config.description) agentConfig.description = config.description; if (config.model) agentConfig.model = config.model; + if (config.variant) agentConfig.variant = config.variant; if (config.temperature !== undefined) agentConfig.temperature = config.temperature; if (config.top_p !== undefined) agentConfig.top_p = config.top_p; if (config.prompt) agentConfig.prompt = config.prompt; @@ -395,6 +398,7 @@ export const useAgentsStore = create()( if (config.mode !== undefined) agentConfig.mode = config.mode; if (config.description !== undefined) agentConfig.description = config.description; if (config.model !== undefined) agentConfig.model = config.model; + if ('variant' in config) agentConfig.variant = config.variant ?? null; if (config.temperature !== undefined) agentConfig.temperature = config.temperature; if (config.top_p !== undefined) agentConfig.top_p = config.top_p; if (config.prompt !== undefined) agentConfig.prompt = config.prompt;