From 9f1b0b35e47b47424f650e624a267316f8dd2950 Mon Sep 17 00:00:00 2001 From: zhangyijun Date: Thu, 16 Jul 2026 18:05:08 +0800 Subject: [PATCH] feat(conversation): add runtime thinking mode selector --- .../components/agent/AcpModelSelector.tsx | 23 +++++++++---- .../agent/runtimeSelectorOptions.tsx | 21 ++++++++++++ .../hooks/agent/useAcpConfigOptions.ts | 2 +- .../conversation/platforms/acp/AcpSendBox.tsx | 25 ++++++++++---- .../platforms/aionrs/AionrsModelSelector.tsx | 16 ++++++--- .../platforms/aionrs/AionrsSendBox.tsx | 25 ++++++++++---- .../src/renderer/services/i18n/i18n-keys.d.ts | 4 +++ .../services/i18n/locales/de-DE/agent.json | 6 ++++ .../services/i18n/locales/en-US/agent.json | 6 ++++ .../services/i18n/locales/es-ES/agent.json | 6 ++++ .../services/i18n/locales/fa-IR/agent.json | 6 ++++ .../services/i18n/locales/fr-FR/agent.json | 6 ++++ .../services/i18n/locales/ja-JP/agent.json | 6 ++++ .../services/i18n/locales/ko-KR/agent.json | 6 ++++ .../services/i18n/locales/pt-BR/agent.json | 6 ++++ .../services/i18n/locales/ru-RU/agent.json | 6 ++++ .../services/i18n/locales/tr-TR/agent.json | 6 ++++ .../services/i18n/locales/uk-UA/agent.json | 6 ++++ .../services/i18n/locales/zh-CN/agent.json | 6 ++++ .../services/i18n/locales/zh-TW/agent.json | 6 ++++ .../utils/model/agentRuntimeCatalog.ts | 1 + tests/unit/acpConfigOptions.test.ts | 24 +++++++++++++ .../renderer/AcpModelSelector.dom.test.tsx | 30 ++++++++++++++++ .../renderer/AionrsModelSelector.dom.test.tsx | 34 +++++++++++++++++++ 24 files changed, 260 insertions(+), 23 deletions(-) diff --git a/packages/desktop/src/renderer/components/agent/AcpModelSelector.tsx b/packages/desktop/src/renderer/components/agent/AcpModelSelector.tsx index 52b048d355..07143d2470 100644 --- a/packages/desktop/src/renderer/components/agent/AcpModelSelector.tsx +++ b/packages/desktop/src/renderer/components/agent/AcpModelSelector.tsx @@ -17,7 +17,9 @@ import RuntimeSelectorPill, { RuntimeSelectorLoadingIndicator } from './RuntimeS import { composeRuntimeSelectorLabel, getCurrentThoughtLevelLabel, + isThinkingModeOption, isConfigSetting, + localizeThinkingModeOption, RUNTIME_SUBMENU_TRIGGER_PROPS, RuntimeSelectorCheckedItem, RuntimeSelectorModelList, @@ -69,6 +71,15 @@ const AcpModelSelector: React.FC<{ }); const defaultModelLabel = t('common.defaultModel'); + const thinkingMode = isThinkingModeOption(thoughtLevel); + const displayThoughtLevel = localizeThinkingModeOption(thoughtLevel, { + enabled: t('agent.thinkingMode.enabled'), + disabled: t('agent.thinkingMode.disabled'), + }); + const thoughtControlLabel = thinkingMode ? t('agent.thinkingMode.label') : t('agent.thoughtLevel.label'); + const thoughtSwitchSuccess = thinkingMode + ? t('agent.thinkingMode.switchSuccess') + : t('agent.thoughtLevel.switchSuccess'); const rawDisplayLabel = (model_info?.current_model_id && model_info.available_models.find((m) => m.id === model_info.current_model_id)?.label) || @@ -81,19 +92,19 @@ const AcpModelSelector: React.FC<{ defaultModelLabel, fallbackLabel: t('conversation.welcome.useCliModel'), }); - const combinedLabel = composeRuntimeSelectorLabel({ modelLabel: display_label, thoughtLevel }); + const combinedLabel = composeRuntimeSelectorLabel({ modelLabel: display_label, thoughtLevel: displayThoughtLevel }); const isRuntimeSetting = isConfigSetting(setStatus); const handleThoughtLevelSelect = useCallback( async (value: string) => { if (!thoughtLevel || value === thoughtLevel.currentValue || isRuntimeSetting) return; try { await setConfigOption(thoughtLevel.id, value); - Message.success(t('agent.thoughtLevel.switchSuccess')); + Message.success(thoughtSwitchSuccess); } catch (error) { Message.error(t(configErrorMessageKey(error))); } }, - [isRuntimeSetting, setConfigOption, thoughtLevel, t] + [isRuntimeSetting, setConfigOption, thoughtLevel, thoughtSwitchSuccess] ); const tooltipContent = combinedLabel; @@ -170,12 +181,12 @@ const AcpModelSelector: React.FC<{ triggerProps={RUNTIME_SUBMENU_TRIGGER_PROPS} title={ } > - {thoughtLevel.options.map((item) => ( + {displayThoughtLevel?.options.map((item) => ( { + if (!option || option.options.length !== 2) return false; + const values = new Set(option.options.map((item) => item.value.toLowerCase())); + return values.has('enabled') && values.has('disabled'); +}; + +export const localizeThinkingModeOption = ( + option: AcpDerivedOption | null | undefined, + labels: { enabled: string; disabled: string } +): AcpDerivedOption | null => { + if (!option) return null; + if (!isThinkingModeOption(option)) return option; + return { + ...option, + options: option.options.map((item) => ({ + ...item, + label: item.value.toLowerCase() === 'enabled' ? labels.enabled : labels.disabled, + })), + }; +}; + export const composeRuntimeSelectorLabel = ({ modelLabel, thoughtLevel, diff --git a/packages/desktop/src/renderer/hooks/agent/useAcpConfigOptions.ts b/packages/desktop/src/renderer/hooks/agent/useAcpConfigOptions.ts index 2131fd9614..261a042c1d 100644 --- a/packages/desktop/src/renderer/hooks/agent/useAcpConfigOptions.ts +++ b/packages/desktop/src/renderer/hooks/agent/useAcpConfigOptions.ts @@ -276,7 +276,7 @@ export function useAcpConfigOptions({ setStatus, mode: deriveSelectOption(configOptions, 'mode', ['mode']), model: deriveSelectOption(configOptions, 'model', ['model']), - thoughtLevel: deriveSelectOption(configOptions, 'thought_level', ['thought_level', 'reasoning_effort']), + thoughtLevel: deriveSelectOption(configOptions, 'thought_level', ['thought_level', 'reasoning_effort', 'thinking']), reload, setConfigOption, }; diff --git a/packages/desktop/src/renderer/pages/conversation/platforms/acp/AcpSendBox.tsx b/packages/desktop/src/renderer/pages/conversation/platforms/acp/AcpSendBox.tsx index 21305346fc..227108ad5a 100644 --- a/packages/desktop/src/renderer/pages/conversation/platforms/acp/AcpSendBox.tsx +++ b/packages/desktop/src/renderer/pages/conversation/platforms/acp/AcpSendBox.tsx @@ -4,6 +4,7 @@ import { isBackendHttpError } from '@/common/adapter/httpBridge'; import { isSideQuestionSupported } from '@/common/chat/sideQuestion'; import { parseError, uuid } from '@/common/utils'; import AgentModeSelector from '@/renderer/components/agent/AgentModeSelector'; +import { isThinkingModeOption, localizeThinkingModeOption } from '@/renderer/components/agent/runtimeSelectorOptions'; import CommandQueuePanel from '@/renderer/components/chat/CommandQueuePanel'; import MobileActionSheet, { type MobileActionSheetEntry, @@ -152,6 +153,15 @@ const AcpSendBox: React.FC<{ }); const runtimeMode = runtimeConfig.mode; const runtimeThoughtLevel = runtimeConfig.thoughtLevel; + const runtimeThinkingMode = isThinkingModeOption(runtimeThoughtLevel); + const displayRuntimeThoughtLevel = localizeThinkingModeOption(runtimeThoughtLevel, { + enabled: t('agent.thinkingMode.enabled'), + disabled: t('agent.thinkingMode.disabled'), + }); + const thoughtControlLabel = runtimeThinkingMode ? t('agent.thinkingMode.label') : t('agent.thoughtLevel.label'); + const thoughtSwitchSuccess = runtimeThinkingMode + ? t('agent.thinkingMode.switchSuccess') + : t('agent.thoughtLevel.switchSuccess'); const handleThoughtLevelSetOption = useCallback( async (optionId: string, value: string) => runtimeConfig.setConfigOption(optionId, value), [runtimeConfig] @@ -499,18 +509,18 @@ Please check your local CLI tool authentication status`, }); } - if (runtimeThoughtLevel) { + if (runtimeThoughtLevel && displayRuntimeThoughtLevel) { entries.push({ key: 'thought-level', icon: , - label: t('agent.thoughtLevel.label'), + label: thoughtControlLabel, meta: - runtimeThoughtLevel.options.find((item) => item.value === runtimeThoughtLevel.currentValue)?.label || + displayRuntimeThoughtLevel.options.find((item) => item.value === runtimeThoughtLevel.currentValue)?.label || runtimeThoughtLevel.currentValue || '', submenu: { - title: t('agent.thoughtLevel.label'), - options: runtimeThoughtLevel.options.map((item) => ({ + title: thoughtControlLabel, + options: displayRuntimeThoughtLevel.options.map((item) => ({ key: item.value, label: item.label, description: item.description ?? undefined, @@ -518,7 +528,7 @@ Please check your local CLI tool authentication status`, })), onSelect: (value) => { void handleThoughtLevelSetOption(runtimeThoughtLevel.id, value) - .then(() => Message.success(t('agent.thoughtLevel.switchSuccess'))) + .then(() => Message.success(thoughtSwitchSuccess)) .catch((error) => Message.error(t(configErrorMessageKey(error)))); }, }, @@ -597,6 +607,7 @@ Please check your local CLI tool authentication status`, attachEntries, canSwitchModel, currentMode, + displayRuntimeThoughtLevel, handleSheetModeChange, handleThoughtLevelSetOption, isMobile, @@ -608,6 +619,8 @@ Please check your local CLI tool authentication status`, selectModel, setContent, t, + thoughtControlLabel, + thoughtSwitchSuccess, ]); useAddEventListener('acp.selected.file', setAtPath); diff --git a/packages/desktop/src/renderer/pages/conversation/platforms/aionrs/AionrsModelSelector.tsx b/packages/desktop/src/renderer/pages/conversation/platforms/aionrs/AionrsModelSelector.tsx index b688ee6234..8726eb6520 100644 --- a/packages/desktop/src/renderer/pages/conversation/platforms/aionrs/AionrsModelSelector.tsx +++ b/packages/desktop/src/renderer/pages/conversation/platforms/aionrs/AionrsModelSelector.tsx @@ -9,6 +9,8 @@ import type { AcpConfigSetStatus, AcpDerivedOption } from '@/renderer/hooks/agen import { composeRuntimeSelectorLabel, getCurrentThoughtLevelLabel, + isThinkingModeOption, + localizeThinkingModeOption, RUNTIME_SUBMENU_TRIGGER_PROPS, RuntimeSelectorCheckedItem, RuntimeSelectorModelList, @@ -44,6 +46,12 @@ const AionrsModelSelector: React.FC<{ const defaultModelLabel = t('common.defaultModel'); const current_model = selection?.current_model; + const thinkingMode = isThinkingModeOption(thoughtLevel); + const displayThoughtLevel = localizeThinkingModeOption(thoughtLevel, { + enabled: t('agent.thinkingMode.enabled'), + disabled: t('agent.thinkingMode.disabled'), + }); + const thoughtControlLabel = thinkingMode ? t('agent.thinkingMode.label') : t('agent.thoughtLevel.label'); const renderLogo = () => ; @@ -77,7 +85,7 @@ const AionrsModelSelector: React.FC<{ defaultModelLabel, fallbackLabel: t('conversation.welcome.selectModel'), }); - const combinedLabel = composeRuntimeSelectorLabel({ modelLabel: label, thoughtLevel }); + const combinedLabel = composeRuntimeSelectorLabel({ modelLabel: label, thoughtLevel: displayThoughtLevel }); const handleThoughtLevelSelect = (value: string) => { if (!thoughtLevel || value === thoughtLevel.currentValue || !onSetThoughtLevel) return; void onSetThoughtLevel(thoughtLevel.id, value); @@ -134,12 +142,12 @@ const AionrsModelSelector: React.FC<{ triggerProps={RUNTIME_SUBMENU_TRIGGER_PROPS} title={ } > - {thoughtLevel.options.map((item) => ( + {displayThoughtLevel?.options.map((item) => ( { if (!runtimeMode?.currentValue) return; @@ -511,18 +521,18 @@ const AionrsSendBox: React.FC<{ ...attachEntries, ]; - if (runtimeThoughtLevel) { + if (runtimeThoughtLevel && displayRuntimeThoughtLevel) { entries.splice(1, 0, { key: 'thought-level', icon: , - label: t('agent.thoughtLevel.label'), + label: thoughtControlLabel, meta: - runtimeThoughtLevel.options.find((item) => item.value === runtimeThoughtLevel.currentValue)?.label || + displayRuntimeThoughtLevel.options.find((item) => item.value === runtimeThoughtLevel.currentValue)?.label || runtimeThoughtLevel.currentValue || '', submenu: { - title: t('agent.thoughtLevel.label'), - options: runtimeThoughtLevel.options.map((item) => ({ + title: thoughtControlLabel, + options: displayRuntimeThoughtLevel.options.map((item) => ({ key: item.value, label: item.label, description: item.description ?? undefined, @@ -531,7 +541,7 @@ const AionrsSendBox: React.FC<{ onSelect: (value) => { void runtimeConfig .setConfigOption(runtimeThoughtLevel.id, value) - .then(() => Message.success(t('agent.thoughtLevel.switchSuccess'))) + .then(() => Message.success(thoughtSwitchSuccess)) .catch((error) => Message.error(t(configErrorMessageKey(error)))); }, }, @@ -588,6 +598,7 @@ const AionrsSendBox: React.FC<{ }, [ attachEntries, currentMode, + displayRuntimeThoughtLevel, dynamicModes, handleSheetModeChange, handleSheetModelSelect, @@ -600,6 +611,8 @@ const AionrsSendBox: React.FC<{ runtimeThoughtLevel, setContent, t, + thoughtControlLabel, + thoughtSwitchSuccess, ]); useAddEventListener('aionrs.selected.file', setAtPath); diff --git a/packages/desktop/src/renderer/services/i18n/i18n-keys.d.ts b/packages/desktop/src/renderer/services/i18n/i18n-keys.d.ts index 550d6eb155..5db2492dfd 100644 --- a/packages/desktop/src/renderer/services/i18n/i18n-keys.d.ts +++ b/packages/desktop/src/renderer/services/i18n/i18n-keys.d.ts @@ -56,6 +56,10 @@ export type I18nKey = | 'agent.setup.noAlternatives' | 'agent.setup.notConfigured' | 'agent.setup.switching' + | 'agent.thinkingMode.disabled' + | 'agent.thinkingMode.enabled' + | 'agent.thinkingMode.label' + | 'agent.thinkingMode.switchSuccess' | 'agent.thoughtLevel.label' | 'agent.thoughtLevel.switchFailed' | 'agent.thoughtLevel.switchSuccess' diff --git a/packages/desktop/src/renderer/services/i18n/locales/de-DE/agent.json b/packages/desktop/src/renderer/services/i18n/locales/de-DE/agent.json index a4b6c36d8e..8d8c35193b 100644 --- a/packages/desktop/src/renderer/services/i18n/locales/de-DE/agent.json +++ b/packages/desktop/src/renderer/services/i18n/locales/de-DE/agent.json @@ -44,5 +44,11 @@ "label": "Denk-Stufe", "switchSuccess": "Denk-Stufe gewechselt", "switchFailed": "Wechsel der Denk-Stufe fehlgeschlagen" + }, + "thinkingMode": { + "label": "Denkmodus", + "enabled": "Ein", + "disabled": "Aus", + "switchSuccess": "Denkmodus gewechselt" } } diff --git a/packages/desktop/src/renderer/services/i18n/locales/en-US/agent.json b/packages/desktop/src/renderer/services/i18n/locales/en-US/agent.json index de20a3e47c..dc90fa0c43 100644 --- a/packages/desktop/src/renderer/services/i18n/locales/en-US/agent.json +++ b/packages/desktop/src/renderer/services/i18n/locales/en-US/agent.json @@ -44,5 +44,11 @@ "label": "Thinking Level", "switchSuccess": "Thinking level switched", "switchFailed": "Failed to switch thinking level" + }, + "thinkingMode": { + "label": "Thinking Mode", + "enabled": "On", + "disabled": "Off", + "switchSuccess": "Thinking mode switched" } } diff --git a/packages/desktop/src/renderer/services/i18n/locales/es-ES/agent.json b/packages/desktop/src/renderer/services/i18n/locales/es-ES/agent.json index cd062de2d9..bc2ebe8c8a 100644 --- a/packages/desktop/src/renderer/services/i18n/locales/es-ES/agent.json +++ b/packages/desktop/src/renderer/services/i18n/locales/es-ES/agent.json @@ -44,5 +44,11 @@ "label": "Nivel de razonamiento", "switchSuccess": "Nivel de razonamiento cambiado", "switchFailed": "Error al cambiar el nivel de razonamiento" + }, + "thinkingMode": { + "label": "Modo de razonamiento", + "enabled": "Activado", + "disabled": "Desactivado", + "switchSuccess": "Modo de razonamiento cambiado" } } diff --git a/packages/desktop/src/renderer/services/i18n/locales/fa-IR/agent.json b/packages/desktop/src/renderer/services/i18n/locales/fa-IR/agent.json index 7eb1f18331..4be5aec90f 100644 --- a/packages/desktop/src/renderer/services/i18n/locales/fa-IR/agent.json +++ b/packages/desktop/src/renderer/services/i18n/locales/fa-IR/agent.json @@ -44,5 +44,11 @@ "label": "سطح تفکر", "switchSuccess": "سطح تفکر تغییر کرد", "switchFailed": "تغییر سطح تفکر ناموفق بود" + }, + "thinkingMode": { + "label": "حالت تفکر", + "enabled": "فعال", + "disabled": "غیرفعال", + "switchSuccess": "حالت تفکر تغییر کرد" } } diff --git a/packages/desktop/src/renderer/services/i18n/locales/fr-FR/agent.json b/packages/desktop/src/renderer/services/i18n/locales/fr-FR/agent.json index 698d94992e..ec2295dda9 100644 --- a/packages/desktop/src/renderer/services/i18n/locales/fr-FR/agent.json +++ b/packages/desktop/src/renderer/services/i18n/locales/fr-FR/agent.json @@ -42,5 +42,11 @@ "label": "Niveau de réflexion", "switchSuccess": "Niveau de réflexion changé", "switchFailed": "Échec du changement de niveau de réflexion" + }, + "thinkingMode": { + "label": "Mode de réflexion", + "enabled": "Activé", + "disabled": "Désactivé", + "switchSuccess": "Mode de réflexion modifié" } } diff --git a/packages/desktop/src/renderer/services/i18n/locales/ja-JP/agent.json b/packages/desktop/src/renderer/services/i18n/locales/ja-JP/agent.json index b4a116d7e3..127b1b8006 100644 --- a/packages/desktop/src/renderer/services/i18n/locales/ja-JP/agent.json +++ b/packages/desktop/src/renderer/services/i18n/locales/ja-JP/agent.json @@ -44,5 +44,11 @@ "label": "思考レベル", "switchSuccess": "思考レベルを切り替えました", "switchFailed": "思考レベルの切り替えに失敗しました" + }, + "thinkingMode": { + "label": "思考モード", + "enabled": "オン", + "disabled": "オフ", + "switchSuccess": "思考モードを切り替えました" } } diff --git a/packages/desktop/src/renderer/services/i18n/locales/ko-KR/agent.json b/packages/desktop/src/renderer/services/i18n/locales/ko-KR/agent.json index 456ba82408..c64e54b345 100644 --- a/packages/desktop/src/renderer/services/i18n/locales/ko-KR/agent.json +++ b/packages/desktop/src/renderer/services/i18n/locales/ko-KR/agent.json @@ -44,5 +44,11 @@ "label": "사고 강도", "switchSuccess": "사고 강도가 전환되었습니다", "switchFailed": "사고 강도 전환에 실패했습니다" + }, + "thinkingMode": { + "label": "사고 모드", + "enabled": "켜기", + "disabled": "끄기", + "switchSuccess": "사고 모드가 전환되었습니다" } } diff --git a/packages/desktop/src/renderer/services/i18n/locales/pt-BR/agent.json b/packages/desktop/src/renderer/services/i18n/locales/pt-BR/agent.json index 4a36eb5dd4..2c62d37724 100644 --- a/packages/desktop/src/renderer/services/i18n/locales/pt-BR/agent.json +++ b/packages/desktop/src/renderer/services/i18n/locales/pt-BR/agent.json @@ -44,5 +44,11 @@ "label": "Nível de raciocínio", "switchSuccess": "Nível de raciocínio alterado", "switchFailed": "Falha ao alterar o nível de raciocínio" + }, + "thinkingMode": { + "label": "Modo de raciocínio", + "enabled": "Ativado", + "disabled": "Desativado", + "switchSuccess": "Modo de raciocínio alterado" } } diff --git a/packages/desktop/src/renderer/services/i18n/locales/ru-RU/agent.json b/packages/desktop/src/renderer/services/i18n/locales/ru-RU/agent.json index 580ac7f59d..8fcb4eecae 100644 --- a/packages/desktop/src/renderer/services/i18n/locales/ru-RU/agent.json +++ b/packages/desktop/src/renderer/services/i18n/locales/ru-RU/agent.json @@ -44,5 +44,11 @@ "label": "Уровень рассуждений", "switchSuccess": "Уровень рассуждений переключен", "switchFailed": "Не удалось переключить уровень рассуждений" + }, + "thinkingMode": { + "label": "Режим рассуждений", + "enabled": "Включён", + "disabled": "Выключен", + "switchSuccess": "Режим рассуждений переключён" } } diff --git a/packages/desktop/src/renderer/services/i18n/locales/tr-TR/agent.json b/packages/desktop/src/renderer/services/i18n/locales/tr-TR/agent.json index fa7b1551ae..0cb9149b79 100644 --- a/packages/desktop/src/renderer/services/i18n/locales/tr-TR/agent.json +++ b/packages/desktop/src/renderer/services/i18n/locales/tr-TR/agent.json @@ -44,5 +44,11 @@ "label": "Düşünme düzeyi", "switchSuccess": "Düşünme düzeyi değiştirildi", "switchFailed": "Düşünme düzeyi değiştirilemedi" + }, + "thinkingMode": { + "label": "Düşünme modu", + "enabled": "Açık", + "disabled": "Kapalı", + "switchSuccess": "Düşünme modu değiştirildi" } } diff --git a/packages/desktop/src/renderer/services/i18n/locales/uk-UA/agent.json b/packages/desktop/src/renderer/services/i18n/locales/uk-UA/agent.json index 7ee89bce2b..dd6ef8d3ef 100644 --- a/packages/desktop/src/renderer/services/i18n/locales/uk-UA/agent.json +++ b/packages/desktop/src/renderer/services/i18n/locales/uk-UA/agent.json @@ -44,5 +44,11 @@ "label": "Рівень міркування", "switchSuccess": "Рівень міркування змінено", "switchFailed": "Не вдалося змінити рівень міркування" + }, + "thinkingMode": { + "label": "Режим міркування", + "enabled": "Увімкнено", + "disabled": "Вимкнено", + "switchSuccess": "Режим міркування перемкнено" } } diff --git a/packages/desktop/src/renderer/services/i18n/locales/zh-CN/agent.json b/packages/desktop/src/renderer/services/i18n/locales/zh-CN/agent.json index 486d2a1c83..45e38b4d66 100644 --- a/packages/desktop/src/renderer/services/i18n/locales/zh-CN/agent.json +++ b/packages/desktop/src/renderer/services/i18n/locales/zh-CN/agent.json @@ -44,5 +44,11 @@ "label": "思考强度", "switchSuccess": "思考强度切换成功", "switchFailed": "思考强度切换失败" + }, + "thinkingMode": { + "label": "思考模式", + "enabled": "开启", + "disabled": "关闭", + "switchSuccess": "思考模式切换成功" } } diff --git a/packages/desktop/src/renderer/services/i18n/locales/zh-TW/agent.json b/packages/desktop/src/renderer/services/i18n/locales/zh-TW/agent.json index 8a41af3894..7a24c04da8 100644 --- a/packages/desktop/src/renderer/services/i18n/locales/zh-TW/agent.json +++ b/packages/desktop/src/renderer/services/i18n/locales/zh-TW/agent.json @@ -44,5 +44,11 @@ "label": "思考強度", "switchSuccess": "思考強度切換成功", "switchFailed": "思考強度切換失敗" + }, + "thinkingMode": { + "label": "思考模式", + "enabled": "開啟", + "disabled": "關閉", + "switchSuccess": "思考模式切換成功" } } diff --git a/packages/desktop/src/renderer/utils/model/agentRuntimeCatalog.ts b/packages/desktop/src/renderer/utils/model/agentRuntimeCatalog.ts index 371bcdc181..7fdb9a34ad 100644 --- a/packages/desktop/src/renderer/utils/model/agentRuntimeCatalog.ts +++ b/packages/desktop/src/renderer/utils/model/agentRuntimeCatalog.ts @@ -238,6 +238,7 @@ export function buildAgentRuntimeThoughtLevelOption( return buildSelectOptionFromConfigOptions(normalizeConfigOptions(agent.config_options), 'thought_level', [ 'thought_level', 'reasoning_effort', + 'thinking', ]); } diff --git a/tests/unit/acpConfigOptions.test.ts b/tests/unit/acpConfigOptions.test.ts index bcf460e275..cd65059902 100644 --- a/tests/unit/acpConfigOptions.test.ts +++ b/tests/unit/acpConfigOptions.test.ts @@ -56,6 +56,30 @@ describe('ACP config option derivation', () => { expect(thought?.options.map((item) => item.value)).toEqual(['low', 'high']); }); + it('recognizes a binary thinking option from a third-party runtime', () => { + const backendOptions = [ + { + id: 'thinking', + option_type: 'select', + current_value: 'enabled', + options: [ + { value: 'enabled', name: 'Enabled' }, + { value: 'disabled', name: 'Disabled' }, + ], + }, + ] as AcpConfigOptionDto[]; + + const thought = deriveSelectOption(backendOptions, 'thought_level', [ + 'thought_level', + 'reasoning_effort', + 'thinking', + ]); + + expect(thought?.id).toBe('thinking'); + expect(thought?.currentValue).toBe('enabled'); + expect(thought?.options.map((item) => item.value)).toEqual(['enabled', 'disabled']); + }); + it('accepts only observed set responses with matching current_value', () => { const response: SetConfigOptionResponse = { confirmation: 'observed', diff --git a/tests/unit/renderer/AcpModelSelector.dom.test.tsx b/tests/unit/renderer/AcpModelSelector.dom.test.tsx index 0303227441..44714932ff 100644 --- a/tests/unit/renderer/AcpModelSelector.dom.test.tsx +++ b/tests/unit/renderer/AcpModelSelector.dom.test.tsx @@ -47,6 +47,16 @@ const thoughtLevel: AcpDerivedOption = { ], }; +const thinkingMode: AcpDerivedOption = { + id: 'thinking', + category: 'thought_level', + currentValue: 'enabled', + options: [ + { value: 'enabled', label: 'Enabled' }, + { value: 'disabled', label: 'Disabled' }, + ], +}; + const makeResult = (overrides: Partial = {}): MockAcpModelInfoResult => ({ model_info: modelInfo, canSwitch: true, @@ -96,6 +106,10 @@ vi.mock('react-i18next', () => ({ t: (key: string, options?: { defaultValue?: string }) => { if (key === 'agent.thoughtLevel.label') return 'Thinking Level'; if (key === 'agent.thoughtLevel.switchSuccess') return 'agent.thoughtLevel.switchSuccess'; + if (key === 'agent.thinkingMode.label') return 'Thinking Mode'; + if (key === 'agent.thinkingMode.enabled') return 'On'; + if (key === 'agent.thinkingMode.disabled') return 'Off'; + if (key === 'agent.thinkingMode.switchSuccess') return 'agent.thinkingMode.switchSuccess'; if (key === 'agent.config.commandAck') return 'agent.config.commandAck'; if (key === 'common.model') return 'Model'; if (key === 'common.defaultModel') return 'Default'; @@ -360,6 +374,22 @@ describe('AcpModelSelector runtime options', () => { expect(messageSuccessMock).toHaveBeenCalledWith('agent.thoughtLevel.switchSuccess'); }); + it('renders and switches a binary thinking mode exposed by the runtime', async () => { + const setConfigOption = vi.fn().mockResolvedValue(undefined); + useAcpModelInfoMock.mockReturnValue(makeResult({ thoughtLevel: thinkingMode, setConfigOption })); + + render(); + + expect(screen.getByTestId('acp-model-selector')).toHaveTextContent('GPT-5.2 · On'); + expect(screen.getAllByTestId('submenu-title')[1]).toHaveTextContent('Thinking Mode'); + fireEvent.click(screen.getByText('Off')); + + await waitFor(() => { + expect(setConfigOption).toHaveBeenCalledWith('thinking', 'disabled'); + }); + expect(messageSuccessMock).toHaveBeenCalledWith('agent.thinkingMode.switchSuccess'); + }); + it('keeps the old thought value and shows an error when config update fails', async () => { const setConfigOption = vi.fn().mockRejectedValue(new Error('command_ack')); useAcpModelInfoMock.mockReturnValue(makeResult({ setConfigOption })); diff --git a/tests/unit/renderer/AionrsModelSelector.dom.test.tsx b/tests/unit/renderer/AionrsModelSelector.dom.test.tsx index c5aab041fb..1c3af6e0bc 100644 --- a/tests/unit/renderer/AionrsModelSelector.dom.test.tsx +++ b/tests/unit/renderer/AionrsModelSelector.dom.test.tsx @@ -30,6 +30,16 @@ const thoughtLevel: AcpDerivedOption = { ], }; +const thinkingMode: AcpDerivedOption = { + id: 'thinking', + category: 'thought_level', + currentValue: 'enabled', + options: [ + { value: 'enabled', label: 'Enabled' }, + { value: 'disabled', label: 'Disabled' }, + ], +}; + const makeSelection = (overrides: Partial = {}): AionrsModelSelection => ({ current_model: { ...provider, @@ -72,6 +82,9 @@ vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, options?: { defaultValue?: string }) => { if (key === 'agent.thoughtLevel.label') return 'Thinking Level'; + if (key === 'agent.thinkingMode.label') return 'Thinking Mode'; + if (key === 'agent.thinkingMode.enabled') return 'On'; + if (key === 'agent.thinkingMode.disabled') return 'Off'; if (key === 'common.model') return 'Model'; if (key === 'conversation.welcome.selectModel') return 'Select model'; if (key === 'conversation.welcome.useCliModel') return 'Use CLI model'; @@ -253,4 +266,25 @@ describe('AionrsModelSelector runtime options', () => { expect(onSetThoughtLevel).toHaveBeenCalledWith('reasoning_effort', 'low'); }); }); + + it('renders and switches GLM-5.2 binary thinking mode', async () => { + const onSetThoughtLevel = vi.fn().mockResolvedValue(undefined); + + render( + + ); + + expect(screen.getByTestId('aionrs-model-selector')).toHaveTextContent('gpt-5.2 · On'); + expect(screen.getAllByTestId('submenu-title')[1]).toHaveTextContent('Thinking Mode'); + fireEvent.click(screen.getByText('Off')); + + await waitFor(() => { + expect(onSetThoughtLevel).toHaveBeenCalledWith('thinking', 'disabled'); + }); + }); });