From 791523dffa80e9de5d4434f1af9f4f1d561f39f3 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Thu, 2 Jul 2026 10:09:55 +0000 Subject: [PATCH] fix(ux): keep sidebar action menu open during chat scroll + close on anchor detach (#5347) Clean rebase of nankingjing's #5402 (rebase-first). Co-authored-by: nankingjing --- static/sessions.js | 20 +++++++++++ ...st_issue5347_sidebar_action_menu_scroll.py | 33 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 tests/test_issue5347_sidebar_action_menu_scroll.py diff --git a/static/sessions.js b/static/sessions.js index bde3a1c4..441d5ab8 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -3561,6 +3561,17 @@ function closeSessionActionMenu(){ _sessionActionSessionId = null; } +function _sessionActionMenuShouldIgnoreScrollTarget(target){ + if(!target || typeof target.closest !== 'function') return false; + // #5347: active-chat auto-scroll / manual wheel must not dismiss the sidebar menu. + return Boolean(target.closest('#messages, #msgInner, .messages-inner')); +} + +function _sessionActionMenuShouldRepositionOnScroll(target){ + if(!target || typeof target.closest !== 'function') return false; + return Boolean(target.closest('#sessionList, .session-list')); +} + function _positionSessionActionMenu(anchorEl){ if(!_sessionActionMenu || !anchorEl) return; const rect=anchorEl.getBoundingClientRect(); @@ -4036,6 +4047,15 @@ document.addEventListener('click',e=>{ document.addEventListener('scroll',e=>{ if(!_sessionActionMenu) return; if(_sessionActionMenu.contains(e.target)) return; + if(_sessionActionMenuShouldIgnoreScrollTarget(e.target)) return; + if(_sessionActionMenuShouldRepositionOnScroll(e.target) && _sessionActionAnchor){ + if(!_sessionActionAnchor.isConnected){ + closeSessionActionMenu(); + return; + } + _positionSessionActionMenu(_sessionActionAnchor); + return; + } closeSessionActionMenu(); }, true); document.addEventListener('keydown',e=>{ diff --git a/tests/test_issue5347_sidebar_action_menu_scroll.py b/tests/test_issue5347_sidebar_action_menu_scroll.py new file mode 100644 index 00000000..8f4af279 --- /dev/null +++ b/tests/test_issue5347_sidebar_action_menu_scroll.py @@ -0,0 +1,33 @@ +"""Regression coverage for #5347: chat scroll must not dismiss sidebar action menu.""" +from pathlib import Path + +SESSIONS_JS = (Path(__file__).resolve().parent.parent / "static" / "sessions.js").read_text(encoding="utf-8") + + +def _scroll_listener_block() -> str: + marker = "document.addEventListener('scroll',e=>{" + start = SESSIONS_JS.find(marker) + assert start != -1, "session action menu scroll listener not found" + end = SESSIONS_JS.find("}, true);", start) + assert end != -1, "session action menu scroll listener did not close" + return SESSIONS_JS[start : end + len("}, true);")] + + +def test_chat_scroll_is_ignored_while_session_action_menu_is_open(): + """Streaming/manual chat scroll must not close the sidebar three-dot menu (#5347).""" + block = _scroll_listener_block() + + assert "_sessionActionMenuShouldIgnoreScrollTarget(e.target)" in block + assert "#messages" in SESSIONS_JS + assert "#msgInner" in SESSIONS_JS + + +def test_session_list_scroll_repositions_instead_of_closing_action_menu(): + """Sidebar list scroll should keep the menu aligned with its anchor row.""" + block = _scroll_listener_block() + + assert "_sessionActionMenuShouldRepositionOnScroll(e.target)" in block + assert block.index("_sessionActionMenuShouldRepositionOnScroll(e.target)") < block.index( + "closeSessionActionMenu();" + ) + assert "_positionSessionActionMenu(_sessionActionAnchor);" in block