diff --git a/static/boot.js b/static/boot.js index 70f50ebe..f0a5bc1e 100644 --- a/static/boot.js +++ b/static/boot.js @@ -81,13 +81,20 @@ async function cancelSessionStream(session){ } async function _savedSessionShouldStaySidebarOnly(sid){ + const state = await _savedSessionSidebarOnlyState(sid); + return !!(state&&state.sidebarOnly); +} + +async function _savedSessionSidebarOnlyState(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||session.archived)); + const archived = !!(session&&session.archived); + const running = !!(session&&(session.active_stream_id||session.pending_user_message)); + return {sidebarOnly:archived||running, archived}; }catch(e){ - return false; + return null; } } @@ -2708,7 +2715,13 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility; const saved=urlSession||savedLocal; if(saved){ try{ - if(!urlSession&&savedLocal&&await _savedSessionShouldStaySidebarOnly(savedLocal)){ + const savedSidebarOnlyState=(!urlSession&&savedLocal) + ? await _savedSessionSidebarOnlyState(savedLocal) + : null; + if(savedSidebarOnlyState&&savedSidebarOnlyState.sidebarOnly){ + if(savedSidebarOnlyState.archived){ + try{localStorage.removeItem('hermes-webui-session');}catch(_){} + } S.session=null; S.messages=[]; S.activeStreamId=null; S.busy=false; S._bootReady=true; syncTopbar();syncWorkspacePanelState(); diff --git a/tests/test_1694_root_saved_running_policy.py b/tests/test_1694_root_saved_running_policy.py index 00a72234..9e1793e2 100644 --- a/tests/test_1694_root_saved_running_policy.py +++ b/tests/test_1694_root_saved_running_policy.py @@ -55,14 +55,14 @@ def test_root_saved_running_session_is_checked_before_load_session_projection(): "saved running-session root guard must run before loadSession(saved), " "otherwise loadSession already projects the session into the active pane" ) - assert "_savedSessionShouldStaySidebarOnly" in block, ( + assert "_savedSessionSidebarOnlyState" 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") + helper_idx = BOOT_JS.find("async function _savedSessionSidebarOnlyState") 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, ( @@ -76,6 +76,10 @@ def test_saved_running_session_helper_uses_metadata_only_and_runtime_markers(): "helper must skip auto-opening archived localStorage sessions so root " "boot lands on the empty state instead of reopening archived chats" ) + assert "sidebarOnly:archived||running" in helper.replace(" ", ""), ( + "helper must report the sidebar-only decision without conflating archived " + "sessions with running sessions" + ) assert "loadSession(" not in helper, ( "helper must not call loadSession(), because that would already project " "the saved session into the active pane" @@ -85,7 +89,7 @@ def test_saved_running_session_helper_uses_metadata_only_and_runtime_markers(): 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") + helper_pos = block.find("_savedSessionSidebarOnlyState") render_pos = block.find("await renderSessionList()", helper_pos) empty_pos = block.find("$('emptyState').style.display=''", helper_pos) return_pos = block.find("return;", helper_pos) @@ -93,3 +97,18 @@ def test_root_saved_running_sidebar_only_path_renders_empty_state_and_sidebar(): 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)" + + +def test_root_archived_saved_session_clears_stale_localstorage_pointer(): + """Archived root-restore skips projection and clears stale saved-session state.""" + block = _boot_saved_session_block() + helper_pos = block.find("_savedSessionSidebarOnlyState") + clear_guard = "if(savedSidebarOnlyState.archived)" + guard_pos = block.find(clear_guard, helper_pos) + clear_pos = block.find("localStorage.removeItem('hermes-webui-session')", guard_pos) + render_pos = block.find("await renderSessionList()", helper_pos) + load_pos = block.find("await loadSession(saved, {preserveActiveInput:true})") + assert guard_pos > helper_pos, "archived sidebar-only path must be distinguished" + assert clear_pos > guard_pos, "archived saved session must clear stale localStorage pointer" + assert clear_pos < render_pos, "stale pointer should be cleared before the sidebar-only return" + assert render_pos < load_pos, "archived saved session must return before loadSession(saved)"