mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 03:00:23 +00:00
fix(ui): restore rail-era app titlebar state (v0.50.226) (#1163)
Merged as v0.50.226. Integration branch absorbed @aronprins's original PR #1141 with one reviewer fix from @nesquena (`1d11646`: queue hide tooltip updated to reference the queue pill, not the removed titlebar badge). **Full gate results:** - 2595 tests passing ✅ - Browser QA 21/21 (desktop 1440×900 + mobile iPhone 14) ✅ - Independent review: APPROVED by @nesquena ✅ Thank you @aronprins for the clean PR — the titlebar is properly restored.
This commit is contained in:
@@ -345,6 +345,15 @@
|
||||
workspace subtree) and never enumerate blocked system roots. (`api/routes.py`,
|
||||
`api/workspace.py`, `static/panels.js`, `static/style.css`) (partial for #616)
|
||||
|
||||
## [v0.50.226] — 2026-04-27
|
||||
|
||||
### Fixed
|
||||
- **App titlebar restored to rail-era centered layout** — removes the TPS metering chip
|
||||
from the top bar, centers the title and subtitle, and restores the message count in the
|
||||
subtitle slot. Queue state no longer overrides the titlebar subtitle slot.
|
||||
(`static/index.html`, `static/panels.js`, `static/style.css`, `static/ui.js`,
|
||||
`tests/test_app_titlebar_restore.py`)
|
||||
|
||||
## [v0.50.183] — 2026-04-24
|
||||
|
||||
### Added
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
@@ -72,7 +72,6 @@
|
||||
</span>
|
||||
<span class="app-titlebar-title" id="appTitlebarTitle">Hermes</span>
|
||||
<span class="app-titlebar-sub" id="appTitlebarSub" hidden></span>
|
||||
<div class="tps-chip" id="tpsStat" title="Tokens per second / minute">0.0 t/s · 0.0 high</div>
|
||||
</div>
|
||||
<div class="app-titlebar-spacer" aria-hidden="true"></div>
|
||||
</header>
|
||||
|
||||
@@ -33,6 +33,8 @@ function syncAppTitlebar() {
|
||||
let subText = '';
|
||||
if (panel === 'chat' && typeof S !== 'undefined' && S && S.session) {
|
||||
mainText = S.session.title || (typeof t === 'function' ? t('untitled') : 'Untitled');
|
||||
const vis = Array.isArray(S.messages) ? S.messages.filter(m => m && m.role && m.role !== 'tool') : [];
|
||||
if (typeof t === 'function') subText = t('n_messages', vis.length);
|
||||
} else {
|
||||
const key = APP_TITLEBAR_KEYS[panel];
|
||||
mainText = key && typeof t === 'function' ? t(key) : (panel.charAt(0).toUpperCase() + panel.slice(1));
|
||||
|
||||
+4
-6
@@ -207,12 +207,8 @@
|
||||
body,header,footer,aside,nav,main,div,button,input,textarea,select{transition-property:background-color,border-color,color;transition-duration:.15s;transition-timing-function:ease;}
|
||||
body{background:var(--bg);color:var(--text);height:100vh;height:100dvh;overflow:hidden;display:flex;flex-direction:column;}
|
||||
.layout{display:flex;width:100%;flex:1 1 auto;min-height:0;}
|
||||
.app-titlebar{display:flex;align-items:center;justify-content:space-between;height:38px;flex-shrink:0;background:var(--sidebar);border-bottom:1px solid var(--border);padding:0 12px;padding-top:env(safe-area-inset-top,0);padding-left:max(12px,env(safe-area-inset-left,0));padding-right:max(12px,env(safe-area-inset-right,0));box-sizing:content-box;font-size:12px;color:var(--muted);user-select:none;-webkit-app-region:drag;position:relative;z-index:20;}
|
||||
.app-titlebar-inner{display:flex;align-items:center;gap:8px;min-width:0;max-width:100%;flex:1 1 auto;justify-content:space-between;}
|
||||
.tps-chip{
|
||||
font-size:11px;font-family:ui-monospace,'SF Mono',monospace;color:var(--muted);
|
||||
white-space:nowrap;letter-spacing:.02em;flex-shrink:0;
|
||||
}
|
||||
.app-titlebar{display:flex;align-items:center;justify-content:center;height:38px;flex-shrink:0;background:var(--sidebar);border-bottom:1px solid var(--border);padding:0 12px;padding-top:env(safe-area-inset-top,0);padding-left:max(12px,env(safe-area-inset-left,0));padding-right:max(12px,env(safe-area-inset-right,0));box-sizing:content-box;font-size:12px;color:var(--muted);user-select:none;-webkit-app-region:drag;position:relative;z-index:20;}
|
||||
.app-titlebar-inner{display:flex;align-items:center;gap:8px;min-width:0;max-width:100%;justify-content:center;}
|
||||
.app-titlebar-icon{display:inline-flex;align-items:center;color:var(--accent);}
|
||||
.app-titlebar-title{font-size:12px;font-weight:600;color:var(--text);letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:60vw;}
|
||||
.app-titlebar-sub{font-size:10px;color:var(--muted);background:var(--hover-bg);padding:2px 7px;border-radius:4px;font-family:'SF Mono',ui-monospace,monospace;white-space:nowrap;flex-shrink:0;}
|
||||
@@ -845,7 +841,9 @@
|
||||
.sidebar.mobile-open{left:0;}
|
||||
.sidebar .resize-handle{display:none;}
|
||||
/* Hamburger button (in app titlebar on mobile) */
|
||||
.app-titlebar{justify-content:space-between;}
|
||||
.app-titlebar-hamburger,.app-titlebar-spacer{display:flex;}
|
||||
.app-titlebar-inner{flex:1 1 auto;}
|
||||
/* Overlay backdrop */
|
||||
.mobile-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);
|
||||
z-index:199;-webkit-tap-highlight-color:transparent;}
|
||||
|
||||
+2
-44
@@ -1200,10 +1200,10 @@ function _renderQueueChips(sid){
|
||||
clearBtn.onclick=()=>{q.length=0;_saveAndRefresh();};
|
||||
actions.appendChild(mergeBtn);
|
||||
actions.appendChild(clearBtn);
|
||||
// Hide button — collapses flyout entirely; titlebar "N queued" re-shows it
|
||||
// Hide button — collapses flyout entirely; queue pill re-shows it
|
||||
const hideBtn=document.createElement('button');
|
||||
hideBtn.className='queue-card-icon-btn';
|
||||
hideBtn.title='Hide queue (click the titlebar badge to show again)';
|
||||
hideBtn.title='Hide queue (click the queue pill to show again)';
|
||||
hideBtn.setAttribute('aria-label','Hide queue panel');
|
||||
hideBtn.innerHTML=li('chevron-down',14);
|
||||
hideBtn.onclick=()=>{
|
||||
@@ -1346,45 +1346,6 @@ function _updateQueuePill(sid,count){
|
||||
}
|
||||
}
|
||||
|
||||
function _syncQueueTitlebar(sid,count){
|
||||
const sub=document.getElementById('appTitlebarSub');
|
||||
if(!sub) return;
|
||||
if(count>0){
|
||||
const label=typeof t==='function'?t('queued_count',count):(count===1?'1 queued':`${count} queued`);
|
||||
sub.textContent=label;
|
||||
sub.hidden=false;
|
||||
sub.setAttribute('role','button');
|
||||
sub.setAttribute('tabindex','0');
|
||||
sub.setAttribute('aria-label',label);
|
||||
sub.style.cursor='pointer';sub.style.color='var(--muted)';sub.style.fontWeight='600';sub.style.fontSize='11px';sub.style.background='var(--hover-bg)';sub.style.padding='2px 6px';sub.style.borderRadius='6px';sub.style.border='1px solid var(--border)';
|
||||
const toggleQueue=()=>{
|
||||
const activeSid=S.session&&S.session.session_id;
|
||||
if(!activeSid) return;
|
||||
const qCard=document.getElementById('queueCard');
|
||||
if(!qCard) return;
|
||||
if(qCard.classList.contains('visible')){
|
||||
qCard.classList.remove('visible');
|
||||
// Show pill since flyout is now hidden
|
||||
const liveCount=_getSessionQueue(activeSid,false).length;
|
||||
_updateQueuePill(activeSid,liveCount);
|
||||
} else {
|
||||
qCard.classList.add('visible');
|
||||
// Hide pill since flyout is now showing
|
||||
_updateQueuePill(activeSid,0);
|
||||
if(typeof scrollToBottom==='function') scrollToBottom();
|
||||
}
|
||||
};
|
||||
sub.onclick=toggleQueue;
|
||||
sub.onkeydown=(e)=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();toggleQueue();}};
|
||||
sub.title='Click to show/hide queue panel';
|
||||
} else {
|
||||
sub.textContent='';sub.hidden=true;
|
||||
sub.removeAttribute('role');sub.removeAttribute('tabindex');sub.removeAttribute('aria-label');
|
||||
sub.style.cssText='';
|
||||
sub.onclick=null;sub.onkeydown=null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateQueueBadge(sessionId){
|
||||
const sid=sessionId||(S.session&&S.session.session_id);
|
||||
const count=sid?getQueuedSessionCount(sid):0;
|
||||
@@ -1409,8 +1370,6 @@ function updateQueueBadge(sessionId){
|
||||
_updateQueuePill(sid,0);
|
||||
}
|
||||
}
|
||||
// Only update titlebar if this is the active session
|
||||
if(!S.session||sid===S.session.session_id) _syncQueueTitlebar(sid,count);
|
||||
}
|
||||
function showToast(msg,ms,type){const el=$('toast');if(!el)return;const s=String(msg==null?'':msg);let t=type;if(!t){const low=s.toLowerCase();if(/fail|error|denied|invalid|unavailable|no active|no workspace match|no model match|no personalities/.test(low))t='error';else if(/warn|queued|takes effect|skipped|fallback/.test(low))t='warning';else if(/saved|created|imported|restored|switched|set to|updated|duplicated|moved to|renamed|deleted|complete|pinned|archived|cleared|stopped/.test(low))t='success';else t='info';}el.textContent=s;el.className='toast show '+t;clearTimeout(el._t);el._t=setTimeout(()=>{el.classList.remove('show');},ms||2800);}
|
||||
|
||||
@@ -1859,7 +1818,6 @@ function syncTopbar(){
|
||||
const vis=S.messages.filter(m=>m&&m.role&&m.role!=='tool');
|
||||
const _topbarMeta=$('topbarMeta');if(_topbarMeta)_topbarMeta.textContent=t('n_messages',vis.length);
|
||||
if(typeof syncAppTitlebar==='function') syncAppTitlebar();
|
||||
if(typeof _syncQueueTitlebar==='function'){const _qs=S.session&&S.session.session_id;_syncQueueTitlebar(_qs,_qs?getQueuedSessionCount(_qs):0);}
|
||||
// If a profile switch just happened, apply its model rather than the session's stale value.
|
||||
// S._pendingProfileModel is set by switchToProfile() and cleared here after one application.
|
||||
const modelOverride=S._pendingProfileModel;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
INDEX_HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
|
||||
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
||||
PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
|
||||
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_app_titlebar_no_longer_contains_tps_chip():
|
||||
assert 'id="tpsStat"' not in INDEX_HTML
|
||||
|
||||
|
||||
def test_app_titlebar_returns_to_centered_desktop_layout():
|
||||
assert ".app-titlebar{display:flex;align-items:center;justify-content:center;" in STYLE_CSS
|
||||
assert ".app-titlebar-inner{display:flex;align-items:center;gap:8px;min-width:0;max-width:100%;justify-content:center;}" in STYLE_CSS
|
||||
|
||||
|
||||
def test_app_titlebar_subtitle_shows_message_count_again():
|
||||
assert "subText = t('n_messages', vis.length);" in PANELS_JS
|
||||
|
||||
|
||||
def test_queue_updates_do_not_hijack_app_titlebar_subtitle():
|
||||
assert "_syncQueueTitlebar" not in UI_JS
|
||||
Reference in New Issue
Block a user