Merge pull request #5241 — page archived sidebar rows + archived-search refetch (#5200, @santastabber)

Release v0.51.757 — page archived sidebar rows + archived-search refetch (#5200, @santastabber)
This commit is contained in:
nesquena-hermes authored and GitHub committed 2026-06-29 15:38:06 -07:00
commit dd4d4b2e90
7 files changed
+309 -27

No files matched your search

+1
View File
@@ -35,6 +35,7 @@
### Fixed
- **The session sidebar stays fast with a very large archive, and archived search still finds everything.** Archived sidebar rows are now paged (capped per request) instead of all loaded at once, so an instance with thousands of archived sessions no longer pays to render them all. When you turn on "Show archived" and start a search, the sidebar transparently refetches without the archive cap so a title/id match beyond the first archived page is still reachable, then restores normal paging when the search clears. Default (non-search) paging keeps the existing 2000-row safety cap, and a render-generation guard prevents a slower capped/uncapped response from overwriting newer sidebar state. Thanks @santastabber. (#5200)
- **Post-restart config reloads no longer stampede under concurrent traffic.** Right after a restart (or profile switch), several in-flight requests could all see the config cache as stale and each trigger a full `config.yaml` re-parse at once, briefly stalling the single-process server. Stale reads now route through a shared gate that re-checks freshness inside the config lock (double-checked locking), so concurrent stale readers collapse to a single refresh instead of a thundering herd. Forced reloads (writes, profile switches) and the in-memory-override guard keep their existing semantics. Thanks @rodboev. (#5235, fixes #5220)
- **Fewer false "Connection Lost" banners on instances with large session state.** Under heavy session/sidebar state, transient slow responses and benign hidden-tab SSE churn could flap the offline banner even though the server was fine. The probe now waits for consecutive failures before declaring offline (while genuine signals — `navigator.onLine` going offline, startup failures — still surface immediately and bypass that delay), sidebar refreshes coalesce while one is in flight by draining the latest queued request instead of dropping it, the per-session SSE suspends/resumes cleanly across tab-visibility changes (reopening from a dedicated holder so it can't get stuck closed), and large-transcript message loads get a longer (120s) timeout so they don't trip the failure counter. A true outage still shows the banner and clears on recovery. Thanks @franksong2702. (#5201)
- **Polish UI: the "Virtualize long transcripts" setting is now translated** (label + description), and a stray UTF-8 byte-order mark was stripped from the top of `static/i18n.js`. Locale-only change; English and other locales are unaffected. Thanks @leszek3737. (#5216)
+14
View File
@@ -127,7 +127,19 @@ def _session_list_cache_key(
visible_only: bool = False,
source_filter: str | None = None,
sidebar_source: str | None = None,
archived_limit: int | None = None,
archived_offset: int = 0,
) -> tuple:
normalized_archived_limit = None
if archived_limit is not None:
try:
normalized_archived_limit = max(0, int(archived_limit))
except (TypeError, ValueError):
normalized_archived_limit = None
try:
normalized_archived_offset = max(0, int(archived_offset or 0))
except (TypeError, ValueError):
normalized_archived_offset = 0
return (
_session_list_cache_profile_scope(active_profile),
bool(all_profiles),
@@ -139,6 +151,8 @@ def _session_list_cache_key(
bool(visible_only),
source_filter,
sidebar_source,
normalized_archived_limit,
normalized_archived_offset,
)
+57 -12
View File
@@ -481,6 +481,21 @@ def _query_flag(parsed_url, name: str) -> bool:
return raw in ('1', 'true', 'yes', 'on')
def _query_positive_int(parsed_url, name: str, *, default=None, maximum: int | None = None):
"""Return a non-negative integer query parameter, or default when absent/invalid."""
qs = parse_qs(parsed_url.query)
raw = qs.get(name, [''])[0]
try:
value = int(str(raw).strip())
except (TypeError, ValueError):
return default
if value < 0:
return default
if maximum is not None:
value = min(value, int(maximum))
return value
def _session_visible_to_active_profile(session_profile, handler=None) -> bool:
"""Return whether a detail-load session belongs to the active profile.
@@ -1695,6 +1710,8 @@ def _build_session_list_cache_payload(
visible_only: bool = False,
source_filter: str | None = None,
sidebar_source: str | None = None,
archived_limit: int | None = None,
archived_offset: int = 0,
diag=None,
) -> dict:
diag_stage = diag.stage if diag is not None else lambda *_a, **_k: None
@@ -1901,15 +1918,6 @@ def _build_session_list_cache_payload(
if s.get("archived") and _is_cli_session_for_settings(s)
)
archived_count = archived_webui_count + archived_cli_count
scoped = archived_scoped if include_archived else visible_scoped
webui_session_count = sum(
1 for s in scoped
if not _is_cli_session_for_settings(s)
)
cli_session_count = sum(
1 for s in scoped
if _is_cli_session_for_settings(s)
)
def _filter_sidebar_source(rows: list[dict]) -> list[dict]:
if sidebar_source == "webui":
return [s for s in rows if not _is_cli_session_for_settings(s)]
@@ -1917,12 +1925,38 @@ def _build_session_list_cache_payload(
return [s for s in rows if _is_cli_session_for_settings(s)]
return list(rows)
scoped = _filter_sidebar_source(scoped)
full_scoped_all_sources = archived_scoped if include_archived else visible_scoped
webui_session_count = sum(
1 for s in full_scoped_all_sources
if not _is_cli_session_for_settings(s)
)
cli_session_count = sum(
1 for s in full_scoped_all_sources
if _is_cli_session_for_settings(s)
)
visible_scoped_filtered = _filter_sidebar_source(visible_scoped)
archived_scoped_filtered = _filter_sidebar_source(archived_scoped)
scoped = _filter_sidebar_source(full_scoped_all_sources)
if include_archived and archived_limit is not None:
try:
normalized_archived_limit = max(0, int(archived_limit))
except (TypeError, ValueError):
normalized_archived_limit = None
try:
normalized_archived_offset = max(0, int(archived_offset or 0))
except (TypeError, ValueError):
normalized_archived_offset = 0
if normalized_archived_limit is not None:
visible_rows_for_page = [s for s in visible_scoped_filtered if not s.get("archived")]
archived_rows_for_page = [s for s in archived_scoped_filtered if s.get("archived")]
scoped = visible_rows_for_page + archived_rows_for_page[
normalized_archived_offset: normalized_archived_offset + normalized_archived_limit
]
sidebar_reference_sessions: list[dict] = []
if not include_archived:
sidebar_reference_sessions = _hidden_archived_sidebar_reference_sessions(
_filter_sidebar_source(visible_scoped),
_filter_sidebar_source(archived_scoped),
visible_scoped_filtered,
archived_scoped_filtered,
)
if not include_archived:
diag_stage("filter_archived_sessions")
@@ -1944,6 +1978,8 @@ def _build_session_list_cache_payload(
"webui_session_count": webui_session_count,
"cli_session_count": cli_session_count,
"include_archived": include_archived,
"archived_limit": archived_limit,
"archived_offset": archived_offset,
"all_profiles": all_profiles,
"active_profile": active_profile,
"other_profile_count": other_profile_count,
@@ -1997,6 +2033,9 @@ def _session_list_payload_to_response(payload: dict) -> dict:
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))
if payload.get("archived_limit") is not None:
response["archived_limit"] = int(payload.get("archived_limit") or 0)
response["archived_offset"] = int(payload.get("archived_offset") or 0)
return response
@@ -10739,6 +10778,8 @@ def handle_get(handler, parsed) -> bool:
all_profiles = _all_profiles_enabled(parsed)
include_archived = _query_flag(parsed, "include_archived")
exclude_hidden = _query_flag(parsed, "exclude_hidden")
archived_limit = _query_positive_int(parsed, "archived_limit", default=None, maximum=2000)
archived_offset = _query_positive_int(parsed, "archived_offset", default=0, maximum=200000)
sidebar_source = parse_qs(parsed.query).get("sidebar_source", [""])[0].strip().lower() or None
if sidebar_source not in ("webui", "cli"):
sidebar_source = None
@@ -10755,6 +10796,8 @@ def handle_get(handler, parsed) -> bool:
visible_only=True,
source_filter=agent_session_source_filter,
sidebar_source=sidebar_source,
archived_limit=archived_limit,
archived_offset=archived_offset,
)
# Keep the visible /api/sessions contract unchanged even though the
# heavy lifting now lives in the cache builder: profile scoping via
@@ -10773,6 +10816,8 @@ def handle_get(handler, parsed) -> bool:
visible_only=True,
source_filter=agent_session_source_filter,
sidebar_source=sidebar_source,
archived_limit=archived_limit,
archived_offset=archived_offset,
diag=diag,
),
diag=diag,
+71 -4
View File
@@ -1841,12 +1841,30 @@ function _sessionListExcludeHiddenEnabled() {
return _activeProject===null || _activeProject===NO_PROJECT_FILTER;
}
function _sessionArchivePagingFilterActive() {
let searchActive=false;
try{
const searchEl=typeof $==='function' ? $('sessionSearch') : null;
searchActive=Boolean(searchEl&&String(searchEl.value||'').trim());
}catch(_e){ searchActive=false; }
return Boolean(searchActive||_activeProject);
}
function _sessionListQueryString() {
const qs = new URLSearchParams();
qs.set('sidebar_source', _requestedSessionSidebarSource());
if(_sessionListExcludeHiddenEnabled()) qs.set('exclude_hidden','1');
if(_showAllProfiles) qs.set('all_profiles','1');
if(_showArchived) qs.set('include_archived','1');
if(_showArchived){
qs.set('include_archived','1');
if(!_sessionArchivePagingFilterActive()){
const archiveLimit=Math.min(
SESSION_ARCHIVED_MAX_LOADED_LIMIT,
Math.max(SESSION_ARCHIVED_PAGE_SIZE, Number(_archivedRowsLoadedLimit)||SESSION_ARCHIVED_PAGE_SIZE)
);
qs.set('archived_limit', String(archiveLimit));
}
}
return `?${qs.toString()}`;
}
@@ -3066,6 +3084,8 @@ async function _ensureAllMessagesLoaded() {
}
}
const SESSION_ARCHIVED_PAGE_SIZE = 100;
const SESSION_ARCHIVED_MAX_LOADED_LIMIT = 2000;
let _allSessions = []; // cached for search filter
let _sidebarReferenceSessions = []; // hidden archived ancestor rows used only for nesting/suppression
let _allSessionsScope = null; // {profile, allProfiles} the cache was loaded under (#4167)
@@ -3088,6 +3108,7 @@ let _showAllProfiles = false; // false = filter to active profile only
let _otherProfileCount = 0; // count of sessions from other profiles (server-reported)
let _archivedWebuiCount = 0; // archived WebUI sessions not fetched until requested
let _archivedCliCount = 0; // archived non-WebUI sessions not fetched until requested
let _archivedRowsLoadedLimit = SESSION_ARCHIVED_PAGE_SIZE;
let _serverWebuiSessionCount = null; // explicit server count for WebUI sessions
let _serverCliSessionCount = null; // explicit server count for CLI sessions
let _sessionSourceFilter = 'webui'; // 'webui' keeps WebUI chats separate from read-only CLI sessions
@@ -4962,6 +4983,7 @@ let _searchDebounceTimer = null;
let _contentSearchResults = []; // results from /api/sessions/search content scan
let _lastSessionSearchQuery = '';
let _hideSearchPreviewsAfterSelect = false;
let _archivedSearchPagingQueryActive = false;
let _serverTimeDelta = 0; // ms offset: client clock - server clock (for clock-skew compensation)
let _serverTz = ''; // server timezone offset string (e.g. "+0800", "+0000", "-0500")
@@ -5126,11 +5148,23 @@ function clearSessionSearch(focusInput=true){
if(focusInput) input.focus();
}
function _syncArchivedSearchPagingRefresh(query){
const queryActive=Boolean(String(query||'').trim());
const previous=_archivedSearchPagingQueryActive;
_archivedSearchPagingQueryActive=queryActive;
if(!_showArchived||queryActive===previous) return;
// Archived title/id filtering is client-side. When search becomes active,
// refetch without archived_limit so matches beyond the first archived page are
// reachable; when search clears, refetch again to restore normal archive paging.
if(typeof renderSessionList==='function') void renderSessionList({deferWhileInteracting:false});
}
function filterSessions(){
// Immediate client-side title filter (no flicker)
// Debounced content search via API for message text
syncSessionSearchClear();
const q = ($('sessionSearch').value || '').trim();
_syncArchivedSearchPagingRefresh(q);
if(q!==_lastSessionSearchQuery){
_lastSessionSearchQuery=q;
_hideSearchPreviewsAfterSelect=false;
@@ -6120,8 +6154,16 @@ function renderSessionListFromCache(){
const referenceRaw=_sessionSourceFilter==='cli'?cliReferenceRaw:webuiReferenceRaw;
const isCliView=_sessionSourceFilter==='cli';
const sessions=_renderSidebarRowsFromRawSessions(sessionsRaw, [...referenceRaw, ..._scopedSidebarReferenceRows(isCliView)]);
const renderedWebuiSessionCount=_renderSidebarRowsFromRawSessions(webuiSessionsRaw, [...webuiReferenceRaw, ..._scopedSidebarReferenceRows(false)]).length;
const renderedCliSessionCount=_renderSidebarRowsFromRawSessions(cliSessionsRaw, [...cliReferenceRaw, ..._scopedSidebarReferenceRows(true)]).length;
// Server-provided source bucket counts are authoritative for the current
// payload. When present, skip the expensive cross-bucket render/count pass;
// null is a deliberate "not computed" sentinel consumed only by
// _sessionSourceTabCount's fallback path below.
const renderedWebuiSessionCount=_serverWebuiSessionCount===null
? _renderSidebarRowsFromRawSessions(webuiSessionsRaw, [...webuiReferenceRaw, ..._scopedSidebarReferenceRows(false)]).length
: null;
const renderedCliSessionCount=_serverCliSessionCount===null
? _renderSidebarRowsFromRawSessions(cliSessionsRaw, [...cliReferenceRaw, ..._scopedSidebarReferenceRows(true)]).length
: null;
const webuiSessionTabCount=_sessionSourceTabCount('webui', renderedWebuiSessionCount, renderedCliSessionCount);
const cliSessionTabCount=_sessionSourceTabCount('cli', renderedWebuiSessionCount, renderedCliSessionCount);
_syncSidebarExpansionForActiveSession(sessions, activeSidForSidebar);
@@ -6318,7 +6360,11 @@ function renderSessionListFromCache(){
const toggle=document.createElement('div');
toggle.style.cssText='font-size:10px;padding:4px 10px;color:var(--muted);cursor:pointer;text-align:center;opacity:.7;';
toggle.textContent=_showArchived?'Hide archived':'Show '+archivedCount+' archived';
toggle.onclick=()=>{_showArchived=!_showArchived;renderSessionList();};
toggle.onclick=()=>{
_showArchived=!_showArchived;
if(_showArchived) _archivedRowsLoadedLimit=SESSION_ARCHIVED_PAGE_SIZE;
renderSessionList();
};
list.appendChild(toggle);
}
// Empty state for active project filter
@@ -6467,6 +6513,27 @@ function renderSessionListFromCache(){
list.scrollTop=listScrollTopBeforeRender;
_resyncSessionVirtualWindowAfterRender(list, listScrollTopBeforeRender, virtualWindow);
}
const archivePagingFilterActive=_sessionArchivePagingFilterActive();
if(_showArchived&&!archivePagingFilterActive){
const activeArchivedTotal=_sessionSourceFilter==='cli'?_archivedCliCount:_archivedWebuiCount;
const loadedArchivedCount=sidebarRows.filter(s=>s&&s.archived&&(_sessionSourceFilter==='cli'?_isCliSession(s):!_isCliSession(s))).length;
const archiveLoadCapReached=Number(_archivedRowsLoadedLimit||0)>=SESSION_ARCHIVED_MAX_LOADED_LIMIT;
const remainingArchived=archiveLoadCapReached?0:Math.max(0, Number(activeArchivedTotal||0)-loadedArchivedCount);
if(remainingArchived>0){
const more=document.createElement('div');
more.className='session-archive-more';
more.style.cssText='font-size:10px;padding:6px 10px;color:var(--muted);cursor:pointer;text-align:center;opacity:.8;';
more.textContent='Load '+Math.min(SESSION_ARCHIVED_PAGE_SIZE, remainingArchived)+' more archived ('+remainingArchived+' remaining)';
more.onclick=()=>{
_archivedRowsLoadedLimit=Math.min(
SESSION_ARCHIVED_MAX_LOADED_LIMIT,
Math.max(SESSION_ARCHIVED_PAGE_SIZE, Number(_archivedRowsLoadedLimit)||SESSION_ARCHIVED_PAGE_SIZE)+SESSION_ARCHIVED_PAGE_SIZE
);
renderSessionList();
};
list.appendChild(more);
}
}
// Select mode toggle button (only when NOT in select mode)
if(!_sessionSelectMode){
const toggleBtn=document.createElement('div');toggleBtn.className='session-select-toggle';
@@ -304,6 +304,7 @@ def test_session_list_query_string_respects_sidebar_source_and_flags():
src = SESSIONS_JS.read_text(encoding="utf-8")
requested_source_fn = _extract_function(src, "_requestedSessionSidebarSource")
exclude_hidden_fn = _extract_function(src, "_sessionListExcludeHiddenEnabled")
archive_filter_fn = _extract_function(src, "_sessionArchivePagingFilterActive")
query_fn = _extract_function(src, "_sessionListQueryString")
script = f"""
global.window = {{ _showCliSessions: true }};
@@ -311,27 +312,97 @@ global._activeProject = null;
global._sessionSourceFilter = 'cli';
global._showAllProfiles = true;
global._showArchived = false;
global.SESSION_ARCHIVED_PAGE_SIZE = 100;
global.SESSION_ARCHIVED_MAX_LOADED_LIMIT = 2000;
global._archivedRowsLoadedLimit = 100;
global.NO_PROJECT_FILTER = '__none__';
let searchValue = '';
global.$ = (id) => id === 'sessionSearch' ? {{ value: searchValue }} : null;
{requested_source_fn}
{exclude_hidden_fn}
{archive_filter_fn}
{query_fn}
const first = _sessionListQueryString();
window._showCliSessions = false;
global._showArchived = true;
const second = _sessionListQueryString();
searchValue = 'old archived title';
const searchFiltered = _sessionListQueryString();
searchValue = '';
global._activeProject = 'project-1';
const projectFiltered = _sessionListQueryString();
global._activeProject = null;
global._archivedRowsLoadedLimit = 2500;
const capped = _sessionListQueryString();
global._activeProject = '__none__';
global.NO_PROJECT_FILTER = '__none__';
global._showAllProfiles = false;
global._showArchived = false;
const third = _sessionListQueryString();
console.log(JSON.stringify({{ first, second, third }}));
console.log(JSON.stringify({{ first, second, searchFiltered, projectFiltered, capped, third }}));
"""
body = _run_node(script)
assert body["first"] == "?sidebar_source=cli&exclude_hidden=1&all_profiles=1"
assert body["second"] == "?sidebar_source=webui&exclude_hidden=1&all_profiles=1&include_archived=1"
assert body["second"] == "?sidebar_source=webui&exclude_hidden=1&all_profiles=1&include_archived=1&archived_limit=100"
assert body["searchFiltered"] == "?sidebar_source=webui&exclude_hidden=1&all_profiles=1&include_archived=1"
assert body["projectFiltered"] == "?sidebar_source=webui&all_profiles=1&include_archived=1"
assert body["capped"] == "?sidebar_source=webui&exclude_hidden=1&all_profiles=1&include_archived=1&archived_limit=2000"
assert body["third"] == "?sidebar_source=webui&exclude_hidden=1"
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_archived_search_input_refetches_uncapped_then_restores_paging():
src = SESSIONS_JS.read_text(encoding="utf-8")
requested_source_fn = _extract_function(src, "_requestedSessionSidebarSource")
exclude_hidden_fn = _extract_function(src, "_sessionListExcludeHiddenEnabled")
archive_filter_fn = _extract_function(src, "_sessionArchivePagingFilterActive")
query_fn = _extract_function(src, "_sessionListQueryString")
sync_archive_fn = _extract_function(src, "_syncArchivedSearchPagingRefresh")
filter_fn = _extract_function(src, "filterSessions")
script = f"""
global.window = {{ _showCliSessions: false }};
global._activeProject = null;
global.NO_PROJECT_FILTER = '__none__';
global._sessionSourceFilter = 'webui';
global._showAllProfiles = false;
global._showArchived = true;
global.SESSION_ARCHIVED_PAGE_SIZE = 100;
global.SESSION_ARCHIVED_MAX_LOADED_LIMIT = 2000;
global._archivedRowsLoadedLimit = 100;
global._archivedSearchPagingQueryActive = false;
global._lastSessionSearchQuery = '';
global._hideSearchPreviewsAfterSelect = false;
global._contentSearchResults = [];
global._searchDebounceTimer = null;
const calls = [];
let searchValue = '';
global.$ = (id) => id === 'sessionSearch' ? {{ value: searchValue }} : null;
global.syncSessionSearchClear = () => {{}};
global.renderSessionList = () => {{ calls.push(_sessionListQueryString()); return Promise.resolve(); }};
global.renderSessionListFromCache = () => {{}};
global.clearTimeout = () => {{}};
global.setTimeout = () => 1;
global.api = () => Promise.resolve({{ sessions: [] }});
{requested_source_fn}
{exclude_hidden_fn}
{archive_filter_fn}
{query_fn}
{sync_archive_fn}
{filter_fn}
searchValue = 'page two title';
filterSessions();
searchValue = '';
filterSessions();
console.log(JSON.stringify({{ calls }}));
"""
body = _run_node(script)
assert body["calls"] == [
"?sidebar_source=webui&exclude_hidden=1&include_archived=1",
"?sidebar_source=webui&exclude_hidden=1&include_archived=1&archived_limit=100",
]
@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")
+69 -2
View File
@@ -128,6 +128,67 @@ def test_sessions_api_fetches_archived_rows_only_when_requested(monkeypatch):
assert enriched_batches == [["visible-active", "archived-history"]]
def test_sessions_api_can_limit_archived_rows_without_hiding_visible_rows(monkeypatch):
rows = [
{"session_id": "visible-a", "title": "Visible A", "profile": "default", "archived": False, "message_count": 1, "updated_at": 50, "last_message_at": 50},
{"session_id": "visible-b", "title": "Visible B", "profile": "default", "archived": False, "message_count": 1, "updated_at": 40, "last_message_at": 40},
{"session_id": "archived-new", "title": "Archived New", "profile": "default", "archived": True, "message_count": 1, "updated_at": 30, "last_message_at": 30},
{"session_id": "archived-mid", "title": "Archived Mid", "profile": "default", "archived": True, "message_count": 1, "updated_at": 20, "last_message_at": 20},
{"session_id": "archived-old", "title": "Archived Old", "profile": "default", "archived": True, "message_count": 1, "updated_at": 10, "last_message_at": 10},
]
enriched_batches = []
monkeypatch.setattr(routes, "all_sessions", lambda **_kwargs: rows)
monkeypatch.setattr(
routes,
"_enrich_sidebar_lineage_metadata",
lambda batch: enriched_batches.append([row["session_id"] for row in batch]),
)
monkeypatch.setattr(routes, "_reconcile_stale_stream_state_for_session_rows", lambda rows: False)
monkeypatch.setattr(routes, "load_settings", lambda: {"show_cli_sessions": False})
monkeypatch.setattr(profiles, "get_active_profile_name", lambda: "default")
routes._session_list_cache_clear()
handler = _FakeHandler()
routes.handle_get(handler, urlparse("http://example.com/api/sessions?include_archived=1&archived_limit=2"))
assert handler.status == 200
body = handler.json_body()
assert [row["session_id"] for row in body["sessions"]] == [
"visible-a",
"visible-b",
"archived-new",
"archived-mid",
]
assert body["archived_count"] == 3
assert body["webui_session_count"] == 5
assert body["archived_limit"] == 2
assert enriched_batches == [["visible-a", "visible-b", "archived-new", "archived-mid"]]
def test_archived_limit_varies_session_list_cache_key():
base = routes._session_list_cache_key(
active_profile="default",
all_profiles=False,
show_cli_sessions=False,
show_previous_messaging_sessions=False,
show_cron_sessions=False,
include_archived=True,
archived_limit=100,
)
larger = routes._session_list_cache_key(
active_profile="default",
all_profiles=False,
show_cli_sessions=False,
show_previous_messaging_sessions=False,
show_cron_sessions=False,
include_archived=True,
archived_limit=200,
)
assert base != larger
def test_sessions_api_legacy_all_sessions_monkeypatch_fallback_is_narrow(monkeypatch):
calls = []
@@ -170,9 +231,15 @@ def test_sessions_api_internal_typeerror_is_not_hidden_by_legacy_fallback(monkey
def test_session_list_fetch_adds_include_archived_only_when_toggle_is_on():
src = (pathlib.Path(__file__).parent.parent / "static" / "sessions.js").read_text(encoding="utf-8")
assert "if(_showArchived) qs.set('include_archived','1');" in src
assert "qs.set('include_archived','1');" in src
assert "const archiveLimit=Math.min(" in src
assert "SESSION_ARCHIVED_MAX_LOADED_LIMIT" in src
assert "qs.set('archived_limit', String(archiveLimit));" in src
assert "api('/api/sessions' + sessionListQS" in src
assert "toggle.onclick=()=>{_showArchived=!_showArchived;renderSessionList();};" in src
assert "if(_showArchived) _archivedRowsLoadedLimit=SESSION_ARCHIVED_PAGE_SIZE;" in src
assert "className='session-archive-more'" in src
assert "_archivedRowsLoadedLimit=Math.min(" in src
assert "Math.max(SESSION_ARCHIVED_PAGE_SIZE, Number(_archivedRowsLoadedLimit)||SESSION_ARCHIVED_PAGE_SIZE)+SESSION_ARCHIVED_PAGE_SIZE" in src
assert "_archivedWebuiCount" in src
assert "sessData.archived_webui_count ?? sessData.archived_count ?? 0" in src
assert "archived_webui_count" in src
+23 -6
View File
@@ -32,8 +32,12 @@ def test_render_uses_single_pass_partition_helper():
assert "_partitionSidebarSessionRows(allMatched, activeSidForSidebar)" in render_body
assert "_renderSidebarRowsFromRawSessions(sessionsRaw, [...referenceRaw, ..._scopedSidebarReferenceRows(isCliView)])" in render_body
assert "const renderedWebuiSessionCount=_renderSidebarRowsFromRawSessions(webuiSessionsRaw, [...webuiReferenceRaw, ..._scopedSidebarReferenceRows(false)]).length;" in render_body
assert "const renderedCliSessionCount=_renderSidebarRowsFromRawSessions(cliSessionsRaw, [...cliReferenceRaw, ..._scopedSidebarReferenceRows(true)]).length;" in render_body
assert "const renderedWebuiSessionCount=_serverWebuiSessionCount===null" in render_body
assert "const renderedCliSessionCount=_serverCliSessionCount===null" in render_body
assert "? _renderSidebarRowsFromRawSessions(webuiSessionsRaw, [...webuiReferenceRaw, ..._scopedSidebarReferenceRows(false)]).length" in render_body
assert "? _renderSidebarRowsFromRawSessions(cliSessionsRaw, [...cliReferenceRaw, ..._scopedSidebarReferenceRows(true)]).length" in render_body
assert ": null;" in render_body
assert "null is a deliberate \"not computed\" sentinel" in render_body
assert "const webuiSessionTabCount=_sessionSourceTabCount('webui', renderedWebuiSessionCount, renderedCliSessionCount);" in render_body
assert "const cliSessionTabCount=_sessionSourceTabCount('cli', renderedWebuiSessionCount, renderedCliSessionCount);" in render_body
assert "const count=filter==='cli'?cliSessionTabCount:webuiSessionTabCount;" in render_body
@@ -66,10 +70,23 @@ def test_partition_helper_keeps_raw_source_counts_while_render_owns_visible_coun
assert "cliReferenceRaw," in _partition_block()
assert "webuiSessionsRaw," in _partition_block()
assert "cliSessionsRaw," in _partition_block()
assert "const renderedWebuiSessionCount=" in render_body
assert "const renderedCliSessionCount=" in render_body
assert "_renderSidebarRowsFromRawSessions(webuiSessionsRaw, [...webuiReferenceRaw, ..._scopedSidebarReferenceRows(false)]).length" in render_body
assert "_renderSidebarRowsFromRawSessions(cliSessionsRaw, [...cliReferenceRaw, ..._scopedSidebarReferenceRows(true)]).length" in render_body
assert "const renderedWebuiSessionCount=_serverWebuiSessionCount===null" in render_body
assert "const renderedCliSessionCount=_serverCliSessionCount===null" in render_body
assert "? _renderSidebarRowsFromRawSessions(webuiSessionsRaw, [...webuiReferenceRaw, ..._scopedSidebarReferenceRows(false)]).length" in render_body
assert "? _renderSidebarRowsFromRawSessions(cliSessionsRaw, [...cliReferenceRaw, ..._scopedSidebarReferenceRows(true)]).length" in render_body
assert "function _countRenderedSidebarRowsFromRawSessions" not in SESSIONS_JS
assert "function _renderSidebarRowsFromRawSessions(sessionsRaw, referenceSessionsRaw){" in SESSIONS_JS
assert "_attachChildSessionsToSidebarRows(_collapseSessionLineageForSidebar(sessionsRaw), sessionsRaw, referenceRows)" in SESSIONS_JS
def test_archive_load_more_uses_source_wide_loaded_count_and_hides_under_filters():
render_body = _function_block("renderSessionListFromCache")
assert "function _sessionArchivePagingFilterActive()" in SESSIONS_JS
assert "const archivePagingFilterActive=_sessionArchivePagingFilterActive();" in render_body
assert "if(_showArchived&&!archivePagingFilterActive){" in render_body
assert "const loadedArchivedCount=sidebarRows.filter" in render_body
assert "const archiveLoadCapReached=Number(_archivedRowsLoadedLimit||0)>=SESSION_ARCHIVED_MAX_LOADED_LIMIT;" in render_body
assert "const remainingArchived=archiveLoadCapReached?0:Math.max(0, Number(activeArchivedTotal||0)-loadedArchivedCount);" in render_body
assert "const remainingArchived=Math.max(0, Number(activeArchivedTotal||0)-loadedArchivedCount);" not in render_body
assert "orderedSessions.filter(s=>s&&s.archived).length" not in render_body