mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-28 04:30:18 +00:00
feat: separate CLI sessions in sidebar
This commit is contained in:
@@ -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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
+56
-3
@@ -847,6 +847,29 @@ function _isCliSession(session) {
|
||||
return session.is_cli_session === true;
|
||||
}
|
||||
|
||||
function _sessionSourceLabel(filter, count) {
|
||||
const n = Number(count) || 0;
|
||||
return filter === 'cli' ? `CLI sessions (${n})` : `WebUI sessions (${n})`;
|
||||
}
|
||||
|
||||
function _setSessionSourceFilter(filter) {
|
||||
const next = filter === 'cli' ? 'cli' : 'webui';
|
||||
if (_sessionSourceFilter === next) return;
|
||||
_sessionSourceFilter = next;
|
||||
_activeProject = null;
|
||||
_selectedSessions.clear();
|
||||
_sessionSelectMode = false;
|
||||
try { localStorage.setItem('hermes-session-source-filter', next); } catch (_e) {}
|
||||
renderSessionListFromCache();
|
||||
}
|
||||
|
||||
function _restoreSessionSourceFilter() {
|
||||
try {
|
||||
const raw = localStorage.getItem('hermes-session-source-filter');
|
||||
if (raw === 'cli' || raw === 'webui') _sessionSourceFilter = raw;
|
||||
} catch (_e) {}
|
||||
}
|
||||
|
||||
function _normalizeMessageForCliImportComparison(message) {
|
||||
if (!message || typeof message !== 'object') return message;
|
||||
const clone = { ...message };
|
||||
@@ -1433,6 +1456,8 @@ const NO_PROJECT_FILTER = '__none__';
|
||||
let _activeProject = null; // project_id filter (null = show all, NO_PROJECT_FILTER = unassigned only)
|
||||
let _showAllProfiles = false; // false = filter to active profile only
|
||||
let _otherProfileCount = 0; // count of sessions from other profiles (server-reported)
|
||||
let _sessionSourceFilter = 'webui'; // 'webui' keeps WebUI chats separate from read-only CLI sessions
|
||||
_restoreSessionSourceFilter();
|
||||
let _sessionActionMenu = null;
|
||||
let _sessionActionAnchor = null;
|
||||
let _sessionActionSessionId = null;
|
||||
@@ -2726,6 +2751,14 @@ function renderSessionListFromCache(){
|
||||
(activeSidForSidebar&&s.session_id===activeSidForSidebar) ||
|
||||
(S.session&&s.session_id===S.session.session_id&&(S.session.message_count||0)>0)
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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);}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user