diff --git a/CHANGELOG.md b/CHANGELOG.md
index a74c1752..b396d34c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/docs/pr-assets/restore-top-titlebar-after.png b/docs/pr-assets/restore-top-titlebar-after.png
new file mode 100644
index 00000000..bd3e9da7
Binary files /dev/null and b/docs/pr-assets/restore-top-titlebar-after.png differ
diff --git a/docs/pr-assets/restore-top-titlebar-before.png b/docs/pr-assets/restore-top-titlebar-before.png
new file mode 100644
index 00000000..48cf083d
Binary files /dev/null and b/docs/pr-assets/restore-top-titlebar-before.png differ
diff --git a/static/index.html b/static/index.html
index c7bf6c5a..fbd25267 100644
--- a/static/index.html
+++ b/static/index.html
@@ -72,7 +72,6 @@
Hermes
-
0.0 t/s · 0.0 high
diff --git a/static/panels.js b/static/panels.js
index 2e526c10..73eac013 100644
--- a/static/panels.js
+++ b/static/panels.js
@@ -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));
diff --git a/static/style.css b/static/style.css
index df494c2d..5265ddd8 100644
--- a/static/style.css
+++ b/static/style.css
@@ -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;}
diff --git a/static/ui.js b/static/ui.js
index e9dfd268..2ddae0af 100644
--- a/static/ui.js
+++ b/static/ui.js
@@ -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;
diff --git a/tests/test_app_titlebar_restore.py b/tests/test_app_titlebar_restore.py
new file mode 100644
index 00000000..155e3761
--- /dev/null
+++ b/tests/test_app_titlebar_restore.py
@@ -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