From 73a209aca7d1c34571bf8a2fec1c236e2edf992f Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Tue, 23 Jun 2026 06:10:16 -0400 Subject: [PATCH] fix(#4766): close review gaps in sidebar source filtering --- api/routes.py | 9 +- static/sessions.js | 2 +- .../test_issue4766_sidebar_source_pushdown.py | 84 +++++++++++++++++++ 3 files changed, 91 insertions(+), 4 deletions(-) diff --git a/api/routes.py b/api/routes.py index c6c4e404..019fbf1c 100644 --- a/api/routes.py +++ b/api/routes.py @@ -2145,14 +2145,12 @@ def _session_list_payload_to_response(payload: dict) -> dict: for s in runtime_rows: item = _sidebar_session_response_item(s) if isinstance(s, dict) else {} safe_merged.append(item) - return { + response = { "sessions": safe_merged, "cli_count": int(payload.get("cli_count", 0)), "archived_count": int(payload.get("archived_count", 0)), "archived_webui_count": int(payload.get("archived_webui_count", 0)), "archived_cli_count": int(payload.get("archived_cli_count", 0)), - "webui_session_count": int(payload.get("webui_session_count", 0)), - "cli_session_count": int(payload.get("cli_session_count", 0)), "include_archived": bool(payload.get("include_archived", False)), "all_profiles": bool(payload.get("all_profiles", False)), "active_profile": payload.get("active_profile"), @@ -2160,6 +2158,11 @@ def _session_list_payload_to_response(payload: dict) -> dict: "server_time": time.time(), "server_tz": time.strftime("%z"), } + if "webui_session_count" in payload: + response["webui_session_count"] = int(payload.get("webui_session_count", 0)) + if "cli_session_count" in payload: + response["cli_session_count"] = int(payload.get("cli_session_count", 0)) + return response def _get_cached_session_list_payload( diff --git a/static/sessions.js b/static/sessions.js index 5863de55..30ed46f2 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -1569,7 +1569,7 @@ function _setSessionSourceFilter(filter) { _selectedSessions.clear(); _sessionSelectMode = false; try { localStorage.setItem('hermes-session-source-filter', next); } catch (_e) {} - renderSessionListFromCache(); + void renderSessionList({deferWhileInteracting:false}); } function _restoreSessionSourceFilter() { diff --git a/tests/test_issue4766_sidebar_source_pushdown.py b/tests/test_issue4766_sidebar_source_pushdown.py index 3331baaf..e81fb45a 100644 --- a/tests/test_issue4766_sidebar_source_pushdown.py +++ b/tests/test_issue4766_sidebar_source_pushdown.py @@ -3,6 +3,8 @@ import io import json from pathlib import Path +import shutil +import subprocess from urllib.parse import urlparse import api.profiles as profiles @@ -12,6 +14,7 @@ import pytest ROOT = Path(__file__).resolve().parents[1] SESSIONS_JS = ROOT / "static" / "sessions.js" +NODE = shutil.which("node") class _FakeHandler: @@ -82,6 +85,22 @@ def _handle_sessions(url): return handler +def _extract_function(source_text, function_name): + marker = f"function {function_name}(" + start = source_text.index(marker) + brace_start = source_text.index("{", start) + depth = 0 + for index in range(brace_start, len(source_text)): + char = source_text[index] + if char == "{": + depth += 1 + elif char == "}": + depth -= 1 + if depth == 0: + return source_text[start : index + 1] + raise AssertionError(f"Could not extract {function_name}") + + @pytest.fixture(autouse=True) def _clear_cache(): routes._session_list_cache_clear() @@ -229,6 +248,71 @@ def test_frontend_avoids_cli_bucket_request_when_cli_hidden(): assert "window._showCliSessions ? _sessionSourceFilter : 'webui'" in src +@pytest.mark.skipif(NODE is None, reason="node not on PATH") +def test_session_source_switch_fetches_selected_bucket(): + src = SESSIONS_JS.read_text(encoding="utf-8") + fn_source = _extract_function(src, "_setSessionSourceFilter") + script = f""" +const renderCalls = []; +global._sessionSourceFilter = 'webui'; +global._activeProject = 'demo-project'; +global._selectedSessions = new Set(['first', 'second']); +global._sessionSelectMode = true; +global.localStorage = {{ + writes: [], + setItem(key, value) {{ + this.writes.push([key, value]); + }}, +}}; +global.renderSessionList = (opts) => {{ + renderCalls.push(opts); + return Promise.resolve(); +}}; +{fn_source} +_setSessionSourceFilter('cli'); +console.log(JSON.stringify({{ + sourceFilter: global._sessionSourceFilter, + activeProject: global._activeProject, + selectedSize: global._selectedSessions.size, + sessionSelectMode: global._sessionSelectMode, + storageWrites: global.localStorage.writes, + renderCalls, +}})); +""" + proc = subprocess.run([NODE, "-e", script], capture_output=True, text=True, check=True) + body = json.loads(proc.stdout) + + assert body["sourceFilter"] == "cli" + assert body["activeProject"] is None + assert body["selectedSize"] == 0 + assert body["sessionSelectMode"] is False + assert body["storageWrites"] == [["hermes-session-source-filter", "cli"]] + assert body["renderCalls"] == [{"deferWhileInteracting": False}] + + +def test_session_list_response_omits_bucket_counts_when_missing(monkeypatch): + monkeypatch.setattr(routes, "_session_list_cache_overlay_runtime_rows", lambda rows: rows) + monkeypatch.setattr(routes, "_sidebar_session_response_item", lambda row: row) + + body = routes._session_list_payload_to_response( + { + "sessions": [{"session_id": "webui-1", "title": "WebUI Session"}], + "cli_count": 0, + "archived_count": 0, + "archived_webui_count": 0, + "archived_cli_count": 0, + "include_archived": False, + "all_profiles": False, + "active_profile": "default", + "other_profile_count": 0, + } + ) + + assert "webui_session_count" not in body + assert "cli_session_count" not in body + assert body["sessions"][0]["session_id"] == "webui-1" + + def test_payload_row_count_regression(monkeypatch): rows = _session_rows(webui_count=30, cli_count=20) _install_common_monkeypatches(monkeypatch, rows)