From 4e71fb75d7b7fc744f863e668933b627e4d1fb3a Mon Sep 17 00:00:00 2001 From: Dennis Soong Date: Fri, 8 May 2026 23:38:09 +0800 Subject: [PATCH] fix: show collapsed session segment count --- static/i18n.js | 10 ++++++ static/sessions.js | 19 +++++++++++ static/style.css | 2 ++ tests/test_session_lineage_collapse.py | 47 ++++++++++++++++++++++++++ 4 files changed, 78 insertions(+) diff --git a/static/i18n.js b/static/i18n.js index e910228c..ffe70f84 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -606,6 +606,7 @@ const LOCALES = { workspace_desc: 'Add and switch workspaces for your sessions.', session_meta_messages: (n) => `${n} msg${n === 1 ? '' : 's'}`, session_meta_children: (n) => `${n} child${n === 1 ? '' : 'ren'}`, + session_meta_segments: (n) => `${n} segment${n === 1 ? '' : 's'}`, new_profile: 'New profile', transcript: 'Transcript', download_transcript: 'Download as Markdown', @@ -1626,6 +1627,7 @@ const LOCALES = { workspace_desc: 'セッション用のワークスペースを追加・切り替えします。', session_meta_messages: (n) => `${n} 件`, session_meta_children: (n) => `${n} 子`, + session_meta_segments: (n) => `${n} セグメント`, new_profile: '新規プロファイル', transcript: 'トランスクリプト', download_transcript: 'Markdown としてダウンロード', @@ -2461,6 +2463,7 @@ const LOCALES = { workspace_desc: 'Добавляйте рабочие пространства и переключайтесь между ними в своих сеансах.', session_meta_messages: (n) => `${n} сообщ.`, session_meta_children: (n) => `${n} ${n === 1 ? 'дочерн.' : 'дочерн.'}`, + session_meta_segments: (n) => `${n} сегм.`, new_profile: 'Новый профиль', transcript: 'Транскрипт', download_transcript: 'Скачать как Markdown', @@ -3415,6 +3418,7 @@ const LOCALES = { workspace_desc: 'Añade y cambia espacios de trabajo para tus sesiones.', session_meta_messages: (n) => `${n} mens.`, session_meta_children: (n) => `${n} ${n === 1 ? 'hijo' : 'hijos'}`, + session_meta_segments: (n) => `${n} ${n === 1 ? 'segmento' : 'segmentos'}`, new_profile: 'Nuevo perfil', transcript: 'Transcripción', download_transcript: 'Descargar como Markdown', @@ -4347,6 +4351,7 @@ const LOCALES = { workspace_desc: 'Workspaces hinzufügen und wechseln.', session_meta_messages: (n) => `${n} Nachr.`, session_meta_children: (n) => `${n} ${n === 1 ? 'Subagent' : 'Subagents'}`, + session_meta_segments: (n) => `${n} Segment${n === 1 ? '' : 'e'}`, new_profile: 'Neues Profil', transcript: 'Protokoll', download_transcript: 'Als Markdown herunterladen', @@ -5322,6 +5327,7 @@ const LOCALES = { workspace_desc: '为你的会话添加并切换工作区。', session_meta_messages: (n) => `${n} 条消息`, session_meta_children: (n) => `${n} 子会话`, + session_meta_segments: (n) => `${n} 段`, new_profile: '新配置', transcript: '记录', download_transcript: '下载为 Markdown', @@ -6207,6 +6213,7 @@ const LOCALES = { current_task_list: '\u76ee\u524d\u4efb\u52d9\u6e05\u55ae', session_meta_messages: (n) => `${n} 則訊息`, session_meta_children: (n) => `${n} 則子`, + session_meta_segments: (n) => `${n} 段`, new_profile: '\u65b0\u914d\u7f6e\u6a94', transcript: '\u8a18\u9304', download_transcript: '\u4e0b\u8f09\u8a18\u9304', @@ -6388,6 +6395,7 @@ const LOCALES = { provider_mismatch_warning: (provider) => `提供者不符:會話使用 ${provider}`, session_meta_messages: (n) => `${n} 則訊息`, session_meta_children: (n) => `${n} 則子`, + session_meta_segments: (n) => `${n} 段`, settings_label_model: '\u9810\u8a2d\u6a21\u578b', skill_created: '\u6280\u80fd\u5df2\u5efa\u7acb', skill_file_load_failed: '\u8f09\u5165\u6a94\u6848\u5931\u6557\uff1a', @@ -7314,6 +7322,7 @@ const LOCALES = { workspace_desc: 'Adicionar e trocar workspaces para suas sessões.', session_meta_messages: (n) => `${n} msg${n === 1 ? '' : 's'}`, session_meta_children: (n) => `${n} child${n === 1 ? '' : 'ren'}`, + session_meta_segments: (n) => `${n} segment${n === 1 ? '' : 's'}`, new_profile: 'Novo perfil', transcript: 'Transcrição', download_transcript: 'Baixar como Markdown', @@ -8232,6 +8241,7 @@ const LOCALES = { workspace_desc: '세션용 워크스페이스를 추가하고 전환합니다.', session_meta_messages: (n) => `${n} msg${n === 1 ? '' : 's'}`, session_meta_children: (n) => `${n} child${n === 1 ? '' : 'ren'}`, + session_meta_segments: (n) => `${n} segment${n === 1 ? '' : 's'}`, new_profile: 'New profile', transcript: '대화 기록', download_transcript: 'Download as Markdown', diff --git a/static/sessions.js b/static/sessions.js index 1ab89dc5..e0b29990 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -1835,6 +1835,16 @@ function _sessionLineageContainsSession(s, sid){ return false; } +function _sessionSegmentCount(s){ + if(!s) return 0; + const counts=[]; + if(typeof s._lineage_collapsed_count==='number') counts.push(s._lineage_collapsed_count); + if(typeof s._compression_segment_count==='number') counts.push(s._compression_segment_count); + if(Array.isArray(s._lineage_segments)) counts.push(s._lineage_segments.length); + const count=Math.max(0,...counts.map(n=>Number.isFinite(n)?n:0)); + return count>1?count:0; +} + function _sidebarLineageKeyForRow(s){ if(!s) return null; return s._lineage_key||s._lineage_root_id||s.lineage_root_id||s.parent_session_id||s.session_id||null; @@ -2393,6 +2403,15 @@ function renderSessionListFromCache(){ ts.className='session-time'+(hasAttentionState?' is-hidden':''); ts.textContent=hasAttentionState?'':_formatRelativeSessionTime(tsMs); titleRow.appendChild(title); + const segmentCount=_sessionSegmentCount(s); + if(segmentCount>0){ + const segmentCountEl=document.createElement('span'); + segmentCountEl.className='session-lineage-count'; + const segmentLabel=t('session_meta_segments', segmentCount); + segmentCountEl.textContent=segmentLabel; + segmentCountEl.title=segmentLabel; + 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); if(childCount>0){ const childCountEl=document.createElement('span'); diff --git a/static/style.css b/static/style.css index dbd791b1..5ca1ebfa 100644 --- a/static/style.css +++ b/static/style.css @@ -2654,6 +2654,8 @@ main.main.showing-logs > #mainLogs{display:flex;} .session-item.archived .session-title{font-style:italic;} /* ── 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-item.active .session-lineage-count{color:var(--accent-text);background:rgba(255,255,255,.14);} .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));} diff --git a/tests/test_session_lineage_collapse.py b/tests/test_session_lineage_collapse.py index 868c416b..d8d162f2 100644 --- a/tests/test_session_lineage_collapse.py +++ b/tests/test_session_lineage_collapse.py @@ -208,3 +208,50 @@ console.log(JSON.stringify(rows)); assert [row["session_id"] for row in rows] == ["telegram_parent", "webui_tip"] assert rows[1].get("_orphan_child_session") is True assert "_child_sessions" not in rows[0] + + +def test_session_segment_count_prefers_visible_collapsed_backend_and_materialized_counts(): + 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('_sessionSegmentCount')); +const cases = [ + _sessionSegmentCount({{_lineage_collapsed_count:3, _compression_segment_count:2, _lineage_segments:[{{session_id:'a'}}, {{session_id:'b'}}]}}), + _sessionSegmentCount({{_compression_segment_count:25}}), + _sessionSegmentCount({{_lineage_segments:[{{session_id:'tip'}}, {{session_id:'root'}}, {{session_id:'older'}}]}}), + _sessionSegmentCount({{_lineage_collapsed_count:1, _compression_segment_count:1}}), + _sessionSegmentCount(null), +]; +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(): + 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 + assert "const segmentCount=_sessionSegmentCount(s);" in js + 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_session_meta_segments_locale_key_is_defined_for_sidebar_locales(): + i18n = (REPO_ROOT / "static" / "i18n.js").read_text(encoding="utf-8") + assert i18n.count("session_meta_segments:") >= i18n.count("session_meta_messages:")