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:
nesquena-hermes
2026-04-27 11:43:32 -07:00
committed by GitHub
parent 5192ca5de5
commit dca8624454
8 changed files with 42 additions and 51 deletions
+9
View File
@@ -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

-1
View File
@@ -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>
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+25
View File
@@ -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