feat(reasoning): full /reasoning CLI parity — show|hide + effort levels via config.yaml (#812)
Closes #461 Adds full /reasoning CLI parity to the WebUI slash command system: - /reasoning show|on → window._showThinking = true; writes display.show_reasoning to config.yaml (same key as CLI); mirrors to settings.json for boot.js - /reasoning hide|off → same in reverse; re-renders immediately - /reasoning none|minimal|low|medium|high|xhigh → POST /api/reasoning → writes agent.reasoning_effort to config.yaml; takes effect next turn (matching CLI semantics) - /reasoning (no args) → GET /api/reasoning → live status toast from config.yaml - Autocomplete shows all 8 options: show|hide|none|minimal|low|medium|high|xhigh - Profile-isolated: _get_config_path() is thread-local so per-profile settings never bleed across - Boot hydration: window._showThinking initialised from settings.json show_thinking on page load - Inspect.signature guard in streaming.py so older hermes-agent builds don't TypeError 28 new tests, 1708/1708 total passing. Full browser QA on port 8789 with isolated state. CLI/config.yaml sync verified with hermes_constants.parse_reasoning_effort().
This commit is contained in:
@@ -764,6 +764,7 @@ function applyBotName(){
|
||||
window._showCliSessions=!!s.show_cli_sessions;
|
||||
window._soundEnabled=!!s.sound_enabled;
|
||||
window._notificationsEnabled=!!s.notifications_enabled;
|
||||
window._showThinking=s.show_thinking!==false;
|
||||
window._sidebarDensity=(s.sidebar_density==='detailed'?'detailed':'compact');
|
||||
window._botName=s.bot_name||'Hermes';
|
||||
const appearance=_normalizeAppearance(s.theme,s.skin);
|
||||
@@ -785,6 +786,7 @@ function applyBotName(){
|
||||
window._showCliSessions=false;
|
||||
window._soundEnabled=false;
|
||||
window._notificationsEnabled=false;
|
||||
window._showThinking=true;
|
||||
window._sidebarDensity='compact';
|
||||
window._botName='Hermes';
|
||||
_bootSettings={check_for_updates:false};
|
||||
|
||||
@@ -20,12 +20,12 @@ const COMMANDS=[
|
||||
{name:'undo', desc:t('cmd_undo'), fn:cmdUndo},
|
||||
{name:'status', desc:t('cmd_status'), fn:cmdStatus},
|
||||
{name:'voice', desc:t('cmd_voice'), fn:cmdVoice},
|
||||
{name:'reasoning', desc:t('cmd_reasoning'), fn:cmdReasoning, arg:'show|hide|none|minimal|low|medium|high|xhigh', subArgs:['show','hide','none','minimal','low','medium','high','xhigh']},
|
||||
];
|
||||
|
||||
const SLASH_SUBARG_SOURCES={
|
||||
model:{desc:t('cmd_model'), subArgs:'models'},
|
||||
personality:{desc:t('cmd_personality'), subArgs:'personalities'},
|
||||
reasoning:{desc:'Set reasoning effort', subArgs:['low','medium','high']},
|
||||
};
|
||||
|
||||
function parseCommand(text){
|
||||
@@ -41,8 +41,10 @@ function executeCommand(text){
|
||||
if(!parsed)return false;
|
||||
const cmd=COMMANDS.find(c=>c.name===parsed.name);
|
||||
if(!cmd)return false;
|
||||
cmd.fn(parsed.args);
|
||||
return true;
|
||||
// A handler may return `false` to opt out of interception — e.g. /reasoning
|
||||
// with an effort level falls through so the agent's own handler sees it,
|
||||
// preserving the pre-existing pass-through behaviour for that subcommand.
|
||||
return cmd.fn(parsed.args) !== false;
|
||||
}
|
||||
|
||||
function getMatchingCommands(prefix){
|
||||
@@ -572,6 +574,56 @@ async function cmdStatus(){
|
||||
renderMessages();
|
||||
}catch(e){showToast(t('status_load_failed')+e.message);}
|
||||
}
|
||||
function cmdReasoning(args){
|
||||
const arg=(args||'').trim().toLowerCase();
|
||||
const BRAIN='\uD83E\uDDE0';
|
||||
// Matches hermes_constants.VALID_REASONING_EFFORTS + 'none' (CLI parity).
|
||||
const EFFORTS=['none','minimal','low','medium','high','xhigh'];
|
||||
// Shared status renderer used by the no-args branch and as a fallback.
|
||||
function _fmtStatus(st){
|
||||
const vis=(st && st.show_reasoning===false)?'off':'on';
|
||||
const eff=(st && st.reasoning_effort)||'default';
|
||||
return BRAIN+' Reasoning effort: '+eff+' \u00B7 display: '+vis
|
||||
+' | /reasoning show|hide|none|minimal|low|medium|high|xhigh';
|
||||
}
|
||||
if(!arg){
|
||||
// Status — read from the same config.yaml keys the CLI uses.
|
||||
api('/api/reasoning').then(function(st){showToast(_fmtStatus(st));})
|
||||
.catch(function(){showToast(BRAIN+' /reasoning — status unavailable');});
|
||||
return true;
|
||||
}
|
||||
if(arg==='show'||arg==='on'||arg==='hide'||arg==='off'){
|
||||
const on=(arg==='show'||arg==='on');
|
||||
// Update the UI render gate immediately for responsiveness.
|
||||
window._showThinking=on;
|
||||
if(typeof renderMessages==='function') renderMessages();
|
||||
// Persist via /api/reasoning → config.yaml display.show_reasoning
|
||||
// (CLI reads the same key). Also mirror into WebUI settings.json
|
||||
// show_thinking so boot.js picks it up on reload without hitting
|
||||
// /api/reasoning on every page load.
|
||||
api('/api/reasoning',{method:'POST',body:JSON.stringify({display:arg})}).catch(function(){});
|
||||
api('/api/settings',{method:'POST',body:JSON.stringify({show_thinking:on})}).catch(function(){});
|
||||
showToast(BRAIN+' Thinking blocks: '+(on?'on':'off')+' (saved)');
|
||||
return true;
|
||||
}
|
||||
if(EFFORTS.includes(arg)){
|
||||
// Persist via /api/reasoning → config.yaml agent.reasoning_effort.
|
||||
// Takes effect on the NEXT session/turn (agent re-reads config at
|
||||
// construction time), matching CLI semantics where `/reasoning high`
|
||||
// also forces an agent re-init.
|
||||
api('/api/reasoning',{method:'POST',body:JSON.stringify({effort:arg})})
|
||||
.then(function(st){
|
||||
const eff=(st && st.reasoning_effort)||arg;
|
||||
showToast(BRAIN+' Reasoning effort set to '+eff+' (saved; applies to next turn)');
|
||||
})
|
||||
.catch(function(e){
|
||||
showToast(BRAIN+' Failed to set effort: '+(e && e.message ? e.message : arg));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
showToast('Unknown argument: '+arg+' \u2014 use show|hide|'+EFFORTS.join('|'));
|
||||
return true;
|
||||
}
|
||||
function cmdVoice(){
|
||||
const mic=document.getElementById('btnMic');
|
||||
if(mic&&mic.style.display!=='none'&&!mic.disabled){try{mic.click();return;}catch(_){}}
|
||||
|
||||
@@ -195,6 +195,7 @@ const LOCALES = {
|
||||
settings_label_language: 'Language',
|
||||
settings_label_token_usage: 'Show token usage',
|
||||
settings_label_sidebar_density: 'Sidebar density',
|
||||
cmd_reasoning: 'Toggle thinking block visibility (show/hide) or set effort level',
|
||||
settings_label_cli_sessions: 'Show agent sessions',
|
||||
settings_label_sync_insights: 'Sync to insights',
|
||||
settings_label_check_updates: 'Check for updates',
|
||||
@@ -626,6 +627,7 @@ const LOCALES = {
|
||||
settings_label_language: 'Язык',
|
||||
settings_label_token_usage: 'Показывать использование токенов',
|
||||
settings_label_sidebar_density: 'Плотность боковой панели',
|
||||
cmd_reasoning: 'Toggle thinking block visibility (show/hide) or set effort level',
|
||||
settings_label_cli_sessions: 'Показывать сеансы агента',
|
||||
settings_label_sync_insights: 'Синхронизировать с Insights',
|
||||
settings_label_check_updates: 'Проверять обновления',
|
||||
@@ -1085,6 +1087,7 @@ const LOCALES = {
|
||||
settings_label_language: 'Idioma',
|
||||
settings_label_token_usage: 'Mostrar uso de tokens',
|
||||
settings_label_sidebar_density: 'Densidad de la barra lateral',
|
||||
cmd_reasoning: 'Toggle thinking block visibility (show/hide) or set effort level',
|
||||
settings_label_cli_sessions: 'Mostrar sesiones de CLI',
|
||||
settings_label_sync_insights: 'Sincronizar con insights',
|
||||
settings_label_check_updates: 'Buscar actualizaciones',
|
||||
@@ -1516,6 +1519,7 @@ const LOCALES = {
|
||||
settings_label_language: 'Sprache',
|
||||
settings_label_token_usage: 'Token-Verbrauch anzeigen',
|
||||
settings_label_sidebar_density: 'Seitenleistendichte',
|
||||
cmd_reasoning: 'Toggle thinking block visibility (show/hide) or set effort level',
|
||||
settings_label_cli_sessions: 'Agent-Sitzungen anzeigen',
|
||||
settings_label_sync_insights: 'Mit Insights synchronisieren',
|
||||
settings_label_check_updates: 'Nach Updates suchen',
|
||||
@@ -1748,6 +1752,7 @@ const LOCALES = {
|
||||
settings_label_language: '\u8bed\u8a00',
|
||||
settings_label_token_usage: '\u663e\u793a token \u7528\u91cf',
|
||||
settings_label_sidebar_density: '侧边栏密度',
|
||||
cmd_reasoning: 'Toggle thinking block visibility (show/hide) or set effort level',
|
||||
settings_label_cli_sessions: '\u663e\u793a CLI \u4f1a\u8bdd',
|
||||
settings_label_sync_insights: '\u540c\u6b65\u5230 insights',
|
||||
settings_label_check_updates: '\u68c0\u67e5\u66f4\u65b0',
|
||||
@@ -2163,6 +2168,7 @@ const LOCALES = {
|
||||
settings_label_language: '\u8a9d\u8a00',
|
||||
settings_label_token_usage: '\u986f\u793a token \u7528\u91cf',
|
||||
settings_label_sidebar_density: '側邊欄密度',
|
||||
cmd_reasoning: 'Toggle thinking block visibility (show/hide) or set effort level',
|
||||
settings_label_cli_sessions: '\u986f\u793a CLI \u6703\u8a71',
|
||||
settings_label_sync_insights: '\u540c\u6b65\u5230 insights',
|
||||
settings_label_check_updates: '\u6aa2\u67e5\u66f4\u65b0',
|
||||
|
||||
@@ -295,6 +295,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
return {thinkingText:'', displayText:raw, inThinking:false};
|
||||
}
|
||||
function _renderLiveThinking(parsed){
|
||||
if(window._showThinking===false){removeThinking();return;}
|
||||
const text=(parsed&&parsed.thinkingText)||'';
|
||||
if(text||(parsed&&parsed.inThinking)){
|
||||
if(typeof updateThinking==='function') updateThinking(text||'Thinking…');
|
||||
|
||||
@@ -1272,6 +1272,7 @@ async function loadSettingsPanel(){
|
||||
if(soundCb){soundCb.checked=!!settings.sound_enabled;soundCb.addEventListener('change',_markSettingsDirty,{once:false});}
|
||||
const notifCb=$('settingsNotificationsEnabled');
|
||||
if(notifCb){notifCb.checked=!!settings.notifications_enabled;notifCb.addEventListener('change',_markSettingsDirty,{once:false});}
|
||||
// show_thinking has no settings panel checkbox — controlled via /reasoning show|hide
|
||||
const sidebarDensitySel=$('settingsSidebarDensity');
|
||||
if(sidebarDensitySel){
|
||||
sidebarDensitySel.value=settings.sidebar_density==='detailed'?'detailed':'compact';
|
||||
@@ -1309,6 +1310,7 @@ function _applySavedSettingsUi(saved, body, opts){
|
||||
window._showCliSessions=showCliSessions;
|
||||
window._soundEnabled=body.sound_enabled;
|
||||
window._notificationsEnabled=body.notifications_enabled;
|
||||
window._showThinking=body.show_thinking!==false;
|
||||
window._sidebarDensity=sidebarDensity==='detailed'?'detailed':'compact';
|
||||
window._botName=body.bot_name||'Hermes';
|
||||
if(typeof applyBotName==='function') applyBotName();
|
||||
@@ -1353,6 +1355,7 @@ async function saveSettings(andClose){
|
||||
body.check_for_updates=!!($('settingsCheckUpdates')||{}).checked;
|
||||
body.sound_enabled=!!($('settingsSoundEnabled')||{}).checked;
|
||||
body.notifications_enabled=!!($('settingsNotificationsEnabled')||{}).checked;
|
||||
body.show_thinking=window._showThinking!==false;
|
||||
body.sidebar_density=sidebarDensity;
|
||||
const botName=(($('settingsBotName')||{}).value||'').trim();
|
||||
body.bot_name=botName||'Hermes';
|
||||
|
||||
@@ -1573,7 +1573,7 @@ function renderMessages(){
|
||||
seg.setAttribute('data-live-assistant','1');
|
||||
}
|
||||
if(_ERR_MSG_RE.test(String(content||'').trim())) seg.dataset.error='1';
|
||||
if(thinkingText) seg.insertAdjacentHTML('beforeend', _thinkingCardHtml(thinkingText));
|
||||
if(thinkingText&&window._showThinking!==false) seg.insertAdjacentHTML('beforeend', _thinkingCardHtml(thinkingText));
|
||||
const hasVisibleBody=!!(String(content||'').trim()||filesHtml);
|
||||
if(hasVisibleBody){
|
||||
seg.insertAdjacentHTML('beforeend', `${filesHtml}<div class="msg-body">${bodyHtml}</div>${footHtml}`);
|
||||
|
||||
Reference in New Issue
Block a user