Stage 374: PR #2431 — fix(chat): render pending assistant placeholder before /api/chat/start returns by @Michaelyklam (fixes #2429)

Co-authored-by: Michael Lam <michael@example.local>
This commit is contained in:
nesquena-hermesandMichael Lam committed 2026-05-17 02:49:35 +00:00
1 parent 13265b7085
commit a33cd4195b
3 files changed
+25 -4

No files matched your search

+1 -1
View File
@@ -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'){
+3 -2
View File
@@ -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){
+21 -1
View File
@@ -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