1616if TYPE_CHECKING :
1717 from .progress import ProgressReporter
1818
19+ # Track current stream task to prevent interleaving (mutable container to avoid global statement)
20+ _stream_state : dict [str , asyncio .Task | None ] = {"current_task" : None }
21+
1922
2023class StreamSimulatorConfig (BaseModel ):
2124 """Configuration for buffered stream simulation."""
@@ -37,6 +40,9 @@ def simulate_stream(
3740 Starts simulation in background and returns immediately. This is the
3841 primary interface for stream simulation throughout the codebase.
3942
43+ Cancels any in-flight stream task before starting a new one to prevent
44+ interleaved chunks from multiple generations appearing scrambled in the TUI.
45+
4046 Args:
4147 progress_reporter: ProgressReporter instance or None
4248 content: Text to simulate streaming
@@ -51,6 +57,11 @@ def simulate_stream(
5157 if not progress_reporter or not _config .enabled :
5258 return None
5359
60+ # Cancel any in-flight stream to prevent interleaving
61+ current_task = _stream_state ["current_task" ]
62+ if current_task is not None and not current_task .done ():
63+ current_task .cancel ()
64+
5465 async def _simulate_impl () -> None :
5566 """Internal implementation of chunk emission."""
5667 if not content :
@@ -59,10 +70,17 @@ async def _simulate_impl() -> None:
5970 delay = _config .chunk_delay_ms / 1000.0
6071 chunk_size = _config .chunk_size
6172
62- for i in range (0 , len (content ), chunk_size ):
63- chunk = content [i : i + chunk_size ]
64- progress_reporter .emit_chunk (source , chunk , ** metadata )
65- if delay > 0 and i + chunk_size < len (content ):
66- await asyncio .sleep (delay )
67-
68- return asyncio .create_task (_simulate_impl ())
73+ try :
74+ for i in range (0 , len (content ), chunk_size ):
75+ # Await before emitting to ensure cancellation is processed
76+ # and event loop is not blocked when delay is 0
77+ if i > 0 :
78+ await asyncio .sleep (delay )
79+ chunk = content [i : i + chunk_size ]
80+ progress_reporter .emit_chunk (source , chunk , ** metadata )
81+ except asyncio .CancelledError :
82+ # Gracefully handle cancellation
83+ pass
84+
85+ _stream_state ["current_task" ] = asyncio .create_task (_simulate_impl ())
86+ return _stream_state ["current_task" ]
0 commit comments