Add interactive timeout popup for slow participants

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 <noreply@anthropic.com>
This commit is contained in:
rob 2026-01-08 23:31:49 -04:00
parent c8ca9ea91d
commit 76f22cd601
1 changed files with 74 additions and 1 deletions

View File

@ -674,6 +674,11 @@ class DiscussionGUI:
self._output_lines = [] self._output_lines = []
self._diagram_textures = {} # Cache for loaded textures 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 # Read-aloud state
self._reading_session_id: Optional[str] = None self._reading_session_id: Optional[str] = None
self._reading_button_tag: 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("Waiting for responses...")
self._add_output("") 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 # Tail log file while waiting
last_pos = 0 last_pos = 0
import time
while any(p.poll() is None for p in processes.values()): 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: try:
if log_file.exists(): if log_file.exists():
with open(log_file, 'r') as f: with open(log_file, 'r') as f:
@ -2424,6 +2461,38 @@ class DiscussionGUI:
dpg.add_spacer(width=20) dpg.add_spacer(width=20)
dpg.add_button(label="OK", callback=lambda: dpg.delete_item(window_tag)) 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]: def _get_templates(self) -> list[str]:
"""Get list of available template names.""" """Get list of available template names."""
templates = [] templates = []
@ -4137,6 +4206,10 @@ final = json.dumps(parsed)''',
dpg.set_value("output_text", current + new_text + "\n") dpg.set_value("output_text", current + new_text + "\n")
self._last_output_index = len(self._output_lines) 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 # Check if turn completed and needs refresh
if getattr(self, '_turn_complete', False): if getattr(self, '_turn_complete', False):
self._turn_complete = False self._turn_complete = False