diff --git a/CHANGELOG.md b/CHANGELOG.md index 89792caf..7dbe8a69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## [Unreleased] +### Fixed + +- Keep the sidebar conversation actions menu open while session-list refreshes, stream updates, or panel-resync repairs arrive, so the three-dot menu beside chat titles remains usable while the user is interacting with it. ## [v0.51.107] — 2026-05-21 — Release CE (stage-400 — 8-PR batch — pinned-sessions-limit getter rename + uploaded-file user-turn dedupe + active-run repair guard + incremental KaTeX streaming + profile default model on fresh boot + French locale completion + update-check error surfacing + release-update apply path) @@ -17,6 +20,7 @@ - **PR #2717** by @ai-ag2026 — Surface update-check fetch errors in the UI instead of failing silently. The background `api/updates/check` request previously swallowed network failures, so an offline / blocked-CDN scenario showed no indication that the version banner couldn't render. Now the failure is logged and exposed to the System panel's update-status card. - **PR #2719** by @ai-ag2026 — Apply release-update target correctly when the user clicks "Check for updates" after a prior dismissal: clears the `sessionStorage` check-once stamp and forces banner re-evaluation. The prior path silently no-op'd because the once-per-tab guard fired before the explicit user click could re-trigger the fetch. + ## [v0.51.106] — 2026-05-21 — Release CD (stage-399 — 3-PR batch — restamped state.db replay dedupe + context_messages dedupe so agent doesn't see duplicates + empty _partial bloat fix) ### Fixed diff --git a/static/sessions.js b/static/sessions.js index 9d366767..d864e0a6 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -2970,6 +2970,11 @@ function _resyncSessionVirtualWindowAfterRender(list, expectedScrollTop, virtual function renderSessionListFromCache(){ // Don't re-render while user is actively renaming a session (would destroy the input) if(_renamingSid) return; + // Keep the per-conversation actions menu stable while the user is trying to + // click it. Sidebar syncs, stream/unread updates, and panel-resync repairs can + // all call this while the fixed-position menu is open; rebuilding the row DOM + // here removes the anchor and makes the menu feel unclickable. + if(_sessionActionMenu) return; closeSessionActionMenu(); // Purge stale INFLIGHT entries for sessions the server confirms are NOT // streaming. This runs on every list refresh to prevent memory leaks from diff --git a/tests/test_configurable_pinned_sessions_limit.py b/tests/test_configurable_pinned_sessions_limit.py index f818cc0b..3dba102c 100644 --- a/tests/test_configurable_pinned_sessions_limit.py +++ b/tests/test_configurable_pinned_sessions_limit.py @@ -53,6 +53,7 @@ def test_pin_limit_setting_is_exposed_and_wired_through_ui(): assert "settings.pinned_sessions_limit" in PANELS_JS assert "window._pinnedSessionsLimit=parseInt(s.pinned_sessions_limit||3,10)||3" in BOOT_JS assert "function _getPinnedSessionsLimit()" in SESSIONS_JS + assert "function _pinnedSessionsLimit()" not in SESSIONS_JS assert "_pinnedSessionCount()>=_getPinnedSessionsLimit()" in SESSIONS_JS diff --git a/tests/test_issue2508_session_pin_cap.py b/tests/test_issue2508_session_pin_cap.py index a2cf203f..2799d32a 100644 --- a/tests/test_issue2508_session_pin_cap.py +++ b/tests/test_issue2508_session_pin_cap.py @@ -76,6 +76,7 @@ def test_session_pin_cap_has_backend_and_frontend_guards(): assert 'function _pinnedSessionCount()' in SESSIONS_JS assert 'function _getPinnedSessionsLimit()' in SESSIONS_JS + assert 'function _pinnedSessionsLimit()' not in SESSIONS_JS assert 'const pinLimitReached=!session.pinned&&_pinnedSessionCount()>=_getPinnedSessionsLimit();' in SESSIONS_JS assert 'Only ${limit} conversations can be pinned' in SESSIONS_JS assert ".session-action-opt.is-disabled{opacity:.55;cursor:not-allowed;}" in STYLE_CSS diff --git a/tests/test_session_action_menu_regression.py b/tests/test_session_action_menu_regression.py new file mode 100644 index 00000000..e3d36cdc --- /dev/null +++ b/tests/test_session_action_menu_regression.py @@ -0,0 +1,31 @@ +"""Regression checks for per-conversation action menu click stability.""" +from pathlib import Path + +SESSIONS_JS = (Path(__file__).resolve().parent.parent / "static" / "sessions.js").read_text(encoding="utf-8") + + +def _function_block(src: str, name: str) -> str: + marker = f"function {name}" + start = src.find(marker) + assert start != -1, f"{name} not found" + brace = src.find("{", start) + assert brace != -1, f"{name} body not found" + depth = 1 + i = brace + 1 + while i < len(src) and depth: + if src[i] == "{": + depth += 1 + elif src[i] == "}": + depth -= 1 + i += 1 + assert depth == 0, f"{name} body did not close" + return src[start:i] + + +def test_session_list_refresh_does_not_close_open_conversation_actions(): + """Sidebar refreshes must not eat the three-dot menu before users can click it.""" + body = _function_block(SESSIONS_JS, "renderSessionListFromCache") + + assert "if(_renamingSid) return;" in body + assert "if(_sessionActionMenu) return;" in body + assert body.index("if(_sessionActionMenu) return;") < body.index("closeSessionActionMenu();")