feat: make bot name configurable

This commit is contained in:
TaraTheStar
2026-04-06 05:10:59 +00:00
parent e1c2e7e3d6
commit e8a8fceb26
9 changed files with 47 additions and 10 deletions

View File

@@ -26,3 +26,6 @@
# Path to your Hermes config.yaml (for toolsets and model config) # Path to your Hermes config.yaml (for toolsets and model config)
# HERMES_CONFIG_PATH=~/.hermes/config.yaml # HERMES_CONFIG_PATH=~/.hermes/config.yaml
# Display name for the assistant in the UI (default: Hermes)
# HERMES_WEBUI_BOT_NAME=Hermes

View File

@@ -680,6 +680,7 @@ _SETTINGS_DEFAULTS = {
'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
'theme': 'dark', # active UI theme name (no enum gate -- allows custom themes) '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 'password_hash': None, # SHA-256 hash; None = auth disabled
} }

View File

@@ -116,7 +116,13 @@ def handle_get(handler, parsed) -> bool:
content_type='text/html; charset=utf-8') content_type='text/html; charset=utf-8')
if parsed.path == '/login': 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(
'<title>Hermes — Sign in</title>',
f'<title>{_bot} — Sign in</title>',
).replace('<h1>Hermes</h1>', f'<h1>{_bot}</h1>')
return t(handler, _page, content_type='text/html; charset=utf-8')
if parsed.path == '/api/auth/status': if parsed.path == '/api/auth/status':
from api.auth import is_auth_enabled, parse_cookie, verify_session from api.auth import is_auth_enabled, parse_cookie, verify_session

View File

