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

* 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:
nesquena-hermes
2026-06-02 20:24:48 -07:00
committed by GitHub
parent 0b5458f3da
commit fdfb935b5e
4 changed files with 99 additions and 6 deletions

View File

@@ -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

View File

@@ -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) ||

View 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

View File

@@ -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