From 861c29b35bddf3d4bc878d05144624402e248870 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Sat, 27 Jun 2026 22:11:13 -0400 Subject: [PATCH 1/4] fix(#4731): enlarge mobile message action tap targets --- static/style.css | 2 ++ .../test_issue4731_mobile_message_actions.py | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 tests/test_issue4731_mobile_message_actions.py diff --git a/static/style.css b/static/style.css index 83c749cc..9d54b288 100644 --- a/static/style.css +++ b/static/style.css @@ -6008,6 +6008,8 @@ main.main > .main-view:not([id="mainChat"]):not([id="mainSettings"]) .main-view- .msg-row[data-role="assistant"] .msg-foot, .assistant-turn .msg-foot{opacity:1;} .msg-actions{opacity:1;} + .msg-actions{gap:4px;} + .msg-action-btn{min-width:40px;min-height:40px;padding:8px;display:inline-flex;align-items:center;justify-content:center;} } /* ── /btw ephemeral side-question bubble ── */ diff --git a/tests/test_issue4731_mobile_message_actions.py b/tests/test_issue4731_mobile_message_actions.py new file mode 100644 index 00000000..0d37db11 --- /dev/null +++ b/tests/test_issue4731_mobile_message_actions.py @@ -0,0 +1,32 @@ +"""Static regression coverage for the mobile message action touch-target fix.""" +from pathlib import Path + + +STYLE_CSS = (Path(__file__).resolve().parent.parent / "static" / "style.css").read_text(encoding="utf-8") + + +def _mobile_block(): + marker = "@media(max-width:640px){" + start = STYLE_CSS.index(marker) + end = STYLE_CSS.index("/* ── /btw ephemeral side-question bubble ── */", start) + return STYLE_CSS[start:end] + + +def test_mobile_block_enlarges_msg_action_buttons(): + block = _mobile_block() + assert ".msg-action-btn{min-width:40px;min-height:40px;padding:8px;display:inline-flex;align-items:center;justify-content:center;}" in block + + +def test_mobile_block_widens_msg_actions_gap(): + block = _mobile_block() + assert ".msg-actions{gap:4px;}" in block + + +def test_mobile_block_keeps_visibility_override(): + block = _mobile_block() + assert ".msg-actions{opacity:1;}" in block + + +def test_desktop_base_rules_remain_small_and_untouched(): + assert ".msg-actions{display:flex;align-items:center;gap:2px;" in STYLE_CSS + assert ".msg-action-btn{background:none;border:none;color:var(--muted);cursor:pointer;font-size:13px;padding:2px 5px;" in STYLE_CSS From a51e58efc0da7972133899e256b7d2be46965e2f Mon Sep 17 00:00:00 2001 From: Paladin173 <35980893+Paladin173@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:38:20 -0500 Subject: [PATCH 2/4] fix: skip archived saved session on boot --- static/boot.js | 2 +- static/sessions.js | 1 + tests/test_1694_root_saved_running_policy.py | 4 ++++ tests/test_session_action_menu_regression.py | 13 +++++++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/static/boot.js b/static/boot.js index b3daf912..70f50ebe 100644 --- a/static/boot.js +++ b/static/boot.js @@ -85,7 +85,7 @@ async function _savedSessionShouldStaySidebarOnly(sid){ 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)); + return !!(session&&(session.active_stream_id||session.pending_user_message||session.archived)); }catch(e){ return false; } diff --git a/static/sessions.js b/static/sessions.js index 4aa8e388..2a991bc1 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -3558,6 +3558,7 @@ async function _archiveSession(session, archived=true, beforeListRender=null){ const cached=(_allSessions||[]).find(s=>s&&s.session_id===session.session_id); if(cached) cached.archived=archived; if(S.session&&S.session.session_id===session.session_id) S.session.archived=archived; + try{ if(archived&&session.session_id&&localStorage.getItem('hermes-webui-session')===session.session_id) localStorage.removeItem('hermes-webui-session'); }catch(_){ } showToast(session.archived?_sessionArchiveToast(response,session):t('session_restored')); if(renderHold) await renderHold; if(_showArchived&&!_sessionPrefersReducedMotion()) _sessionSwipeReturnOffsets.set(session.session_id,'0px'); diff --git a/tests/test_1694_root_saved_running_policy.py b/tests/test_1694_root_saved_running_policy.py index e5ebb218..00a72234 100644 --- a/tests/test_1694_root_saved_running_policy.py +++ b/tests/test_1694_root_saved_running_policy.py @@ -72,6 +72,10 @@ def test_saved_running_session_helper_uses_metadata_only_and_runtime_markers(): 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 "session.archived" in helper, ( + "helper must skip auto-opening archived localStorage sessions so root " + "boot lands on the empty state instead of reopening archived chats" + ) assert "loadSession(" not in helper, ( "helper must not call loadSession(), because that would already project " "the saved session into the active pane" diff --git a/tests/test_session_action_menu_regression.py b/tests/test_session_action_menu_regression.py index e9fd77c1..f3fe576b 100644 --- a/tests/test_session_action_menu_regression.py +++ b/tests/test_session_action_menu_regression.py @@ -46,6 +46,19 @@ def test_archive_action_repaints_sidebar_before_full_refresh(): assert helper_body.index(api_call) < helper_body.index(optimistic) < helper_body.index(cached_render) < helper_body.index(full_refresh) +def test_archive_action_clears_saved_session_pointer_for_archived_current_session(): + """Archiving the saved active session should not leave boot localStorage stale.""" + helper_body = _function_block(SESSIONS_JS, "_archiveSession") + stale_saved_pointer_guard = ( + "try{ if(archived&&session.session_id&&localStorage.getItem('hermes-webui-session')===session.session_id) " + "localStorage.removeItem('hermes-webui-session'); }catch(_){ }" + ) + + assert stale_saved_pointer_guard in helper_body + assert helper_body.index("if(S.session&&S.session.session_id===session.session_id) S.session.archived=archived;") < helper_body.index(stale_saved_pointer_guard) + assert helper_body.index(stale_saved_pointer_guard) < helper_body.index("showToast(session.archived?_sessionArchiveToast(response,session):t('session_restored'));") + + def test_delete_action_repaints_sidebar_before_loading_remaining_sessions(): """Delete should remove the row locally before loading replacement session data.""" body = _function_block(SESSIONS_JS, "deleteSession") From aceb5411eebc9643103afd1eff2556df04d7d9f1 Mon Sep 17 00:00:00 2001 From: Paladin173 <35980893+Paladin173@users.noreply.github.com> Date: Sat, 27 Jun 2026 22:16:42 -0500 Subject: [PATCH 3/4] Clear archived saved session on boot restore --- static/boot.js | 19 ++++++++++++--- tests/test_1694_root_saved_running_policy.py | 25 +++++++++++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) 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)" From 53af91c5f2dfc4416488d2440374baba21075ffe Mon Sep 17 00:00:00 2001 From: Paladin173 <35980893+Paladin173@users.noreply.github.com> Date: Sat, 27 Jun 2026 22:31:56 -0500 Subject: [PATCH 4/4] Update boot restore static guard test --- tests/test_session_cross_tab_sync.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_session_cross_tab_sync.py b/tests/test_session_cross_tab_sync.py index 419db30a..9f02386f 100644 --- a/tests/test_session_cross_tab_sync.py +++ b/tests/test_session_cross_tab_sync.py @@ -43,7 +43,10 @@ def test_boot_prefers_url_session_over_local_storage_session(): assert "const urlSession=(typeof _sessionIdFromLocation==='function')?_sessionIdFromLocation():null;" 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 + assert "const savedSidebarOnlyState=(!urlSession&&savedLocal)" in BOOT_JS + assert "? await _savedSessionSidebarOnlyState(savedLocal)" in BOOT_JS + assert "if(savedSidebarOnlyState&&savedSidebarOnlyState.sidebarOnly)" in BOOT_JS + assert "if(savedSidebarOnlyState.archived)" in BOOT_JS def test_api_helper_resolves_against_document_base_not_session_path():