@@ -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()=>{ (async()=>{
// Load send key preference // Load send key preference
let _bootSettings={}; 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) // Non-blocking update check (fire-and-forget, once per tab session)
// ?test_updates=1 in URL forces banner display for testing (bypasses sessionStorage guards) // ?test_updates=1 in URL forces banner display for testing (bypasses sessionStorage guards)
const _testUpdates=new URLSearchParams(location.search).get('test_updates')==='1'; const _testUpdates=new URLSearchParams(location.search).get('test_updates')==='1';

View File

@@ -372,6 +372,11 @@
</label> </label>
<div style="font-size:11px;color:var(--muted);margin-top:4px">Show a banner when newer versions of the WebUI or Agent are available. Runs a background git fetch periodically.</div> <div style="font-size:11px;color:var(--muted);margin-top:4px">Show a banner when newer versions of the WebUI or Agent are available. Runs a background git fetch periodically.</div>
</div> </div>
<div class="settings-field">
<label for="settingsBotName">Assistant Name</label>
<div style="font-size:11px;color:var(--muted);margin-bottom:6px">Display name for the assistant throughout the UI. Defaults to Hermes.</div>
<input type="text" id="settingsBotName" placeholder="Hermes" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px;font-size:13px">
</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

@@ -93,8 +93,9 @@ async function send(){
assistantRow=document.createElement('div');assistantRow.className='msg-row'; assistantRow=document.createElement('div');assistantRow.className='msg-row';
assistantBody=document.createElement('div');assistantBody.className='msg-body'; assistantBody=document.createElement('div');assistantBody.className='msg-body';
const role=document.createElement('div');role.className='msg-role assistant'; const role=document.createElement('div');role.className='msg-role assistant';
const icon=document.createElement('div');icon.className='role-icon assistant';icon.textContent='H'; const _bn=window._botName||'Hermes';
const lbl=document.createElement('span');lbl.style.fontSize='12px';lbl.textContent='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); role.appendChild(icon);role.appendChild(lbl);
assistantRow.appendChild(role);assistantRow.appendChild(assistantBody); assistantRow.appendChild(role);assistantRow.appendChild(assistantBody);
$('msgInner').appendChild(assistantRow); $('msgInner').appendChild(assistantRow);

View File

@@ -1009,6 +1009,9 @@ async function loadSettingsPanel(){
if(syncCb){syncCb.checked=!!settings.sync_to_insights;syncCb.addEventListener('change',_markSettingsDirty,{once:false});} if(syncCb){syncCb.checked=!!settings.sync_to_insights;syncCb.addEventListener('change',_markSettingsDirty,{once:false});}
const updateCb=$('settingsCheckUpdates'); const updateCb=$('settingsCheckUpdates');
if(updateCb){updateCb.checked=settings.check_for_updates!==false;updateCb.addEventListener('change',_markSettingsDirty,{once:false});} 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) // Password field: always blank (we don't send hash back)
const pwField=$('settingsPassword'); const pwField=$('settingsPassword');
if(pwField){pwField.value='';pwField.addEventListener('input',_markSettingsDirty,{once:false});} if(pwField){pwField.value='';pwField.addEventListener('input',_markSettingsDirty,{once:false});}
@@ -1042,6 +1045,8 @@ async function saveSettings(andClose){
body.show_cli_sessions=showCliSessions; body.show_cli_sessions=showCliSessions;
body.sync_to_insights=!!($('settingsSyncInsights')||{}).checked; body.sync_to_insights=!!($('settingsSyncInsights')||{}).checked;
body.check_for_updates=!!($('settingsCheckUpdates')||{}).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 // Password: only act if the field has content; blank = leave auth unchanged
if(pw && pw.trim()){ if(pw && pw.trim()){
try{ try{
@@ -1060,6 +1065,8 @@ async function saveSettings(andClose){
window._sendKey=sendKey||'enter'; window._sendKey=sendKey||'enter';
window._showTokenUsage=showTokenUsage; window._showTokenUsage=showTokenUsage;
window._showCliSessions=showCliSessions; window._showCliSessions=showCliSessions;
window._botName=body.bot_name;
if(typeof applyBotName==='function') applyBotName();
_settingsDirty=false; _settingsThemeOnOpen=theme; _settingsDirty=false; _settingsThemeOnOpen=theme;
const bar=$('settingsUnsavedBar'); if(bar) bar.style.display='none'; const bar=$('settingsUnsavedBar'); if(bar) bar.style.display='none';
renderMessages(); renderMessages();

View File

@@ -45,7 +45,7 @@ async function loadSession(sid){
if(tc&&tc.name) appendLiveToolCard(tc); if(tc&&tc.name) appendLiveToolCard(tc);
} }
syncTopbar();await loadDir('.');renderMessages();appendThinking(); syncTopbar();await loadDir('.');renderMessages();appendThinking();
setBusy(true);setStatus('Hermes is thinking\u2026'); setBusy(true);setStatus((window._botName||'Hermes')+' is thinking\u2026');
startApprovalPolling(sid); startApprovalPolling(sid);
}else{ }else{
MSG_QUEUE.length=0;updateQueueBadge(); // clear queue for the viewed session 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){ if(remaining.sessions&&remaining.sessions.length){
await loadSession(remaining.sessions[0].session_id); await loadSession(remaining.sessions[0].session_id);
}else{ }else{
$('topbarTitle').textContent='Hermes'; $('topbarTitle').textContent=window._botName||'Hermes';
$('topbarMeta').textContent='Start a new conversation'; $('topbarMeta').textContent='Start a new conversation';
$('msgInner').innerHTML=''; $('msgInner').innerHTML='';
$('emptyState').style.display=''; $('emptyState').style.display='';

View File

@@ -237,7 +237,7 @@ function setStatus(t){
txt.textContent=t; txt.textContent=t;
bar.style.display=''; bar.style.display='';
// Show dismiss X only for static/error messages, not transient busy ones // 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'; if(dismiss)dismiss.style.display=(!transient && !S.busy)?'inline':'none';
} }
} }
@@ -402,7 +402,7 @@ async function checkInflightOnBoot(sid) {
function syncTopbar(){ function syncTopbar(){
if(!S.session){ if(!S.session){
document.title='Hermes'; document.title=window._botName||'Hermes';
// Show default workspace name even without a session // Show default workspace name even without a session
const sidebarName=$('sidebarWsName'); const sidebarName=$('sidebarWsName');
if(sidebarName && sidebarName.textContent==='Workspace'){ if(sidebarName && sidebarName.textContent==='Workspace'){
@@ -412,7 +412,7 @@ function syncTopbar(){
} }
const sessionTitle=S.session.title||'Untitled'; const sessionTitle=S.session.title||'Untitled';
$('topbarTitle').textContent=sessionTitle; $('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'); const vis=S.messages.filter(m=>m&&m.role&&m.role!=='tool');
$('topbarMeta').textContent=`${vis.length} messages`; $('topbarMeta').textContent=`${vis.length} messages`;
// If a profile switch just happened, apply its model rather than the session's stale value. // 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 ? `<button class="msg-action-btn" title="Regenerate response" onclick="regenerateResponse(this)">&#8635;</button>` : ''; const retryBtn = isLastAssistant ? `<button class="msg-action-btn" title="Regenerate response" onclick="regenerateResponse(this)">&#8635;</button>` : '';
const tsVal=m._ts||m.timestamp; const tsVal=m._ts||m.timestamp;
const tsTitle=tsVal?new Date(tsVal*1000).toLocaleString():''; const tsTitle=tsVal?new Date(tsVal*1000).toLocaleString():'';
row.innerHTML=`<div class="msg-role ${m.role}" ${tsTitle?`title="${esc(tsTitle)}"`:''}><div class="role-icon ${m.role}">${isUser?'Y':'H'}</div><span style="font-size:12px">${isUser?'You':'Hermes'}</span>${tsTitle?`<span class="msg-time">${new Date(tsVal*1000).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}</span>`:''}<span class="msg-actions">${editBtn}<button class="msg-copy-btn msg-action-btn" title="Copy" onclick="copyMsg(this)">&#128203;</button>${retryBtn}</span></div>${filesHtml}<div class="msg-body">${bodyHtml}</div>`; const _bn=window._botName||'Hermes';
row.innerHTML=`<div class="msg-role ${m.role}" ${tsTitle?`title="${esc(tsTitle)}"`:''}><div class="role-icon ${m.role}">${isUser?'Y':_bn.charAt(0).toUpperCase()}</div><span style="font-size:12px">${isUser?'You':esc(_bn)}</span>${tsTitle?`<span class="msg-time">${new Date(tsVal*1000).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}</span>`:''}<span class="msg-actions">${editBtn}<button class="msg-copy-btn msg-action-btn" title="Copy" onclick="copyMsg(this)">&#128203;</button>${retryBtn}</span></div>${filesHtml}<div class="msg-body">${bodyHtml}</div>`;
row.dataset.rawText = String(content).trim(); row.dataset.rawText = String(content).trim();
inner.appendChild(row); inner.appendChild(row);
} }