From a48e47dd1cd53115eccdd802461df1ebb6a63d15 Mon Sep 17 00:00:00 2001 From: Michael Lam Date: Sun, 17 May 2026 20:40:20 -0700 Subject: [PATCH] feat: separate CLI sessions in sidebar --- CHANGELOG.md | 4 ++ docs/pr-media/2351/after-source-tabs.png | Bin 0 -> 2355 bytes docs/pr-media/2351/before-cli-mixed.png | Bin 0 -> 2355 bytes static/sessions.js | 59 +++++++++++++++++- static/style.css | 5 ++ ...est_issue2351_cli_session_source_filter.py | 31 +++++++++ 6 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 docs/pr-media/2351/after-source-tabs.png create mode 100644 docs/pr-media/2351/before-cli-mixed.png create mode 100644 tests/test_issue2351_cli_session_source_filter.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ed15eec3..04bf9c79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- **PR #2506** by @Michaelyklam (refs #2351) — Add a read-only WebUI/CLI session source switch in the chat sidebar when agent session sync is enabled. WebUI conversations stay in the default list, while imported CLI/agent sessions are surfaced under a separate `CLI sessions` tab with counts so large CLI histories do not clutter the normal conversation list. + ## [v0.51.91] — 2026-05-18 — Release BO (stage-384 — 5-PR full sweep batch — reasoning-replay history fix + archive-extract per-session inbox + fallback streaming warnings + sanitized custom-provider env hints + Slice 3c queue/goal adapter routing) ### Fixed diff --git a/docs/pr-media/2351/after-source-tabs.png b/docs/pr-media/2351/after-source-tabs.png new file mode 100644 index 0000000000000000000000000000000000000000..2279fd3004940274c8e3f47d4f10930c3f6cf87a GIT binary patch literal 2355 zcmeAS@N?(olHy`uVBq!ia0y~yU~FSxV7kD;1Qe0J>u{8Tf#Zdzi(^Q|oHtht1sN20 z7&gisk7E8I-qkBFB)D^TK7-Ubh7=7>jzA%SMJfs|o(>(88U$1s6}_08rm(b(N{j{x zrHR2nb~G)GX0OqLWwb~gtz|~*SDLjfJ~1*d{Qp0H!p|4LrVksa&BM&lBE@=rfyVu5 PKoJH{S3j3^P6u{8Tf#Zdzi(^Q|oHtht1sN20 z7&gisk7E8I-qkBFB)D^TK7-Ubh7=7>jzA%SMJfs|o(>(88U$1s6}_08rm(b(N{j{x zrHR2nb~G)GX0OqLWwb~gtz|~*SDLjfJ~1*d{Qp0H!p|4LrVksa&BM&lBE@=rfyVu5 PKoJH{S3j3^P60) ); + const webuiSessionCount = withMessages.filter(s=>!_isCliSession(s)).length; + const cliSessionCount = withMessages.filter(s=>_isCliSession(s)).length; + if(_sessionSourceFilter==='cli' && !window._showCliSessions && cliSessionCount===0){ + _sessionSourceFilter='webui'; + } + const sourceFiltered = _sessionSourceFilter==='cli' + ? withMessages.filter(s=>_isCliSession(s)) + : withMessages.filter(s=>!_isCliSession(s)); // The server is authoritative for profile scoping (#1611): it filters by // active profile when no query param is set, and returns the aggregate when // we send ?all_profiles=1. The renamed-root cross-alias (a row tagged @@ -2733,7 +2766,7 @@ function renderSessionListFromCache(){ // in _profiles_match, and a strict-equality client filter would reject those // rows incorrectly. So we trust the wire data and skip the redundant client // filter entirely. - const profileFiltered=withMessages; + const profileFiltered=sourceFiltered; // Filter by active project. NO_PROJECT_FILTER sentinel asks for sessions // with no project_id; otherwise filter to the matching project_id, or // pass through when no filter is active. @@ -2768,6 +2801,21 @@ function renderSessionListFromCache(){ list.appendChild(batchBar); if(_sessionSelectMode&&_selectedSessions.size>0){batchBar.style.display='flex';_renderBatchActionBar();} else{batchBar.style.display='none';} + if(window._showCliSessions || cliSessionCount>0){ + const sourceTabs=document.createElement('div'); + sourceTabs.className='session-source-tabs'; + for(const filter of ['webui','cli']){ + const count=filter==='cli'?cliSessionCount:webuiSessionCount; + const btn=document.createElement('button'); + btn.type='button'; + btn.className='session-source-tab'+(_sessionSourceFilter===filter?' active':''); + btn.textContent=_sessionSourceLabel(filter,count); + btn.setAttribute('aria-pressed', _sessionSourceFilter===filter?'true':'false'); + btn.onclick=()=>_setSessionSourceFilter(filter); + sourceTabs.appendChild(btn); + } + list.appendChild(sourceTabs); + } // Project filter bar — show when there are real projects OR there are // unassigned sessions (so the Unassigned chip has something to filter to). const hasUnprojected=profileFiltered.some(s=>!s.project_id); @@ -2850,9 +2898,14 @@ function renderSessionListFromCache(){ list.appendChild(toggle); } // Empty state for active project filter - if(_activeProject&&sessions.length===0){ + if(_sessionSourceFilter==='cli'&&sessions.length===0){ const empty=document.createElement('div'); - empty.style.cssText='padding:20px 14px;color:var(--muted);font-size:12px;text-align:center;opacity:.7;'; + empty.className='session-empty-note'; + empty.textContent=window._showCliSessions?'No CLI sessions found.':'Enable Show agent sessions in Settings to list CLI sessions here.'; + list.appendChild(empty); + } else if(_activeProject&&sessions.length===0){ + const empty=document.createElement('div'); + empty.className='session-empty-note'; empty.textContent=_activeProject===NO_PROJECT_FILTER?'No unassigned sessions.':'No sessions in this project yet.'; list.appendChild(empty); } diff --git a/static/style.css b/static/style.css index e4714e21..0921d97c 100644 --- a/static/style.css +++ b/static/style.css @@ -3024,6 +3024,11 @@ main.main.showing-logs > #mainLogs{display:flex;} .mermaid-rendered svg{max-width:100%;height:auto;} /* ── Session projects ── */ +.session-source-tabs{display:flex;gap:4px;padding:4px 10px 8px;flex-shrink:0;} +.session-source-tab{flex:1;min-width:0;border:1px solid var(--border2);border-radius:10px;background:var(--input-bg);color:var(--muted);font-size:10px;font-weight:700;line-height:1.2;padding:5px 6px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:background .15s,color .15s,border-color .15s;} +.session-source-tab:hover{background:rgba(255,255,255,.08);color:var(--text);} +.session-source-tab.active{background:var(--accent-bg);color:var(--accent-text);border-color:var(--accent-bg);} +.session-empty-note{padding:20px 14px;color:var(--muted);font-size:12px;text-align:center;opacity:.7;} .project-bar{display:flex;gap:4px;padding:4px 10px 8px;flex-wrap:wrap;align-items:center;flex-shrink:0;} .project-chip{font-size:10px;font-weight:600;padding:3px 8px;border-radius:12px;cursor:pointer;border:1px solid var(--border2);background:var(--input-bg);color:var(--muted);transition:all .15s;white-space:nowrap;display:inline-flex;align-items:center;gap:4px;} .project-chip:hover{background:rgba(255,255,255,.08);color:var(--text);} diff --git a/tests/test_issue2351_cli_session_source_filter.py b/tests/test_issue2351_cli_session_source_filter.py new file mode 100644 index 00000000..efe2a8f6 --- /dev/null +++ b/tests/test_issue2351_cli_session_source_filter.py @@ -0,0 +1,31 @@ +"""Regression coverage for issue #2351 CLI session list separation.""" +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SESSIONS_JS = ROOT / "static" / "sessions.js" +STYLE_CSS = ROOT / "static" / "style.css" + + +def test_sidebar_has_separate_webui_and_cli_session_source_tabs(): + src = SESSIONS_JS.read_text(encoding="utf-8") + assert "let _sessionSourceFilter = 'webui'" in src + assert "hermes-session-source-filter" in src + assert "session-source-tabs" in src + assert "WebUI sessions" in src + assert "CLI sessions" in src + assert "_sessionSourceFilter==='cli'" in src + + +def test_cli_filter_keeps_cli_rows_out_of_default_webui_list(): + src = SESSIONS_JS.read_text(encoding="utf-8") + assert "const webuiSessionCount = withMessages.filter(s=>!_isCliSession(s)).length" in src + assert "const cliSessionCount = withMessages.filter(s=>_isCliSession(s)).length" in src + assert "? withMessages.filter(s=>_isCliSession(s))" in src + assert ": withMessages.filter(s=>!_isCliSession(s))" in src + + +def test_session_source_tabs_have_dedicated_sidebar_styles(): + css = STYLE_CSS.read_text(encoding="utf-8") + assert ".session-source-tabs" in css + assert ".session-source-tab.active" in css + assert ".session-empty-note" in css