mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 19:20:16 +00:00
feat: expand collapsed session lineage segments
This commit is contained in:
+53
-3
@@ -1097,6 +1097,7 @@ let _sessionActionMenu = null;
|
||||
let _sessionActionAnchor = null;
|
||||
let _sessionActionSessionId = null;
|
||||
const _expandedChildSessionKeys = new Set();
|
||||
const _expandedLineageKeys = new Set();
|
||||
let _sessionVisibleSidebarIds = [];
|
||||
const SESSION_VIRTUAL_ROW_HEIGHT = 52;
|
||||
const SESSION_VIRTUAL_BUFFER_ROWS = 12;
|
||||
@@ -1943,6 +1944,9 @@ function _syncSidebarExpansionForActiveSession(rows, activeSid){
|
||||
if(Array.isArray(row._child_sessions)&&row._child_sessions.some(child=>child&&child.session_id===activeSid)){
|
||||
_expandedChildSessionKeys.add(key);
|
||||
}
|
||||
if(Array.isArray(row._lineage_segments)&&row._lineage_segments.some(seg=>seg&&seg.session_id===activeSid&&seg.session_id!==row.session_id)){
|
||||
_expandedLineageKeys.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2426,13 +2430,34 @@ function renderSessionListFromCache(){
|
||||
ts.className='session-time'+(hasAttentionState?' is-hidden':'');
|
||||
ts.textContent=hasAttentionState?'':_formatRelativeSessionTime(tsMs);
|
||||
titleRow.appendChild(title);
|
||||
const lineageKey=_sidebarLineageKeyForRow(s);
|
||||
const segmentCount=_sessionSegmentCount(s);
|
||||
const lineageSegments=Array.isArray(s._lineage_segments)?s._lineage_segments.filter(seg=>seg&&seg.session_id&&seg.session_id!==s.session_id):[];
|
||||
const canExpandLineageSegments=Boolean(lineageKey&&segmentCount>1&&lineageSegments.length>0);
|
||||
const lineageSegmentsExpanded=canExpandLineageSegments&&_expandedLineageKeys.has(lineageKey);
|
||||
if(segmentCount>0){
|
||||
const segmentCountEl=document.createElement('span');
|
||||
segmentCountEl.className='session-lineage-count';
|
||||
segmentCountEl.className='session-lineage-count'+(canExpandLineageSegments?' expandable':'');
|
||||
const segmentLabel=t('session_meta_segments', segmentCount);
|
||||
segmentCountEl.textContent=segmentLabel;
|
||||
segmentCountEl.title=segmentLabel;
|
||||
if(canExpandLineageSegments){
|
||||
segmentCountEl.setAttribute('role','button');
|
||||
segmentCountEl.setAttribute('tabindex','0');
|
||||
segmentCountEl.setAttribute('aria-expanded',lineageSegmentsExpanded?'true':'false');
|
||||
['pointerdown','pointerup','click'].forEach(ev=>segmentCountEl.addEventListener(ev,e=>e.stopPropagation()));
|
||||
const toggleLineageSegments=(e)=>{
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if(_expandedLineageKeys.has(lineageKey)) _expandedLineageKeys.delete(lineageKey);
|
||||
else _expandedLineageKeys.add(lineageKey);
|
||||
renderSessionListFromCache();
|
||||
};
|
||||
segmentCountEl.onclick=toggleLineageSegments;
|
||||
segmentCountEl.onkeydown=(e)=>{
|
||||
if(e.key==='Enter'||e.key===' '){toggleLineageSegments(e);}
|
||||
};
|
||||
}
|
||||
titleRow.appendChild(segmentCountEl);
|
||||
}
|
||||
const childCount=typeof s._child_session_count==='number'?s._child_session_count:(Array.isArray(s._child_sessions)?s._child_sessions.length:0);
|
||||
@@ -2489,7 +2514,32 @@ function renderSessionListFromCache(){
|
||||
meta.textContent=metaBits.join(' · ');
|
||||
sessionText.appendChild(meta);
|
||||
}
|
||||
const lineageKey=_sidebarLineageKeyForRow(s);
|
||||
if(lineageSegmentsExpanded){
|
||||
const lineageList=document.createElement('div');
|
||||
lineageList.className='session-lineage-segments';
|
||||
['pointerdown','pointerup','click'].forEach(ev=>lineageList.addEventListener(ev,e=>e.stopPropagation()));
|
||||
const sortedSegments=[...lineageSegments].sort((a,b)=>_sessionTimestampMs(b)-_sessionTimestampMs(a));
|
||||
for(const seg of sortedSegments){
|
||||
const row=document.createElement('button');
|
||||
row.type='button';
|
||||
row.className='session-lineage-segment'+(activeSidForSidebar&&seg.session_id===activeSidForSidebar?' active':'');
|
||||
const segTitle=seg.title||'Untitled segment';
|
||||
const segTime=_formatRelativeSessionTime(_sessionTimestampMs(seg));
|
||||
row.textContent=`-> ${segTitle} - ${segTime}`;
|
||||
row.title='Open lineage segment';
|
||||
row.onclick=async(e)=>{
|
||||
e.stopPropagation();
|
||||
if(seg.is_cli_session){
|
||||
try{await api('/api/session/import_cli',{method:'POST',body:JSON.stringify({session_id:seg.session_id})});}
|
||||
catch(_e){ /* read-only fallback */ }
|
||||
}
|
||||
await loadSession(seg.session_id);
|
||||
renderSessionListFromCache();
|
||||
};
|
||||
lineageList.appendChild(row);
|
||||
}
|
||||
sessionText.appendChild(lineageList);
|
||||
}
|
||||
if(childCount>0&&Array.isArray(s._child_sessions)&&_expandedChildSessionKeys.has(lineageKey)){
|
||||
const childList=document.createElement('div');
|
||||
childList.className='session-child-sessions';
|
||||
@@ -2686,7 +2736,7 @@ function renderSessionListFromCache(){
|
||||
_pointerActive=false;
|
||||
if(_renamingSid) return;
|
||||
if(actions&&actions.contains(e.target)) return;
|
||||
if(e.target&&e.target.closest&&e.target.closest('.session-child-count,.session-child-sessions,.session-child-session')) return;
|
||||
if(e.target&&e.target.closest&&e.target.closest('.session-child-count,.session-child-sessions,.session-child-session,.session-lineage-count,.session-lineage-segments,.session-lineage-segment')) return;
|
||||
if(_sessionSelectMode){e.stopPropagation();if(!readOnly)toggleSessionSelect(s.session_id);return;}
|
||||
// If the pointer moved enough to be a drag, cancel any pending tap
|
||||
if(_isDragging){clearTimeout(_tapTimer);_tapTimer=null;_lastTapTime=0;_clearDragTimer=setTimeout(()=>{el.classList.remove('dragging');_clearDragTimer=null;},50);return;}
|
||||
|
||||
@@ -2670,7 +2670,12 @@ main.main.showing-logs > #mainLogs{display:flex;}
|
||||
|
||||
/* ── Subagent session tree (#494) ── */
|
||||
.session-lineage-count{display:inline-flex;align-items:center;justify-content:center;height:16px;font-size:10px;font-weight:600;padding:0 6px;border-radius:999px;background:rgba(148,163,184,.14);color:var(--muted);margin-left:6px;flex-shrink:0;user-select:none;cursor:default;}
|
||||
.session-lineage-count.expandable{cursor:pointer;}
|
||||
.session-lineage-count.expandable:hover{background:rgba(148,163,184,.24);color:var(--text);}
|
||||
.session-item.active .session-lineage-count{color:var(--accent-text);background:rgba(255,255,255,.14);}
|
||||
.session-lineage-segments{display:flex;flex-direction:column;gap:3px;margin-top:6px;margin-left:12px;padding-left:8px;border-left:1px dashed rgba(148,163,184,.22);}
|
||||
.session-lineage-segment{appearance:none;border:0;background:transparent;color:var(--muted);font:inherit;font-size:11px;text-align:left;padding:3px 4px;border-radius:5px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
.session-lineage-segment:hover,.session-lineage-segment.active{background:rgba(148,163,184,.12);color:var(--text);}
|
||||
.session-child-count{display:inline-flex;align-items:center;justify-content:center;height:16px;font-size:10px;font-weight:600;padding:0 6px;border-radius:999px;background:rgba(99,179,237,.16);color:#63b3ed;margin-left:6px;flex-shrink:0;user-select:none;cursor:pointer;}
|
||||
.session-child-count:hover{background:rgba(99,179,237,.26);color:#90cdf4;}
|
||||
.session-child-sessions{display:flex;flex-direction:column;gap:3px;margin-top:6px;margin-left:12px;padding-left:8px;border-left:1px solid var(--border,rgba(255,255,255,.1));}
|
||||
|
||||
@@ -283,7 +283,7 @@ console.log(JSON.stringify(cases));
|
||||
assert json.loads(_run_node(source)) == [3, 25, 3, 0, 0]
|
||||
|
||||
|
||||
def test_sidebar_lineage_segment_badge_is_passive_and_localized():
|
||||
def test_sidebar_lineage_segment_badge_is_localized():
|
||||
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")
|
||||
css = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
||||
assert "session-lineage-count" in js
|
||||
@@ -291,8 +291,60 @@ def test_sidebar_lineage_segment_badge_is_passive_and_localized():
|
||||
assert "t('session_meta_segments', segmentCount)" in js
|
||||
assert "titleRow.appendChild(segmentCountEl);" in js
|
||||
assert ".session-lineage-count{" in css
|
||||
assert "cursor:default" in css
|
||||
assert "session-lineage-count,.session-lineage-segments,.session-lineage-segment" not in js
|
||||
|
||||
|
||||
def test_lineage_segment_expansion_static_contract():
|
||||
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")
|
||||
css = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
||||
assert "const _expandedLineageKeys = new Set();" in js
|
||||
assert "session-lineage-count,.session-lineage-segments,.session-lineage-segment" in js
|
||||
assert "segmentCountEl.setAttribute('aria-expanded'" in js
|
||||
assert "_expandedLineageKeys.has(lineageKey)" in js
|
||||
assert "_expandedLineageKeys.add(lineageKey)" in js
|
||||
assert "_expandedLineageKeys.delete(lineageKey)" in js
|
||||
assert "className='session-lineage-segments'" in js
|
||||
assert "className='session-lineage-segment'" in js
|
||||
assert "await loadSession(seg.session_id);" in js
|
||||
assert ".session-lineage-count.expandable{" in css
|
||||
assert ".session-lineage-count.expandable:hover" in css
|
||||
assert ".session-lineage-segments{" in css
|
||||
assert ".session-lineage-segment{" in css
|
||||
|
||||
|
||||
def test_active_hidden_lineage_segment_auto_expands_parent():
|
||||
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);
|
||||
}}
|
||||
const _expandedChildSessionKeys = new Set();
|
||||
const _expandedLineageKeys = new Set();
|
||||
eval(extractFunc('_sidebarLineageKeyForRow'));
|
||||
eval(extractFunc('_syncSidebarExpansionForActiveSession'));
|
||||
const rows = [{{
|
||||
session_id:'seg10',
|
||||
_lineage_key:'root',
|
||||
_lineage_segments:[
|
||||
{{session_id:'seg10', updated_at:100}},
|
||||
{{session_id:'seg9', updated_at:90}},
|
||||
{{session_id:'seg8', updated_at:80}},
|
||||
],
|
||||
}}];
|
||||
_syncSidebarExpansionForActiveSession(rows, 'seg8');
|
||||
console.log(JSON.stringify({{lineage:[..._expandedLineageKeys], child:[..._expandedChildSessionKeys]}}));
|
||||
"""
|
||||
assert json.loads(_run_node(source)) == {"lineage": ["root"], "child": []}
|
||||
|
||||
|
||||
def test_session_meta_segments_locale_key_is_defined_for_sidebar_locales():
|
||||
|
||||
Reference in New Issue
Block a user