@@ -262,9 +262,74 @@ def _conversation_text(self) -> str:
262262 # ------------------------------------------------------------------
263263 # direct commands (no LLM agent loop)
264264 # ------------------------------------------------------------------
265+ def _run_with_status (
266+ self ,
267+ worker : Callable [[], None ],
268+ * ,
269+ status_text : str ,
270+ cancel_message : str ,
271+ ) -> None :
272+ """Run *worker* in a background thread with status bar updates.
273+
274+ Sets the status bar to *status_text*, renders the spinner while
275+ the worker is in flight, and handles KeyboardInterrupt with
276+ *cancel_message*. Used by /compact and /summary to avoid
277+ duplicating the thread + Live boilerplate.
278+ """
279+ self .status = status_text
280+ self ._current_tool = ""
281+ self .agent_running = True
282+ self ._data_event .clear ()
283+
284+ thread = threading .Thread (target = worker , daemon = True )
285+ thread .start ()
286+ try :
287+ if self .console .is_dumb_terminal :
288+ while thread .is_alive ():
289+ self ._data_event .wait (timeout = 0.1 )
290+ self ._data_event .clear ()
291+ self .console .print (self ._status_bar ())
292+ self ._flush ()
293+ else :
294+ with Live (
295+ self ._status_bar (),
296+ console = self .console ,
297+ refresh_per_second = 30 ,
298+ screen = False ,
299+ ) as live :
300+ while thread .is_alive ():
301+ self ._data_event .wait (timeout = 0.1 )
302+ self ._data_event .clear ()
303+ live .update (self ._status_bar ())
304+ self ._flush ()
305+ except KeyboardInterrupt :
306+ self .console .print (f"\n [dim]{ cancel_message } [/dim]" )
307+ self ._flush ()
308+ finally :
309+ self .agent_running = False
310+ self .status = ""
311+ self ._data_event .set ()
312+
265313 def _run_compact (self ) -> None :
266314 """Compact the current conversation directly."""
267- ok , msg = self .session .compact_conversation ()
315+ result : dict [str , Any ] = {}
316+
317+ def worker () -> None :
318+ try :
319+ ok , msg = self .session .compact_conversation ()
320+ result ["ok" ] = ok
321+ result ["msg" ] = msg
322+ except Exception as e : # noqa: BLE001 - surfaced to the user
323+ result ["ok" ] = False
324+ result ["msg" ] = f"Compaction failed: { e } "
325+
326+ self ._run_with_status (
327+ worker ,
328+ status_text = " ⏳ compacting" ,
329+ cancel_message = "compact cancelled — the result may still be applied" ,
330+ )
331+ ok = result .get ("ok" , False )
332+ msg = result .get ("msg" , "Compaction failed: unknown error." )
268333 if ok :
269334 # The shared conversation was replaced: sync the TUI's own
270335 # history too, or the next run would restart from the old
@@ -394,17 +459,7 @@ def _run_model_command(self, arg: str) -> None:
394459 self ._model_switch_by_name (arg )
395460
396461 def _run_summary (self ) -> None :
397- """Append a summary of the conversation (tools disabled).
398-
399- The summary request is synchronous (non-streaming), so it runs
400- in a worker thread like the agent loop: the main thread keeps
401- rendering the status bar / spinner while the request is in
402- flight, and the result is printed when it lands.
403- """
404- self .status = " ⏳ summarizing"
405- self ._current_tool = ""
406- self .agent_running = True
407- self ._data_event .clear ()
462+ """Append a summary of the conversation (tools disabled)."""
408463 result : dict [str , str ] = {}
409464
410465 def worker () -> None :
@@ -413,34 +468,11 @@ def worker() -> None:
413468 except Exception as e : # noqa: BLE001 - surfaced to the user
414469 result ["msg" ] = f"Summary failed: { e } "
415470
416- thread = threading .Thread (target = worker , daemon = True )
417- thread .start ()
418- try :
419- if self .console .is_dumb_terminal :
420- while thread .is_alive ():
421- self ._data_event .wait (timeout = 0.1 )
422- self ._data_event .clear ()
423- self .console .print (self ._status_bar ())
424- self ._flush ()
425- else :
426- with Live (
427- self ._status_bar (),
428- console = self .console ,
429- refresh_per_second = 30 ,
430- screen = False ,
431- ) as live :
432- while thread .is_alive ():
433- self ._data_event .wait (timeout = 0.1 )
434- self ._data_event .clear ()
435- live .update (self ._status_bar ())
436- self ._flush ()
437- except KeyboardInterrupt :
438- self .console .print ("\n [dim]summary cancelled — the result may still be appended[/dim]" )
439- self ._flush ()
440- finally :
441- self .agent_running = False
442- self .status = ""
443- self ._data_event .set ()
471+ self ._run_with_status (
472+ worker ,
473+ status_text = " ⏳ summarizing" ,
474+ cancel_message = "summary cancelled — the result may still be appended" ,
475+ )
444476 msg = result .get ("msg" , "Summary failed: unknown error." )
445477 self .conversation_history = list (self .session .last_messages )
446478 self ._history_dirty = True
0 commit comments