From d63cc57e1e2dcc2a8330118344b0ad6b1e4bdc39 Mon Sep 17 00:00:00 2001 From: Wasim Yousef Said Date: Wed, 1 Apr 2026 09:28:38 +0200 Subject: [PATCH] fix: clear tool status badge immediately after tool execution (#4733) * fix: clear tool status badge immediately after tool execution The tool status timer badge (Searching 1s, 2s...) persisted after tool calls finished because the status clear event was only sent at the start of the next generation iteration, not after tool execution completed. Backend: yield status clear after all tools finish in the agentic loop iteration, before continue starts the next generation pass. Frontend: debounce badge visibility by 300ms so sub-second tool calls dont flash the badge. * Fix debounce regression for consecutive tool calls Only apply the 300ms show-delay when transitioning from idle to tool-active. When switching between consecutive tools in the same turn (e.g. web_search -> python), keep the badge visible immediately so it does not flicker or disappear during multi-tool runs. * Delay wasActiveRef reset to bridge inter-iteration tool gaps The backend emits a status-clear event between tool iterations, which was resetting wasActiveRef immediately and causing the next tool to be re-debounced (300ms hidden gap between consecutive tools in the same turn). Now the ref reset is delayed by 500ms so a follow-up tool within the same agentic turn shows the badge immediately, while a genuinely new turn still gets the debounce. * Use thread lifecycle to track tool-run boundaries Replace the 500ms wall-clock timeout with the actual thread.isRunning state to determine when wasActiveRef should reset. This properly handles all cases: - Consecutive tools within the same run stay visible without flicker - The badge hides only when the thread run actually ends - New turns always get a fresh 300ms debounce on the first tool - No heuristic timeout that can misfire on slow or fast inference * Consolidate wasActiveRef reset into single effect Removes the separate isThreadRunning effect to avoid a race where the ref resets before the tool-status effect reads it (when isThreadRunning flips to false before setToolStatus(null) from the adapter's finally block). Now wasActiveRef resets only when both toolStatus is null AND the thread run has ended, eliminating any flicker on the last tool of a run. * Simplify debounce: use visible state instead of ref tracking Drop wasActiveRef entirely and use the visible state as the debounce gate. When the badge is not yet on screen, debounce for 300ms before showing. When already visible from a prior tool, keep showing immediately. This correctly handles all cases: - All fast tools (<300ms) are suppressed, not just the first - Consecutive tools after the badge is shown stay visible - Badge persists across inter-iteration clears while thread runs - New turns get a fresh debounce after visible resets --------- Co-authored-by: Daniel Han --- studio/backend/core/inference/llama_cpp.py | 2 ++ .../src/components/assistant-ui/thread.tsx | 25 ++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index 894dd25cf7..eb3776e603 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -2740,6 +2740,8 @@ class LlamaCppBackend: tool_msg["tool_call_id"] = tool_call_id conversation.append(tool_msg) + # Clear tool status badge before next generation iteration + yield {"type": "status", "text": ""} # Continue the loop to let model respond with context continue diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx index d688822815..aae027c74b 100644 --- a/studio/frontend/src/components/assistant-ui/thread.tsx +++ b/studio/frontend/src/components/assistant-ui/thread.tsx @@ -437,21 +437,40 @@ const CodeToolsToggle: FC = () => { const ToolStatusDisplay: FC = () => { const toolStatus = useChatRuntimeStore((s) => s.toolStatus); + const isThreadRunning = useAuiState(({ thread }) => thread.isRunning); const [elapsed, setElapsed] = useState(0); + const [visible, setVisible] = useState(false); useEffect(() => { if (!toolStatus) { setElapsed(0); + if (!isThreadRunning) { + setVisible(false); + } return; } + setElapsed(0); + + // Debounce badge visibility by 300ms when the badge is not + // already on screen. Once visible from a prior tool, consecutive + // tools show immediately so the badge does not flicker. Fast + // tool calls that all complete under 300ms never show the badge. + let showTimer: ReturnType | undefined; + if (!visible) { + showTimer = setTimeout(() => setVisible(true), 300); + } + const interval = setInterval(() => { setElapsed((prev) => prev + 1); }, 1000); - return () => clearInterval(interval); - }, [toolStatus]); + return () => { + clearInterval(interval); + if (showTimer) clearTimeout(showTimer); + }; + }, [toolStatus, isThreadRunning]); - if (!toolStatus) return null; + if (!toolStatus || !visible) return null; const isRunning = toolStatus.startsWith("Running"); const StatusIcon = isRunning ? TerminalIcon : GlobeIcon; return (