fix(sidebar): keep hidden archived parents as child references
This commit is contained in:
1 parent
b7c332cfc1
commit
5ed0ba6ef6
4 files changed
+159
-5
No files matched your search
+72
-4
@@ -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
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user