From e201bceafb1943693c7477c4644efd1c9df8b0c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 25 Aug 2026 05:49:27 +0000 Subject: [PATCH] one card per tool call, however often it is announced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit a tool's input ARRIVES — the SDK emits tool:start again as more of it streams in — and this appended a new message every time. an AskUserQuestion therefore rendered twice: once half-built, with two of its three options still missing their descriptions, then again complete. the second carried the answer; the first sat above it forever, showing an unanswered question that could not be answered. upsert on toolCallId instead, and only when there is one — a call with no id cannot be recognised later, and collapsing those on anything else would merge two genuinely different calls. the existing message's other fields survive: `output` arrives on tool:result, and a late tool:start must not erase it. Co-Authored-By: Claude Opus 5 Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043 --- .../officerdev/src/hooks/useChat.ts | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/workspaces/officerdev/src/hooks/useChat.ts b/src/workspaces/officerdev/src/hooks/useChat.ts index fcfd5394..340f6aa8 100644 --- a/src/workspaces/officerdev/src/hooks/useChat.ts +++ b/src/workspaces/officerdev/src/hooks/useChat.ts @@ -456,10 +456,32 @@ export function useChat(initialSessionId?: string, initialModel?: string | null, toolInput: msg.toolInput, toolCallId: msg.toolCallId, }; + + /* + One card per tool call, however many times the harness announces it. + + A tool's input ARRIVES — the SDK emits `tool:start` again as more of it streams in — and this + appended every time, so an AskUserQuestion showed up twice: once half-built with two of its three + options still missing their descriptions, then again complete. The second carried the answer and + the first sat above it forever, unanswerable. + + Keyed on `toolCallId`, and only when there is one: a call without an id cannot be recognised + later, and collapsing those on anything else would merge two genuinely different calls. + */ + const upsert = (list: ChatMessage[]): ChatMessage[] => { + const at = msg.toolCallId ? list.findIndex((m) => m.role === 'tool' && m.toolCallId === msg.toolCallId) : -1; + if (at === -1) return [...list, call]; + // Keep whatever the earlier event already carried — `output` arrives on `tool:result` and a + // late `tool:start` must not erase it. + const next = [...list]; + const existing = next[at]!; + if (existing.role !== 'tool') return [...list, call]; + next[at] = { ...existing, toolName: msg.toolName, toolInput: msg.toolInput }; + return next; + }; + const parent = msg.parentToolUseId; - setMessages((prev) => - parent ? (withChildren(prev, parent, (kids) => [...kids, call]) ?? [...prev, call]) : [...prev, call], - ); + setMessages((prev) => (parent ? (withChildren(prev, parent, upsert) ?? upsert(prev)) : upsert(prev))); break; }