From b313751acf96fe13ebb7a42b66cb4e6f5b199e0c Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 20 Mar 2026 05:14:42 -0700 Subject: [PATCH] fix(display): show spinners and tool progress during streaming mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When streaming was enabled, two visual feedback mechanisms were completely suppressed: 1. The thinking spinner (TUI toolbar) was skipped because the entire spinner block was gated on 'not self._has_stream_consumers()'. Now the thinking_callback fires in streaming mode too — the raw KawaiiSpinner is still skipped (would conflict with streamed tokens) but the TUI toolbar widget works fine alongside streaming. 2. Tool progress lines (the ┊ feed) were invisible because _vprint was blanket-suppressed when stream consumers existed. But during tool execution, no tokens are actively streaming, so printing is safe. Added an _executing_tools flag that _vprint respects to allow output during tool execution even with stream consumers registered. --- run_agent.py | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/run_agent.py b/run_agent.py index 878188f95..7e8463aa6 100644 --- a/run_agent.py +++ b/run_agent.py @@ -518,6 +518,10 @@ class AIAgent: self.stream_delta_callback = stream_delta_callback self._last_reported_tool = None # Track for "new tool" mode + # Tool execution state — allows _vprint during tool execution + # even when stream consumers are registered (no tokens streaming then) + self._executing_tools = False + # Interrupt mechanism for breaking out of tool loops self._interrupt_requested = False self._interrupt_message = None # Optional message that triggered interrupt @@ -1031,12 +1035,16 @@ class AIAgent: pass def _vprint(self, *args, force: bool = False, **kwargs): - """Verbose print — suppressed when streaming TTS is active. + """Verbose print — suppressed when actively streaming tokens. Pass ``force=True`` for error/warning messages that should always be shown even during streaming playback (TTS or display). + + During tool execution (``_executing_tools`` is True), printing is + allowed even with stream consumers registered because no tokens + are being streamed at that point. """ - if not force and self._has_stream_consumers(): + if not force and self._has_stream_consumers() and not self._executing_tools: return self._safe_print(*args, **kwargs) @@ -4311,14 +4319,19 @@ class AIAgent: """ tool_calls = assistant_message.tool_calls - if not _should_parallelize_tool_batch(tool_calls): - return self._execute_tool_calls_sequential( + # Allow _vprint during tool execution even with stream consumers + self._executing_tools = True + try: + if not _should_parallelize_tool_batch(tool_calls): + return self._execute_tool_calls_sequential( + assistant_message, messages, effective_task_id, api_call_count + ) + + return self._execute_tool_calls_concurrent( assistant_message, messages, effective_task_id, api_call_count ) - - return self._execute_tool_calls_concurrent( - assistant_message, messages, effective_task_id, api_call_count - ) + finally: + self._executing_tools = False def _invoke_tool(self, function_name: str, function_args: dict, effective_task_id: str) -> str: """Invoke a single tool and return the result string. No display logic. @@ -5375,14 +5388,17 @@ class AIAgent: self._vprint(f"\n{self.log_prefix}🔄 Making API call #{api_call_count}/{self.max_iterations}...") self._vprint(f"{self.log_prefix} 📊 Request size: {len(api_messages)} messages, ~{approx_tokens:,} tokens (~{total_chars:,} chars)") self._vprint(f"{self.log_prefix} 🔧 Available tools: {len(self.tools) if self.tools else 0}") - elif not self._has_stream_consumers(): - # Animated thinking spinner in quiet mode (skip during streaming) + else: + # Animated thinking spinner in quiet mode face = random.choice(KawaiiSpinner.KAWAII_THINKING) verb = random.choice(KawaiiSpinner.THINKING_VERBS) if self.thinking_callback: # CLI TUI mode: use prompt_toolkit widget instead of raw spinner + # (works in both streaming and non-streaming modes) self.thinking_callback(f"{face} {verb}...") - else: + elif not self._has_stream_consumers(): + # Raw KawaiiSpinner only when no streaming consumers + # (would conflict with streamed token output) spinner_type = random.choice(['brain', 'sparkle', 'pulse', 'moon', 'star']) thinking_spinner = KawaiiSpinner(f"{face} {verb}...", spinner_type=spinner_type) thinking_spinner.start()