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 <bash@opencode.itc.local>
This commit is contained in:
bashrusakh
2026-06-23 23:22:59 +03:00
committed by GitHub
co-authored by Leonid Skorobogatyy
parent 3d3674d4dd
commit e44efa97d6
13 changed files with 155 additions and 2 deletions
@@ -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<string, unknown>;
}>;
};
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<number | undefined>(undefined);
const [topP, setTopP] = React.useState<number | undefined>(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<string[]>([]);
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('');
}}
/>
</div>
</div>
<div data-settings-item="agents.variant" className={cn("py-1.5", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "sm:w-56 shrink-0")}>
<div className="flex items-center gap-1.5">
<span className="typography-ui-label text-foreground">{t('settings.agents.page.field.variant')}</span>
<Tooltip>
<TooltipTrigger asChild>
<Icon name="information" className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
</TooltipTrigger>
<TooltipContent sideOffset={8} className="max-w-xs">
{t('settings.agents.page.field.variantTooltip')}
</TooltipContent>
</Tooltip>
</div>
<span className="typography-meta text-muted-foreground">{t('settings.agents.page.field.variantHint')}</span>
</div>
<div className={cn('flex items-center gap-2', isMobile ? 'w-full' : 'w-fit')}>
{shouldUseVariantSelect ? (
<Select
value={selectedVariantValue}
onValueChange={(value) => setVariant(value === '__default' ? '' : value)}
>
<SelectTrigger className={cn('max-w-full', isMobile ? 'w-full' : 'w-fit min-w-[10rem]')}>
<SelectValue placeholder={t('settings.agents.page.field.variantPlaceholder')}>
{(value) => value === '__default' ? t('chat.modelControls.default') : value}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="__default">{t('chat.modelControls.default')}</SelectItem>
{variantOptions.map((variantOption) => (
<SelectItem key={variantOption} value={variantOption}>{variantOption}</SelectItem>
))}
</SelectContent>
</Select>
) : (
<>
<Input
value={variant}
onChange={(event) => setVariant(event.target.value)}
placeholder={t('settings.agents.page.field.variantPlaceholder')}
disabled={!model && !variant}
className={cn('h-7 w-40', isMobile && 'w-full')}
/>
{variant && (
<Button
size="sm"
type="button"
variant="ghost"
onClick={() => setVariant('')}
className="h-7 w-7 px-0 text-muted-foreground hover:text-foreground"
aria-label={t('settings.common.actions.clear')}
title={t('settings.common.actions.clear')}
>
<Icon name="close" className="h-3.5 w-3.5" />
</Button>
)}
</>
)}
</div>
</div>
<div data-settings-item="agents.temperature" className={cn("py-1.5", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "sm:w-56 shrink-0")}>
<div className="flex items-center gap-1.5">
@@ -232,6 +232,7 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ 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<AgentsSidebarProps> = ({ onItemSelect }) =>
name: sanitizedName,
description: renameDialogAgent.description,
model: renameModelStr,
variant: renameDialogAgent.variant,
temperature: renameDialogAgent.temperature,
top_p: renameDialogAgent.topP,
prompt: renameDialogAgent.prompt,
@@ -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',
@@ -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",
@@ -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',
@@ -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',
@@ -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.',
@@ -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",
@@ -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",
@@ -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',
@@ -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',
+7
View File
@@ -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',
+4
View File
@@ -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<AgentsStore>()(
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<AgentsStore>()(
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;