fix: make TPS header display optional

This commit is contained in:
Michael Lam authored and test committed 2026-05-04 21:26:43 +00:00
1 parent 3ad8846a27
commit 89099928db
9 files changed
+93 -11

No files matched your search

+2
View File
@@ -3082,6 +3082,7 @@ _SETTINGS_DEFAULTS = {
"onboarding_completed": False, "onboarding_completed": False,
"send_key": "enter", # 'enter' or 'ctrl+enter' "send_key": "enter", # 'enter' or 'ctrl+enter'
"show_token_usage": False, # show input/output token badge below assistant messages "show_token_usage": False, # show input/output token badge below assistant messages
"show_tps": False, # show tokens-per-second chip in assistant message headers
"show_cli_sessions": False, # merge CLI sessions from state.db into the sidebar "show_cli_sessions": False, # merge CLI sessions from state.db into the sidebar
"sync_to_insights": False, # mirror WebUI token usage to state.db for /insights "sync_to_insights": False, # mirror WebUI token usage to state.db for /insights
"check_for_updates": True, # check if webui/agent repos are behind upstream "check_for_updates": True, # check if webui/agent repos are behind upstream
@@ -3207,6 +3208,7 @@ _SETTINGS_ENUM_VALUES = {
_SETTINGS_BOOL_KEYS = { _SETTINGS_BOOL_KEYS = {
"onboarding_completed", "onboarding_completed",
"show_token_usage", "show_token_usage",
"show_tps",
"show_cli_sessions", "show_cli_sessions",
"sync_to_insights", "sync_to_insights",
"check_for_updates", "check_for_updates",
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

+2
View File
@@ -1238,6 +1238,7 @@ function applyBotName(){
_bootSettings=s; _bootSettings=s;
window._sendKey=s.send_key||'enter'; window._sendKey=s.send_key||'enter';
window._showTokenUsage=!!s.show_token_usage; window._showTokenUsage=!!s.show_token_usage;
window._showTps=!!s.show_tps;
window._showCliSessions=!!s.show_cli_sessions; window._showCliSessions=!!s.show_cli_sessions;
window._soundEnabled=!!s.sound_enabled; window._soundEnabled=!!s.sound_enabled;
window._notificationsEnabled=!!s.notifications_enabled; window._notificationsEnabled=!!s.notifications_enabled;
@@ -1271,6 +1272,7 @@ function applyBotName(){
}catch(e){ }catch(e){
window._sendKey='enter'; window._sendKey='enter';
window._showTokenUsage=false; window._showTokenUsage=false;
window._showTps=false;
window._showCliSessions=false; window._showCliSessions=false;
window._soundEnabled=false; window._soundEnabled=false;
window._notificationsEnabled=false; window._notificationsEnabled=false;
+7
View File
@@ -829,6 +829,13 @@
</label> </label>
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_token_usage">Displays input/output token count below each assistant reply. Also toggled with <code>/usage</code>.</div> <div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_token_usage">Displays input/output token count below each assistant reply. Also toggled with <code>/usage</code>.</div>
</div> </div>
<div class="settings-field">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" id="settingsShowTps" style="width:15px;height:15px;accent-color:var(--accent)">
<span>Show token speed (TPS)</span>
</label>
<div style="font-size:11px;color:var(--muted);margin-top:4px">Displays tokens per second in assistant message headers while streaming and after a response completes. Off by default.</div>
</div>
<div class="settings-field"> <div class="settings-field">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer"> <label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" id="settingsSimplifiedToolCalling" style="width:15px;height:15px;accent-color:var(--accent)"> <input type="checkbox" id="settingsSimplifiedToolCalling" style="width:15px;height:15px;accent-color:var(--accent)">
+15 -3
View File
@@ -2922,6 +2922,8 @@ function _preferencesPayloadFromUi(){
if(langSel) payload.language=langSel.value; if(langSel) payload.language=langSel.value;
const showUsageCb=$('settingsShowTokenUsage'); const showUsageCb=$('settingsShowTokenUsage');
if(showUsageCb) payload.show_token_usage=showUsageCb.checked; if(showUsageCb) payload.show_token_usage=showUsageCb.checked;
const showTpsCb=$('settingsShowTps');
if(showTpsCb) payload.show_tps=showTpsCb.checked;
const simplifiedToolCb=$('settingsSimplifiedToolCalling'); const simplifiedToolCb=$('settingsSimplifiedToolCalling');
if(simplifiedToolCb) payload.simplified_tool_calling=simplifiedToolCb.checked; if(simplifiedToolCb) payload.simplified_tool_calling=simplifiedToolCb.checked;
const apiRedactCb=$('settingsApiRedact'); const apiRedactCb=$('settingsApiRedact');
@@ -2988,6 +2990,11 @@ async function _autosavePreferencesSettings(payload){
if(typeof clearMessageRenderCache==='function') clearMessageRenderCache(); if(typeof clearMessageRenderCache==='function') clearMessageRenderCache();
if(typeof renderMessages==='function') renderMessages(); if(typeof renderMessages==='function') renderMessages();
} }
if(payload&&payload.show_tps!==undefined){
window._showTps=!!(saved&&saved.show_tps);
if(typeof clearMessageRenderCache==='function') clearMessageRenderCache();
if(typeof renderMessages==='function') renderMessages();
}
_settingsPreferencesAutosaveRetryPayload=null; _settingsPreferencesAutosaveRetryPayload=null;
_setPreferencesAutosaveStatus('saved'); _setPreferencesAutosaveStatus('saved');
// Only clear the global dirty flag and hide the unsaved-changes bar when // Only clear the global dirty flag and hide the unsaved-changes bar when
@@ -3128,6 +3135,8 @@ async function loadSettingsPanel(){
} }
const showUsageCb=$('settingsShowTokenUsage'); const showUsageCb=$('settingsShowTokenUsage');
if(showUsageCb){showUsageCb.checked=!!settings.show_token_usage;showUsageCb.addEventListener('change',_schedulePreferencesAutosave,{once:false});} if(showUsageCb){showUsageCb.checked=!!settings.show_token_usage;showUsageCb.addEventListener('change',_schedulePreferencesAutosave,{once:false});}
const showTpsCb=$('settingsShowTps');
if(showTpsCb){showTpsCb.checked=!!settings.show_tps;showTpsCb.addEventListener('change',_schedulePreferencesAutosave,{once:false});}
const simplifiedToolCb=$('settingsSimplifiedToolCalling'); const simplifiedToolCb=$('settingsSimplifiedToolCalling');
if(simplifiedToolCb){simplifiedToolCb.checked=settings.simplified_tool_calling!==false;simplifiedToolCb.addEventListener('change',_schedulePreferencesAutosave,{once:false});} if(simplifiedToolCb){simplifiedToolCb.checked=settings.simplified_tool_calling!==false;simplifiedToolCb.addEventListener('change',_schedulePreferencesAutosave,{once:false});}
const apiRedactCb=$('settingsApiRedact'); const apiRedactCb=$('settingsApiRedact');
@@ -3562,9 +3571,10 @@ function _setSettingsAuthButtonsVisible(active){
} }
function _applySavedSettingsUi(saved, body, opts){ function _applySavedSettingsUi(saved, body, opts){
const {sendKey,showTokenUsage,showCliSessions,theme,skin,language,sidebarDensity,fontSize}=opts; const {sendKey,showTokenUsage,showTps,showCliSessions,theme,skin,language,sidebarDensity,fontSize}=opts;
window._sendKey=sendKey||'enter'; window._sendKey=sendKey||'enter';
window._showTokenUsage=showTokenUsage; window._showTokenUsage=showTokenUsage;
window._showTps=showTps;
window._showCliSessions=showCliSessions; window._showCliSessions=showCliSessions;
window._soundEnabled=body.sound_enabled; window._soundEnabled=body.sound_enabled;
window._notificationsEnabled=body.notifications_enabled; window._notificationsEnabled=body.notifications_enabled;
@@ -3652,6 +3662,7 @@ async function saveSettings(andClose){
const modelChanged=(model||'')!==(_settingsHermesDefaultModelOnOpen||''); const modelChanged=(model||'')!==(_settingsHermesDefaultModelOnOpen||'');
const sendKey=($('settingsSendKey')||{}).value; const sendKey=($('settingsSendKey')||{}).value;
const showTokenUsage=!!($('settingsShowTokenUsage')||{}).checked; const showTokenUsage=!!($('settingsShowTokenUsage')||{}).checked;
const showTps=!!($('settingsShowTps')||{}).checked;
const showCliSessions=!!($('settingsShowCliSessions')||{}).checked; const showCliSessions=!!($('settingsShowCliSessions')||{}).checked;
const pw=($('settingsPassword')||{}).value; const pw=($('settingsPassword')||{}).value;
const theme=($('settingsTheme')||{}).value||'dark'; const theme=($('settingsTheme')||{}).value||'dark';
@@ -3668,6 +3679,7 @@ async function saveSettings(andClose){
body.font_size=fontSize; body.font_size=fontSize;
body.language=language; body.language=language;
body.show_token_usage=showTokenUsage; body.show_token_usage=showTokenUsage;
body.show_tps=showTps;
body.simplified_tool_calling=!!($('settingsSimplifiedToolCalling')||{}).checked; body.simplified_tool_calling=!!($('settingsSimplifiedToolCalling')||{}).checked;
body.api_redact_enabled=!!($('settingsApiRedact')||{}).checked; body.api_redact_enabled=!!($('settingsApiRedact')||{}).checked;
body.show_cli_sessions=showCliSessions; body.show_cli_sessions=showCliSessions;
@@ -3693,7 +3705,7 @@ async function saveSettings(andClose){
if(typeof showToast==='function') showToast('Failed to update default model — settings saved'); if(typeof showToast==='function') showToast('Failed to update default model — settings saved');
} }
} }
_applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,skin,language,sidebarDensity,fontSize}); _applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showTps,showCliSessions,theme,skin,language,sidebarDensity,fontSize});
showToast(t(saved.auth_just_enabled?'settings_saved_pw':'settings_saved_pw_updated')); showToast(t(saved.auth_just_enabled?'settings_saved_pw':'settings_saved_pw_updated'));
_settingsDirty=false; _settingsDirty=false;
_resetSettingsPanelState(); _resetSettingsPanelState();
@@ -3712,7 +3724,7 @@ async function saveSettings(andClose){
if(typeof showToast==='function') showToast('Failed to update default model — settings saved'); if(typeof showToast==='function') showToast('Failed to update default model — settings saved');
} }
} }
_applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showCliSessions,theme,skin,language,sidebarDensity,fontSize}); _applySavedSettingsUi(saved, body, {sendKey,showTokenUsage,showTps,showCliSessions,theme,skin,language,sidebarDensity,fontSize});
showToast(t('settings_saved')); showToast(t('settings_saved'));
_settingsDirty=false; _settingsDirty=false;
_resetSettingsPanelState(); _resetSettingsPanelState();
+6 -3
View File
@@ -3185,9 +3185,12 @@ function _formatTurnTps(value){
const fixed=n>=100?Math.round(n).toLocaleString():n>=10?n.toFixed(1):n.toFixed(1); const fixed=n>=100?Math.round(n).toLocaleString():n>=10?n.toFixed(1):n.toFixed(1);
return `${fixed} t/s`; return `${fixed} t/s`;
} }
function isTpsDisplayEnabled(){
return window._showTps===true;
}
function _assistantRoleHtml(tsTitle='', tpsText=''){ function _assistantRoleHtml(tsTitle='', tpsText=''){
const _bn=window._botName||'Hermes'; const _bn=window._botName||'Hermes';
const tps=tpsText?`<span class="msg-tps-inline" title="Tokens per second">${esc(tpsText)}</span>`:''; const tps=(isTpsDisplayEnabled()&&tpsText)?`<span class="msg-tps-inline" title="Tokens per second">${esc(tpsText)}</span>`:'';
return `<div class="msg-role assistant" ${tsTitle?`title="${esc(tsTitle)}"`:''}><div class="role-icon assistant">${esc(_bn.charAt(0).toUpperCase())}</div><span style="font-size:12px">${esc(_bn)}</span>${tps}</div>`; return `<div class="msg-role assistant" ${tsTitle?`title="${esc(tsTitle)}"`:''}><div class="role-icon assistant">${esc(_bn.charAt(0).toUpperCase())}</div><span style="font-size:12px">${esc(_bn)}</span>${tps}</div>`;
} }
function _setAssistantTurnTps(turn, tpsText=''){ function _setAssistantTurnTps(turn, tpsText=''){
@@ -3206,7 +3209,7 @@ function _setAssistantTurnTps(turn, tpsText=''){
chip.textContent=text; chip.textContent=text;
} }
function _setLiveAssistantTps(value){ function _setLiveAssistantTps(value){
_setAssistantTurnTps($('liveAssistantTurn'), _formatTurnTps(value)); _setAssistantTurnTps($('liveAssistantTurn'), isTpsDisplayEnabled()?_formatTurnTps(value):'');
} }
function _createAssistantTurn(tsTitle='', tpsText=''){ function _createAssistantTurn(tsTitle='', tpsText=''){
const row=document.createElement('div'); const row=document.createElement('div');
@@ -3862,7 +3865,7 @@ function renderMessages(){
} }
if(!currentAssistantTurn){ if(!currentAssistantTurn){
currentAssistantTurn=_createAssistantTurn(tsTitle, _formatTurnTps(m._turnTps)); currentAssistantTurn=_createAssistantTurn(tsTitle, isTpsDisplayEnabled()?_formatTurnTps(m._turnTps):'');
inner.appendChild(currentAssistantTurn); inner.appendChild(currentAssistantTurn);
} }
const seg=document.createElement('div'); const seg=document.createElement('div');
+6 -5
View File
@@ -3,9 +3,9 @@
Mirrors the structure of test_1003_appearance_autosave.py to verify the Mirrors the structure of test_1003_appearance_autosave.py to verify the
preferences-panel autosave pattern is wired correctly: preferences-panel autosave pattern is wired correctly:
- All 13 preference fields use _schedulePreferencesAutosave (not _markSettingsDirty) - All 14 preference fields use _schedulePreferencesAutosave (not _markSettingsDirty)
- Password field MUST still call _markSettingsDirty (security: never autosave) - Password field MUST still call _markSettingsDirty (security: never autosave)
- _preferencesPayloadFromUi covers all 13 fields - _preferencesPayloadFromUi covers all 14 fields
- _setPreferencesAutosaveStatus uses the shared i18n keys - _setPreferencesAutosaveStatus uses the shared i18n keys
- Status div exists in static/index.html - Status div exists in static/index.html
- _autosavePreferencesSettings clears the dirty flag and hides the unsaved bar - _autosavePreferencesSettings clears the dirty flag and hides the unsaved bar
@@ -38,6 +38,7 @@ PREFERENCE_FIELDS_AUTOSAVE = [
("settingsSendKey", "send_key"), ("settingsSendKey", "send_key"),
("settingsLanguage", "language"), ("settingsLanguage", "language"),
("settingsShowTokenUsage", "show_token_usage"), ("settingsShowTokenUsage", "show_token_usage"),
("settingsShowTps", "show_tps"),
("settingsSimplifiedToolCalling", "simplified_tool_calling"), ("settingsSimplifiedToolCalling", "simplified_tool_calling"),
("settingsShowCliSessions", "show_cli_sessions"), ("settingsShowCliSessions", "show_cli_sessions"),
("settingsSyncInsights", "sync_to_insights"), ("settingsSyncInsights", "sync_to_insights"),
@@ -51,8 +52,8 @@ PREFERENCE_FIELDS_AUTOSAVE = [
] ]
def test_all_13_preference_fields_have_autosave_payload_entries(): def test_all_14_preference_fields_have_autosave_payload_entries():
"""_preferencesPayloadFromUi must include all 13 preference fields.""" """_preferencesPayloadFromUi must include all 14 preference fields."""
block = _function_block(PANELS_JS, "_preferencesPayloadFromUi") block = _function_block(PANELS_JS, "_preferencesPayloadFromUi")
for dom_id, field in PREFERENCE_FIELDS_AUTOSAVE: for dom_id, field in PREFERENCE_FIELDS_AUTOSAVE:
assert f"$('{dom_id}')" in block, \ assert f"$('{dom_id}')" in block, \
@@ -62,7 +63,7 @@ def test_all_13_preference_fields_have_autosave_payload_entries():
def test_preference_fields_use_schedule_autosave_not_mark_dirty(): def test_preference_fields_use_schedule_autosave_not_mark_dirty():
"""All 12 listener attachments (excluding bot_name's debounce wrapper) must """All 13 listener attachments (excluding bot_name's debounce wrapper) must
use _schedulePreferencesAutosave. bot_name uses a wrapper but still use _schedulePreferencesAutosave. bot_name uses a wrapper but still
eventually calls _schedulePreferencesAutosave.""" eventually calls _schedulePreferencesAutosave."""
panel = _load_settings_panel_block() panel = _load_settings_panel_block()
@@ -8,8 +8,12 @@ Product decision:
from pathlib import Path from pathlib import Path
REPO = Path(__file__).resolve().parent.parent REPO = Path(__file__).resolve().parent.parent
CONFIG_PY = (REPO / "api" / "config.py").read_text(encoding="utf-8")
STREAMING_PY = (REPO / "api" / "streaming.py").read_text(encoding="utf-8") STREAMING_PY = (REPO / "api" / "streaming.py").read_text(encoding="utf-8")
BOOT_JS = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
INDEX_HTML = (REPO / "static" / "index.html").read_text(encoding="utf-8")
MESSAGES_JS = (REPO / "static" / "messages.js").read_text(encoding="utf-8") MESSAGES_JS = (REPO / "static" / "messages.js").read_text(encoding="utf-8")
PANELS_JS = (REPO / "static" / "panels.js").read_text(encoding="utf-8")
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8") UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8") CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
@@ -57,3 +61,54 @@ def test_backend_marks_streaming_metering_availability_explicitly():
assert "record_token(stream_id, len(STREAM_PARTIAL_TEXT[stream_id]))" not in STREAMING_PY, ( assert "record_token(stream_id, len(STREAM_PARTIAL_TEXT[stream_id]))" not in STREAMING_PY, (
"live TPS must not be derived from streamed character count / byte-size estimates" "live TPS must not be derived from streamed character count / byte-size estimates"
) )
def test_tps_display_setting_is_default_off_and_persisted():
assert '"show_tps": False' in CONFIG_PY, "TPS display should be disabled by default"
assert '"show_tps"' in CONFIG_PY and "_SETTINGS_BOOL_KEYS" in CONFIG_PY, (
"TPS display should be a persisted boolean WebUI setting"
)
assert "settingsShowTps" in INDEX_HTML, "Preferences needs a user-facing TPS display toggle"
assert "payload.show_tps=showTpsCb.checked" in PANELS_JS, (
"Preferences autosave should persist the TPS display toggle through /api/settings"
)
assert "showTpsCb.checked=!!settings.show_tps" in PANELS_JS, (
"Settings panel should hydrate the TPS toggle from persisted settings"
)
assert "window._showTps=!!s.show_tps" in BOOT_JS, (
"Boot should hydrate show_tps into a runtime flag"
)
assert "window._showTps=false" in BOOT_JS, (
"Boot fallback should keep TPS hidden when settings cannot load"
)
def test_tps_display_hot_applies_when_preferences_autosave():
fn_start = PANELS_JS.find("async function _autosavePreferencesSettings")
assert fn_start != -1, "preferences autosave function should exist"
fn_end = PANELS_JS.find("function _retryPreferencesAutosave", fn_start)
assert fn_end != -1, "retry function should follow preferences autosave"
fn = PANELS_JS[fn_start:fn_end]
assert "payload&&payload.show_tps!==undefined" in fn, (
"TPS preference autosave must detect the show_tps field specifically"
)
assert "window._showTps=!!(saved&&saved.show_tps)" in fn, (
"TPS preference autosave should update the runtime flag from the saved response"
)
assert "clearMessageRenderCache" in fn and "renderMessages" in fn, (
"TPS preference autosave should re-render the open transcript without refresh"
)
def test_tps_header_rendering_respects_display_setting():
assert "function isTpsDisplayEnabled()" in UI_JS, "TPS visibility should be centralized"
assert "return window._showTps===true" in UI_JS, "TPS should only render when explicitly enabled"
assert "const tps=(isTpsDisplayEnabled()&&tpsText)" in UI_JS, (
"settled assistant headers must suppress TPS when the setting is off"
)
assert "isTpsDisplayEnabled()?_formatTurnTps(value):''" in UI_JS, (
"live TPS updates must remove/suppress the chip when the setting is off"
)
assert "isTpsDisplayEnabled()?_formatTurnTps(m._turnTps):''" in UI_JS, (
"reloaded assistant messages must not render persisted TPS while disabled"
)