@@ -15,6 +15,7 @@ from textual.worker import Worker, get_current_worker |
| 15 | 15 | |
| 16 | 16 | from ..agent.loop import Agent, AgentEvent |
| 17 | 17 | from .adapter import ( |
| 18 | + ClearStream, |
| 18 | 19 | CompletionCheckPerformed, |
| 19 | 20 | ConfidenceAssessed, |
| 20 | 21 | CritiquePerformed, |
@@ -65,6 +66,7 @@ class LoaderApp(App): |
| 65 | 66 | self.adapter = EventAdapter(self) |
| 66 | 67 | self._start_time: float = 0.0 |
| 67 | 68 | self._current_streaming: StreamingText | None = None |
| 69 | + self._streamed_content: bool = False # Track if any content was streamed |
| 68 | 70 | self._tool_widget_queue: list[ToolCallWidget] = [] # Queue of pending tool widgets |
| 69 | 71 | self._timer_handle = None |
| 70 | 72 | |
@@ -243,6 +245,14 @@ class LoaderApp(App): |
| 243 | 245 | # Message handlers from adapter |
| 244 | 246 | def on_thinking_started(self, message: ThinkingStarted) -> None: |
| 245 | 247 | """Handle thinking started (may be called multiple times per task).""" |
| 248 | + # Finalize any previous streaming widget to prevent appending to old content |
| 249 | + if self._current_streaming is not None: |
| 250 | + self._current_streaming.stop_streaming() |
| 251 | + self._current_streaming = None |
| 252 | + |
| 253 | + # Reset streamed content flag for this response iteration |
| 254 | + self._streamed_content = False |
| 255 | + |
| 246 | 256 | # Status is already set in on_input_area_submitted, but ensure it stays on |
| 247 | 257 | if not self.is_generating: |
| 248 | 258 | self.is_generating = True |
@@ -262,10 +272,22 @@ class LoaderApp(App): |
| 262 | 272 | self._current_streaming.append(message.content) |
| 263 | 273 | msg_area.scroll_end(animate=False) |
| 264 | 274 | |
| 275 | + # Track that we've shown actual content |
| 276 | + if message.content.strip(): |
| 277 | + self._streamed_content = True |
| 278 | + |
| 265 | 279 | if message.is_end: |
| 266 | 280 | self._current_streaming.stop_streaming() |
| 267 | 281 | self._current_streaming = None |
| 268 | 282 | |
| 283 | + def on_clear_stream(self, message: ClearStream) -> None: |
| 284 | + """Clear/remove the current streaming content (used when raw tool calls are detected).""" |
| 285 | + if self._current_streaming is not None: |
| 286 | + # Remove the streaming widget entirely - it contained raw JSON tool calls |
| 287 | + self._current_streaming.remove() |
| 288 | + self._current_streaming = None |
| 289 | + self._streamed_content = False |
| 290 | + |
| 269 | 291 | def on_tool_call_started(self, message: ToolCallStarted) -> None: |
| 270 | 292 | """Handle tool call start.""" |
| 271 | 293 | msg_area = self.query_one("#message-area", ScrollableContainer) |
@@ -355,8 +377,10 @@ class LoaderApp(App): |
| 355 | 377 | |
| 356 | 378 | def on_response_complete(self, message: ResponseComplete) -> None: |
| 357 | 379 | """Handle response completion.""" |
| 358 | | - # Response was already streamed, nothing extra needed |
| 359 | | - pass |
| 380 | + # If no content was streamed but we have a response, display it |
| 381 | + # This handles cases like empty LLM responses with fallback messages |
| 382 | + if not self._streamed_content and message.content.strip(): |
| 383 | + self._add_message(message.content) |
| 360 | 384 | |
| 361 | 385 | def on_steering_received(self, message: SteeringReceived) -> None: |
| 362 | 386 | """Handle steering message being processed by agent.""" |