feat: 'Show CLI sessions' toggle in Settings (#61)

Adds a server-side boolean setting (default: false) that controls whether
CLI sessions from state.db appear in the sidebar. Off by default so the
sidebar is clean until the user explicitly opts in.

- api/config.py: add show_cli_sessions to _SETTINGS_DEFAULTS and _SETTINGS_BOOL_KEYS
- api/routes.py: gate get_cli_sessions() call on the setting at request time
- static/index.html: checkbox in settings panel with description
- static/panels.js: load/save checkbox, refresh session list on save
- static/boot.js: load on startup alongside send_key and show_token_usage

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
This commit is contained in:
nesquena-hermes
2026-04-03 21:06:23 -07:00
committed by GitHub
parent 1a4d56c215
commit 66f95e08c2
5 changed files with 23 additions and 6 deletions

View File

@@ -633,6 +633,7 @@ _SETTINGS_DEFAULTS = {
'default_workspace': str(DEFAULT_WORKSPACE), 'default_workspace': str(DEFAULT_WORKSPACE),
'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_cli_sessions': False, # merge CLI sessions from state.db into the sidebar
'password_hash': None, # SHA-256 hash; None = auth disabled 'password_hash': None, # SHA-256 hash; None = auth disabled
} }
@@ -652,7 +653,7 @@ _SETTINGS_ALLOWED_KEYS = set(_SETTINGS_DEFAULTS.keys()) - {'password_hash'}
_SETTINGS_ENUM_VALUES = { _SETTINGS_ENUM_VALUES = {
'send_key': {'enter', 'ctrl+enter'}, 'send_key': {'enter', 'ctrl+enter'},
} }
_SETTINGS_BOOL_KEYS = {'show_token_usage'} _SETTINGS_BOOL_KEYS = {'show_token_usage', 'show_cli_sessions'}
def save_settings(settings: dict) -> dict: def save_settings(settings: dict) -> dict:
"""Save settings to disk. Returns the merged settings. Ignores unknown keys.""" """Save settings to disk. Returns the merged settings. Ignores unknown keys."""

View File

@@ -188,10 +188,13 @@ def handle_get(handler, parsed):
if parsed.path == '/api/sessions': if parsed.path == '/api/sessions':
webui_sessions = all_sessions() webui_sessions = all_sessions()
cli = get_cli_sessions() settings = load_settings()
# Deduplicate: WebUI sessions always win if same session_id if settings.get('show_cli_sessions'):
webui_ids = {s['session_id'] for s in webui_sessions} cli = get_cli_sessions()
deduped_cli = [s for s in cli if s['session_id'] not in webui_ids] webui_ids = {s['session_id'] for s in webui_sessions}
deduped_cli = [s for s in cli if s['session_id'] not in webui_ids]
else:
deduped_cli = []
merged = webui_sessions + deduped_cli merged = webui_sessions + deduped_cli
merged.sort(key=lambda s: s.get('updated_at', 0) or 0, reverse=True) merged.sort(key=lambda s: s.get('updated_at', 0) or 0, reverse=True)
return j(handler, {'sessions': merged, 'cli_count': len(deduped_cli)}) return j(handler, {'sessions': merged, 'cli_count': len(deduped_cli)})

View File

