diff --git a/.env.example b/.env.example index c5381d2..938f023 100644 --- a/.env.example +++ b/.env.example @@ -26,3 +26,6 @@ # Path to your Hermes config.yaml (for toolsets and model config) # HERMES_CONFIG_PATH=~/.hermes/config.yaml + +# Display name for the assistant in the UI (default: Hermes) +# HERMES_WEBUI_BOT_NAME=Hermes diff --git a/api/config.py b/api/config.py index ba6b5de..bb79483 100644 --- a/api/config.py +++ b/api/config.py @@ -680,6 +680,7 @@ _SETTINGS_DEFAULTS = { '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 'theme': 'dark', # active UI theme name (no enum gate -- allows custom themes) + 'bot_name': os.getenv('HERMES_WEBUI_BOT_NAME', 'Hermes'), # display name for the assistant 'password_hash': None, # SHA-256 hash; None = auth disabled } diff --git a/api/routes.py b/api/routes.py index f50ef63..4fe16f9 100644 --- a/api/routes.py +++ b/api/routes.py @@ -116,7 +116,13 @@ def handle_get(handler, parsed) -> bool: content_type='text/html; charset=utf-8') if parsed.path == '/login': - return t(handler, _LOGIN_PAGE_HTML, content_type='text/html; charset=utf-8') + import html as _html + _bot = _html.escape(load_settings().get('bot_name', 'Hermes')) + _page = _LOGIN_PAGE_HTML.replace( + 'Hermes — Sign in', + f'{_bot} — Sign in', + ).replace('

Hermes

', f'

{_bot}

