Merge origin/master into anchor fallback audit

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Frank SongandPaperclip committed 2026-06-28 15:02:02 +08:00
commit 0f41d01d0f
7 files changed
+94 -7

No files matched your search

+16 -3
View File
@@ -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));
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();
+1
View File
@@ -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');
+2
View File
@@ -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 ── */
+26 -3
View File
@@ -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, (
@@ -72,6 +72,14 @@ 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 "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"
@@ -81,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)
@@ -89,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)"
@@ -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
@@ -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")
+4 -1
View File
@@ -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():