@@ -308,7 +308,7 @@ document.querySelectorAll('.suggestion').forEach(btn=>{
(async()=>{ (async()=>{
// Load send key preference // Load send key preference
try{const s=await api('/api/settings');window._sendKey=s.send_key||'enter';window._showTokenUsage=!!s.show_token_usage;}catch(e){window._sendKey='enter';window._showTokenUsage=false;} try{const s=await api('/api/settings');window._sendKey=s.send_key||'enter';window._showTokenUsage=!!s.show_token_usage;window._showCliSessions=!!s.show_cli_sessions;}catch(e){window._sendKey='enter';window._showTokenUsage=false;window._showCliSessions=false;}
// Fetch active profile // Fetch active profile
try{const p=await api('/api/profile/active');S.activeProfile=p.name||'default';}catch(e){S.activeProfile='default';} try{const p=await api('/api/profile/active');S.activeProfile=p.name||'default';}catch(e){S.activeProfile='default';}
// Update profile chip label immediately // Update profile chip label immediately

View File

@@ -331,6 +331,13 @@
</label> </label>
<div style="font-size:11px;color:var(--muted);margin-top:4px">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">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="settingsShowCliSessions" style="width:15px;height:15px;accent-color:var(--accent)">
Show CLI sessions in sidebar
</label>
<div style="font-size:11px;color:var(--muted);margin-top:4px">Merges sessions from the Hermes CLI (state.db) into the session list. Click a CLI session to import it and continue the conversation.</div>
</div>
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px"> <div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
<label for="settingsPassword">Access Password</label> <label for="settingsPassword">Access Password</label>
<div style="font-size:11px;color:var(--muted);margin-bottom:6px">Enter a new password to set or change it. Leave blank to keep current setting.</div> <div style="font-size:11px;color:var(--muted);margin-bottom:6px">Enter a new password to set or change it. Leave blank to keep current setting.</div>

View File

@@ -960,6 +960,8 @@ async function loadSettingsPanel(){
if(sendKeySel) sendKeySel.value=settings.send_key||'enter'; if(sendKeySel) sendKeySel.value=settings.send_key||'enter';
const showUsageCb=$('settingsShowTokenUsage'); const showUsageCb=$('settingsShowTokenUsage');
if(showUsageCb) showUsageCb.checked=!!settings.show_token_usage; if(showUsageCb) showUsageCb.checked=!!settings.show_token_usage;
const showCliCb=$('settingsShowCliSessions');
if(showCliCb) showCliCb.checked=!!settings.show_cli_sessions;
// Password field: always blank (we don't send hash back) // Password field: always blank (we don't send hash back)
const pwField=$('settingsPassword'); const pwField=$('settingsPassword');
if(pwField) pwField.value=''; if(pwField) pwField.value='';
@@ -982,12 +984,14 @@ async function saveSettings(){
const workspace=($('settingsWorkspace')||{}).value; const workspace=($('settingsWorkspace')||{}).value;
const sendKey=($('settingsSendKey')||{}).value; const sendKey=($('settingsSendKey')||{}).value;
const showTokenUsage=!!($('settingsShowTokenUsage')||{}).checked; const showTokenUsage=!!($('settingsShowTokenUsage')||{}).checked;
const showCliSessions=!!($('settingsShowCliSessions')||{}).checked;
const pw=($('settingsPassword')||{}).value; const pw=($('settingsPassword')||{}).value;
const body={}; const body={};
if(model) body.default_model=model; if(model) body.default_model=model;
if(workspace) body.default_workspace=workspace; if(workspace) body.default_workspace=workspace;
if(sendKey) body.send_key=sendKey; if(sendKey) body.send_key=sendKey;
body.show_token_usage=showTokenUsage; body.show_token_usage=showTokenUsage;
body.show_cli_sessions=showCliSessions;
// Password: only act if the field has content; blank = leave auth unchanged // Password: only act if the field has content; blank = leave auth unchanged
if(pw && pw.trim()){ if(pw && pw.trim()){
try{ try{
@@ -1003,7 +1007,9 @@ async function saveSettings(){
await api('/api/settings',{method:'POST',body:JSON.stringify(body)}); await api('/api/settings',{method:'POST',body:JSON.stringify(body)});
window._sendKey=sendKey||'enter'; window._sendKey=sendKey||'enter';
window._showTokenUsage=showTokenUsage; window._showTokenUsage=showTokenUsage;
window._showCliSessions=showCliSessions;
renderMessages(); renderMessages();
if(typeof renderSessionList==='function') renderSessionList();
showToast('Settings saved'); showToast('Settings saved');
toggleSettings(); toggleSettings();
}catch(e){ }catch(e){