diff --git a/static/messages.js b/static/messages.js index 12b43b7a..7d40e2f9 100644 --- a/static/messages.js +++ b/static/messages.js @@ -273,7 +273,7 @@ async function send(){ const userMsg={role:'user',content:displayText,attachments:uploaded.length?uploadedNames:undefined,_ts:Date.now()/1000}; S.toolCalls=[]; // clear tool calls from previous turn clearLiveToolCards(); // clear any leftover live cards from last turn - S.messages.push(userMsg);renderMessages();appendThinking();setBusy(true); + S.messages.push(userMsg);renderMessages();appendThinking('',{pending:true});setBusy(true); // First optimistic pass: make the local user turn visible before /api/chat/start // can save pending state on the server. if(typeof upsertActiveSessionForLocalTurn==='function'){ diff --git a/static/ui.js b/static/ui.js index bbee01f7..2bfca893 100644 --- a/static/ui.js +++ b/static/ui.js @@ -7033,11 +7033,12 @@ function finalizeThinkingCard(){ _syncToolCallGroupSummary(group); } } -function appendThinking(text=''){ +function appendThinking(text='', options){ // Guard: ignore if session was switched during an async SSE stream. // The old stream's reasoning events can still fire after switch; // without this check they would pollute the new session's DOM. - if(!S.session||!S.activeStreamId) return; + const allowPendingPlaceholder=!!(options&&options.pending===true); + if(!S.session||(!S.activeStreamId&&!allowPendingPlaceholder)) return; $('emptyState').style.display='none'; let turn=$('liveAssistantTurn'); if(!turn){ diff --git a/tests/test_sidebar_first_turn_visibility.py b/tests/test_sidebar_first_turn_visibility.py index 77ba7860..cff1611b 100644 --- a/tests/test_sidebar_first_turn_visibility.py +++ b/tests/test_sidebar_first_turn_visibility.py @@ -16,7 +16,7 @@ class TestSidebarFirstTurnVisibility: "send() must optimistically upsert the active session into the sidebar " "as soon as the local user message is pushed." ) - push_idx = src.index("S.messages.push(userMsg);renderMessages();appendThinking();setBusy(true);") + push_idx = src.index("S.messages.push(userMsg);renderMessages();appendThinking('',{pending:true});setBusy(true);") helper_idx = src.index("upsertActiveSessionForLocalTurn", push_idx) start_idx = src.index("api('/api/chat/start'", push_idx) assert helper_idx < start_idx, ( @@ -29,6 +29,26 @@ class TestSidebarFirstTurnVisibility: "that race can overwrite the optimistic first-turn row with an empty list." ) + def test_messages_send_renders_pending_assistant_placeholder_before_chat_start(self): + messages = read("static/messages.js") + ui = read("static/ui.js") + send_start = messages.index("async function send()") + push_idx = messages.index("appendThinking('',{pending:true})", send_start) + start_idx = messages.index("api('/api/chat/start'", send_start) + assert push_idx < start_idx, ( + "send() should render an assistant-side pending placeholder before " + "/api/chat/start or the first SSE event returns." + ) + append_start = ui.index("function appendThinking(text='', options)") + append_end = ui.index("function updateThinking", append_start) + append_body = ui[append_start:append_end] + assert "allowPendingPlaceholder" in append_body + assert "options.pending===true" in append_body.replace(" ", ""), ( + "appendThinking() must allow the explicit pre-stream placeholder path " + "without weakening the stale-stream guard for ordinary SSE updates." + ) + assert "(!S.activeStreamId&&!allowPendingPlaceholder)" in append_body.replace(" ", "") + def test_sessions_js_has_local_turn_upsert_helper(self): src = read("static/sessions.js") assert "function upsertActiveSessionForLocalTurn" in src