mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-26 11:40:26 +00:00
fix: keep saved running sessions sidebar-only on root boot
Root page loads should not automatically project a localStorage-saved running session into the active pane. Keep explicit /session/<sid> behavior unchanged while leaving the saved session discoverable from the sidebar. (cherry picked from commit bb60cf21d911a84e285363bcecf46fb441181fb9)
This commit is contained in:
+21
-1
@@ -42,6 +42,17 @@ async function cancelSessionStream(session){
|
||||
if(typeof renderSessionList==='function') renderSessionList();
|
||||
}
|
||||
|
||||
async function _savedSessionShouldStaySidebarOnly(sid){
|
||||
if(!sid) return false;
|
||||
try{
|
||||
const data = await api(`/api/session?session_id=${encodeURIComponent(sid)}&messages=0&resolve_model=0`);
|
||||
const session = data&&data.session;
|
||||
return !!(session&&(session.active_stream_id||session.pending_user_message));
|
||||
}catch(e){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mobile navigation ──────────────────────────────────────────────────────
|
||||
let _workspacePanelMode='closed'; // 'closed' | 'browse' | 'preview'
|
||||
|
||||
@@ -1346,9 +1357,18 @@ function applyBotName(){
|
||||
// Initialize reasoning chip on boot (fixes #1103 — chip hidden until session load)
|
||||
if(typeof fetchReasoningChip==='function') fetchReasoningChip();
|
||||
const urlSession=(typeof _sessionIdFromLocation==='function')?_sessionIdFromLocation():null;
|
||||
const saved=urlSession||localStorage.getItem('hermes-webui-session');
|
||||
const savedLocal=localStorage.getItem('hermes-webui-session');
|
||||
const saved=urlSession||savedLocal;
|
||||
if(saved){
|
||||
try{
|
||||
if(!urlSession&&savedLocal&&await _savedSessionShouldStaySidebarOnly(savedLocal)){
|
||||
S.session=null; S.messages=[]; S.activeStreamId=null; S.busy=false;
|
||||
S._bootReady=true;
|
||||
syncTopbar();syncWorkspacePanelState();
|
||||
$('emptyState').style.display='';
|
||||
await renderSessionList();if(typeof startGatewaySSE==='function')startGatewaySSE();
|
||||
return;
|
||||
}
|
||||
await loadSession(saved);
|
||||
// If the restored session has no messages it is an ephemeral scratch pad —
|
||||
// treat the page as a fresh start rather than resuming a blank conversation.
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Regression tests for #1694 root boot policy around saved running sessions.
|
||||
|
||||
The active pane is only a projection. A root `/` tab restored from
|
||||
``localStorage['hermes-webui-session']`` should not automatically project into a
|
||||
saved session that is still running, because that makes the new tab inherit the
|
||||
running pane's busy/stream state even though the user did not explicitly open
|
||||
that session.
|
||||
|
||||
Explicit `/session/<sid>` reload remains different: it should still restore and
|
||||
reattach to the requested running session.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO = Path(__file__).parent.parent
|
||||
BOOT_JS = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _boot_saved_session_block() -> str:
|
||||
marker = "const urlSession="
|
||||
start = BOOT_JS.find(marker)
|
||||
assert start > 0, "boot saved-session restore block not found"
|
||||
end_marker = "// no saved session"
|
||||
end = BOOT_JS.find(end_marker, start)
|
||||
assert end > start, "no-saved-session marker not found after restore block"
|
||||
return BOOT_JS[start:end]
|
||||
|
||||
|
||||
def test_root_boot_distinguishes_url_session_from_localstorage_saved_session():
|
||||
"""Root restore and explicit URL restore must be separate decisions."""
|
||||
block = _boot_saved_session_block()
|
||||
assert "const savedLocal=" in block, (
|
||||
"boot must keep the localStorage session separate from urlSession so "
|
||||
"root `/` policy can differ from explicit `/session/<sid>` reload"
|
||||
)
|
||||
compact = block.replace(" ", "")
|
||||
assert "constsaved=urlSession||savedLocal" in compact, (
|
||||
"boot should still prefer explicit URL sessions over saved localStorage sessions"
|
||||
)
|
||||
|
||||
|
||||
def test_root_saved_running_session_is_checked_before_load_session_projection():
|
||||
"""A saved running localStorage session should be detected before loadSession()."""
|
||||
block = _boot_saved_session_block()
|
||||
guard = "!urlSession&&savedLocal"
|
||||
guard_pos = block.replace(" ", "").find(guard)
|
||||
load_pos = block.find("await loadSession(saved)")
|
||||
assert guard_pos >= 0, (
|
||||
"root `/` boot must have a !urlSession && savedLocal guard for saved "
|
||||
"running sessions before projecting them into the active pane"
|
||||
)
|
||||
assert load_pos >= 0, "loadSession(saved) call not found"
|
||||
assert guard_pos < load_pos, (
|
||||
"saved running-session root guard must run before loadSession(saved), "
|
||||
"otherwise loadSession already projects the session into the active pane"
|
||||
)
|
||||
assert "_savedSessionShouldStaySidebarOnly" in block, (
|
||||
"boot should delegate the saved-running metadata check to a named helper"
|
||||
)
|
||||
|
||||
|
||||
def test_saved_running_session_helper_uses_metadata_only_and_runtime_markers():
|
||||
"""The helper should inspect metadata without loading messages or attaching SSE."""
|
||||
helper_idx = BOOT_JS.find("async function _savedSessionShouldStaySidebarOnly")
|
||||
assert helper_idx > 0, "saved-running root policy helper not found"
|
||||
helper = BOOT_JS[helper_idx:helper_idx + 1200]
|
||||
assert "/api/session?session_id=" in helper, (
|
||||
"helper should inspect session metadata via /api/session before deciding"
|
||||
)
|
||||
assert "messages=0" in helper, "helper must avoid loading full messages"
|
||||
assert "resolve_model=0" in helper, "helper must avoid unnecessary model resolution"
|
||||
assert "active_stream_id" in helper, "helper must treat active_stream_id as running"
|
||||
assert "pending_user_message" in helper, "helper must treat pending_user_message as running"
|
||||
assert "loadSession(" not in helper, (
|
||||
"helper must not call loadSession(), because that would already project "
|
||||
"the saved session into the active pane"
|
||||
)
|
||||
|
||||
|
||||
def test_root_saved_running_sidebar_only_path_renders_empty_state_and_sidebar():
|
||||
"""Skipping projection should still leave the app usable and sidebar visible."""
|
||||
block = _boot_saved_session_block()
|
||||
helper_pos = block.find("_savedSessionShouldStaySidebarOnly")
|
||||
render_pos = block.find("await renderSessionList()", helper_pos)
|
||||
empty_pos = block.find("$('emptyState').style.display=''", helper_pos)
|
||||
return_pos = block.find("return;", helper_pos)
|
||||
assert helper_pos >= 0, "saved-running helper call not found"
|
||||
assert empty_pos > helper_pos, "sidebar-only path must show the empty state"
|
||||
assert render_pos > helper_pos, "sidebar-only path must render the session list"
|
||||
assert return_pos > render_pos, "sidebar-only path should return before loadSession(saved)"
|
||||
@@ -41,7 +41,9 @@ def test_session_switch_updates_url_path_for_tab_local_anchor():
|
||||
|
||||
def test_boot_prefers_url_session_over_local_storage_session():
|
||||
assert "const urlSession=(typeof _sessionIdFromLocation==='function')?_sessionIdFromLocation():null;" in BOOT_JS
|
||||
assert "const saved=urlSession||localStorage.getItem('hermes-webui-session');" in BOOT_JS
|
||||
assert "const savedLocal=localStorage.getItem('hermes-webui-session');" in BOOT_JS
|
||||
assert "const saved=urlSession||savedLocal;" in BOOT_JS
|
||||
assert "if(!urlSession&&savedLocal&&await _savedSessionShouldStaySidebarOnly(savedLocal))" in BOOT_JS
|
||||
|
||||
|
||||
def test_api_helper_resolves_against_document_base_not_session_path():
|
||||
|
||||
Reference in New Issue
Block a user