studio/frontend: return string from RunningToolIndicator selector
Returning a fresh object literal from useAuiState's selector each
render breaks assistant-ui's referential-equality memo and causes
infinite re-render loops (verified: the page crashed to React's
'Something went wrong' boundary). Return a sentinel string instead
('tool:<name>' | 'working' | '') and parse it in the render body so
selector identity stays stable.
This commit is contained in:
parent
e174126049
commit
7147f423e2
1 changed files with 17 additions and 19 deletions
|
|
@ -1019,34 +1019,32 @@ const CancelledIndicator: FC = () => {
|
||||||
// Pins activity to the bottom of the assistant bubble so the chat
|
// Pins activity to the bottom of the assistant bubble so the chat
|
||||||
// doesn't look frozen during the gap between "model wrote intent" and
|
// doesn't look frozen during the gap between "model wrote intent" and
|
||||||
// "tool actually invoked", or between back-to-back tool calls.
|
// "tool actually invoked", or between back-to-back tool calls.
|
||||||
// - When a tool-call part is currently `running`, shows the tool name.
|
//
|
||||||
// - When the message is still streaming but no tool is mid-execution
|
// Return value is a string ("tool:<name>" | "working" | "") rather than
|
||||||
// (e.g. model is generating the tool-call decision tokens, or has
|
// an object so useAuiState's referential-equality memoization holds
|
||||||
// just rendered the lead-in text "Let me build it:"), falls back to
|
// across renders -- returning a fresh object each render would invalidate
|
||||||
// a neutral "Working..." so the user sees that work is still happening.
|
// equality and force the component to rerender every frame.
|
||||||
const RunningToolIndicator: FC = () => {
|
const RunningToolIndicator: FC = () => {
|
||||||
const state = useAuiState(({ message }) => {
|
const signal = useAuiState(({ message }) => {
|
||||||
if (message.status?.type !== "running") return null;
|
if (message.status?.type !== "running") return "";
|
||||||
const parts = message.parts;
|
const parts = message.parts;
|
||||||
// Most recent running tool wins, if any.
|
|
||||||
for (let i = parts.length - 1; i >= 0; i -= 1) {
|
for (let i = parts.length - 1; i >= 0; i -= 1) {
|
||||||
const p = parts[i] as
|
const p = parts[i] as
|
||||||
| { type?: string; toolName?: string; status?: { type?: string } }
|
| { type?: string; toolName?: string; status?: { type?: string } }
|
||||||
| undefined;
|
| undefined;
|
||||||
if (p?.type === "tool-call" && p.status?.type === "running") {
|
if (p?.type === "tool-call" && p.status?.type === "running") {
|
||||||
return { kind: "tool" as const, name: p.toolName ?? "tool" };
|
return `tool:${p.toolName ?? "tool"}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// No tool currently running, but message itself is still streaming.
|
// No tool currently running, but message is still streaming.
|
||||||
// GeneratingIndicator only shows while content.length === 0, so once
|
// GeneratingIndicator hides once content.length > 0, so once any
|
||||||
// any text has rendered we lose its dots. Surface a generic "Working..."
|
// text has rendered we lose its dots. Surface a generic "Working..."
|
||||||
// so the chat doesn't go silent between phases.
|
// so the chat doesn't go silent between phases.
|
||||||
if (parts.length > 0) {
|
if (parts.length > 0) return "working";
|
||||||
return { kind: "working" as const };
|
return "";
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
});
|
||||||
if (!state) return null;
|
if (!signal) return null;
|
||||||
|
const toolName = signal.startsWith("tool:") ? signal.slice(5) : null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="running-tool-indicator"
|
data-slot="running-tool-indicator"
|
||||||
|
|
@ -1057,9 +1055,9 @@ const RunningToolIndicator: FC = () => {
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className="aui-running-tool-indicator-dot inline-block size-2 animate-pulse rounded-full bg-muted-foreground/60"
|
className="aui-running-tool-indicator-dot inline-block size-2 animate-pulse rounded-full bg-muted-foreground/60"
|
||||||
/>
|
/>
|
||||||
{state.kind === "tool" ? (
|
{toolName ? (
|
||||||
<span>
|
<span>
|
||||||
Running <span className="font-mono text-xs">{state.name}</span>...
|
Running <span className="font-mono text-xs">{toolName}</span>...
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span>Working...</span>
|
<span>Working...</span>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue