From c52bd6c57e7efd332e23a65d3cfb03431f59ed49 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Tue, 19 May 2026 16:42:00 +0000 Subject: [PATCH] studio/frontend: surface tool-call activity during Generating phase Until now, the assistant message rendered a static `Generating...` label whenever `message.content.length === 0 && status === "running"`. That covers the moment between Send and the first chunk landing, but also covers the much-longer window where the model is mid-way through emitting a `...` block: the closed pair gets stripped by the backend / chat-adapter before reaching the message parts, so content stays empty for the full tool-call duration. Users see the static label for many seconds and assume the model is stuck. Two changes: 1. Animated activity. The `` now wraps a pulsing dot (same dot style as `RunningToolIndicator`) so even when the label can't say anything more specific, the user sees the bubble is alive. 2. Tool-call probe. If any text / reasoning part has been emitted and it contains the opening of any known tool-call shape (``, ``, `<|python_tag|>`, `[TOOL_CALLS]`), switch the label from `Generating...` to `Calling tool...`. The probe runs in the existing useAuiState selector so memo invalidation only happens when content actually changes. The `RunningToolIndicator` path (which renders once a real tool-call part has landed) is unchanged. The two indicators are mutually exclusive because RunningToolIndicator requires a tool-call part and GeneratingIndicator bails out the moment any non-text part shows up. --- .../src/components/assistant-ui/thread.tsx | 58 ++++++++++++++++--- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx index d35f223142..98d8800563 100644 --- a/studio/frontend/src/components/assistant-ui/thread.tsx +++ b/studio/frontend/src/components/assistant-ui/thread.tsx @@ -987,15 +987,59 @@ const MessageError: FC = () => { ); }; +// Heuristic: text the model emits before a tool actually starts often +// includes the opening of one of the tool-call markup shapes the +// backend strips. When that shape appears in the assistant's +// content but no tool-call PART has landed yet (because the backend +// only emits tool_start after the markup parses cleanly), the user +// otherwise stares at a frozen "Generating..." for the whole time it +// takes the model to finish writing the tool call. +const TOOL_CALL_PROBE = /||<\|python_tag\|>|\[TOOL_CALLS\]/i; + const GeneratingIndicator: FC = () => { - const show = useAuiState( - ({ message }) => - message.content.length === 0 && message.status?.type === "running", - ); - if (!show) { + const phase = useAuiState(({ message }) => { + if (message.status?.type !== "running") return null; + const content = message.content; + if (!content || content.length === 0) return "generating"; + // If every part is empty (placeholder text added but no chars yet) + // we still want to surface generating activity. + let allEmpty = true; + let sawToolSignal = false; + for (const p of content) { + const t = (p as { type?: string }).type; + if (t === "text" || t === "reasoning") { + const text = ((p as { text?: string }).text ?? ""); + if (text.length > 0) { + allEmpty = false; + if (TOOL_CALL_PROBE.test(text)) { + sawToolSignal = true; + } + } + } else { + // tool-call / source / etc. mean a real part exists; the + // RunningToolIndicator or message body handles those. + return null; + } + } + if (sawToolSignal) return "calling-tool"; + if (allEmpty) return "generating"; return null; - } - return Generating...; + }); + if (!phase) return null; + return ( + + + {phase === "calling-tool" ? "Calling tool..." : "Generating..."} + + ); }; // Placeholder when stop fires before any visible content (e.g. mid-think).