diff --git a/static/boot.js b/static/boot.js index c932108f..1ff1087a 100644 --- a/static/boot.js +++ b/static/boot.js @@ -1465,6 +1465,17 @@ window._hermesTtsSynth=function(id, text, opts){ window._voiceModeDeactivate=_deactivate; window._voiceModeImmediateSend=_voiceModeSend; })(); +function _currentSessionIsReusableEmptyChat(){ + if(!S.session) return false; + const hasVisibleMessages=Array.isArray(S.messages) + && S.messages.some(m=>m&&m.role&&m.role!=='tool'); + return (S.session.message_count||0)===0 + && !hasVisibleMessages + && !S.busy + && !S.session.active_stream_id + && !S.session.pending_user_message; +} + $('fileInput').onchange=e=>{addFiles(Array.from(e.target.files));e.target.value='';}; $('btnNewChat').onclick=async()=>{ // If the current session has no messages AND nothing is in flight, just focus @@ -1478,11 +1489,7 @@ $('btnNewChat').onclick=async()=>{ // couldn't actually start a parallel chat. Use the same in-flight signal as // `_restoreSettledSession()` in messages.js: an active stream id or a queued // pending user message means the session is real, not empty. - if(S.session - && (S.session.message_count||0)===0 - && !S.busy - && !S.session.active_stream_id - && !S.session.pending_user_message){ + if(_currentSessionIsReusableEmptyChat()){ $('msg').focus();closeMobileSidebar();return; } if(typeof _restoreRememberedNewChatDraftSession==='function' @@ -1746,11 +1753,7 @@ document.addEventListener('keydown',async e=>{ // the composer rather than creating another empty session that will clutter // the sidebar list (#1171). See the matching guard in $('btnNewChat').onclick // and bug #1432 for why the in-flight check is needed. - if(S.session - && (S.session.message_count||0)===0 - && !S.busy - && !S.session.active_stream_id - && !S.session.pending_user_message){ + if(_currentSessionIsReusableEmptyChat()){ $('msg').focus();return; } // Cmd/Ctrl+K should always create a new conversation, even while the current diff --git a/tests/test_issue_new_chat_draft_restore.py b/tests/test_issue_new_chat_draft_restore.py index 50d02465..7f6c7436 100644 --- a/tests/test_issue_new_chat_draft_restore.py +++ b/tests/test_issue_new_chat_draft_restore.py @@ -53,6 +53,30 @@ def test_new_chat_button_restores_remembered_draft_before_creating_session(): ) +def test_new_chat_empty_reuse_guard_checks_loaded_visible_messages(): + helper_start = BOOT_JS.find("function _currentSessionIsReusableEmptyChat()") + helper_end = BOOT_JS.find("$('fileInput').onchange", helper_start) + assert helper_start != -1 and helper_end != -1, "empty-chat reuse helper not found" + helper = BOOT_JS[helper_start:helper_end] + assert "S.session.message_count||0" in helper, ( + "empty-chat reuse should still honor the server-side session message count" + ) + assert "S.messages.some(m=>m&&m.role&&m.role!=='tool')" in helper, ( + "empty-chat reuse must not focus-only when loaded transcript messages are visible" + ) + assert "S.session.active_stream_id" in helper and "S.session.pending_user_message" in helper, ( + "empty-chat reuse must continue to treat in-flight zero-count sessions as real sessions" + ) + + button_body = _btn_new_chat_handler() + assert "if(_currentSessionIsReusableEmptyChat())" in button_body + shortcut_start = BOOT_JS.find("if((e.metaKey||e.ctrlKey)&&e.key==='k')") + shortcut_end = BOOT_JS.find("// Cmd/Ctrl+, opens/closes Settings", shortcut_start) + assert shortcut_start != -1 and shortcut_end != -1, "Cmd/Ctrl+K handler block not found" + shortcut_body = BOOT_JS[shortcut_start:shortcut_end] + assert "if(_currentSessionIsReusableEmptyChat())" in shortcut_body + + def test_restore_helper_validates_candidate_with_session_metadata(): assert "const NEW_CHAT_DRAFT_SESSION_KEY = 'hermes-new-chat-draft-session';" in SESSIONS_JS assert "async function _restoreRememberedNewChatDraftSession()" in SESSIONS_JS