perf(sessions): page archived sidebar rows
This commit is contained in:
1 parent
61f07f6005
commit
0674a0198c
6 files changed
+308
-27
No files matched your search
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user