Merge pull request #2506

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
nesquena-hermes
2026-05-27 00:38:40 +00:00
6 changed files with 93 additions and 3 deletions
+1
View File
@@ -6,6 +6,7 @@
### Added
- WebUI can now opt into a `webui_prefill_messages_script` / `HERMES_WEBUI_PREFILL_MESSAGES_SCRIPT` hook for dynamic browser-turn prefill context from local notes or recall systems. The script output is capped at 256 KiB, normalized to ephemeral prefill messages, and browser status still hides message bodies while redacting script errors.
- Added 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. (Refs #2351)
## [v0.51.140] — 2026-05-26 — Release DL (stage-batch22 — 5-PR hold-bucket reassessment)
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

+56 -3
View File
@@ -906,6 +906,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 };
@@ -1551,6 +1574,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;
@@ -3238,6 +3263,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
@@ -3245,7 +3278,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.
@@ -3285,6 +3318,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);
@@ -3367,9 +3415,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);
}
+5
View File
@@ -3353,6 +3353,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