') + return t(handler, _page, content_type='text/html; charset=utf-8') if parsed.path == '/api/auth/status': from api.auth import is_auth_enabled, parse_cookie, verify_session diff --git a/static/boot.js b/static/boot.js index 4e74ff8..e6d6a5e 100644 --- a/static/boot.js +++ b/static/boot.js @@ -306,10 +306,23 @@ document.querySelectorAll('.suggestion').forEach(btn=>{ }; })(); +function applyBotName(){ + const name=window._botName||'Hermes'; + document.title=name; + const sidebarH1=document.querySelector('.sidebar-header h1'); + if(sidebarH1) sidebarH1.textContent=name; + const logo=document.querySelector('.sidebar-header .logo'); + if(logo) logo.textContent=name.charAt(0).toUpperCase(); + const topbarTitle=$('topbarTitle'); + if(topbarTitle && (!S.session)) topbarTitle.textContent=name; + const msg=$('msg'); + if(msg) msg.placeholder='Message '+name+'\u2026'; +} + (async()=>{ // Load send key preference let _bootSettings={}; - try{const s=await api('/api/settings');_bootSettings=s;window._sendKey=s.send_key||'enter';window._showTokenUsage=!!s.show_token_usage;window._showCliSessions=!!s.show_cli_sessions;const _theme=s.theme||'dark';document.documentElement.dataset.theme=_theme;localStorage.setItem('hermes-theme',_theme);}catch(e){window._sendKey='enter';window._showTokenUsage=false;window._showCliSessions=false;_bootSettings={check_for_updates:false};} + try{const s=await api('/api/settings');_bootSettings=s;window._sendKey=s.send_key||'enter';window._showTokenUsage=!!s.show_token_usage;window._showCliSessions=!!s.show_cli_sessions;window._botName=s.bot_name||'Hermes';const _theme=s.theme||'dark';document.documentElement.dataset.theme=_theme;localStorage.setItem('hermes-theme',_theme);applyBotName();}catch(e){window._sendKey='enter';window._showTokenUsage=false;window._showCliSessions=false;window._botName='Hermes';_bootSettings={check_for_updates:false};} // Non-blocking update check (fire-and-forget, once per tab session) // ?test_updates=1 in URL forces banner display for testing (bypasses sessionStorage guards) const _testUpdates=new URLSearchParams(location.search).get('test_updates')==='1'; diff --git a/static/index.html b/static/index.html index c55d7e3..0ff59d6 100644 --- a/static/index.html +++ b/static/index.html @@ -372,6 +372,11 @@
Show a banner when newer versions of the WebUI or Agent are available. Runs a background git fetch periodically.
+
+ +
Display name for the assistant throughout the UI. Defaults to Hermes.
+ +
Enter a new password to set or change it. Leave blank to keep current setting.
diff --git a/static/messages.js b/static/messages.js index 257994e..284ca5b 100644 --- a/static/messages.js +++ b/static/messages.js @@ -93,8 +93,9 @@ async function send(){ assistantRow=document.createElement('div');assistantRow.className='msg-row'; assistantBody=document.createElement('div');assistantBody.className='msg-body'; const role=document.createElement('div');role.className='msg-role assistant'; - const icon=document.createElement('div');icon.className='role-icon assistant';icon.textContent='H'; - const lbl=document.createElement('span');lbl.style.fontSize='12px';lbl.textContent='Hermes'; + const _bn=window._botName||'Hermes'; + const icon=document.createElement('div');icon.className='role-icon assistant';icon.textContent=_bn.charAt(0).toUpperCase(); + const lbl=document.createElement('span');lbl.style.fontSize='12px';lbl.textContent=_bn; role.appendChild(icon);role.appendChild(lbl); assistantRow.appendChild(role);assistantRow.appendChild(assistantBody); $('msgInner').appendChild(assistantRow); diff --git a/static/panels.js b/static/panels.js index bb3ea9a..3866366 100644 --- a/static/panels.js +++ b/static/panels.js @@ -1009,6 +1009,9 @@ async function loadSettingsPanel(){ if(syncCb){syncCb.checked=!!settings.sync_to_insights;syncCb.addEventListener('change',_markSettingsDirty,{once:false});} const updateCb=$('settingsCheckUpdates'); if(updateCb){updateCb.checked=settings.check_for_updates!==false;updateCb.addEventListener('change',_markSettingsDirty,{once:false});} + // Bot name + const botNameField=$('settingsBotName'); + if(botNameField){botNameField.value=settings.bot_name||'Hermes';botNameField.addEventListener('input',_markSettingsDirty,{once:false});} // Password field: always blank (we don't send hash back) const pwField=$('settingsPassword'); if(pwField){pwField.value='';pwField.addEventListener('input',_markSettingsDirty,{once:false});} @@ -1042,6 +1045,8 @@ async function saveSettings(andClose){ body.show_cli_sessions=showCliSessions; body.sync_to_insights=!!($('settingsSyncInsights')||{}).checked; body.check_for_updates=!!($('settingsCheckUpdates')||{}).checked; + const botName=(($('settingsBotName')||{}).value||'').trim(); + body.bot_name=botName||'Hermes'; // Password: only act if the field has content; blank = leave auth unchanged if(pw && pw.trim()){ try{ @@ -1060,6 +1065,8 @@ async function saveSettings(andClose){ window._sendKey=sendKey||'enter'; window._showTokenUsage=showTokenUsage; window._showCliSessions=showCliSessions; + window._botName=body.bot_name; + if(typeof applyBotName==='function') applyBotName(); _settingsDirty=false; _settingsThemeOnOpen=theme; const bar=$('settingsUnsavedBar'); if(bar) bar.style.display='none'; renderMessages(); diff --git a/static/sessions.js b/static/sessions.js index e655745..ed0c0f9 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -45,7 +45,7 @@ async function loadSession(sid){ if(tc&&tc.name) appendLiveToolCard(tc); } syncTopbar();await loadDir('.');renderMessages();appendThinking(); - setBusy(true);setStatus('Hermes is thinking\u2026'); + setBusy(true);setStatus((window._botName||'Hermes')+' is thinking\u2026'); startApprovalPolling(sid); }else{ MSG_QUEUE.length=0;updateQueueBadge(); // clear queue for the viewed session @@ -429,7 +429,7 @@ async function deleteSession(sid){ if(remaining.sessions&&remaining.sessions.length){ await loadSession(remaining.sessions[0].session_id); }else{ - $('topbarTitle').textContent='Hermes'; + $('topbarTitle').textContent=window._botName||'Hermes'; $('topbarMeta').textContent='Start a new conversation'; $('msgInner').innerHTML=''; $('emptyState').style.display=''; diff --git a/static/ui.js b/static/ui.js index fe78f19..b4d09ce 100644 --- a/static/ui.js +++ b/static/ui.js @@ -237,7 +237,7 @@ function setStatus(t){ txt.textContent=t; bar.style.display=''; // Show dismiss X only for static/error messages, not transient busy ones - const transient = t.endsWith('…') || t === 'Hermes is thinking…'; + const transient = t.endsWith('…') || t === (window._botName||'Hermes')+' is thinking\u2026'; if(dismiss)dismiss.style.display=(!transient && !S.busy)?'inline':'none'; } } @@ -402,7 +402,7 @@ async function checkInflightOnBoot(sid) { function syncTopbar(){ if(!S.session){ - document.title='Hermes'; + document.title=window._botName||'Hermes'; // Show default workspace name even without a session const sidebarName=$('sidebarWsName'); if(sidebarName && sidebarName.textContent==='Workspace'){ @@ -412,7 +412,7 @@ function syncTopbar(){ } const sessionTitle=S.session.title||'Untitled'; $('topbarTitle').textContent=sessionTitle; - document.title=sessionTitle+' \u2014 Hermes'; + document.title=sessionTitle+' \u2014 '+(window._botName||'Hermes'); const vis=S.messages.filter(m=>m&&m.role&&m.role!=='tool'); $('topbarMeta').textContent=`${vis.length} messages`; // If a profile switch just happened, apply its model rather than the session's stale value. @@ -505,7 +505,8 @@ function renderMessages(){ const retryBtn = isLastAssistant ? `` : ''; const tsVal=m._ts||m.timestamp; const tsTitle=tsVal?new Date(tsVal*1000).toLocaleString():''; - row.innerHTML=`
${isUser?'Y':'H'}
${isUser?'You':'Hermes'}${tsTitle?`${new Date(tsVal*1000).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}`:''}${editBtn}${retryBtn}
${filesHtml}
${bodyHtml}
`; + const _bn=window._botName||'Hermes'; + row.innerHTML=`
${isUser?'Y':_bn.charAt(0).toUpperCase()}
${isUser?'You':esc(_bn)}${tsTitle?`${new Date(tsVal*1000).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}`:''}${editBtn}${retryBtn}
${filesHtml}
${bodyHtml}
`; row.dataset.rawText = String(content).trim(); inner.appendChild(row); }