feat(mobile): prioritize titlebar session title with tap-to-reveal and long-press actions (#4520)

This commit is contained in:
Rod Boev committed 2026-06-20 21:14:54 -04:00
1 parent 36611b7e20
commit f8af4ecc6d
3 files changed
+112 -1

No files matched your search

+80 -1
View File
@@ -57,7 +57,7 @@ function syncAppTitlebar() {
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);
subText = String(vis.length);
sourceLabel = S.session.source_label || S.session.source_tag || S.session.raw_source || '';
// Recovered sidecars stamp source_label 'WebUI' (api/session_recovery.py); don't badge a native session as its own source (#3338).
if (/^webui$/i.test(sourceLabel)) sourceLabel = '';
@@ -151,6 +151,85 @@ function syncAppTitlebar() {
inp.select();
};
}
// Mobile touch interactions — wired once per element lifetime.
if ('ontouchstart' in window && !titleEl._mobileTouchWired) {
titleEl._mobileTouchWired = true;
// Tap-to-reveal full title popover
let _titlePopover = null;
const _dismissTitlePopover = () => {
if (_titlePopover) { _titlePopover.remove(); _titlePopover = null; }
};
titleEl.addEventListener('click', function _onTitleClick(e) {
if (_renamingAppTitlebar) return;
if (_titlePopover) {
_dismissTitlePopover();
if (typeof titleEl.ondblclick === 'function') titleEl.ondblclick(e);
return;
}
e.stopPropagation();
const pop = document.createElement('div');
pop.className = 'app-titlebar-title-popover';
pop.textContent = (S && S.session && S.session.title) ||
(typeof t === 'function' ? t('untitled') : 'Untitled');
document.body.appendChild(pop);
const rect = titleEl.getBoundingClientRect();
pop.style.top = (rect.bottom + 6) + 'px';
pop.style.left = Math.max(8, rect.left) + 'px';
pop.style.maxWidth = (window.innerWidth - 16) + 'px';
_titlePopover = pop;
const _outside = (ev) => {
if (!pop.contains(ev.target) && ev.target !== titleEl) {
_dismissTitlePopover();
document.removeEventListener('click', _outside, true);
}
};
setTimeout(() => document.addEventListener('click', _outside, true), 0);
}, { passive: true });
// Long-press → session action menu (mirrors project-chip pattern, sessions.js:5362-5407)
if (panel === 'chat' && S && S.session && !S.session.read_only && !S.session.is_read_only &&
typeof _openSessionActionMenu === 'function') {
let _lpTimer = null;
let _lpHandled = false;
let _lpStartX = 0, _lpStartY = 0;
const _lpDelay = typeof SESSION_LONG_PRESS_DELAY_MS !== 'undefined' ?
SESSION_LONG_PRESS_DELAY_MS : 400;
titleEl.addEventListener('touchstart', (e) => {
const touch = e.changedTouches && e.changedTouches[0];
if (!touch) return;
if (_lpTimer) { clearTimeout(_lpTimer); _lpTimer = null; }
_lpHandled = false; _lpStartX = touch.clientX; _lpStartY = touch.clientY;
titleEl.classList.add('long-pressing');
_lpTimer = setTimeout(() => {
_lpTimer = null;
if (_lpHandled) return;
_lpHandled = true;
titleEl.classList.remove('long-pressing');
_openSessionActionMenu(S.session, titleEl);
}, _lpDelay);
}, { passive: true });
titleEl.addEventListener('touchmove', (e) => {
if (!_lpTimer) return;
const touch = e.changedTouches && e.changedTouches[0];
if (!touch) return;
if (Math.abs(touch.clientX - _lpStartX) > 10 || Math.abs(touch.clientY - _lpStartY) > 10) {
clearTimeout(_lpTimer); _lpTimer = null;
titleEl.classList.remove('long-pressing');
}
}, { passive: true });
titleEl.addEventListener('touchend', (e) => {
clearTimeout(_lpTimer); _lpTimer = null;
titleEl.classList.remove('long-pressing');
if (_lpHandled) { e.preventDefault(); e.stopPropagation(); }
}, { passive: false });
titleEl.addEventListener('touchcancel', () => {
clearTimeout(_lpTimer); _lpTimer = null; _lpHandled = false;
titleEl.classList.remove('long-pressing');
}, { passive: true });
}
}
}
function _beginSettingsPanelSession() {
+4
View File
@@ -989,6 +989,7 @@
.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;}
.app-titlebar-sub[hidden]{display:none;}
.app-titlebar-title-popover{position:fixed;z-index:300;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:13px;color:var(--text);box-shadow:0 4px 16px rgba(0,0,0,.18);word-break:break-word;line-height:1.4;max-width:calc(100vw - 16px);pointer-events:auto;}
.app-titlebar-hamburger,.app-titlebar-spacer{display:none;width:44px;height:44px;flex-shrink:0;}
.app-titlebar-hamburger{-webkit-app-region:no-drag;align-items:center;justify-content:center;background:none;border:none;color:var(--muted);border-radius:8px;cursor:pointer;padding:0;-webkit-tap-highlight-color:transparent;transition:background-color .15s,color .15s;}
.app-titlebar-hamburger:hover{background:var(--hover-bg);color:var(--text);}
@@ -2356,6 +2357,9 @@
.app-titlebar-hamburger,.app-titlebar-spacer{display:flex;}
.app-titlebar-new-chat{display:inline-flex;}
.app-titlebar-inner{flex:1 1 auto;}
.app-titlebar-title{max-width:72vw;}
.app-titlebar-sub{font-size:9px;padding:1px 5px;}
.app-titlebar-title.long-pressing{opacity:.6;transition:opacity .1s;}
.system-health-panel.insights-card{gap:10px;padding:12px;}
.system-health-head{align-items:flex-start;}
.system-health-metrics{grid-template-columns:1fr;gap:8px;}
@@ -0,0 +1,28 @@
"""Static regressions for mobile titlebar session title priority (#4520)."""
import re, pathlib, pytest
ROOT = pathlib.Path(__file__).parent.parent
HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
def test_titlebar_title_element_exists():
assert 'id="appTitlebarTitle"' in HTML
assert 'id="appTitlebarSub"' in HTML
def test_syncappTitlebar_compact_metadata():
assert 'subText = String(vis.length)' in PANELS_JS
assert "t('n_messages', vis.length)" not in PANELS_JS
def test_long_press_handler_wired():
assert '_lpTimer' in PANELS_JS
assert 'SESSION_LONG_PRESS_DELAY_MS' in PANELS_JS
assert 'long-pressing' in PANELS_JS
def test_session_action_menu_callable_from_titlebar():
assert '_openSessionActionMenu(S.session, titleEl)' in PANELS_JS