hermes-webui: sort child sessions so read-only delegates are last

This commit is contained in:
igorroncevic committed 2026-07-10 18:13:15 +02:00
1 parent d7c3c2b734
commit 2f5c911e2f
2 files changed
+102 -1

No files matched your search

+23 -1
View File
@@ -6328,6 +6328,8 @@ function _attachChildSessionsToSidebarRows(collapsedRows, rawSessions, rawRefere
};
const orphans=[];
const renderableChildIds=new Set((rawSessions||[]).map(s=>s&&s.session_id).filter(Boolean));
const childAttachOrderById=new Map();
let childAttachCursor=0;
const attachQueueById=new Map();
for(const candidate of [...(rawSessions||[]),...(referenceSessions||[])]){
if(candidate&&candidate.session_id&&!attachQueueById.has(candidate.session_id)) attachQueueById.set(candidate.session_id,candidate);
@@ -6382,6 +6384,9 @@ function _attachChildSessionsToSidebarRows(collapsedRows, rawSessions, rawRefere
}
if(parentRow){
const childCopy={...child};
if(!childAttachOrderById.has(childCopy.session_id)){
childAttachOrderById.set(childCopy.session_id, childAttachCursor++);
}
if(parentSegment){
childCopy._parent_segment_id=parentSegment.session_id;
childCopy._parent_segment_title=_sessionDisplayTitle(parentSegment)||child.parent_title||'Untitled';
@@ -6410,6 +6415,23 @@ function _attachChildSessionsToSidebarRows(collapsedRows, rawSessions, rawRefere
orphans.push({...child,_orphan_child_session:true});
}
}
const resolveReadOnlySession = typeof _isReadOnlySession === 'function'
? _isReadOnlySession
: ((session) => !!(session && session.read_only));
for(const row of rows){
if(Array.isArray(row._child_sessions)&&row._child_sessions.length>1){
row._child_sessions.sort((a,b)=>{
const readOnlyCmp = Number(resolveReadOnlySession(a))-Number(resolveReadOnlySession(b));
if(readOnlyCmp!==0) return readOnlyCmp;
const aOrder = childAttachOrderById.get(a&&a.session_id) || 0;
const bOrder = childAttachOrderById.get(b&&b.session_id) || 0;
return aOrder-bOrder;
});
}
if(Array.isArray(row._child_sessions)){
row._child_session_count=row._child_sessions.length;
}
}
return [...rows,...orphans];
}
@@ -7523,7 +7545,7 @@ function renderSessionListFromCache(){
const childList=document.createElement('div');
childList.className='session-child-sessions';
['pointerdown','pointerup','click','touchstart','touchmove','touchend','touchcancel'].forEach(ev=>childList.addEventListener(ev,e=>e.stopPropagation()));
const sortedChildren=[...s._child_sessions].sort((a,b)=>_sessionTimestampMs(b)-_sessionTimestampMs(a));
const sortedChildren=[...s._child_sessions];
const openChildSession=async(childSession)=>{
// #5409: close mobile sidebar synchronously before navigation
if(typeof closeMobileSidebar==='function')closeMobileSidebar();
+79
View File
@@ -554,6 +554,85 @@ console.log(JSON.stringify(rows));
assert rows[0]["_child_sessions"][0]["session_id"] == "subagent_child"
def test_read_only_child_sessions_are_stacked_after_writable_children():
"""Read-only delegated children should render after fork/writable children."""
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);
}}
eval(extractFunc('_isReadOnlySession'));
eval(extractFunc('_isChildSession'));
eval(extractFunc('_isForkWithResolvableParent'));
eval(extractFunc('_sidebarLineageKeyForRow'));
eval(extractFunc('_attachChildSessionsToSidebarRows'));
const collapsed = [{{
session_id:'parent',
title:'Parent conversation',
message_count:3,
updated_at: 1700000010,
last_message_at: 1700000010,
}}];
const raw = [
collapsed[0],
{{
session_id:'read_only_subagent',
title:'Retained Subagent',
parent_session_id:'parent',
relationship_type:'child_session',
read_only: true,
raw_source:'subagent',
source_tag:'subagent',
session_source:'other',
source_label:'Subagent',
updated_at: 1700000030,
last_message_at: 1700000030,
}},
{{
session_id:'fork_child',
title:'Fork',
parent_session_id:'parent',
session_source:'fork',
raw_source:'webui',
source_tag:'webui',
source_label:'WebUI',
updated_at: 1700000040,
last_message_at: 1700000040,
}},
{{
session_id:'writable_subagent',
title:'Writable Subagent',
parent_session_id:'parent',
relationship_type:'child_session',
read_only: false,
raw_source:'subagent',
source_tag:'subagent',
session_source:'other',
source_label:'Subagent',
updated_at: 1700000020,
last_message_at: 1700000020,
}},
];
const rows = _attachChildSessionsToSidebarRows(collapsed, raw);
console.log(JSON.stringify(rows[0]._child_sessions.map(row => row.session_id)));
"""
rows = json.loads(_run_node(source))
assert rows == ["fork_child", "writable_subagent", "read_only_subagent"]
assert "const sortedChildren=[...s._child_sessions];" in js
assert "const sortedChildren=[...s._child_sessions].sort" not in js
def test_parent_source_label_display_text_does_not_force_external_orphaning():
"""Display-only source labels must not drive machine source classification."""
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")