tenseleyflow/loader / 61dd56d

Browse files

fix: UI streaming improvements for multi-turn conversations

- Add ClearStream message to adapter for hiding ugly raw tool call JSON
- Add on_clear_stream handler to remove streaming widget when raw JSON detected
- Add _streamed_content flag to track if content was shown
- Finalize previous streaming widget in on_thinking_started to prevent appending
- Show fallback content in on_response_complete when nothing was streamed
Authored by espadonne
SHA
61dd56d4cbc1a2c152dac132947c5506f3183d56
Parents
3a46b3d
Tree
ac2c4a2

2 changed files

StatusFile+-
M src/loader/ui/adapter.py 10 0
M src/loader/ui/app.py 26 2
src/loader/ui/adapter.pymodified
@@ -109,6 +109,13 @@ class SteeringReceived(Message):
109109
     content: str
110110
 
111111
 
112
+@dataclass
113
+class ClearStream(Message):
114
+    """Clear the current streaming content (used when raw tool calls are detected)."""
115
+
116
+    pass
117
+
118
+
112119
 @dataclass
113120
 class DecompositionCreated(Message):
114121
     """Task was decomposed into subtasks."""
@@ -210,6 +217,9 @@ class EventAdapter:
210217
                     StreamChunk(content=event.content, is_end=event.is_stream_end)
211218
                 )
212219
 
220
+            case "clear_stream":
221
+                self.app.post_message(ClearStream())
222
+
213223
             case "plan":
214224
                 self.app.post_message(PlanCreated(content=event.content))
215225
 
src/loader/ui/app.pymodified
@@ -15,6 +15,7 @@ from textual.worker import Worker, get_current_worker
1515
 
1616
 from ..agent.loop import Agent, AgentEvent
1717
 from .adapter import (
18
+    ClearStream,
1819
     CompletionCheckPerformed,
1920
     ConfidenceAssessed,
2021
     CritiquePerformed,
@@ -65,6 +66,7 @@ class LoaderApp(App):
6566
         self.adapter = EventAdapter(self)
6667
         self._start_time: float = 0.0
6768
         self._current_streaming: StreamingText | None = None
69
+        self._streamed_content: bool = False  # Track if any content was streamed
6870
         self._tool_widget_queue: list[ToolCallWidget] = []  # Queue of pending tool widgets
6971
         self._timer_handle = None
7072
 
@@ -243,6 +245,14 @@ class LoaderApp(App):
243245
     # Message handlers from adapter
244246
     def on_thinking_started(self, message: ThinkingStarted) -> None:
245247
         """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
+
246256
         # Status is already set in on_input_area_submitted, but ensure it stays on
247257
         if not self.is_generating:
248258
             self.is_generating = True
@@ -262,10 +272,22 @@ class LoaderApp(App):
262272
         self._current_streaming.append(message.content)
263273
         msg_area.scroll_end(animate=False)
264274
 
275
+        # Track that we've shown actual content
276
+        if message.content.strip():
277
+            self._streamed_content = True
278
+
265279
         if message.is_end:
266280
             self._current_streaming.stop_streaming()
267281
             self._current_streaming = None
268282
 
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
+
269291
     def on_tool_call_started(self, message: ToolCallStarted) -> None:
270292
         """Handle tool call start."""
271293
         msg_area = self.query_one("#message-area", ScrollableContainer)
@@ -355,8 +377,10 @@ class LoaderApp(App):
355377
 
356378
     def on_response_complete(self, message: ResponseComplete) -> None:
357379
         """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)
360384
 
361385
     def on_steering_received(self, message: SteeringReceived) -> None:
362386
         """Handle steering message being processed by agent."""