perf(sessions): page archived sidebar rows

This commit is contained in:
Jamie (Hermes) authored and nesquena-hermes committed 2026-06-29 22:24:35 +00:00
1 parent 61f07f6005
commit 0674a0198c
6 files changed
+308 -27

No files matched your search

+14
View File
@@ -127,7 +127,19 @@ def _session_list_cache_key(
visible_only: bool = False, visible_only: bool = False,
source_filter: str | None = None, source_filter: str | None = None,
sidebar_source: str | None = None, sidebar_source: str | None = None,
archived_limit: int | None = None,
archived_offset: int = 0,
) -> tuple: ) -> 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 ( return (
_session_list_cache_profile_scope(active_profile), _session_list_cache_profile_scope(active_profile),
bool(all_profiles), bool(all_profiles),
@@ -139,6 +151,8 @@ def _session_list_cache_key(
bool(visible_only), bool(visible_only),
source_filter, source_filter,
sidebar_source, 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') 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: def _session_visible_to_active_profile(session_profile, handler=None) -> bool:
"""Return whether a detail-load session belongs to the active profile. """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, visible_only: bool = False,
source_filter: str | None = None, source_filter: str | None = None,
sidebar_source: str | None = None, sidebar_source: str | None = None,
archived_limit: int | None = None,
archived_offset: int = 0,
diag=None, diag=None,
) -> dict: ) -> dict:
diag_stage = diag.stage if diag is not None else lambda *_a, **_k: None 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) if s.get("archived") and _is_cli_session_for_settings(s)
) )
archived_count = archived_webui_count + archived_cli_count 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]: def _filter_sidebar_source(rows: list[dict]) -> list[dict]:
if sidebar_source == "webui": if sidebar_source == "webui":
return [s for s in rows if not _is_cli_session_for_settings(s)] 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 [s for s in rows if _is_cli_session_for_settings(s)]
return list(rows) 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] = [] sidebar_reference_sessions: list[dict] = []
if not include_archived: if not include_archived:
sidebar_reference_sessions = _hidden_archived_sidebar_reference_sessions( sidebar_reference_sessions = _hidden_archived_sidebar_reference_sessions(
_filter_sidebar_source(visible_scoped), visible_scoped_filtered,
_filter_sidebar_source(archived_scoped), archived_scoped_filtered,
) )
if not include_archived: if not include_archived:
diag_stage("filter_archived_sessions") diag_stage("filter_archived_sessions")
@@ -1944,6 +1978,8 @@ def _build_session_list_cache_payload(
"webui_session_count": webui_session_count, "webui_session_count": webui_session_count,
"cli_session_count": cli_session_count, "cli_session_count": cli_session_count,
"include_archived": include_archived, "include_archived": include_archived,
"archived_limit": archived_limit,
"archived_offset": archived_offset,
"all_profiles": all_profiles, "all_profiles": all_profiles,
"active_profile": active_profile, "active_profile": active_profile,
"other_profile_count": other_profile_count, "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)) response["webui_session_count"] = int(payload.get("webui_session_count", 0))
if "cli_session_count" in payload: if "cli_session_count" in payload:
response["cli_session_count"] = int(payload.get("cli_session_count", 0)) 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 return response
@@ -10739,6 +10778,8 @@ def handle_get(handler, parsed) -> bool:
all_profiles = _all_profiles_enabled(parsed) all_profiles = _all_profiles_enabled(parsed)
include_archived = _query_flag(parsed, "include_archived") include_archived = _query_flag(parsed, "include_archived")
exclude_hidden = _query_flag(parsed, "exclude_hidden") 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 sidebar_source = parse_qs(parsed.query).get("sidebar_source", [""])[0].strip().lower() or None
if sidebar_source not in ("webui", "cli"): if sidebar_source not in ("webui", "cli"):
sidebar_source = None sidebar_source = None
@@ -10755,6 +10796,8 @@ def handle_get(handler, parsed) -> bool:
visible_only=True, visible_only=True,
source_filter=agent_session_source_filter, source_filter=agent_session_source_filter,
sidebar_source=sidebar_source, sidebar_source=sidebar_source,
archived_limit=archived_limit,
archived_offset=archived_offset,
) )
# Keep the visible /api/sessions contract unchanged even though the # Keep the visible /api/sessions contract unchanged even though the
# heavy lifting now lives in the cache builder: profile scoping via # heavy lifting now lives in the cache builder: profile scoping via
@@ -10773,6 +10816,8 @@ def handle_get(handler, parsed) -> bool:
visible_only=True, visible_only=True,
source_filter=agent_session_source_filter, source_filter=agent_session_source_filter,
sidebar_source=sidebar_source, sidebar_source=sidebar_source,
archived_limit=archived_limit,
archived_offset=archived_offset,
diag=diag, diag=diag,
), ),
diag=diag, diag=diag,
+71 -4
View File
@@ -1841,12 +1841,30 @@ function _sessionListExcludeHiddenEnabled() {
return _activeProject===null || _activeProject===NO_PROJECT_FILTER; 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() { function _sessionListQueryString() {
const qs = new URLSearchParams(); const qs = new URLSearchParams();
qs.set('sidebar_source', _requestedSessionSidebarSource()); qs.set('sidebar_source', _requestedSessionSidebarSource());
if(_sessionListExcludeHiddenEnabled()) qs.set('exclude_hidden','1'); if(_sessionListExcludeHiddenEnabled()) qs.set('exclude_hidden','1');
if(_showAllProfiles) qs.set('all_profiles','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()}`; 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 _allSessions = []; // cached for search filter
let _sidebarReferenceSessions = []; // hidden archived ancestor rows used only for nesting/suppression let _sidebarReferenceSessions = []; // hidden archived ancestor rows used only for nesting/suppression
let _allSessionsScope = null; // {profile, allProfiles} the cache was loaded under (#4167) 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 _otherProfileCount = 0; // count of sessions from other profiles (server-reported)
let _archivedWebuiCount = 0; // archived WebUI sessions not fetched until requested let _archivedWebuiCount = 0; // archived WebUI sessions not fetched until requested
let _archivedCliCount = 0; // archived non-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 _serverWebuiSessionCount = null; // explicit server count for WebUI sessions
let _serverCliSessionCount = null; // explicit server count for CLI sessions let _serverCliSessionCount = null; // explicit server count for CLI sessions
let _sessionSourceFilter = 'webui'; // 'webui' keeps WebUI chats separate from read-only 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 _contentSearchResults = []; // results from /api/sessions/search content scan
let _lastSessionSearchQuery = ''; let _lastSessionSearchQuery = '';
let _hideSearchPreviewsAfterSelect = false; let _hideSearchPreviewsAfterSelect = false;
let _archivedSearchPagingQueryActive = false;
let _serverTimeDelta = 0; // ms offset: client clock - server clock (for clock-skew compensation) let _serverTimeDelta = 0; // ms offset: client clock - server clock (for clock-skew compensation)
let _serverTz = ''; // server timezone offset string (e.g. "+0800", "+0000", "-0500") let _serverTz = ''; // server timezone offset string (e.g. "+0800", "+0000", "-0500")
@@ -5126,11 +5148,23 @@ function clearSessionSearch(focusInput=true){
if(focusInput) input.focus(); 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(){ function filterSessions(){
// Immediate client-side title filter (no flicker) // Immediate client-side title filter (no flicker)
// Debounced content search via API for message text // Debounced content search via API for message text
syncSessionSearchClear(); syncSessionSearchClear();
const q = ($('sessionSearch').value || '').trim(); const q = ($('sessionSearch').value || '').trim();
_syncArchivedSearchPagingRefresh(q);
if(q!==_lastSessionSearchQuery){ if(q!==_lastSessionSearchQuery){
_lastSessionSearchQuery=q; _lastSessionSearchQuery=q;
_hideSearchPreviewsAfterSelect=false; _hideSearchPreviewsAfterSelect=false;
@@ -6120,8 +6154,16 @@ function renderSessionListFromCache(){
const referenceRaw=_sessionSourceFilter==='cli'?cliReferenceRaw:webuiReferenceRaw; const referenceRaw=_sessionSourceFilter==='cli'?cliReferenceRaw:webuiReferenceRaw;
const isCliView=_sessionSourceFilter==='cli'; const isCliView=_sessionSourceFilter==='cli';
const sessions=_renderSidebarRowsFromRawSessions(sessionsRaw, [...referenceRaw, ..._scopedSidebarReferenceRows(isCliView)]); const sessions=_renderSidebarRowsFromRawSessions(sessionsRaw, [...referenceRaw, ..._scopedSidebarReferenceRows(isCliView)]);
const renderedWebuiSessionCount=_renderSidebarRowsFromRawSessions(webuiSessionsRaw, [...webuiReferenceRaw, ..._scopedSidebarReferenceRows(false)]).length; // Server-provided source bucket counts are authoritative for the current
const renderedCliSessionCount=_renderSidebarRowsFromRawSessions(cliSessionsRaw, [...cliReferenceRaw, ..._scopedSidebarReferenceRows(true)]).length; // 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 webuiSessionTabCount=_sessionSourceTabCount('webui', renderedWebuiSessionCount, renderedCliSessionCount);
const cliSessionTabCount=_sessionSourceTabCount('cli', renderedWebuiSessionCount, renderedCliSessionCount); const cliSessionTabCount=_sessionSourceTabCount('cli', renderedWebuiSessionCount, renderedCliSessionCount);
_syncSidebarExpansionForActiveSession(sessions, activeSidForSidebar); _syncSidebarExpansionForActiveSession(sessions, activeSidForSidebar);
@@ -6318,7 +6360,11 @@ function renderSessionListFromCache(){
const toggle=document.createElement('div'); 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.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.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); list.appendChild(toggle);
} }
// Empty state for active project filter // Empty state for active project filter
@@ -6467,6 +6513,27 @@ function renderSessionListFromCache(){
list.scrollTop=listScrollTopBeforeRender; list.scrollTop=listScrollTopBeforeRender;
_resyncSessionVirtualWindowAfterRender(list, listScrollTopBeforeRender, virtualWindow); _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) // Select mode toggle button (only when NOT in select mode)
if(!_sessionSelectMode){ if(!_sessionSelectMode){
const toggleBtn=document.createElement('div');toggleBtn.className='session-select-toggle'; 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") src = SESSIONS_JS.read_text(encoding="utf-8")
requested_source_fn = _extract_function(src, "_requestedSessionSidebarSource") requested_source_fn = _extract_function(src, "_requestedSessionSidebarSource")
exclude_hidden_fn = _extract_function(src, "_sessionListExcludeHiddenEnabled") exclude_hidden_fn = _extract_function(src, "_sessionListExcludeHiddenEnabled")
archive_filter_fn = _extract_function(src, "_sessionArchivePagingFilterActive")
query_fn = _extract_function(src, "_sessionListQueryString") query_fn = _extract_function(src, "_sessionListQueryString")
script = f""" script = f"""
global.window = {{ _showCliSessions: true }}; global.window = {{ _showCliSessions: true }};
@@ -311,27 +312,97 @@ global._activeProject = null;
global._sessionSourceFilter = 'cli'; global._sessionSourceFilter = 'cli';
global._showAllProfiles = true; global._showAllProfiles = true;
global._showArchived = false; 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} {requested_source_fn}
{exclude_hidden_fn} {exclude_hidden_fn}
{archive_filter_fn}
{query_fn} {query_fn}
const first = _sessionListQueryString(); const first = _sessionListQueryString();
window._showCliSessions = false; window._showCliSessions = false;
global._showArchived = true; global._showArchived = true;
const second = _sessionListQueryString(); 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._activeProject = '__none__';
global.NO_PROJECT_FILTER = '__none__';
global._showAllProfiles = false; global._showAllProfiles = false;
global._showArchived = false; global._showArchived = false;
const third = _sessionListQueryString(); const third = _sessionListQueryString();
console.log(JSON.stringify({{ first, second, third }})); console.log(JSON.stringify({{ first, second, searchFiltered, projectFiltered, capped, third }}));
""" """
body = _run_node(script) body = _run_node(script)
assert body["first"] == "?sidebar_source=cli&exclude_hidden=1&all_profiles=1" 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" 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") @pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_session_source_switch_fetches_selected_bucket(): def test_session_source_switch_fetches_selected_bucket():
src = SESSIONS_JS.read_text(encoding="utf-8") 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"]] 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): def test_sessions_api_legacy_all_sessions_monkeypatch_fallback_is_narrow(monkeypatch):
calls = [] 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(): 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") 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 "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 "_archivedWebuiCount" in src
assert "sessData.archived_webui_count ?? sessData.archived_count ?? 0" in src assert "sessData.archived_webui_count ?? sessData.archived_count ?? 0" in src
assert "archived_webui_count" 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 "_partitionSidebarSessionRows(allMatched, activeSidForSidebar)" in render_body
assert "_renderSidebarRowsFromRawSessions(sessionsRaw, [...referenceRaw, ..._scopedSidebarReferenceRows(isCliView)])" 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 renderedWebuiSessionCount=_serverWebuiSessionCount===null" in render_body
assert "const renderedCliSessionCount=_renderSidebarRowsFromRawSessions(cliSessionsRaw, [...cliReferenceRaw, ..._scopedSidebarReferenceRows(true)]).length;" 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 webuiSessionTabCount=_sessionSourceTabCount('webui', renderedWebuiSessionCount, renderedCliSessionCount);" in render_body
assert "const cliSessionTabCount=_sessionSourceTabCount('cli', 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 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 "cliReferenceRaw," in _partition_block()
assert "webuiSessionsRaw," in _partition_block() assert "webuiSessionsRaw," in _partition_block()
assert "cliSessionsRaw," in _partition_block() assert "cliSessionsRaw," in _partition_block()
assert "const renderedWebuiSessionCount=" in render_body assert "const renderedWebuiSessionCount=_serverWebuiSessionCount===null" in render_body
assert "const renderedCliSessionCount=" in render_body assert "const renderedCliSessionCount=_serverCliSessionCount===null" in render_body
assert "_renderSidebarRowsFromRawSessions(webuiSessionsRaw, [...webuiReferenceRaw, ..._scopedSidebarReferenceRows(false)]).length" in render_body assert "? _renderSidebarRowsFromRawSessions(webuiSessionsRaw, [...webuiReferenceRaw, ..._scopedSidebarReferenceRows(false)]).length" in render_body
assert "_renderSidebarRowsFromRawSessions(cliSessionsRaw, [...cliReferenceRaw, ..._scopedSidebarReferenceRows(true)]).length" in render_body assert "? _renderSidebarRowsFromRawSessions(cliSessionsRaw, [...cliReferenceRaw, ..._scopedSidebarReferenceRows(true)]).length" in render_body
assert "function _countRenderedSidebarRowsFromRawSessions" not in SESSIONS_JS assert "function _countRenderedSidebarRowsFromRawSessions" not in SESSIONS_JS
assert "function _renderSidebarRowsFromRawSessions(sessionsRaw, referenceSessionsRaw){" in SESSIONS_JS assert "function _renderSidebarRowsFromRawSessions(sessionsRaw, referenceSessionsRaw){" in SESSIONS_JS
assert "_attachChildSessionsToSidebarRows(_collapseSessionLineageForSidebar(sessionsRaw), sessionsRaw, referenceRows)" 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