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; }