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);
}