fix(sidebar): keep hidden archived parents as child references

This commit is contained in:
santastabber authored and nesquena-hermes committed 2026-06-29 00:13:32 +00:00
1 parent b7c332cfc1
commit 5ed0ba6ef6
4 files changed
+159 -5

No files matched your search

+72 -4
View File
@@ -1908,10 +1908,20 @@ def _build_session_list_cache_payload(
1 for s in scoped
if _is_cli_session_for_settings(s)
)
if sidebar_source == "webui":
scoped = [s for s in scoped if not _is_cli_session_for_settings(s)]
elif sidebar_source == "cli":
scoped = [s 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)]
if sidebar_source == "cli":
return [s for s in rows if _is_cli_session_for_settings(s)]
return list(rows)
scoped = _filter_sidebar_source(scoped)
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),
)
if not include_archived:
diag_stage("filter_archived_sessions")
diag_stage("visible_lineage_metadata")
@@ -1921,6 +1931,10 @@ def _build_session_list_cache_payload(
dict(s) if isinstance(s, dict) else {}
for s in scoped
],
"sidebar_reference_sessions": [
dict(s) if isinstance(s, dict) else {}
for s in sidebar_reference_sessions
],
"cli_count": len(deduped_cli),
"archived_count": archived_count,
"archived_webui_count": archived_webui_count,
@@ -1957,8 +1971,15 @@ def _session_list_payload_to_response(payload: dict) -> dict:
for s in runtime_rows:
item = _sidebar_session_response_item(s, redact_enabled=_redact_enabled) if isinstance(s, dict) else {}
safe_merged.append(item)
safe_reference = []
for s in payload.get("sidebar_reference_sessions", []) or []:
item = _sidebar_session_response_item(s, redact_enabled=_redact_enabled) if isinstance(s, dict) else {}
if item:
item["_sidebar_reference_only"] = True
safe_reference.append(item)
response = {
"sessions": safe_merged,
"sidebar_reference_sessions": safe_reference,
"cli_count": int(payload.get("cli_count", 0)),
"archived_count": int(payload.get("archived_count", 0)),
"archived_webui_count": int(payload.get("archived_webui_count", 0)),
@@ -1977,6 +1998,53 @@ def _session_list_payload_to_response(payload: dict) -> dict:
return response
def _hidden_archived_sidebar_reference_sessions(
visible_rows: list[dict],
archived_rows: list[dict],
) -> list[dict]:
"""Return hidden archived ancestors needed for client-side sidebar nesting.
The default sidebar payload intentionally omits archived sessions. The
browser still needs a tiny reference row for an archived parent/ancestor so
`_attachChildSessionsToSidebarRows()` can suppress its visible child rows
instead of rendering them as orphan top-level conversations (#4293).
"""
archived_by_id = {
str(row.get("session_id")): row
for row in archived_rows
if isinstance(row, dict) and row.get("archived") and row.get("session_id")
}
if not archived_by_id:
return []
references: list[dict] = []
added: set[str] = set()
visible_ids = {
str(row.get("session_id"))
for row in visible_rows
if isinstance(row, dict) and row.get("session_id")
}
for row in visible_rows:
if not isinstance(row, dict):
continue
parent_id = str(row.get("parent_session_id") or "").strip()
seen: set[str] = set()
while parent_id and parent_id not in seen:
seen.add(parent_id)
if parent_id in visible_ids:
break
parent = archived_by_id.get(parent_id)
if not parent:
break
if parent_id not in added:
references.append(parent)
added.add(parent_id)
parent_id = str(parent.get("parent_session_id") or "").strip()
return references
def _get_cached_session_list_payload(
*,
key: tuple,
+9 -1
View File
@@ -3024,6 +3024,7 @@ async function _ensureAllMessagesLoaded() {
}
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)
let _sessionAttentionSoundPrimed = false;
const _sessionAttentionSoundState = new Map();
@@ -4169,6 +4170,9 @@ function _applySessionListPayload(sessData, projData){
const serverSessions=_optimisticallyRemovedSessionIds.size
? (sessData.sessions||[]).filter(s=>s&&!_optimisticallyRemovedSessionIds.has(s.session_id))
: (sessData.sessions||[]);
_sidebarReferenceSessions = Array.isArray(sessData.sidebar_reference_sessions)
? sessData.sidebar_reference_sessions
: [];
_reconcileActiveSessionIdleStateFromList(serverSessions);
_allSessions = _mergeOptimisticFirstTurnSessions(serverSessions);
// Tag the cache with the scope it was loaded under (active profile +
@@ -4305,6 +4309,7 @@ async function _runRenderSessionListRefresh(opts, _gen){
renderSessionListFromCache();
} else {
_allSessions = [];
_sidebarReferenceSessions = [];
_allSessionsScope = _curScope;
_clearSessionSourceTabCounts();
renderSessionListFromCache();
@@ -5989,7 +5994,10 @@ function _partitionSidebarSessionRows(allMatched, activeSidForSidebar){
}
function _renderSidebarRowsFromRawSessions(sessionsRaw, referenceSessionsRaw){
const referenceRows=Array.isArray(referenceSessionsRaw)?referenceSessionsRaw:sessionsRaw;
let referenceRows=Array.isArray(referenceSessionsRaw)?referenceSessionsRaw:sessionsRaw;
if(typeof _sidebarReferenceSessions!=='undefined'&&Array.isArray(_sidebarReferenceSessions)&&_sidebarReferenceSessions.length){
referenceRows=[...referenceRows,..._sidebarReferenceSessions];
}
return _attachChildSessionsToSidebarRows(_collapseSessionLineageForSidebar(sessionsRaw), sessionsRaw, referenceRows);
}
@@ -156,6 +156,49 @@ def test_sidebar_source_webui_excludes_cli_rows(monkeypatch):
assert set(enriched[0]) == expected
def test_sidebar_source_webui_includes_hidden_archived_parent_reference(monkeypatch):
rows = [
{
"session_id": "webui-parent",
"title": "Archived parent",
"profile": "default",
"archived": True,
"message_count": 10,
"updated_at": 1000,
"last_message_at": 1000,
"source": "webui",
"raw_source": "webui",
"session_source": "webui",
"source_tag": "webui",
},
{
"session_id": "webui-child",
"title": "Subagent Session",
"profile": "default",
"archived": False,
"message_count": 4,
"updated_at": 1100,
"last_message_at": 1100,
"source": "subagent",
"raw_source": "subagent",
"session_source": "other",
"source_tag": "subagent",
"parent_session_id": "webui-parent",
"relationship_type": "child_session",
},
]
_install_common_monkeypatches(monkeypatch, rows)
handler = _handle_sessions("http://example.com/api/sessions?sidebar_source=webui&exclude_hidden=1")
body = handler.json_body()
assert handler.status == 200
assert [row["session_id"] for row in body["sessions"]] == ["webui-child"]
assert [row["session_id"] for row in body["sidebar_reference_sessions"]] == ["webui-parent"]
assert body["sidebar_reference_sessions"][0]["archived"] is True
assert body["sidebar_reference_sessions"][0]["_sidebar_reference_only"] is True
def test_sidebar_source_cli_excludes_webui_rows(monkeypatch):
rows = _session_rows(webui_count=30, cli_count=20)
_install_common_monkeypatches(monkeypatch, rows)
+35
View File
@@ -421,6 +421,41 @@ console.log(JSON.stringify(rows));
assert "_child_sessions" not in rows[0]
def test_sidebar_reference_rows_suppress_source_filtered_archived_parent_child():
"""Source-filtered payloads keep archived parents as non-rendered references."""
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")
source = f"""
const src = {js!r};
function extractFunc(name) {{
const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\(');
const start = src.search(re);
if (start < 0) throw new Error(name + ' not found');
let i = src.indexOf('{{', start);
let depth = 1; i++;
while (depth > 0 && i < src.length) {{
if (src[i] === '{{') depth++;
else if (src[i] === '}}') depth--;
i++;
}}
return src.slice(start, i);
}}
var _showArchived = false;
var _sidebarReferenceSessions = [{{session_id:'parent', title:'Archived parent', archived:true, updated_at:10, last_message_at:10}}];
eval(extractFunc('_sessionTimestampMs'));
eval(extractFunc('_isChildSession'));
eval(extractFunc('_isForkWithResolvableParent'));
eval(extractFunc('_sessionLineageKey'));
eval(extractFunc('_sidebarLineageKeyForRow'));
eval(extractFunc('_collapseSessionLineageForSidebar'));
eval(extractFunc('_attachChildSessionsToSidebarRows'));
eval(extractFunc('_renderSidebarRowsFromRawSessions'));
const child = {{session_id:'child', title:'Subagent', parent_session_id:'parent', relationship_type:'child_session', updated_at:20, last_message_at:20}};
const rows = _renderSidebarRowsFromRawSessions([child], [child]);
console.log(JSON.stringify(rows));
"""
assert json.loads(_run_node(source)) == []
def test_archived_hidden_parent_suppresses_child_and_fork_orphans():
"""Archived parents should not leave child/delegate clutter behind (#4293)."""
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")