Release v0.51.227 — Release GU (stage-p11 — keep active New Chat visible in sidebar #3408) (#3461)
Some checks failed
Release & Docker / release (push) Has been cancelled
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(sidebar): keep active New Chat visible before first message (#3408, @AJV20) Squashed net diff of #3408. Injects ONLY the active ephemeral session into the sidebar render rows (when the server list omits it) so a freshly-created New Chat stays visible/selected before its first turn; inactive empty sessions stay filtered as before. New Chat also resets a CLI source-filter back to webui so the active chat isn't immediately hidden. * fix(sidebar): gate active-row reinjection to 0-message ephemeral only (#3408 Codex follow-up) Codex review found _ensureActiveSessionRowPresent re-injected ANY active session after search-merge — so an active conversation WITH messages that was correctly filtered out by the search query would pollute unrelated search results. Gate the reinjection to Number(activeRow.message_count||0)<=0 so only the freshly-created 0-message ephemeral chat is re-added; an active chat with messages stays filtered by search as before. Added a regression test asserting the gate. --------- Co-authored-by: nesquena-hermes <[email protected]>
This commit is contained in:
@@ -3,6 +3,11 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.227] — 2026-06-03 — Release GU (stage-p11 — keep the active New Chat visible in the sidebar)
|
||||
|
||||
### Fixed
|
||||
- A freshly-created **New Chat** now stays visible and selected in the sidebar before its first message is sent. The sidebar intentionally filters inactive 0-message sessions, but that filter also hid the *currently active* blank chat until the user sent a turn — so starting a New Chat could make the selected row vanish from the list. The active ephemeral session is now injected into the sidebar render rows (only when the server-side list omits it), while inactive empty sessions stay filtered as before. Starting a New Chat from a CLI-filtered sidebar also switches the source filter back to WebUI so the active chat isn't immediately hidden (#3408, @AJV20).
|
||||
|
||||
## [v0.51.226] — 2026-06-03 — Release GT (stage-p9 — mobile composer context-usage ring + activity-feed default-expand setting)
|
||||
|
||||
### Added
|
||||
|
||||
@@ -520,6 +520,7 @@ async function newSession(flash, options={}){
|
||||
}
|
||||
const data=await api('/api/session/new',{method:'POST',body:JSON.stringify(reqBody)});
|
||||
S.session=data.session;S.messages=data.session.messages||[];
|
||||
if(_sessionSourceFilter==='cli') _sessionSourceFilter='webui';
|
||||
S.lastUsage={...(data.session.last_usage||{})};
|
||||
if(flash)S.session._flash=true;
|
||||
try{localStorage.setItem('hermes-webui-session',S.session.session_id);}catch(_){}
|
||||
@@ -3732,6 +3733,41 @@ function upsertActiveSessionForLocalTurn({title='', messageCount=0, timestampMs=
|
||||
renderSessionListFromCache();
|
||||
}
|
||||
|
||||
function _sessionRowsWithActiveEphemeralSession(rows){
|
||||
rows=Array.isArray(rows)?rows:[];
|
||||
if(!S.session||!S.session.session_id) return rows;
|
||||
const sid=S.session.session_id;
|
||||
if(rows.some(s=>s&&s.session_id===sid)) return rows;
|
||||
const nowSec=Math.floor(Date.now()/1000);
|
||||
const activeRow={
|
||||
...S.session,
|
||||
session_id:sid,
|
||||
title:S.session.title||'New Chat',
|
||||
display_title:S.session.display_title||S.session.title||'New Chat',
|
||||
message_count:0,
|
||||
last_message_at:S.session.last_message_at||S.session.updated_at||nowSec,
|
||||
updated_at:S.session.updated_at||S.session.last_message_at||nowSec,
|
||||
profile:S.session.profile||S.activeProfile||'default',
|
||||
is_streaming:false,
|
||||
};
|
||||
return [activeRow,...rows];
|
||||
}
|
||||
|
||||
function _ensureActiveSessionRowPresent(rows, sourceRows){
|
||||
rows=Array.isArray(rows)?rows:[];
|
||||
const activeSid=_activeSessionIdForSidebar();
|
||||
if(!activeSid||rows.some(s=>s&&s.session_id===activeSid)) return rows;
|
||||
const activeRow=(Array.isArray(sourceRows)?sourceRows:[]).find(s=>s&&s.session_id===activeSid);
|
||||
// Only re-inject the active FRESHLY-CREATED 0-message ephemeral chat. An active
|
||||
// conversation that already has messages and was filtered out by the search
|
||||
// query must stay filtered — re-adding it here would pollute unrelated search
|
||||
// results with the current chat (#3408 review, Codex).
|
||||
if(activeRow && Number(activeRow.message_count||0)<=0){
|
||||
return [activeRow,...rows];
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function clearOptimisticSessionStreaming(sid){
|
||||
sid=sid||(S.session&&S.session.session_id)||'';
|
||||
if(!sid) return;
|
||||
@@ -3892,14 +3928,16 @@ function renderSessionListFromCache(){
|
||||
const searchQueryRaw=($('sessionSearch').value||'').trim();
|
||||
const q=searchQueryRaw.toLowerCase();
|
||||
const activeSidForSidebar=_activeSessionIdForSidebar();
|
||||
const sidebarRows=_sessionRowsWithActiveEphemeralSession(_allSessions);
|
||||
// Merge direct session-id/link matches, title matches, then content matches (deduped).
|
||||
// Direct matches must not disable content search: if a user pasted the same
|
||||
// session id into another conversation, that content hit should still appear.
|
||||
const allMatched=_sessionSearchMergeMatches(_allSessions,searchQueryRaw,_contentSearchResults);
|
||||
// Never surface ephemeral 0-message sessions in the sidebar — they only become
|
||||
// real once the first message is sent. The server already filters them, but this
|
||||
// guard ensures a brand-new active session doesn't flash into the list while
|
||||
// _allSessions is stale from a prior render (#1171).
|
||||
const searchMatches=_sessionSearchMergeMatches(sidebarRows,searchQueryRaw,_contentSearchResults);
|
||||
const allMatched=_ensureActiveSessionRowPresent(searchMatches,sidebarRows);
|
||||
// Keep inactive ephemeral 0-message sessions out of the sidebar — they only
|
||||
// become real once the first message is sent. The server already filters them.
|
||||
// Exception: the active freshly-created chat is injected above so it remains
|
||||
// visible/selected until the user sends the first turn or switches away.
|
||||
const withMessages=allMatched.filter(s=>
|
||||
(s.message_count||0)>0 ||
|
||||
_sessionAttentionState(s) ||
|
||||
|
||||
48
tests/test_active_empty_session_sidebar.py
Normal file
48
tests/test_active_empty_session_sidebar.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_active_empty_session_is_injected_into_sidebar_rows():
|
||||
assert "function _sessionRowsWithActiveEphemeralSession(rows)" in SESSIONS_JS
|
||||
helper_start = SESSIONS_JS.index("function _sessionRowsWithActiveEphemeralSession(rows)")
|
||||
helper_end = SESSIONS_JS.index("function renderSessionListFromCache()", helper_start)
|
||||
helper = SESSIONS_JS[helper_start:helper_end]
|
||||
|
||||
assert "S.session" in helper
|
||||
assert "message_count:0" in helper
|
||||
assert "title:S.session.title||'New Chat'" in helper
|
||||
assert "rows.some(s=>s&&s.session_id===sid)" in helper
|
||||
|
||||
|
||||
def test_new_session_switches_sidebar_back_to_webui_source():
|
||||
new_session = SESSIONS_JS[SESSIONS_JS.index("async function newSession"):SESSIONS_JS.index("async function loadSession")]
|
||||
assert "if(_sessionSourceFilter==='cli') _sessionSourceFilter='webui';" in new_session
|
||||
|
||||
|
||||
def test_sidebar_search_uses_active_ephemeral_rows_before_filtering():
|
||||
render_start = SESSIONS_JS.index("function renderSessionListFromCache()")
|
||||
render_end = SESSIONS_JS.index("function _showProjectPicker", render_start)
|
||||
render_body = SESSIONS_JS[render_start:render_end]
|
||||
|
||||
assert "const sidebarRows=_sessionRowsWithActiveEphemeralSession(_allSessions);" in render_body
|
||||
assert "const searchMatches=_sessionSearchMergeMatches(sidebarRows,searchQueryRaw,_contentSearchResults);" in render_body
|
||||
assert "const allMatched=_ensureActiveSessionRowPresent(searchMatches,sidebarRows);" in render_body
|
||||
|
||||
|
||||
def test_active_row_reinjection_gated_to_zero_message_ephemeral_only():
|
||||
"""#3408 review (Codex): _ensureActiveSessionRowPresent must only re-add the
|
||||
active FRESHLY-CREATED 0-message chat after search-merge. An active conversation
|
||||
that already has messages and was filtered out by the search query must stay
|
||||
filtered — re-adding it would pollute unrelated search results with the current
|
||||
chat."""
|
||||
start = SESSIONS_JS.index("function _ensureActiveSessionRowPresent(rows, sourceRows)")
|
||||
end = SESSIONS_JS.index("function clearOptimisticSessionStreaming", start)
|
||||
body = SESSIONS_JS[start:end]
|
||||
|
||||
# The reinjection is gated on a 0-message check, not an unconditional prepend.
|
||||
assert "Number(activeRow.message_count||0)<=0" in body
|
||||
assert "[activeRow,...rows]" in body
|
||||
# The unconditional return that shipped in the original PR must be gone.
|
||||
assert "return activeRow?[activeRow,...rows]:rows;" not in body
|
||||
@@ -183,7 +183,9 @@ def test_conversation_filter_merges_direct_title_and_content_matches_without_dro
|
||||
def test_conversation_filter_keeps_content_search_results_when_query_is_session_id():
|
||||
assert "function _sessionSearchMergeMatches" in SESSIONS_JS
|
||||
assert "function _sessionSearchDirectAndTitleMatches" in SESSIONS_JS
|
||||
assert "const allMatched=_sessionSearchMergeMatches(_allSessions,searchQueryRaw,_contentSearchResults);" in SESSIONS_JS
|
||||
assert "const sidebarRows=_sessionRowsWithActiveEphemeralSession(_allSessions);" in SESSIONS_JS
|
||||
assert "const searchMatches=_sessionSearchMergeMatches(sidebarRows,searchQueryRaw,_contentSearchResults);" in SESSIONS_JS
|
||||
assert "const allMatched=_ensureActiveSessionRowPresent(searchMatches,sidebarRows);" in SESSIONS_JS
|
||||
assert "const directAndTitleMatches=_sessionSearchDirectAndTitleMatches(_allSessions,currentQ);" in SESSIONS_JS
|
||||
assert "const directOrTitleIds=new Set(directAndTitleMatches.map(s=>s.session_id));" in SESSIONS_JS
|
||||
assert "!directOrTitleIds.has(s.session_id)" in SESSIONS_JS
|
||||
|
||||
Reference in New Issue
Block a user