studio/frontend: hide RunningToolIndicator once final answer streams
Codex review at 14:52Z on commit d64b4b84 caught a follow-on UX bug.
The fallback returned "working" whenever ANY tool-call part existed
in the message, including AFTER the last tool finished and the model
started streaming its text answer. That left a misleading pulsing
"Working..." row under visibly-streaming prose for the entire final
answer.
Tighten the selector to walk parts from the tail and decide based on
the last meaningful part type:
- text part with non-empty content -> indicator hidden (answer is
currently streaming, so any pulse is misleading);
- reasoning part -> indicator hidden (the reasoning panel already
shows progress);
- tool-call part with status=running -> "Running <name>...";
- tool-call part otherwise (completed, awaiting next answer) ->
"Working...";
- empty text placeholder or unknown part type -> keep walking.
In the common reasoning -> tool -> tool-result -> text path, the
indicator now appears only while a tool is actually executing or
during the tool->text boundary, never under the streaming answer.
This commit is contained in:
parent
b9fcb1ba56
commit
e379958b78
1 changed files with 24 additions and 17 deletions
|
|
@ -1020,10 +1020,10 @@ const CancelledIndicator: FC = () => {
|
|||
// doesn't look frozen during the gap between "model wrote intent" and
|
||||
// "tool actually invoked", or between back-to-back tool calls.
|
||||
//
|
||||
// Scope: only fires for tool-using responses. Plain text-only answers
|
||||
// keep using GeneratingIndicator (while content is empty) and then
|
||||
// nothing -- otherwise every text streaming response would get a
|
||||
// misleading "Working..." row.
|
||||
// Scope: only fires when the tail of the message has no streaming
|
||||
// answer content yet. Once the model has emitted reasoning or non-empty
|
||||
// text after the last tool, those parts already visibly show progress
|
||||
// and the "Working..." row would be misleading.
|
||||
//
|
||||
// Return value is a string ("tool:<name>" | "working" | "") rather than
|
||||
// an object so useAuiState's referential-equality memoization holds
|
||||
|
|
@ -1033,24 +1033,31 @@ const RunningToolIndicator: FC = () => {
|
|||
const signal = useAuiState(({ message }) => {
|
||||
if (message.status?.type !== "running") return "";
|
||||
const parts = message.parts;
|
||||
let sawToolCall = false;
|
||||
let runningTool: string | null = null;
|
||||
// Single pass: find any tool-call part (signals tools-are-in-play)
|
||||
// and the most-recent running one (drives the specific indicator).
|
||||
// Walk from the tail. The first meaningful part decides what's
|
||||
// currently visible to the user: a streaming text/reasoning part
|
||||
// means the answer is already moving and the indicator must stay
|
||||
// hidden; a tool-call part means we're either mid-tool (specific
|
||||
// "Running ..." label) or in a pending gap before the next answer
|
||||
// segment ("Working...").
|
||||
for (let i = parts.length - 1; i >= 0; i -= 1) {
|
||||
const p = parts[i] as
|
||||
| { type?: string; toolName?: string; status?: { type?: string } }
|
||||
| { type?: string; text?: string; toolName?: string; status?: { type?: string } }
|
||||
| undefined;
|
||||
if (p?.type !== "tool-call") continue;
|
||||
sawToolCall = true;
|
||||
if (!runningTool && p.status?.type === "running") {
|
||||
runningTool = p.toolName ?? "tool";
|
||||
const t = p?.type;
|
||||
if (t === "text") {
|
||||
// Non-empty text after the last tool -> model is streaming the
|
||||
// final answer. Skip empty text placeholders that some adapters
|
||||
// emit before the first delta.
|
||||
if (typeof p?.text === "string" && p.text.length > 0) return "";
|
||||
continue;
|
||||
}
|
||||
if (t === "reasoning") return "";
|
||||
if (t === "tool-call") {
|
||||
if (p?.status?.type === "running") return `tool:${p.toolName ?? "tool"}`;
|
||||
return "working";
|
||||
}
|
||||
// Other part types (source attachments, etc.) -- keep walking.
|
||||
}
|
||||
if (runningTool) return `tool:${runningTool}`;
|
||||
// Only fall back to "Working..." if tools have been in play. A pure
|
||||
// text-only response (no tool-call part ever) skips the fallback.
|
||||
if (sawToolCall) return "working";
|
||||
return "";
|
||||
});
|
||||
if (!signal) return null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue