Merge origin/master into PR #5271

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Frank Song
2026-07-01 03:41:54 +08:00
5 changed files with 130 additions and 2 deletions
+2
View File
@@ -35,6 +35,8 @@
### Fixed
- **Mobile titlebar now prioritizes the session title, with tap-to-reveal and long-press actions.** On mobile the titlebar gives the session title priority (ellipsized when long) over surrounding chrome; tapping the title reveals the full title in a popover, and a long-press opens the session actions. Listener-leak-safe (tap wired once per element, outside-click handler cleaned up on dismiss/switch; long-press guarded against double-fire, cancelled on touchmove). Read-only sessions are skipped, and desktop is untouched (touch-gated). Thanks @rodboev. (#4574, #4520)
- **Mobile approval dialog and touch targets are easier to tap.** At mobile width the approval card's action buttons now lay out as a clean 2-column grid (Allow once / Allow session / Always allow / Deny) with the "Skip all this session" YOLO button full-width below, every button at a 44px minimum touch target; the titlebar new-chat/reload and the mobile sidebar-close controls are also bumped to 44px. Mobile-scoped (inside the mobile media query) — desktop layout is unchanged. Thanks @disco32r. (#5019)
- **`/pet` now works in the WebUI, handing off to the Desktop Companion extension.** Typing `/pet` (and its subcommands) is intercepted client-side and routed to the petdex companion extension with graceful per-stage guidance when the extension isn't installed; the command output is XSS-safe. Thanks @rodboev. (#5168, #4843)
+97 -1
View File
@@ -59,7 +59,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 = '';
@@ -153,6 +153,102 @@ function syncAppTitlebar() {
inp.select();
};
}
// Dismiss stale popover on session/panel switch
const _existingPop = document.querySelector('.app-titlebar-title-popover');
if (_existingPop) {
_existingPop.remove(); titleEl._titlePopover = null;
if (titleEl._popoverOutsideHandler) {
document.removeEventListener('click', titleEl._popoverOutsideHandler, true);
titleEl._popoverOutsideHandler = null;
}
}
// Mobile touch interactions
if ('ontouchstart' in window) {
// Tap-to-reveal full title popover — wired once per element lifetime
if (!titleEl._mobileTouchWired) {
titleEl._mobileTouchWired = true;
titleEl._titlePopover = null;
const _dismissTitlePopover = () => {
if (titleEl._titlePopover) { titleEl._titlePopover.remove(); titleEl._titlePopover = null; }
if (titleEl._popoverOutsideHandler) {
document.removeEventListener('click', titleEl._popoverOutsideHandler, true);
titleEl._popoverOutsideHandler = null;
}
};
titleEl.addEventListener('click', function _onTitleClick(e) {
if (_renamingAppTitlebar) return;
if (titleEl._titlePopover) {
_dismissTitlePopover();
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';
titleEl._titlePopover = pop;
const _outside = titleEl._popoverOutsideHandler = (ev) => {
if (!pop.contains(ev.target) && ev.target !== titleEl) {
_dismissTitlePopover();
document.removeEventListener('click', _outside, true);
titleEl._popoverOutsideHandler = null;
}
};
setTimeout(() => document.addEventListener('click', _outside, true), 0);
}, { passive: true });
}
// Long-press → session action menu (re-evaluated each sync so late-arriving sessions attach)
if (!titleEl._mobileLpWired && panel === 'chat' && S && S.session &&
!S.session.read_only && !S.session.is_read_only &&
typeof _openSessionActionMenu === 'function') {
titleEl._mobileLpWired = true;
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
@@ -1386,6 +1386,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);}
@@ -2824,6 +2825,9 @@
.app-titlebar-hamburger,.app-titlebar-spacer{display:flex;}
.app-titlebar-new-chat,.app-titlebar-reload{display:inline-flex;width:44px;height:44px;}
.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;}
+1 -1
View File
@@ -18,7 +18,7 @@ def test_app_titlebar_returns_to_centered_desktop_layout():
def test_app_titlebar_subtitle_shows_message_count_again():
assert "subText = t('n_messages', vis.length);" in PANELS_JS
assert "subText = String(vis.length);" in PANELS_JS
def test_queue_updates_do_not_hijack_app_titlebar_subtitle():
@@ -0,0 +1,26 @@
"""Static regressions for mobile titlebar session title priority (#4520)."""
import pathlib
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")
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