From 76f22cd601c8a11a8f471b2f3047603cf7f487cb Mon Sep 17 00:00:00 2001 From: rob Date: Thu, 8 Jan 2026 23:31:49 -0400 Subject: [PATCH] Add interactive timeout popup for slow participants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a participant takes longer than 60 seconds during a discussion turn, the GUI now shows a modal popup asking the user whether to: - Wait 60 more seconds (extends the timeout) - Abort (kills the participant process) This provides better user control over slow AI providers instead of just timing out silently. Implementation: - Track start times and elapsed time per participant in _run_turn_thread - Signal main thread via _slow_participant when timeout exceeded - _poll_background_tasks checks for signal and shows popup - _show_timeout_popup creates modal with Wait/Abort buttons - Callbacks set _timeout_response to communicate back to worker thread 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/discussions/ui/gui.py | 75 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/src/discussions/ui/gui.py b/src/discussions/ui/gui.py index 5ed17ec..39a6bfc 100644 --- a/src/discussions/ui/gui.py +++ b/src/discussions/ui/gui.py @@ -674,6 +674,11 @@ class DiscussionGUI: self._output_lines = [] self._diagram_textures = {} # Cache for loaded textures + # Slow participant timeout tracking + self._slow_participant = None # Alias of slow participant + self._slow_participant_elapsed = 0 # How long they've been running + self._timeout_response = None # User's response: "wait" or "abort" + # Read-aloud state self._reading_session_id: Optional[str] = None self._reading_button_tag: Optional[str] = None @@ -1889,10 +1894,42 @@ class DiscussionGUI: self._add_output("Waiting for responses...") self._add_output("") + # Track start times and timeout state + import time + start_times = {alias: time.time() for alias in processes} + timeout_threshold = 60 # Initial timeout in seconds + warned_participants = set() # Track who we've warned about + # Tail log file while waiting last_pos = 0 - import time while any(p.poll() is None for p in processes.values()): + current_time = time.time() + + # Check for slow participants + for alias, process in processes.items(): + if process.poll() is None and alias not in warned_participants: + elapsed = current_time - start_times[alias] + if elapsed > timeout_threshold: + # Signal main thread to show timeout dialog + self._slow_participant = alias + self._slow_participant_elapsed = int(elapsed) + self._timeout_response = None + + # Wait for user response (main thread will set this) + while self._timeout_response is None and process.poll() is None: + time.sleep(0.1) + + if self._timeout_response == "abort": + self._add_output(f" Aborting {alias} (user requested)") + process.kill() + warned_participants.add(alias) + elif self._timeout_response == "wait": + # Give another 60 seconds + start_times[alias] = current_time + self._add_output(f" Extending timeout for {alias}") + + self._slow_participant = None + try: if log_file.exists(): with open(log_file, 'r') as f: @@ -2424,6 +2461,38 @@ class DiscussionGUI: dpg.add_spacer(width=20) dpg.add_button(label="OK", callback=lambda: dpg.delete_item(window_tag)) + def _show_timeout_popup(self): + """Show popup when a participant is taking too long.""" + window_tag = "timeout_popup" + if dpg.does_item_exist(window_tag): + return # Already showing + + alias = self._slow_participant + elapsed = self._slow_participant_elapsed + + def on_wait(): + """User chose to wait longer.""" + self._timeout_response = "wait" + dpg.delete_item(window_tag) + + def on_abort(): + """User chose to abort this participant.""" + self._timeout_response = "abort" + dpg.delete_item(window_tag) + + with dpg.window(label="Slow Participant", tag=window_tag, modal=True, + width=450, height=160, pos=[475, 350], no_collapse=True): + dpg.add_text(f"Participant '{alias}' is taking longer than expected.", + color=(255, 200, 100)) + dpg.add_text(f"Running for {elapsed} seconds...") + dpg.add_spacer(height=15) + dpg.add_text("Would you like to wait longer or abort this participant?") + dpg.add_spacer(height=15) + with dpg.group(horizontal=True): + dpg.add_button(label="Wait 60 more seconds", callback=on_wait, width=180) + dpg.add_spacer(width=20) + dpg.add_button(label="Abort", callback=on_abort, width=100) + def _get_templates(self) -> list[str]: """Get list of available template names.""" templates = [] @@ -4137,6 +4206,10 @@ final = json.dumps(parsed)''', dpg.set_value("output_text", current + new_text + "\n") self._last_output_index = len(self._output_lines) + # Check for slow participant timeout popup + if self._slow_participant and not dpg.does_item_exist("timeout_popup"): + self._show_timeout_popup() + # Check if turn completed and needs refresh if getattr(self, '_turn_complete', False): self._turn_complete = False