From 1e5f20f56db403ba680c7c37bdd4842fd2fa6a1c Mon Sep 17 00:00:00 2001 From: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com> Date: Sat, 23 May 2026 19:18:00 +0200 Subject: [PATCH] fix: preserve settled tool cards after stream completion --- static/messages.js | 11 +++++++++-- tests/test_streaming_markdown.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/static/messages.js b/static/messages.js index a3e7e067..82f231b5 100644 --- a/static/messages.js +++ b/static/messages.js @@ -1707,10 +1707,17 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ } } } - if(d.session.tool_calls&&d.session.tool_calls.length){ + const hasMessageToolMetadata=S.messages.some(m=>{ + if(!m||m.role!=='assistant') return false; + const hasTc=Array.isArray(m.tool_calls)&&m.tool_calls.length>0; + const hasPartialTc=Array.isArray(m._partial_tool_calls)&&m._partial_tool_calls.length>0; + const hasTu=Array.isArray(m.content)&&m.content.some(p=>p&&p.type==='tool_use'); + return hasTc||hasPartialTc||hasTu; + }); + if(!hasMessageToolMetadata&&d.session.tool_calls&&d.session.tool_calls.length){ S.toolCalls=d.session.tool_calls.map(tc=>({...tc,done:true})); } else { - S.toolCalls=S.toolCalls.map(tc=>({...tc,done:true})); + S.toolCalls=hasMessageToolMetadata?[]:S.toolCalls.map(tc=>({...tc,done:true})); } if(typeof _copyActivityDisclosureState==='function'&&lastAsst){ const assistantIdx=S.messages.indexOf(lastAsst); diff --git a/tests/test_streaming_markdown.py b/tests/test_streaming_markdown.py index 4506c32e..aa49ad27 100644 --- a/tests/test_streaming_markdown.py +++ b/tests/test_streaming_markdown.py @@ -450,6 +450,21 @@ class TestDoneEventSmd: "the possibly-stale _scrollPinned flag." ) + def test_done_handler_prefers_message_tool_metadata_for_settled_render(self): + """If final messages already contain tool metadata, renderMessages() + should derive anchored settled cards from those messages. + + Falling back to session-level tool_calls unconditionally can hide cards + after pagination/windowing because those anchors may not line up with + the active message array. + """ + fn = self.get_fn() + assert fn, "'done' handler not found" + done_before_render = fn[:fn.index("renderMessages({preserveScroll:true})")] + assert "const hasMessageToolMetadata=S.messages.some" in done_before_render + assert "!hasMessageToolMetadata&&d.session.tool_calls&&d.session.tool_calls.length" in done_before_render + assert "S.toolCalls=hasMessageToolMetadata?[]:S.toolCalls.map" in done_before_render + # ── 7. apperror event: smd parser ends cleanly ───────────────────────────────