massgen 0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of massgen might be problematic. Click here for more details.

Files changed (76) hide show
  1. massgen/__init__.py +94 -0
  2. massgen/agent_config.py +507 -0
  3. massgen/backend/CLAUDE_API_RESEARCH.md +266 -0
  4. massgen/backend/Function calling openai responses.md +1161 -0
  5. massgen/backend/GEMINI_API_DOCUMENTATION.md +410 -0
  6. massgen/backend/OPENAI_RESPONSES_API_FORMAT.md +65 -0
  7. massgen/backend/__init__.py +25 -0
  8. massgen/backend/base.py +180 -0
  9. massgen/backend/chat_completions.py +228 -0
  10. massgen/backend/claude.py +661 -0
  11. massgen/backend/gemini.py +652 -0
  12. massgen/backend/grok.py +187 -0
  13. massgen/backend/response.py +397 -0
  14. massgen/chat_agent.py +440 -0
  15. massgen/cli.py +686 -0
  16. massgen/configs/README.md +293 -0
  17. massgen/configs/creative_team.yaml +53 -0
  18. massgen/configs/gemini_4o_claude.yaml +31 -0
  19. massgen/configs/news_analysis.yaml +51 -0
  20. massgen/configs/research_team.yaml +51 -0
  21. massgen/configs/single_agent.yaml +18 -0
  22. massgen/configs/single_flash2.5.yaml +44 -0
  23. massgen/configs/technical_analysis.yaml +51 -0
  24. massgen/configs/three_agents_default.yaml +31 -0
  25. massgen/configs/travel_planning.yaml +51 -0
  26. massgen/configs/two_agents.yaml +39 -0
  27. massgen/frontend/__init__.py +20 -0
  28. massgen/frontend/coordination_ui.py +945 -0
  29. massgen/frontend/displays/__init__.py +24 -0
  30. massgen/frontend/displays/base_display.py +83 -0
  31. massgen/frontend/displays/rich_terminal_display.py +3497 -0
  32. massgen/frontend/displays/simple_display.py +93 -0
  33. massgen/frontend/displays/terminal_display.py +381 -0
  34. massgen/frontend/logging/__init__.py +9 -0
  35. massgen/frontend/logging/realtime_logger.py +197 -0
  36. massgen/message_templates.py +431 -0
  37. massgen/orchestrator.py +1222 -0
  38. massgen/tests/__init__.py +10 -0
  39. massgen/tests/multi_turn_conversation_design.md +214 -0
  40. massgen/tests/multiturn_llm_input_analysis.md +189 -0
  41. massgen/tests/test_case_studies.md +113 -0
  42. massgen/tests/test_claude_backend.py +310 -0
  43. massgen/tests/test_grok_backend.py +160 -0
  44. massgen/tests/test_message_context_building.py +293 -0
  45. massgen/tests/test_rich_terminal_display.py +378 -0
  46. massgen/tests/test_v3_3agents.py +117 -0
  47. massgen/tests/test_v3_simple.py +216 -0
  48. massgen/tests/test_v3_three_agents.py +272 -0
  49. massgen/tests/test_v3_two_agents.py +176 -0
  50. massgen/utils.py +79 -0
  51. massgen/v1/README.md +330 -0
  52. massgen/v1/__init__.py +91 -0
  53. massgen/v1/agent.py +605 -0
  54. massgen/v1/agents.py +330 -0
  55. massgen/v1/backends/gemini.py +584 -0
  56. massgen/v1/backends/grok.py +410 -0
  57. massgen/v1/backends/oai.py +571 -0
  58. massgen/v1/cli.py +351 -0
  59. massgen/v1/config.py +169 -0
  60. massgen/v1/examples/fast-4o-mini-config.yaml +44 -0
  61. massgen/v1/examples/fast_config.yaml +44 -0
  62. massgen/v1/examples/production.yaml +70 -0
  63. massgen/v1/examples/single_agent.yaml +39 -0
  64. massgen/v1/logging.py +974 -0
  65. massgen/v1/main.py +368 -0
  66. massgen/v1/orchestrator.py +1138 -0
  67. massgen/v1/streaming_display.py +1190 -0
  68. massgen/v1/tools.py +160 -0
  69. massgen/v1/types.py +245 -0
  70. massgen/v1/utils.py +199 -0
  71. massgen-0.0.3.dist-info/METADATA +568 -0
  72. massgen-0.0.3.dist-info/RECORD +76 -0
  73. massgen-0.0.3.dist-info/WHEEL +5 -0
  74. massgen-0.0.3.dist-info/entry_points.txt +2 -0
  75. massgen-0.0.3.dist-info/licenses/LICENSE +204 -0
  76. massgen-0.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3497 @@
1
+ """
2
+ Rich Terminal Display for MassGen Coordination
3
+
4
+ Enhanced terminal interface using Rich library with live updates,
5
+ beautiful formatting, code highlighting, and responsive layout.
6
+ """
7
+
8
+ import re
9
+ import time
10
+ import threading
11
+ import asyncio
12
+ import os
13
+ import sys
14
+ import select
15
+ import tty
16
+ import termios
17
+ import subprocess
18
+ import signal
19
+ from pathlib import Path
20
+ from concurrent.futures import ThreadPoolExecutor
21
+ from typing import List, Optional, Dict, Any
22
+ from .terminal_display import TerminalDisplay
23
+
24
+ try:
25
+ from rich.console import Console
26
+ from rich.live import Live
27
+ from rich.panel import Panel
28
+ from rich.columns import Columns
29
+ from rich.table import Table
30
+ from rich.syntax import Syntax
31
+ from rich.text import Text
32
+ from rich.layout import Layout
33
+ from rich.align import Align
34
+ from rich.progress import Progress, SpinnerColumn, TextColumn
35
+ from rich.status import Status
36
+ from rich.box import ROUNDED, HEAVY, DOUBLE
37
+
38
+ RICH_AVAILABLE = True
39
+ except ImportError:
40
+ RICH_AVAILABLE = False
41
+
42
+ # Provide dummy classes when Rich is not available
43
+ class Layout:
44
+ pass
45
+
46
+ class Panel:
47
+ pass
48
+
49
+ class Console:
50
+ pass
51
+
52
+ class Live:
53
+ pass
54
+
55
+ class Columns:
56
+ pass
57
+
58
+ class Table:
59
+ pass
60
+
61
+ class Syntax:
62
+ pass
63
+
64
+ class Text:
65
+ pass
66
+
67
+ class Align:
68
+ pass
69
+
70
+ class Progress:
71
+ pass
72
+
73
+ class SpinnerColumn:
74
+ pass
75
+
76
+ class TextColumn:
77
+ pass
78
+
79
+ class Status:
80
+ pass
81
+
82
+ ROUNDED = HEAVY = DOUBLE = None
83
+
84
+
85
+ class RichTerminalDisplay(TerminalDisplay):
86
+ """Enhanced terminal display using Rich library for beautiful formatting."""
87
+
88
+ def __init__(self, agent_ids: List[str], **kwargs):
89
+ """Initialize rich terminal display.
90
+
91
+ Args:
92
+ agent_ids: List of agent IDs to display
93
+ **kwargs: Additional configuration options
94
+ - theme: Color theme ('dark', 'light', 'cyberpunk') (default: 'dark')
95
+ - refresh_rate: Display refresh rate in Hz (default: 4)
96
+ - enable_syntax_highlighting: Enable code syntax highlighting (default: True)
97
+ - max_content_lines: Base lines per agent column before scrolling (default: 8)
98
+ - show_timestamps: Show timestamps for events (default: True)
99
+ - enable_status_jump: Enable jumping to latest status when agent status changes (default: True)
100
+ - truncate_web_search_on_status_change: Truncate web search content when status changes (default: True)
101
+ - max_web_search_lines_on_status_change: Max web search lines to keep on status changes (default: 3)
102
+ - enable_flush_output: Enable flush output for final answer display (default: True)
103
+ - flush_char_delay: Delay between characters in flush output (default: 0.03)
104
+ - flush_word_delay: Extra delay after punctuation in flush output (default: 0.08)
105
+ """
106
+ if not RICH_AVAILABLE:
107
+ raise ImportError(
108
+ "Rich library is required for RichTerminalDisplay. "
109
+ "Install with: pip install rich"
110
+ )
111
+
112
+ super().__init__(agent_ids, **kwargs)
113
+
114
+ # Terminal performance detection and adaptive refresh rate
115
+ self._terminal_performance = self._detect_terminal_performance()
116
+ self.refresh_rate = self._get_adaptive_refresh_rate(kwargs.get("refresh_rate"))
117
+
118
+ # Rich-specific configuration
119
+ self.theme = kwargs.get("theme", "dark")
120
+ self.enable_syntax_highlighting = kwargs.get("enable_syntax_highlighting", True)
121
+ self.max_content_lines = kwargs.get("max_content_lines", 8)
122
+ self.max_line_length = kwargs.get("max_line_length", 100)
123
+ self.show_timestamps = kwargs.get("show_timestamps", True)
124
+
125
+ # Initialize Rich console and detect terminal dimensions
126
+ self.console = Console(force_terminal=True, legacy_windows=False)
127
+ self.terminal_size = self.console.size
128
+ # Dynamic column width calculation - will be updated on resize
129
+ self.num_agents = len(agent_ids)
130
+ self.fixed_column_width = max(
131
+ 20, self.terminal_size.width // self.num_agents - 1
132
+ )
133
+ self.agent_panel_height = max(
134
+ 10, self.terminal_size.height - 13
135
+ ) # Reserve space for header(5) + footer(8)
136
+
137
+ self.orchestrator = kwargs.get("orchestrator", None)
138
+
139
+ # Terminal resize handling
140
+ self._resize_lock = threading.Lock()
141
+ self._setup_resize_handler()
142
+
143
+ self.live = None
144
+ self._lock = threading.RLock()
145
+ # Adaptive refresh intervals based on terminal performance
146
+ self._last_update = 0
147
+ self._update_interval = self._get_adaptive_update_interval()
148
+ self._last_full_refresh = 0
149
+ self._full_refresh_interval = self._get_adaptive_full_refresh_interval()
150
+
151
+ # Performance monitoring
152
+ self._refresh_times = []
153
+ self._dropped_frames = 0
154
+ self._performance_check_interval = 5.0 # Check performance every 5 seconds
155
+
156
+ # Async refresh components - more workers for faster updates
157
+ self._refresh_executor = ThreadPoolExecutor(
158
+ max_workers=min(len(agent_ids) * 2 + 8, 20)
159
+ )
160
+ self._agent_panels_cache = {}
161
+ self._header_cache = None
162
+ self._footer_cache = None
163
+ self._layout_update_lock = threading.Lock()
164
+ self._pending_updates = set()
165
+ self._shutdown_flag = False
166
+
167
+ # Priority update queue for critical status changes
168
+ self._priority_updates = set()
169
+ self._status_update_executor = ThreadPoolExecutor(max_workers=4)
170
+
171
+ # Theme configuration
172
+ self._setup_theme()
173
+
174
+ # Interactive mode variables
175
+ self._keyboard_interactive_mode = kwargs.get("keyboard_interactive_mode", True)
176
+ self._safe_keyboard_mode = kwargs.get(
177
+ "safe_keyboard_mode", False
178
+ ) # Non-interfering keyboard mode
179
+ self._key_handler = None
180
+ self._input_thread = None
181
+ self._stop_input_thread = False
182
+ self._original_settings = None
183
+ self._agent_selector_active = (
184
+ False # Flag to prevent duplicate agent selector calls
185
+ )
186
+
187
+ # Store final presentation for re-display
188
+ self._stored_final_presentation = None
189
+ self._stored_presentation_agent = None
190
+ self._stored_vote_results = None
191
+
192
+ # Code detection patterns
193
+ self.code_patterns = [
194
+ r"```(\w+)?\n(.*?)\n```", # Markdown code blocks
195
+ r"`([^`]+)`", # Inline code
196
+ r"def\s+\w+\s*\(", # Python functions
197
+ r"class\s+\w+\s*[:(\s]", # Python classes
198
+ r"import\s+\w+", # Python imports
199
+ r"from\s+\w+\s+import", # Python from imports
200
+ ]
201
+
202
+ # Progress tracking
203
+ self.agent_progress = {agent_id: 0 for agent_id in agent_ids}
204
+ self.agent_activity = {agent_id: "waiting" for agent_id in agent_ids}
205
+
206
+ # Status change tracking to prevent unnecessary refreshes
207
+ self._last_agent_status = {agent_id: "waiting" for agent_id in agent_ids}
208
+ self._last_agent_activity = {agent_id: "waiting" for agent_id in agent_ids}
209
+ self._last_content_hash = {agent_id: "" for agent_id in agent_ids}
210
+
211
+ # Adaptive debounce mechanism for updates
212
+ self._debounce_timers = {}
213
+ self._debounce_delay = self._get_adaptive_debounce_delay()
214
+
215
+ # Layered refresh strategy
216
+ self._critical_updates = set() # Status changes, errors, tool results
217
+ self._normal_updates = set() # Text content, thinking updates
218
+ self._decorative_updates = set() # Progress bars, timestamps
219
+
220
+ # Message filtering settings - tool content always important
221
+ self._important_content_types = {"presentation", "status", "tool", "error"}
222
+ self._status_change_keywords = {
223
+ "completed",
224
+ "failed",
225
+ "waiting",
226
+ "error",
227
+ "voted",
228
+ "voting",
229
+ "tool",
230
+ "vote recorded",
231
+ }
232
+ self._important_event_keywords = {
233
+ "completed",
234
+ "failed",
235
+ "voting",
236
+ "voted",
237
+ "final",
238
+ "error",
239
+ "started",
240
+ "coordination",
241
+ "tool",
242
+ "vote recorded",
243
+ }
244
+
245
+ # Status jump mechanism for web search interruption
246
+ self._status_jump_enabled = kwargs.get(
247
+ "enable_status_jump", True
248
+ ) # Enable jumping to latest status
249
+ self._web_search_truncate_on_status_change = kwargs.get(
250
+ "truncate_web_search_on_status_change", True
251
+ ) # Truncate web search content on status changes
252
+ self._max_web_search_lines = kwargs.get(
253
+ "max_web_search_lines_on_status_change", 3
254
+ ) # Maximum lines to keep from web search when status changes
255
+
256
+ # Flush output configuration for final answer display
257
+ self._enable_flush_output = kwargs.get(
258
+ "enable_flush_output", True
259
+ ) # Enable flush output for final answer
260
+ self._flush_char_delay = kwargs.get(
261
+ "flush_char_delay", 0.03
262
+ ) # Delay between characters
263
+ self._flush_word_delay = kwargs.get(
264
+ "flush_word_delay", 0.08
265
+ ) # Extra delay after punctuation
266
+
267
+ # File-based output system
268
+ self.output_dir = kwargs.get("output_dir", "agent_outputs")
269
+ self.agent_files = {}
270
+ self.system_status_file = None
271
+ self._selected_agent = None
272
+ self._setup_agent_files()
273
+
274
+ # Adaptive text buffering system to accumulate chunks
275
+ self._text_buffers = {agent_id: "" for agent_id in agent_ids}
276
+ self._max_buffer_length = self._get_adaptive_buffer_length()
277
+ self._buffer_timeout = self._get_adaptive_buffer_timeout()
278
+ self._buffer_timers = {agent_id: None for agent_id in agent_ids}
279
+
280
+ # Adaptive batching for updates
281
+ self._update_batch = set()
282
+ self._batch_timer = None
283
+ self._batch_timeout = self._get_adaptive_batch_timeout()
284
+
285
+ def _setup_resize_handler(self):
286
+ """Setup SIGWINCH signal handler for terminal resize detection."""
287
+ if not sys.stdin.isatty():
288
+ return # Skip if not running in a terminal
289
+
290
+ try:
291
+ # Set up signal handler for SIGWINCH (window change)
292
+ signal.signal(signal.SIGWINCH, self._handle_resize_signal)
293
+ except (AttributeError, OSError):
294
+ # SIGWINCH might not be available on all platforms
295
+ pass
296
+
297
+ def _handle_resize_signal(self, signum, frame):
298
+ """Handle SIGWINCH signal when terminal is resized."""
299
+ # Use a separate thread to handle resize to avoid signal handler restrictions
300
+ threading.Thread(target=self._handle_terminal_resize, daemon=True).start()
301
+
302
+ def _handle_terminal_resize(self):
303
+ """Handle terminal resize by recalculating layout and refreshing display."""
304
+ with self._resize_lock:
305
+ try:
306
+ # VSCode-specific resize stabilization
307
+ if self._terminal_performance["type"] == "vscode":
308
+ # VSCode terminal sometimes sends multiple resize events
309
+ # Add delay to let resize settle
310
+ time.sleep(0.05)
311
+
312
+ # Get new terminal size
313
+ new_size = self.console.size
314
+
315
+ # Check if size actually changed
316
+ if (
317
+ new_size.width != self.terminal_size.width
318
+ or new_size.height != self.terminal_size.height
319
+ ):
320
+
321
+ # Update stored terminal size
322
+ old_size = self.terminal_size
323
+ self.terminal_size = new_size
324
+
325
+ # VSCode-specific post-resize delay
326
+ if self._terminal_performance["type"] == "vscode":
327
+ # Give VSCode terminal extra time to stabilize after resize
328
+ time.sleep(0.02)
329
+
330
+ # Recalculate layout dimensions
331
+ self._recalculate_layout()
332
+
333
+ # Clear all caches to force refresh
334
+ self._invalidate_display_cache()
335
+
336
+ # Force a complete display update
337
+ with self._lock:
338
+ # Mark all components for update
339
+ self._pending_updates.add("header")
340
+ self._pending_updates.add("footer")
341
+ self._pending_updates.update(self.agent_ids)
342
+
343
+ # Schedule immediate update
344
+ self._schedule_async_update(force_update=True)
345
+
346
+ # Small delay to allow display to stabilize
347
+ time.sleep(0.1)
348
+
349
+ except Exception:
350
+ # Silently handle errors to avoid disrupting the application
351
+ pass
352
+
353
+ def _recalculate_layout(self):
354
+ """Recalculate layout dimensions based on current terminal size."""
355
+ # Recalculate column width
356
+ self.fixed_column_width = max(
357
+ 20, self.terminal_size.width // self.num_agents - 1
358
+ )
359
+
360
+ # Recalculate panel height (reserve space for header and footer)
361
+ self.agent_panel_height = max(10, self.terminal_size.height - 13)
362
+
363
+ def _invalidate_display_cache(self):
364
+ """Invalidate all cached display components to force refresh."""
365
+ self._agent_panels_cache.clear()
366
+ self._header_cache = None
367
+ self._footer_cache = None
368
+
369
+ def _setup_agent_files(self):
370
+ """Setup individual txt files for each agent and system status file."""
371
+ # Create output directory if it doesn't exist
372
+ Path(self.output_dir).mkdir(parents=True, exist_ok=True)
373
+
374
+ # Initialize file paths for each agent
375
+ for agent_id in self.agent_ids:
376
+ file_path = Path(self.output_dir) / f"{agent_id}.txt"
377
+ self.agent_files[agent_id] = file_path
378
+ # Clear existing file content
379
+ with open(file_path, "w", encoding="utf-8") as f:
380
+ f.write(f"=== {agent_id.upper()} OUTPUT LOG ===\n\n")
381
+
382
+ # Initialize system status file
383
+ self.system_status_file = Path(self.output_dir) / "system_status.txt"
384
+ with open(self.system_status_file, "w", encoding="utf-8") as f:
385
+ f.write("=== SYSTEM STATUS LOG ===\n\n")
386
+
387
+ def _detect_terminal_performance(self):
388
+ """Detect terminal performance characteristics for adaptive refresh rates."""
389
+ terminal_info = {
390
+ "type": "unknown",
391
+ "performance_tier": "medium", # low, medium, high
392
+ "supports_unicode": True,
393
+ "supports_color": True,
394
+ "buffer_size": "normal",
395
+ }
396
+
397
+ try:
398
+ # Get terminal type from environment
399
+ term = os.environ.get("TERM", "").lower()
400
+ term_program = os.environ.get("TERM_PROGRAM", "").lower()
401
+
402
+ # Classify terminal types by performance
403
+ if "iterm.app" in term_program or "iterm" in term_program.lower():
404
+ terminal_info["performance_tier"] = "high"
405
+ terminal_info["type"] = "iterm"
406
+ terminal_info["supports_unicode"] = True
407
+ elif (
408
+ "vscode" in term_program
409
+ or "code" in term_program
410
+ or self._detect_vscode_terminal()
411
+ ):
412
+ # VSCode integrated terminal - needs special handling for flaky behavior
413
+ terminal_info["performance_tier"] = "medium"
414
+ terminal_info["type"] = "vscode"
415
+ terminal_info["supports_unicode"] = True
416
+ terminal_info["buffer_size"] = "large" # VSCode has good buffering
417
+ terminal_info["needs_flush_delay"] = True # Reduce flicker
418
+ terminal_info["refresh_stabilization"] = True # Add stability delays
419
+ elif "apple_terminal" in term_program or term_program == "terminal":
420
+ terminal_info["performance_tier"] = "high"
421
+ terminal_info["type"] = "macos_terminal"
422
+ terminal_info["supports_unicode"] = True
423
+ elif "xterm-256color" in term or "alacritty" in term_program:
424
+ terminal_info["performance_tier"] = "high"
425
+ terminal_info["type"] = "modern"
426
+ elif "screen" in term or "tmux" in term:
427
+ terminal_info["performance_tier"] = "low" # Multiplexers are slower
428
+ terminal_info["type"] = "multiplexer"
429
+ elif "xterm" in term:
430
+ terminal_info["performance_tier"] = "medium"
431
+ terminal_info["type"] = "xterm"
432
+ elif term in ["dumb", "vt100", "vt220"]:
433
+ terminal_info["performance_tier"] = "low"
434
+ terminal_info["type"] = "legacy"
435
+ terminal_info["supports_unicode"] = False
436
+
437
+ # Check for SSH (typically slower)
438
+ if os.environ.get("SSH_CONNECTION") or os.environ.get("SSH_CLIENT"):
439
+ if terminal_info["performance_tier"] == "high":
440
+ terminal_info["performance_tier"] = "medium"
441
+ elif terminal_info["performance_tier"] == "medium":
442
+ terminal_info["performance_tier"] = "low"
443
+
444
+ # Detect color support
445
+ colorterm = os.environ.get("COLORTERM", "").lower()
446
+ if colorterm in ["truecolor", "24bit"]:
447
+ terminal_info["supports_color"] = True
448
+ elif not self.console.is_terminal or term == "dumb":
449
+ terminal_info["supports_color"] = False
450
+
451
+ except Exception:
452
+ # Fallback to safe defaults
453
+ terminal_info["performance_tier"] = "low"
454
+
455
+ return terminal_info
456
+
457
+ def _detect_vscode_terminal(self):
458
+ """Additional VSCode terminal detection using multiple indicators."""
459
+ try:
460
+ # Check for VSCode-specific environment variables
461
+ vscode_indicators = [
462
+ "VSCODE_INJECTION",
463
+ "VSCODE_PID",
464
+ "VSCODE_IPC_HOOK",
465
+ "VSCODE_IPC_HOOK_CLI",
466
+ "TERM_PROGRAM_VERSION",
467
+ ]
468
+
469
+ # Check if any VSCode-specific env vars are present
470
+ for indicator in vscode_indicators:
471
+ if os.environ.get(indicator):
472
+ return True
473
+
474
+ # Check if parent process is code or VSCode
475
+ try:
476
+ import psutil
477
+
478
+ current_process = psutil.Process()
479
+ parent = current_process.parent()
480
+ if parent and (
481
+ "code" in parent.name().lower() or "vscode" in parent.name().lower()
482
+ ):
483
+ return True
484
+ except (ImportError, psutil.NoSuchProcess, psutil.AccessDenied):
485
+ pass
486
+
487
+ # Check for common VSCode terminal patterns in environment
488
+ term_program = os.environ.get("TERM_PROGRAM", "").lower()
489
+ if term_program and any(
490
+ pattern in term_program for pattern in ["code", "vscode"]
491
+ ):
492
+ return True
493
+
494
+ return False
495
+ except Exception:
496
+ return False
497
+
498
+ def _get_adaptive_refresh_rate(self, user_override=None):
499
+ """Get adaptive refresh rate based on terminal performance."""
500
+ if user_override is not None:
501
+ return user_override
502
+
503
+ perf_tier = self._terminal_performance["performance_tier"]
504
+ term_type = self._terminal_performance["type"]
505
+
506
+ # VSCode-specific optimization
507
+ if term_type == "vscode":
508
+ # Lower refresh rate for VSCode to prevent flaky behavior
509
+ # VSCode terminal sometimes has rendering delays
510
+ return 2
511
+
512
+ refresh_rates = {
513
+ "high": 10, # Modern terminals
514
+ "medium": 5, # Standard terminals
515
+ "low": 2, # Multiplexers, SSH, legacy
516
+ }
517
+
518
+ return refresh_rates.get(perf_tier, 8)
519
+
520
+ def _get_adaptive_update_interval(self):
521
+ """Get adaptive update interval based on terminal performance."""
522
+ perf_tier = self._terminal_performance["performance_tier"]
523
+
524
+ intervals = {
525
+ "high": 0.02, # 20ms - very responsive
526
+ "medium": 0.05, # 50ms - balanced
527
+ "low": 0.1, # 100ms - conservative
528
+ }
529
+
530
+ return intervals.get(perf_tier, 0.05)
531
+
532
+ def _get_adaptive_full_refresh_interval(self):
533
+ """Get adaptive full refresh interval based on terminal performance."""
534
+ perf_tier = self._terminal_performance["performance_tier"]
535
+
536
+ intervals = {"high": 0.1, "medium": 0.2, "low": 0.5} # 100ms # 200ms # 500ms
537
+
538
+ return intervals.get(perf_tier, 0.2)
539
+
540
+ def _get_adaptive_debounce_delay(self):
541
+ """Get adaptive debounce delay based on terminal performance."""
542
+ perf_tier = self._terminal_performance["performance_tier"]
543
+ term_type = self._terminal_performance["type"]
544
+
545
+ delays = {"high": 0.01, "medium": 0.03, "low": 0.05} # 10ms # 30ms # 50ms
546
+
547
+ base_delay = delays.get(perf_tier, 0.03)
548
+
549
+ # Increase debounce delay for macOS terminals to reduce flakiness
550
+ if term_type in ["iterm", "macos_terminal"]:
551
+ base_delay *= 2.0 # Double the debounce delay for stability
552
+
553
+ return base_delay
554
+
555
+ def _get_adaptive_buffer_length(self):
556
+ """Get adaptive buffer length based on terminal performance."""
557
+ perf_tier = self._terminal_performance["performance_tier"]
558
+ term_type = self._terminal_performance["type"]
559
+
560
+ lengths = {
561
+ "high": 800, # Longer buffers for fast terminals
562
+ "medium": 500, # Standard buffer length
563
+ "low": 200, # Shorter buffers for slow terminals
564
+ }
565
+
566
+ base_length = lengths.get(perf_tier, 500)
567
+
568
+ # Reduce buffer size for macOS terminals to improve responsiveness
569
+ if term_type in ["iterm", "macos_terminal"]:
570
+ base_length = min(base_length, 400)
571
+
572
+ return base_length
573
+
574
+ def _get_adaptive_buffer_timeout(self):
575
+ """Get adaptive buffer timeout based on terminal performance."""
576
+ perf_tier = self._terminal_performance["performance_tier"]
577
+ term_type = self._terminal_performance["type"]
578
+
579
+ timeouts = {
580
+ "high": 0.5, # Fast flush for responsive terminals
581
+ "medium": 1.0, # Standard timeout
582
+ "low": 2.0, # Longer timeout for slow terminals
583
+ }
584
+
585
+ base_timeout = timeouts.get(perf_tier, 1.0)
586
+
587
+ # Increase buffer timeout for macOS terminals for smoother text flow
588
+ if term_type in ["iterm", "macos_terminal"]:
589
+ base_timeout *= 1.5 # 50% longer timeout for stability
590
+
591
+ return base_timeout
592
+
593
+ def _get_adaptive_batch_timeout(self):
594
+ """Get adaptive batch timeout for update batching."""
595
+ perf_tier = self._terminal_performance["performance_tier"]
596
+
597
+ timeouts = {
598
+ "high": 0.05, # 50ms batching for fast terminals
599
+ "medium": 0.1, # 100ms batching for medium terminals
600
+ "low": 0.2, # 200ms batching for slow terminals
601
+ }
602
+
603
+ return timeouts.get(perf_tier, 0.1)
604
+
605
+ def _monitor_performance(self):
606
+ """Monitor refresh performance and adjust if needed."""
607
+ current_time = time.time()
608
+
609
+ # Clean old refresh time records (keep last 20)
610
+ if len(self._refresh_times) > 20:
611
+ self._refresh_times = self._refresh_times[-20:]
612
+
613
+ # Calculate average refresh time
614
+ if len(self._refresh_times) >= 5:
615
+ avg_refresh_time = sum(self._refresh_times) / len(self._refresh_times)
616
+ expected_refresh_time = 1.0 / self.refresh_rate
617
+
618
+ # If refresh takes too long, downgrade performance
619
+ if avg_refresh_time > expected_refresh_time * 2:
620
+ self._dropped_frames += 1
621
+
622
+ # After 3 dropped frames, reduce refresh rate
623
+ if self._dropped_frames >= 3:
624
+ old_rate = self.refresh_rate
625
+ self.refresh_rate = max(2, int(self.refresh_rate * 0.7))
626
+ self._dropped_frames = 0
627
+
628
+ # Update intervals accordingly
629
+ self._update_interval = 1.0 / self.refresh_rate
630
+ self._full_refresh_interval *= 1.5
631
+
632
+ # Restart live display with new rate if active
633
+ if self.live and self.live.is_started:
634
+ try:
635
+ self.live.refresh_per_second = self.refresh_rate
636
+ except:
637
+ # If live display fails, fallback to simple mode
638
+ self._fallback_to_simple_display()
639
+
640
+ def _create_live_display_with_fallback(self):
641
+ """Create Live display with terminal compatibility checks and fallback."""
642
+ try:
643
+ # Test terminal capabilities
644
+ if not self._test_terminal_capabilities():
645
+ return self._fallback_to_simple_display()
646
+
647
+ # Create Live display with adaptive settings
648
+ live_settings = self._get_adaptive_live_settings()
649
+
650
+ live = Live(self._create_layout(), console=self.console, **live_settings)
651
+
652
+ # Test if Live display works
653
+ try:
654
+ # Quick test start/stop to verify functionality
655
+ live.start()
656
+ live.stop()
657
+ return live
658
+ except Exception:
659
+ # Live display failed, try fallback
660
+ return self._fallback_to_simple_display()
661
+
662
+ except Exception:
663
+ # Any error in setup, use fallback
664
+ return self._fallback_to_simple_display()
665
+
666
+ def _test_terminal_capabilities(self):
667
+ """Test if terminal supports rich Live display features."""
668
+ try:
669
+ # Check if we're in a proper terminal
670
+ if not self.console.is_terminal:
671
+ return False
672
+
673
+ # Check terminal type compatibility
674
+ perf_tier = self._terminal_performance["performance_tier"]
675
+ term_type = self._terminal_performance["type"]
676
+
677
+ # Disable Live for very limited terminals
678
+ if term_type == "legacy" or perf_tier == "low":
679
+ # Allow basic terminals if not too limited
680
+ term = os.environ.get("TERM", "").lower()
681
+ if term in ["dumb", "vt100"]:
682
+ return False
683
+
684
+ # Enable Live for macOS terminals with optimizations
685
+ if term_type in ["iterm", "macos_terminal"]:
686
+ return True
687
+
688
+ # Test basic console functionality
689
+ test_size = self.console.size
690
+ if test_size.width < 20 or test_size.height < 10:
691
+ return False
692
+
693
+ return True
694
+
695
+ except Exception:
696
+ return False
697
+
698
+ def _get_adaptive_live_settings(self):
699
+ """Get Live display settings adapted to terminal performance."""
700
+ perf_tier = self._terminal_performance["performance_tier"]
701
+
702
+ settings = {
703
+ "refresh_per_second": self.refresh_rate,
704
+ "vertical_overflow": "ellipsis",
705
+ "transient": False,
706
+ }
707
+
708
+ # Adjust settings based on performance tier
709
+ if perf_tier == "low":
710
+ settings["refresh_per_second"] = min(settings["refresh_per_second"], 3)
711
+ settings["transient"] = True # Reduce memory usage
712
+ elif perf_tier == "medium":
713
+ settings["refresh_per_second"] = min(settings["refresh_per_second"], 8)
714
+
715
+ # Disable auto_refresh for multiplexers to prevent conflicts
716
+ if self._terminal_performance["type"] == "multiplexer":
717
+ settings["auto_refresh"] = False
718
+
719
+ # macOS terminal-specific optimizations
720
+ if self._terminal_performance["type"] in ["iterm", "macos_terminal"]:
721
+ # Use more conservative refresh rates for macOS terminals to reduce flakiness
722
+ settings["refresh_per_second"] = min(settings["refresh_per_second"], 5)
723
+ # Enable transient mode to reduce flicker
724
+ settings["transient"] = False
725
+ # Ensure vertical overflow is handled gracefully
726
+ settings["vertical_overflow"] = "ellipsis"
727
+
728
+ # VSCode terminal-specific optimizations
729
+ if self._terminal_performance["type"] == "vscode":
730
+ # VSCode terminal needs very conservative refresh to prevent flaky behavior
731
+ settings["refresh_per_second"] = min(settings["refresh_per_second"], 6)
732
+ # Use transient mode to reduce rendering artifacts
733
+ settings["transient"] = False
734
+ # Handle overflow gracefully to prevent layout issues
735
+ settings["vertical_overflow"] = "ellipsis"
736
+ # Reduce auto-refresh frequency for stability
737
+ settings["auto_refresh"] = True
738
+
739
+ return settings
740
+
741
+ def _fallback_to_simple_display(self):
742
+ """Fallback to simple console output when Live display is not supported."""
743
+ self._simple_display_mode = True
744
+
745
+ # Print a simple status message
746
+ try:
747
+ self.console.print(
748
+ "\n[yellow]Terminal compatibility: Using simple display mode[/yellow]"
749
+ )
750
+ self.console.print(
751
+ f"[dim]Monitoring {len(self.agent_ids)} agents...[/dim]\n"
752
+ )
753
+ except:
754
+ # If even basic console fails, use plain print
755
+ print("\nUsing simple display mode...")
756
+ print(f"Monitoring {len(self.agent_ids)} agents...\n")
757
+
758
+ return None # No Live display
759
+
760
+ def _update_display_safe(self):
761
+ """Safely update display with fallback support and terminal-specific synchronization."""
762
+ # Add extra synchronization for macOS terminals and VSCode to prevent race conditions
763
+ term_type = self._terminal_performance["type"]
764
+ use_safe_mode = term_type in ["iterm", "macos_terminal", "vscode"]
765
+
766
+ # VSCode-specific stabilization
767
+ if term_type == "vscode" and self._terminal_performance.get(
768
+ "refresh_stabilization"
769
+ ):
770
+ # Add small delay before refresh to let VSCode terminal stabilize
771
+ time.sleep(0.01)
772
+
773
+ try:
774
+ if use_safe_mode:
775
+ # For macOS terminals and VSCode, use more conservative locking
776
+ with self._layout_update_lock:
777
+ with self._lock: # Double locking for extra safety
778
+ if (
779
+ hasattr(self, "_simple_display_mode")
780
+ and self._simple_display_mode
781
+ ):
782
+ self._update_simple_display()
783
+ else:
784
+ self._update_live_display_safe()
785
+ else:
786
+ with self._layout_update_lock:
787
+ if (
788
+ hasattr(self, "_simple_display_mode")
789
+ and self._simple_display_mode
790
+ ):
791
+ self._update_simple_display()
792
+ else:
793
+ self._update_live_display()
794
+ except Exception:
795
+ # Fallback to simple display on any error
796
+ self._fallback_to_simple_display()
797
+
798
+ # VSCode-specific post-refresh stabilization
799
+ if term_type == "vscode" and self._terminal_performance.get(
800
+ "needs_flush_delay"
801
+ ):
802
+ # Small delay after refresh to prevent flicker
803
+ time.sleep(0.005)
804
+
805
+ def _update_simple_display(self):
806
+ """Update display in simple mode without Live."""
807
+ try:
808
+ # Simple status update every few seconds
809
+ current_time = time.time()
810
+ if not hasattr(self, "_last_simple_update"):
811
+ self._last_simple_update = 0
812
+
813
+ if current_time - self._last_simple_update > 2.0: # Update every 2 seconds
814
+ status_line = f"[{time.strftime('%H:%M:%S')}] Agents: "
815
+ for agent_id in self.agent_ids:
816
+ status = self.agent_status.get(agent_id, "waiting")
817
+ status_line += f"{agent_id}:{status} "
818
+
819
+ try:
820
+ self.console.print(f"\r{status_line[:80]}", end="")
821
+ except:
822
+ print(f"\r{status_line[:80]}", end="")
823
+
824
+ self._last_simple_update = current_time
825
+
826
+ except Exception:
827
+ pass
828
+
829
+ def _update_live_display(self):
830
+ """Update Live display mode."""
831
+ try:
832
+ if self.live:
833
+ self.live.update(self._create_layout())
834
+ except Exception:
835
+ # If Live display fails, switch to simple mode
836
+ self._fallback_to_simple_display()
837
+
838
+ def _update_live_display_safe(self):
839
+ """Update Live display mode with extra safety for macOS terminals."""
840
+ try:
841
+ if self.live and self.live.is_started:
842
+ # For macOS terminals, add a small delay to prevent flickering
843
+ import time
844
+
845
+ time.sleep(0.001) # 1ms delay for terminal synchronization
846
+ self.live.update(self._create_layout())
847
+ elif self.live:
848
+ # If live display exists but isn't started, try to restart it
849
+ try:
850
+ self.live.start()
851
+ self.live.update(self._create_layout())
852
+ except Exception:
853
+ self._fallback_to_simple_display()
854
+ except Exception:
855
+ # If Live display fails, switch to simple mode
856
+ self._fallback_to_simple_display()
857
+
858
+ def _setup_theme(self):
859
+ """Setup color theme configuration."""
860
+ themes = {
861
+ "dark": {
862
+ "primary": "bright_cyan",
863
+ "secondary": "bright_blue",
864
+ "success": "bright_green",
865
+ "warning": "bright_yellow",
866
+ "error": "bright_red",
867
+ "info": "bright_magenta",
868
+ "text": "white",
869
+ "border": "blue",
870
+ "panel_style": "blue",
871
+ "header_style": "bold bright_cyan",
872
+ },
873
+ "light": {
874
+ "primary": "blue",
875
+ "secondary": "cyan",
876
+ "success": "green",
877
+ "warning": "yellow",
878
+ "error": "red",
879
+ "info": "magenta",
880
+ "text": "black",
881
+ "border": "blue",
882
+ "panel_style": "blue",
883
+ "header_style": "bold blue",
884
+ },
885
+ "cyberpunk": {
886
+ "primary": "bright_magenta",
887
+ "secondary": "bright_cyan",
888
+ "success": "bright_green",
889
+ "warning": "bright_yellow",
890
+ "error": "bright_red",
891
+ "info": "bright_blue",
892
+ "text": "bright_white",
893
+ "border": "bright_magenta",
894
+ "panel_style": "bright_magenta",
895
+ "header_style": "bold bright_magenta",
896
+ },
897
+ }
898
+
899
+ self.colors = themes.get(self.theme, themes["dark"])
900
+
901
+ # VSCode terminal-specific color adjustments
902
+ if self._terminal_performance["type"] == "vscode":
903
+ # VSCode terminal sometimes has issues with certain bright colors
904
+ # Use more stable color palette
905
+ vscode_adjustments = {
906
+ "primary": "cyan", # Less bright than bright_cyan
907
+ "secondary": "blue",
908
+ "border": "cyan",
909
+ "panel_style": "cyan",
910
+ }
911
+ self.colors.update(vscode_adjustments)
912
+
913
+ # Set up VSCode-safe emoji mapping for better compatibility
914
+ self._setup_vscode_emoji_fallbacks()
915
+
916
+ def _setup_vscode_emoji_fallbacks(self):
917
+ """Setup emoji fallbacks for VSCode terminal compatibility."""
918
+ # VSCode terminal sometimes has issues with certain Unicode characters
919
+ # Provide ASCII fallbacks for better stability
920
+ self._emoji_fallbacks = {
921
+ "🚀": ">>", # Launch/rocket
922
+ "🎯": ">", # Target
923
+ "💭": "...", # Thinking
924
+ "⚡": "!", # Status update
925
+ "🎨": "*", # Theme
926
+ "📝": "=", # Writing
927
+ "✅": "[OK]", # Success
928
+ "❌": "[X]", # Error
929
+ "⭐": "*", # Important
930
+ "🔍": "?", # Search
931
+ "📊": "|", # Status/data
932
+ }
933
+
934
+ # Only use fallbacks if VSCode terminal has Unicode issues
935
+ # This can be detected at runtime if needed
936
+ if not self._terminal_performance.get("supports_unicode", True):
937
+ self._use_emoji_fallbacks = True
938
+ else:
939
+ self._use_emoji_fallbacks = False
940
+
941
+ def _safe_emoji(self, emoji: str) -> str:
942
+ """Get safe emoji for current terminal, with VSCode fallbacks."""
943
+ if (
944
+ self._terminal_performance["type"] == "vscode"
945
+ and self._use_emoji_fallbacks
946
+ and emoji in self._emoji_fallbacks
947
+ ):
948
+ return self._emoji_fallbacks[emoji]
949
+ return emoji
950
+
951
+ def initialize(self, question: str, log_filename: Optional[str] = None):
952
+ """Initialize the rich display with question and optional log file."""
953
+ self.log_filename = log_filename
954
+ self.question = question
955
+
956
+ # Clear screen
957
+ self.console.clear()
958
+
959
+ # Create initial layout
960
+ self._create_initial_display()
961
+
962
+ # Setup keyboard handling if in interactive mode
963
+ if self._keyboard_interactive_mode:
964
+ self._setup_keyboard_handler()
965
+
966
+ # Start live display with adaptive settings and fallback support
967
+ self.live = self._create_live_display_with_fallback()
968
+ if self.live:
969
+ self.live.start()
970
+
971
+ # Write initial system status
972
+ self._write_system_status()
973
+
974
+ # Interactive mode is handled through input prompts
975
+
976
+ def _create_initial_display(self):
977
+ """Create the initial welcome display."""
978
+ welcome_text = Text()
979
+ welcome_text.append(
980
+ "🚀 MassGen Coordination Dashboard 🚀\n", style=self.colors["header_style"]
981
+ )
982
+ welcome_text.append(
983
+ f"Multi-Agent System with {len(self.agent_ids)} agents\n",
984
+ style=self.colors["primary"],
985
+ )
986
+
987
+ if self.log_filename:
988
+ welcome_text.append(
989
+ f"📁 Log: {self.log_filename}\n", style=self.colors["info"]
990
+ )
991
+
992
+ welcome_text.append(
993
+ f"🎨 Theme: {self.theme.title()}", style=self.colors["secondary"]
994
+ )
995
+
996
+ welcome_panel = Panel(
997
+ welcome_text,
998
+ box=DOUBLE,
999
+ border_style=self.colors["border"],
1000
+ title="[bold]Welcome[/bold]",
1001
+ title_align="center",
1002
+ )
1003
+
1004
+ self.console.print(welcome_panel)
1005
+ self.console.print()
1006
+
1007
+ def _create_layout(self) -> Layout:
1008
+ """Create the main layout structure with cached components."""
1009
+ layout = Layout()
1010
+
1011
+ # Use cached components if available, otherwise create new ones
1012
+ header = self._header_cache if self._header_cache else self._create_header()
1013
+ agent_columns = self._create_agent_columns_from_cache()
1014
+ footer = self._footer_cache if self._footer_cache else self._create_footer()
1015
+
1016
+ # Arrange layout
1017
+ layout.split_column(
1018
+ Layout(header, name="header", size=5),
1019
+ Layout(agent_columns, name="main"),
1020
+ Layout(footer, name="footer", size=8),
1021
+ )
1022
+
1023
+ return layout
1024
+
1025
+ def _create_agent_columns_from_cache(self) -> Columns:
1026
+ """Create agent columns using cached panels with fixed widths."""
1027
+ agent_panels = []
1028
+
1029
+ for agent_id in self.agent_ids:
1030
+ if agent_id in self._agent_panels_cache:
1031
+ agent_panels.append(self._agent_panels_cache[agent_id])
1032
+ else:
1033
+ panel = self._create_agent_panel(agent_id)
1034
+ self._agent_panels_cache[agent_id] = panel
1035
+ agent_panels.append(panel)
1036
+
1037
+ # Use fixed column widths with equal=False to enforce exact sizing
1038
+ return Columns(
1039
+ agent_panels, equal=False, expand=False, width=self.fixed_column_width
1040
+ )
1041
+
1042
+ def _create_header(self) -> Panel:
1043
+ """Create the header panel."""
1044
+ header_text = Text()
1045
+ header_text.append(
1046
+ "🚀 MassGen Multi-Agent Coordination System",
1047
+ style=self.colors["header_style"],
1048
+ )
1049
+
1050
+ if hasattr(self, "question"):
1051
+ header_text.append(
1052
+ f"\n💡 Question: {self.question[:80]}{'...' if len(self.question) > 80 else ''}",
1053
+ style=self.colors["info"],
1054
+ )
1055
+
1056
+ return Panel(
1057
+ Align.center(header_text),
1058
+ box=ROUNDED,
1059
+ border_style=self.colors["border"],
1060
+ height=5,
1061
+ )
1062
+
1063
+ def _create_agent_columns(self) -> Columns:
1064
+ """Create columns for each agent with fixed widths."""
1065
+ agent_panels = []
1066
+
1067
+ for agent_id in self.agent_ids:
1068
+ panel = self._create_agent_panel(agent_id)
1069
+ agent_panels.append(panel)
1070
+
1071
+ # Use fixed column widths with equal=False to enforce exact sizing
1072
+ return Columns(
1073
+ agent_panels, equal=False, expand=False, width=self.fixed_column_width
1074
+ )
1075
+
1076
+ def _setup_keyboard_handler(self):
1077
+ """Setup keyboard handler for interactive agent selection."""
1078
+ try:
1079
+ # Simple key mapping for agent selection
1080
+ self._agent_keys = {}
1081
+ for i, agent_id in enumerate(self.agent_ids):
1082
+ key = str(i + 1)
1083
+ self._agent_keys[key] = agent_id
1084
+
1085
+ # Start background input thread for Live mode
1086
+ if self._keyboard_interactive_mode:
1087
+ self._start_input_thread()
1088
+
1089
+ except ImportError:
1090
+ self._keyboard_interactive_mode = False
1091
+
1092
+ def _start_input_thread(self):
1093
+ """Start background thread for keyboard input during Live mode."""
1094
+ if not sys.stdin.isatty():
1095
+ return # Can't handle input if not a TTY
1096
+
1097
+ self._stop_input_thread = False
1098
+
1099
+ # Choose input method based on safety requirements and terminal type
1100
+ term_type = self._terminal_performance["type"]
1101
+
1102
+ if self._safe_keyboard_mode or term_type in ["iterm", "macos_terminal"]:
1103
+ # Use completely safe method for macOS terminals to avoid conflicts
1104
+ self._input_thread = threading.Thread(
1105
+ target=self._input_thread_worker_safe, daemon=True
1106
+ )
1107
+ self._input_thread.start()
1108
+ else:
1109
+ # Try improved method first, fallback to polling method if needed
1110
+ try:
1111
+ self._input_thread = threading.Thread(
1112
+ target=self._input_thread_worker_improved, daemon=True
1113
+ )
1114
+ self._input_thread.start()
1115
+ except Exception:
1116
+ # Fallback to simpler polling method
1117
+ self._input_thread = threading.Thread(
1118
+ target=self._input_thread_worker_fallback, daemon=True
1119
+ )
1120
+ self._input_thread.start()
1121
+
1122
+ def _input_thread_worker_improved(self):
1123
+ """Improved background thread worker that doesn't interfere with Rich rendering."""
1124
+ try:
1125
+ # Save original terminal settings but don't change to raw mode
1126
+ if sys.stdin.isatty():
1127
+ self._original_settings = termios.tcgetattr(sys.stdin.fileno())
1128
+ # Use canonical mode with minimal changes
1129
+ new_settings = termios.tcgetattr(sys.stdin.fileno())
1130
+ # Enable non-blocking input without full raw mode
1131
+ new_settings[3] = new_settings[3] & ~(termios.ICANON | termios.ECHO)
1132
+ new_settings[6][termios.VMIN] = 0 # Non-blocking
1133
+ new_settings[6][termios.VTIME] = 1 # 100ms timeout
1134
+ termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, new_settings)
1135
+
1136
+ while not self._stop_input_thread:
1137
+ try:
1138
+ # Use select with shorter timeout to be more responsive
1139
+ if select.select([sys.stdin], [], [], 0.05)[0]:
1140
+ char = sys.stdin.read(1)
1141
+ if char:
1142
+ self._handle_key_press(char)
1143
+ except (BlockingIOError, OSError):
1144
+ # Expected in non-blocking mode, continue
1145
+ continue
1146
+
1147
+ except (KeyboardInterrupt, EOFError):
1148
+ pass
1149
+ except Exception as e:
1150
+ # Handle errors gracefully
1151
+ pass
1152
+ finally:
1153
+ # Restore terminal settings
1154
+ self._restore_terminal_settings()
1155
+
1156
+ def _input_thread_worker_fallback(self):
1157
+ """Fallback keyboard input method using simple polling without terminal mode changes."""
1158
+ import time
1159
+
1160
+ # Show instructions to user
1161
+ self.console.print(
1162
+ "\n[dim]Keyboard support active. Press keys during Live display:[/dim]"
1163
+ )
1164
+ self.console.print(
1165
+ "[dim]1-{} to open agent files, 's' for system status, 'q' to quit[/dim]\n".format(
1166
+ len(self.agent_ids)
1167
+ )
1168
+ )
1169
+
1170
+ try:
1171
+ while not self._stop_input_thread:
1172
+ # Use a much simpler approach - just sleep and check flag
1173
+ time.sleep(0.1)
1174
+
1175
+ # In this fallback mode, we rely on the user using Ctrl+C or
1176
+ # external interruption rather than single-key detection
1177
+ # This prevents any terminal mode conflicts
1178
+
1179
+ except (KeyboardInterrupt, EOFError):
1180
+ # Handle graceful shutdown
1181
+ pass
1182
+ except Exception:
1183
+ # Handle any other errors gracefully
1184
+ pass
1185
+
1186
+ def _input_thread_worker_safe(self):
1187
+ """Completely safe keyboard input that never changes terminal settings."""
1188
+ # This method does nothing to avoid any interference with Rich rendering
1189
+ # Keyboard functionality is disabled in safe mode to prevent rendering issues
1190
+ try:
1191
+ while not self._stop_input_thread:
1192
+ time.sleep(0.5) # Just wait without doing anything
1193
+ except:
1194
+ pass
1195
+
1196
+ def _restore_terminal_settings(self):
1197
+ """Restore original terminal settings."""
1198
+ try:
1199
+ if self._original_settings and sys.stdin.isatty():
1200
+ termios.tcsetattr(
1201
+ sys.stdin.fileno(), termios.TCSADRAIN, self._original_settings
1202
+ )
1203
+ self._original_settings = None
1204
+ except:
1205
+ pass
1206
+
1207
+ def _ensure_clean_keyboard_state(self):
1208
+ """Ensure clean keyboard state before starting agent selector."""
1209
+ # Stop input thread completely
1210
+ self._stop_input_thread = True
1211
+ if self._input_thread and self._input_thread.is_alive():
1212
+ try:
1213
+ self._input_thread.join(timeout=0.5)
1214
+ except:
1215
+ pass
1216
+
1217
+ # Restore terminal settings to normal mode
1218
+ self._restore_terminal_settings()
1219
+
1220
+ # Clear any pending keyboard input from stdin buffer
1221
+ try:
1222
+ if sys.stdin.isatty():
1223
+ import termios
1224
+
1225
+ # Flush input buffer to remove any pending keystrokes
1226
+ termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
1227
+ except:
1228
+ pass
1229
+
1230
+ # Small delay to ensure all cleanup is complete
1231
+ import time
1232
+
1233
+ time.sleep(0.1)
1234
+
1235
+ def _handle_key_press(self, key):
1236
+ """Handle key press events for agent selection."""
1237
+ if key in self._agent_keys:
1238
+ agent_id = self._agent_keys[key]
1239
+ self._open_agent_in_default_text_editor(agent_id)
1240
+ elif key == "s":
1241
+ self._open_system_status_in_default_text_editor()
1242
+ elif key == "q":
1243
+ # Quit the application - restore terminal and stop
1244
+ self._stop_input_thread = True
1245
+ self._restore_terminal_settings()
1246
+
1247
+ def _open_agent_in_default_text_editor(self, agent_id: str):
1248
+ """Open agent's txt file in default text editor."""
1249
+ if agent_id not in self.agent_files:
1250
+ return
1251
+
1252
+ file_path = self.agent_files[agent_id]
1253
+ if not file_path.exists():
1254
+ return
1255
+
1256
+ try:
1257
+ # Use system default application to open text files
1258
+ if sys.platform == "darwin": # macOS
1259
+ subprocess.run(["open", str(file_path)], check=False)
1260
+ elif sys.platform.startswith("linux"): # Linux
1261
+ subprocess.run(["xdg-open", str(file_path)], check=False)
1262
+ elif sys.platform == "win32": # Windows
1263
+ subprocess.run(["start", str(file_path)], check=False, shell=True)
1264
+ except (subprocess.CalledProcessError, FileNotFoundError):
1265
+ # Fallback to external app method
1266
+ self._open_agent_in_external_app(agent_id)
1267
+
1268
+ def _open_agent_in_vscode_new_window(self, agent_id: str):
1269
+ """Open agent's txt file in a new VS Code window."""
1270
+ if agent_id not in self.agent_files:
1271
+ return
1272
+
1273
+ file_path = self.agent_files[agent_id]
1274
+ if not file_path.exists():
1275
+ return
1276
+
1277
+ try:
1278
+ # Force open in new VS Code window
1279
+ subprocess.run(["code", "--new-window", str(file_path)], check=False)
1280
+ except (subprocess.CalledProcessError, FileNotFoundError):
1281
+ # Fallback to existing method if VS Code is not available
1282
+ self._open_agent_in_external_app(agent_id)
1283
+
1284
+ def _open_system_status_in_default_text_editor(self):
1285
+ """Open system status file in default text editor."""
1286
+ if not self.system_status_file or not self.system_status_file.exists():
1287
+ return
1288
+
1289
+ try:
1290
+ # Use system default application to open text files
1291
+ if sys.platform == "darwin": # macOS
1292
+ subprocess.run(["open", str(self.system_status_file)], check=False)
1293
+ elif sys.platform.startswith("linux"): # Linux
1294
+ subprocess.run(["xdg-open", str(self.system_status_file)], check=False)
1295
+ elif sys.platform == "win32": # Windows
1296
+ subprocess.run(
1297
+ ["start", str(self.system_status_file)], check=False, shell=True
1298
+ )
1299
+ except (subprocess.CalledProcessError, FileNotFoundError):
1300
+ # Fallback to external app method
1301
+ self._open_system_status_in_external_app()
1302
+
1303
+ def _open_system_status_in_vscode_new_window(self):
1304
+ """Open system status file in a new VS Code window."""
1305
+ if not self.system_status_file or not self.system_status_file.exists():
1306
+ return
1307
+
1308
+ try:
1309
+ # Force open in new VS Code window
1310
+ subprocess.run(
1311
+ ["code", "--new-window", str(self.system_status_file)], check=False
1312
+ )
1313
+ except (subprocess.CalledProcessError, FileNotFoundError):
1314
+ # Fallback to existing method if VS Code is not available
1315
+ self._open_system_status_in_external_app()
1316
+
1317
+ def _open_agent_in_external_app(self, agent_id: str):
1318
+ """Open agent's txt file in external editor or terminal viewer."""
1319
+ if agent_id not in self.agent_files:
1320
+ return
1321
+
1322
+ file_path = self.agent_files[agent_id]
1323
+ if not file_path.exists():
1324
+ return
1325
+
1326
+ try:
1327
+ # Try different methods to open the file
1328
+ if sys.platform == "darwin": # macOS
1329
+ # Try VS Code first, then other editors, then default text editor
1330
+ editors = ["code", "subl", "atom", "nano", "vim", "open"]
1331
+ for editor in editors:
1332
+ try:
1333
+ if editor == "open":
1334
+ subprocess.run(
1335
+ ["open", "-a", "TextEdit", str(file_path)], check=False
1336
+ )
1337
+ else:
1338
+ subprocess.run([editor, str(file_path)], check=False)
1339
+ break
1340
+ except (subprocess.CalledProcessError, FileNotFoundError):
1341
+ continue
1342
+ elif sys.platform.startswith("linux"): # Linux
1343
+ # Try common Linux editors
1344
+ editors = ["code", "gedit", "kate", "nano", "vim", "xdg-open"]
1345
+ for editor in editors:
1346
+ try:
1347
+ subprocess.run([editor, str(file_path)], check=False)
1348
+ break
1349
+ except (subprocess.CalledProcessError, FileNotFoundError):
1350
+ continue
1351
+ elif sys.platform == "win32": # Windows
1352
+ # Try Windows editors
1353
+ editors = ["code", "notepad++", "notepad"]
1354
+ for editor in editors:
1355
+ try:
1356
+ subprocess.run(
1357
+ [editor, str(file_path)], check=False, shell=True
1358
+ )
1359
+ break
1360
+ except (subprocess.CalledProcessError, FileNotFoundError):
1361
+ continue
1362
+
1363
+ except Exception:
1364
+ # If all else fails, show a message that the file exists
1365
+ pass
1366
+
1367
+ def _open_system_status_in_external_app(self):
1368
+ """Open system status file in external editor or terminal viewer."""
1369
+ if not self.system_status_file or not self.system_status_file.exists():
1370
+ return
1371
+
1372
+ try:
1373
+ # Try different methods to open the file
1374
+ if sys.platform == "darwin": # macOS
1375
+ # Try VS Code first, then other editors, then default text editor
1376
+ editors = ["code", "subl", "atom", "nano", "vim", "open"]
1377
+ for editor in editors:
1378
+ try:
1379
+ if editor == "open":
1380
+ subprocess.run(
1381
+ [
1382
+ "open",
1383
+ "-a",
1384
+ "TextEdit",
1385
+ str(self.system_status_file),
1386
+ ],
1387
+ check=False,
1388
+ )
1389
+ else:
1390
+ subprocess.run(
1391
+ [editor, str(self.system_status_file)], check=False
1392
+ )
1393
+ break
1394
+ except (subprocess.CalledProcessError, FileNotFoundError):
1395
+ continue
1396
+ elif sys.platform.startswith("linux"): # Linux
1397
+ # Try common Linux editors
1398
+ editors = ["code", "gedit", "kate", "nano", "vim", "xdg-open"]
1399
+ for editor in editors:
1400
+ try:
1401
+ subprocess.run(
1402
+ [editor, str(self.system_status_file)], check=False
1403
+ )
1404
+ break
1405
+ except (subprocess.CalledProcessError, FileNotFoundError):
1406
+ continue
1407
+ elif sys.platform == "win32": # Windows
1408
+ # Try Windows editors
1409
+ editors = ["code", "notepad++", "notepad"]
1410
+ for editor in editors:
1411
+ try:
1412
+ subprocess.run(
1413
+ [editor, str(self.system_status_file)],
1414
+ check=False,
1415
+ shell=True,
1416
+ )
1417
+ break
1418
+ except (subprocess.CalledProcessError, FileNotFoundError):
1419
+ continue
1420
+
1421
+ except Exception:
1422
+ # If all else fails, show a message that the file exists
1423
+ pass
1424
+
1425
+ def _show_agent_full_content(self, agent_id: str):
1426
+ """Display full content of selected agent from txt file."""
1427
+ if agent_id not in self.agent_files:
1428
+ return
1429
+
1430
+ try:
1431
+ file_path = self.agent_files[agent_id]
1432
+ if file_path.exists():
1433
+ with open(file_path, "r", encoding="utf-8") as f:
1434
+ content = f.read()
1435
+
1436
+ # Add separator instead of clearing screen
1437
+ self.console.print("\n" + "=" * 80 + "\n")
1438
+
1439
+ # Create header
1440
+ header_text = Text()
1441
+ header_text.append(
1442
+ f"📄 {agent_id.upper()} - Full Content",
1443
+ style=self.colors["header_style"],
1444
+ )
1445
+ header_text.append(
1446
+ "\nPress any key to return to main view", style=self.colors["info"]
1447
+ )
1448
+
1449
+ header_panel = Panel(
1450
+ header_text, box=DOUBLE, border_style=self.colors["border"]
1451
+ )
1452
+
1453
+ # Create content panel
1454
+ content_panel = Panel(
1455
+ content,
1456
+ title=f"[bold]{agent_id.upper()} Output[/bold]",
1457
+ border_style=self.colors["border"],
1458
+ box=ROUNDED,
1459
+ )
1460
+
1461
+ self.console.print(header_panel)
1462
+ self.console.print(content_panel)
1463
+
1464
+ # Wait for key press to return
1465
+ input("Press Enter to return to agent selector...")
1466
+
1467
+ # Add separator instead of clearing screen
1468
+ self.console.print("\n" + "=" * 80 + "\n")
1469
+
1470
+ except Exception as e:
1471
+ # Handle errors gracefully
1472
+ pass
1473
+
1474
+ def show_agent_selector(self):
1475
+ """Show agent selector and handle user input."""
1476
+
1477
+ if not self._keyboard_interactive_mode or not hasattr(self, "_agent_keys"):
1478
+ return
1479
+
1480
+ # Prevent duplicate agent selector calls
1481
+ if self._agent_selector_active:
1482
+ return
1483
+
1484
+ self._agent_selector_active = True
1485
+
1486
+ # Ensure clean keyboard state before starting agent selector
1487
+ self._ensure_clean_keyboard_state()
1488
+
1489
+ try:
1490
+ loop_count = 0
1491
+ while True:
1492
+ loop_count += 1
1493
+
1494
+ # Display available options
1495
+
1496
+ options_text = Text()
1497
+ options_text.append(
1498
+ "\nThis is a system inspection interface for diving into the multi-agent collaboration behind the scenes in MassGen. It lets you examine each agent’s original output and compare it to the final MassGen answer in terms of quality. You can explore the detailed communication, collaboration, voting, and decision-making process.\n",
1499
+ style=self.colors["text"],
1500
+ )
1501
+
1502
+ options_text.append(
1503
+ "\n🎮 Select an agent to view full output:\n",
1504
+ style=self.colors["primary"],
1505
+ )
1506
+
1507
+ for key, agent_id in self._agent_keys.items():
1508
+ options_text.append(
1509
+ f" {key}: ", style=self.colors["warning"]
1510
+ )
1511
+ options_text.append(
1512
+ f"Inspect the original answer and working log of agent ", style=self.colors["text"]
1513
+ )
1514
+ options_text.append(
1515
+ f"{agent_id}\n", style=self.colors["warning"]
1516
+ )
1517
+
1518
+ options_text.append(
1519
+ " s: Inpsect the orchestrator working log including the voting process\n", style=self.colors["warning"]
1520
+ )
1521
+
1522
+ # Add option to show final presentation if it's stored
1523
+ if self._stored_final_presentation and self._stored_presentation_agent:
1524
+ options_text.append(
1525
+ f" f: Show final presentation from Selected Agent ({agent_id})\n", style=self.colors["success"]
1526
+ )
1527
+
1528
+ options_text.append(" q: Quit Inspection\n", style=self.colors["info"])
1529
+
1530
+ self.console.print(
1531
+ Panel(
1532
+ options_text,
1533
+ title="[bold]Agent Selector[/bold]",
1534
+ border_style=self.colors["border"],
1535
+ )
1536
+ )
1537
+
1538
+ # Get user input
1539
+ try:
1540
+ choice = input("Enter your choice: ").strip().lower()
1541
+
1542
+ if choice in self._agent_keys:
1543
+ self._show_agent_full_content(self._agent_keys[choice])
1544
+ elif choice == "s":
1545
+ self._show_system_status()
1546
+ elif choice == "f" and self._stored_final_presentation:
1547
+ self._redisplay_final_presentation()
1548
+ elif choice == "q":
1549
+ break
1550
+ else:
1551
+ self.console.print(
1552
+ f"[{self.colors['error']}]Invalid choice. Please try again.[/{self.colors['error']}]"
1553
+ )
1554
+ except KeyboardInterrupt:
1555
+ # Handle Ctrl+C gracefully
1556
+ break
1557
+ finally:
1558
+ # Always reset the flag when exiting
1559
+ self._agent_selector_active = True
1560
+
1561
+ def _redisplay_final_presentation(self):
1562
+ """Redisplay the stored final presentation."""
1563
+ if not self._stored_final_presentation or not self._stored_presentation_agent:
1564
+ self.console.print(
1565
+ f"[{self.colors['error']}]No final presentation stored.[/{self.colors['error']}]"
1566
+ )
1567
+ return
1568
+
1569
+ # Add separator
1570
+ self.console.print("\n" + "=" * 80 + "\n")
1571
+
1572
+ # Display the stored presentation
1573
+ self._display_final_presentation_content(
1574
+ self._stored_presentation_agent, self._stored_final_presentation
1575
+ )
1576
+
1577
+ # Wait for user to continue
1578
+ input("\nPress Enter to return to agent selector...")
1579
+
1580
+ # Add separator
1581
+ self.console.print("\n" + "=" * 80 + "\n")
1582
+
1583
+ def _redisplay_final_presentation(self):
1584
+ """Redisplay the stored final presentation."""
1585
+ if not self._stored_final_presentation or not self._stored_presentation_agent:
1586
+ self.console.print(
1587
+ f"[{self.colors['error']}]No final presentation stored.[/{self.colors['error']}]"
1588
+ )
1589
+ return
1590
+
1591
+ # Add separator
1592
+ self.console.print("\n" + "=" * 80 + "\n")
1593
+
1594
+ # Display the stored presentation
1595
+ self._display_final_presentation_content(
1596
+ self._stored_presentation_agent, self._stored_final_presentation
1597
+ )
1598
+
1599
+ # Wait for user to continue
1600
+ input("\nPress Enter to return to agent selector...")
1601
+
1602
+ # Add separator
1603
+ self.console.print("\n" + "=" * 80 + "\n")
1604
+
1605
+ def _show_system_status(self):
1606
+ """Display system status from txt file."""
1607
+ if not self.system_status_file or not self.system_status_file.exists():
1608
+ self.console.print(
1609
+ f"[{self.colors['error']}]System status file not found.[/{self.colors['error']}]"
1610
+ )
1611
+ return
1612
+
1613
+ try:
1614
+ with open(self.system_status_file, "r", encoding="utf-8") as f:
1615
+ content = f.read()
1616
+
1617
+ # Add separator instead of clearing screen
1618
+ self.console.print("\n" + "=" * 80 + "\n")
1619
+
1620
+ # Create header
1621
+ header_text = Text()
1622
+ header_text.append(
1623
+ "📊 SYSTEM STATUS - Full Log", style=self.colors["header_style"]
1624
+ )
1625
+ header_text.append(
1626
+ "\nPress any key to return to agent selector", style=self.colors["info"]
1627
+ )
1628
+
1629
+ header_panel = Panel(
1630
+ header_text, box=DOUBLE, border_style=self.colors["border"]
1631
+ )
1632
+
1633
+ # Create content panel
1634
+ content_panel = Panel(
1635
+ content,
1636
+ title="[bold]System Status Log[/bold]",
1637
+ border_style=self.colors["border"],
1638
+ box=ROUNDED,
1639
+ )
1640
+
1641
+ self.console.print(header_panel)
1642
+ self.console.print(content_panel)
1643
+
1644
+ # Wait for key press to return
1645
+ input("Press Enter to return to agent selector...")
1646
+
1647
+ # Add separator instead of clearing screen
1648
+ self.console.print("\n" + "=" * 80 + "\n")
1649
+
1650
+ except Exception as e:
1651
+ self.console.print(
1652
+ f"[{self.colors['error']}]Error reading system status file: {e}[/{self.colors['error']}]"
1653
+ )
1654
+
1655
+ def _create_agent_panel(self, agent_id: str) -> Panel:
1656
+ """Create a panel for a specific agent."""
1657
+ # Get agent content
1658
+ agent_content = self.agent_outputs.get(agent_id, [])
1659
+ status = self.agent_status.get(agent_id, "waiting")
1660
+ activity = self.agent_activity.get(agent_id, "waiting")
1661
+
1662
+ # Create content text
1663
+ content_text = Text()
1664
+
1665
+ # Show more lines since we now support scrolling
1666
+ # max_display_lines = min(len(agent_content), self.max_content_lines * 3) if agent_content else 0
1667
+
1668
+ # if max_display_lines == 0:
1669
+ # content_text.append("No activity yet...", style=self.colors['text'])
1670
+ # else:
1671
+ # # Show recent content with scrolling support
1672
+ # display_content = agent_content[-max_display_lines:] if max_display_lines > 0 else agent_content
1673
+
1674
+ # for line in display_content:
1675
+ # formatted_line = self._format_content_line(line)
1676
+ # content_text.append(formatted_line)
1677
+ # content_text.append("\n")
1678
+
1679
+ max_lines = max(0, self.agent_panel_height - 3)
1680
+ if not agent_content:
1681
+ content_text.append("No activity yet...", style=self.colors["text"])
1682
+ else:
1683
+ for line in agent_content[-max_lines:]:
1684
+ formatted_line = self._format_content_line(line)
1685
+ content_text.append(formatted_line)
1686
+ content_text.append("\n")
1687
+
1688
+ # Status indicator
1689
+ status_emoji = self._get_status_emoji(status, activity)
1690
+ status_color = self._get_status_color(status)
1691
+
1692
+ # Get backend info if available
1693
+ backend_name = self._get_backend_name(agent_id)
1694
+
1695
+ # Panel title with click indicator
1696
+ title = f"{status_emoji} {agent_id.upper()}"
1697
+ if backend_name != "Unknown":
1698
+ title += f" ({backend_name})"
1699
+
1700
+ # Add interactive indicator if enabled
1701
+ if self._keyboard_interactive_mode and hasattr(self, "_agent_keys"):
1702
+ agent_key = next(
1703
+ (k for k, v in self._agent_keys.items() if v == agent_id), None
1704
+ )
1705
+ if agent_key:
1706
+ title += f" [Press {agent_key}]"
1707
+
1708
+ # Create panel with scrollable content
1709
+ return Panel(
1710
+ content_text,
1711
+ title=f"[{status_color}]{title}[/{status_color}]",
1712
+ border_style=status_color,
1713
+ box=ROUNDED,
1714
+ height=self.agent_panel_height,
1715
+ width=self.fixed_column_width,
1716
+ )
1717
+
1718
+ def _format_content_line(self, line: str) -> Text:
1719
+ """Format a content line with syntax highlighting and styling."""
1720
+ formatted = Text()
1721
+
1722
+ # Skip empty lines
1723
+ if not line.strip():
1724
+ return formatted
1725
+
1726
+ # Enhanced handling for web search content
1727
+ if self._is_web_search_content(line):
1728
+ return self._format_web_search_line(line)
1729
+
1730
+ # Truncate line if too long
1731
+ if len(line) > self.max_line_length:
1732
+ line = line[: self.max_line_length - 3] + "..."
1733
+
1734
+ # Check for special prefixes and format accordingly
1735
+ if line.startswith("→"):
1736
+ # Tool usage
1737
+ formatted.append("→ ", style=self.colors["warning"])
1738
+ formatted.append(line[2:], style=self.colors["text"])
1739
+ elif line.startswith("🎤"):
1740
+ # Presentation content
1741
+ formatted.append("🎤 ", style=self.colors["success"])
1742
+ formatted.append(line[3:], style=f"bold {self.colors['success']}")
1743
+ elif line.startswith("⚡"):
1744
+ # Working indicator or status jump indicator
1745
+ formatted.append("⚡ ", style=self.colors["warning"])
1746
+ if "jumped to latest" in line:
1747
+ formatted.append(line[3:], style=f"bold {self.colors['info']}")
1748
+ else:
1749
+ formatted.append(line[3:], style=f"italic {self.colors['warning']}")
1750
+ elif self._is_code_content(line):
1751
+ # Code content - apply syntax highlighting
1752
+ if self.enable_syntax_highlighting:
1753
+ formatted = self._apply_syntax_highlighting(line)
1754
+ else:
1755
+ formatted.append(line, style=f"bold {self.colors['info']}")
1756
+ else:
1757
+ # Regular content
1758
+ formatted.append(line, style=self.colors["text"])
1759
+
1760
+ return formatted
1761
+
1762
+ def _format_presentation_content(self, content: str) -> Text:
1763
+ """Format presentation content with enhanced styling for orchestrator queries."""
1764
+ formatted = Text()
1765
+
1766
+ # Split content into lines for better formatting
1767
+ lines = content.split("\n") if "\n" in content else [content]
1768
+
1769
+ for line in lines:
1770
+ if not line.strip():
1771
+ formatted.append("\n")
1772
+ continue
1773
+
1774
+ # Special formatting for orchestrator query responses
1775
+ if line.startswith("**") and line.endswith("**"):
1776
+ # Bold emphasis for important points
1777
+ clean_line = line.strip("*").strip()
1778
+ formatted.append(clean_line, style=f"bold {self.colors['success']}")
1779
+ elif line.startswith("- ") or line.startswith("• "):
1780
+ # Bullet points with enhanced styling
1781
+ formatted.append(line[:2], style=self.colors["primary"])
1782
+ formatted.append(line[2:], style=self.colors["text"])
1783
+ elif line.startswith("#"):
1784
+ # Headers with different styling
1785
+ header_level = len(line) - len(line.lstrip("#"))
1786
+ clean_header = line.lstrip("# ").strip()
1787
+ if header_level <= 2:
1788
+ formatted.append(
1789
+ clean_header, style=f"bold {self.colors['header_style']}"
1790
+ )
1791
+ else:
1792
+ formatted.append(
1793
+ clean_header, style=f"bold {self.colors['primary']}"
1794
+ )
1795
+ elif self._is_code_content(line):
1796
+ # Code blocks in presentations
1797
+ if self.enable_syntax_highlighting:
1798
+ formatted.append(self._apply_syntax_highlighting(line))
1799
+ else:
1800
+ formatted.append(line, style=f"bold {self.colors['info']}")
1801
+ else:
1802
+ # Regular presentation text with enhanced readability
1803
+ formatted.append(line, style=self.colors["text"])
1804
+
1805
+ # Add newline except for the last line
1806
+ if line != lines[-1]:
1807
+ formatted.append("\n")
1808
+
1809
+ return formatted
1810
+
1811
+ def _is_web_search_content(self, line: str) -> bool:
1812
+ """Check if content is from web search and needs special formatting."""
1813
+ web_search_indicators = [
1814
+ "[Provider Tool: Web Search]",
1815
+ "🔍 [Search Query]",
1816
+ "✅ [Provider Tool: Web Search]",
1817
+ "🔍 [Provider Tool: Web Search]",
1818
+ ]
1819
+ return any(indicator in line for indicator in web_search_indicators)
1820
+
1821
+ def _format_web_search_line(self, line: str) -> Text:
1822
+ """Format web search content with better truncation and styling."""
1823
+ formatted = Text()
1824
+
1825
+ # Handle different types of web search lines
1826
+ if "[Provider Tool: Web Search] Starting search" in line:
1827
+ formatted.append("🔍 ", style=self.colors["info"])
1828
+ formatted.append("Web search starting...", style=self.colors["text"])
1829
+ elif "[Provider Tool: Web Search] Searching" in line:
1830
+ formatted.append("🔍 ", style=self.colors["warning"])
1831
+ formatted.append("Searching...", style=self.colors["text"])
1832
+ elif "[Provider Tool: Web Search] Search completed" in line:
1833
+ formatted.append("✅ ", style=self.colors["success"])
1834
+ formatted.append("Search completed", style=self.colors["text"])
1835
+ elif any(
1836
+ pattern in line
1837
+ for pattern in ["🔍 [Search Query]", "Search Query:", "[Search Query]"]
1838
+ ):
1839
+ # Extract and display search query with better formatting
1840
+ # Try different patterns to extract the query
1841
+ query = None
1842
+ patterns = [
1843
+ ("🔍 [Search Query]", ""),
1844
+ ("[Search Query]", ""),
1845
+ ("Search Query:", ""),
1846
+ ("Query:", ""),
1847
+ ]
1848
+
1849
+ for pattern, _ in patterns:
1850
+ if pattern in line:
1851
+ parts = line.split(pattern, 1)
1852
+ if len(parts) > 1:
1853
+ query = parts[1].strip().strip("'\"").strip()
1854
+ break
1855
+
1856
+ if query:
1857
+ # Format the search query nicely
1858
+ if len(query) > 80:
1859
+ # For long queries, show beginning and end
1860
+ query = query[:60] + "..." + query[-17:]
1861
+ formatted.append("🔍 Search: ", style=self.colors["info"])
1862
+ formatted.append(f'"{query}"', style=f"italic {self.colors['text']}")
1863
+ else:
1864
+ formatted.append("🔍 Search query", style=self.colors["info"])
1865
+ else:
1866
+ # For long web search results, truncate more aggressively
1867
+ max_web_length = min(
1868
+ self.max_line_length // 2, 60
1869
+ ) # Much shorter for web content
1870
+ if len(line) > max_web_length:
1871
+ # Try to find a natural break point
1872
+ truncated = line[:max_web_length]
1873
+ # Look for sentence or phrase endings
1874
+ for break_char in [". ", "! ", "? ", ", ", ": "]:
1875
+ last_break = truncated.rfind(break_char)
1876
+ if last_break > max_web_length // 2:
1877
+ truncated = truncated[: last_break + 1]
1878
+ break
1879
+ line = truncated + "..."
1880
+
1881
+ formatted.append(line, style=self.colors["text"])
1882
+
1883
+ return formatted
1884
+
1885
+ def _should_filter_content(self, content: str, content_type: str) -> bool:
1886
+ """Determine if content should be filtered out to reduce noise."""
1887
+ # Never filter important content types
1888
+ if content_type in ["status", "presentation", "error"]:
1889
+ return False
1890
+
1891
+ # Filter out very long web search results that are mostly noise
1892
+ if len(content) > 1000 and self._is_web_search_content(content):
1893
+ # Check if it contains mostly URLs and technical details
1894
+ url_count = content.count("http")
1895
+ technical_indicators = (
1896
+ content.count("[")
1897
+ + content.count("]")
1898
+ + content.count("(")
1899
+ + content.count(")")
1900
+ )
1901
+
1902
+ # If more than 50% seems to be technical metadata, filter it
1903
+ if url_count > 5 or technical_indicators > len(content) * 0.1:
1904
+ return True
1905
+
1906
+ return False
1907
+
1908
+ def _should_filter_line(self, line: str) -> bool:
1909
+ """Determine if a specific line should be filtered out."""
1910
+ # Filter lines that are pure metadata or formatting
1911
+ filter_patterns = [
1912
+ r"^\s*\([^)]+\)\s*$", # Lines with just parenthetical citations
1913
+ r"^\s*\[[^\]]+\]\s*$", # Lines with just bracketed metadata
1914
+ r"^\s*https?://\S+\s*$", # Lines with just URLs
1915
+ r"^\s*\.\.\.\s*$", # Lines with just ellipsis
1916
+ ]
1917
+
1918
+ for pattern in filter_patterns:
1919
+ if re.match(pattern, line):
1920
+ return True
1921
+
1922
+ return False
1923
+
1924
+ def _truncate_web_search_content(self, agent_id: str):
1925
+ """Truncate web search content when important status updates occur."""
1926
+ if agent_id not in self.agent_outputs or not self.agent_outputs[agent_id]:
1927
+ return
1928
+
1929
+ # Find web search content and truncate to keep only recent important lines
1930
+ content_lines = self.agent_outputs[agent_id]
1931
+ web_search_lines = []
1932
+ non_web_search_lines = []
1933
+
1934
+ # Separate web search content from other content
1935
+ for line in content_lines:
1936
+ if self._is_web_search_content(line):
1937
+ web_search_lines.append(line)
1938
+ else:
1939
+ non_web_search_lines.append(line)
1940
+
1941
+ # If there's a lot of web search content, truncate it
1942
+ if len(web_search_lines) > self._max_web_search_lines:
1943
+ # Keep only the first line (search start) and last few lines (search end/results)
1944
+ truncated_web_search = (
1945
+ web_search_lines[:1] # First line (search start)
1946
+ + ["🔍 ... (web search content truncated due to status update) ..."]
1947
+ + web_search_lines[
1948
+ -(self._max_web_search_lines - 2) :
1949
+ ] # Last few lines
1950
+ )
1951
+
1952
+ # Reconstruct the content with truncated web search
1953
+ # Keep recent non-web-search content and add truncated web search
1954
+ recent_non_web = non_web_search_lines[
1955
+ -(max(5, self.max_content_lines - len(truncated_web_search))) :
1956
+ ]
1957
+ self.agent_outputs[agent_id] = recent_non_web + truncated_web_search
1958
+
1959
+ # Add a status jump indicator only if content was actually truncated
1960
+ if len(web_search_lines) > self._max_web_search_lines:
1961
+ self.agent_outputs[agent_id].append("⚡ Status updated - jumped to latest")
1962
+
1963
+ def _is_code_content(self, content: str) -> bool:
1964
+ """Check if content appears to be code."""
1965
+ for pattern in self.code_patterns:
1966
+ if re.search(pattern, content, re.DOTALL | re.IGNORECASE):
1967
+ return True
1968
+ return False
1969
+
1970
+ def _apply_syntax_highlighting(self, content: str) -> Text:
1971
+ """Apply syntax highlighting to content."""
1972
+ try:
1973
+ # Try to detect language
1974
+ language = self._detect_language(content)
1975
+
1976
+ if language:
1977
+ # Use Rich Syntax for highlighting (simplified for now)
1978
+ return Text(content, style=f"bold {self.colors['info']}")
1979
+ else:
1980
+ return Text(content, style=f"bold {self.colors['info']}")
1981
+ except:
1982
+ return Text(content, style=f"bold {self.colors['info']}")
1983
+
1984
+ def _detect_language(self, content: str) -> Optional[str]:
1985
+ """Detect programming language from content."""
1986
+ content_lower = content.lower()
1987
+
1988
+ if any(
1989
+ keyword in content_lower
1990
+ for keyword in ["def ", "import ", "class ", "python"]
1991
+ ):
1992
+ return "python"
1993
+ elif any(
1994
+ keyword in content_lower
1995
+ for keyword in ["function", "var ", "let ", "const "]
1996
+ ):
1997
+ return "javascript"
1998
+ elif any(keyword in content_lower for keyword in ["<", ">", "html", "div"]):
1999
+ return "html"
2000
+ elif any(keyword in content_lower for keyword in ["{", "}", "json"]):
2001
+ return "json"
2002
+
2003
+ return None
2004
+
2005
+ def _get_status_emoji(self, status: str, activity: str) -> str:
2006
+ """Get emoji for agent status."""
2007
+ if status == "working":
2008
+ return "🔄"
2009
+ elif status == "completed":
2010
+ if "voted" in activity.lower():
2011
+ return "🗳️" # Vote emoji for any voting activity
2012
+ elif "failed" in activity.lower():
2013
+ return "❌"
2014
+ else:
2015
+ return "✅"
2016
+ elif status == "waiting":
2017
+ return "⏳"
2018
+ else:
2019
+ return "❓"
2020
+
2021
+ def _get_status_color(self, status: str) -> str:
2022
+ """Get color for agent status."""
2023
+ status_colors = {
2024
+ "working": self.colors["warning"],
2025
+ "completed": self.colors["success"],
2026
+ "waiting": self.colors["info"],
2027
+ "failed": self.colors["error"],
2028
+ }
2029
+ return status_colors.get(status, self.colors["text"])
2030
+
2031
+ def _get_backend_name(self, agent_id: str) -> str:
2032
+ """Get backend name for agent."""
2033
+ try:
2034
+ if (
2035
+ hasattr(self, "orchestrator")
2036
+ and self.orchestrator
2037
+ and hasattr(self.orchestrator, "agents")
2038
+ ):
2039
+ agent = self.orchestrator.agents.get(agent_id)
2040
+ if (
2041
+ agent
2042
+ and hasattr(agent, "backend")
2043
+ and hasattr(agent.backend, "get_provider_name")
2044
+ ):
2045
+ return agent.backend.get_provider_name()
2046
+ except:
2047
+ pass
2048
+ return "Unknown"
2049
+
2050
+ def _create_footer(self) -> Panel:
2051
+ """Create the footer panel with status and events."""
2052
+ footer_content = Text()
2053
+
2054
+ # Agent status summary
2055
+ footer_content.append("📊 Agent Status: ", style=self.colors["primary"])
2056
+
2057
+ status_counts = {}
2058
+ for status in self.agent_status.values():
2059
+ status_counts[status] = status_counts.get(status, 0) + 1
2060
+
2061
+ status_parts = []
2062
+ for status, count in status_counts.items():
2063
+ emoji = self._get_status_emoji(status, status)
2064
+ status_parts.append(f"{emoji} {status.title()}: {count}")
2065
+
2066
+ footer_content.append(" | ".join(status_parts), style=self.colors["text"])
2067
+ footer_content.append("\n")
2068
+
2069
+ # Recent events
2070
+ if self.orchestrator_events:
2071
+ footer_content.append("📋 Recent Events:\n", style=self.colors["primary"])
2072
+ recent_events = self.orchestrator_events[-3:] # Show last 3 events
2073
+ for event in recent_events:
2074
+ footer_content.append(f" • {event}\n", style=self.colors["text"])
2075
+
2076
+ # Log file info
2077
+ if self.log_filename:
2078
+ footer_content.append(
2079
+ f"📁 Log: {self.log_filename}\n", style=self.colors["info"]
2080
+ )
2081
+
2082
+ # Interactive mode instructions
2083
+ if self._keyboard_interactive_mode and hasattr(self, "_agent_keys"):
2084
+ if self._safe_keyboard_mode:
2085
+ footer_content.append(
2086
+ "📂 Safe Mode: Keyboard disabled to prevent rendering issues\n",
2087
+ style=self.colors["warning"],
2088
+ )
2089
+ footer_content.append(
2090
+ f"Output files saved in: {self.output_dir}/",
2091
+ style=self.colors["info"],
2092
+ )
2093
+ else:
2094
+ footer_content.append(
2095
+ "🎮 Live Mode Hotkeys: Press 1-", style=self.colors["primary"]
2096
+ )
2097
+ footer_content.append(
2098
+ f"{len(self.agent_ids)} to open agent files in editor, 's' for system status",
2099
+ style=self.colors["text"],
2100
+ )
2101
+ footer_content.append(
2102
+ f"\n📂 Output files saved in: {self.output_dir}/",
2103
+ style=self.colors["info"],
2104
+ )
2105
+
2106
+ return Panel(
2107
+ footer_content,
2108
+ title="[bold]System Status [Press s][/bold]",
2109
+ border_style=self.colors["border"],
2110
+ box=ROUNDED,
2111
+ )
2112
+
2113
+ def update_agent_content(
2114
+ self, agent_id: str, content: str, content_type: str = "thinking"
2115
+ ):
2116
+ """Update content for a specific agent with rich formatting and file output."""
2117
+
2118
+ if agent_id not in self.agent_ids:
2119
+ return
2120
+
2121
+ with self._lock:
2122
+ # Initialize agent outputs if needed
2123
+ if agent_id not in self.agent_outputs:
2124
+ self.agent_outputs[agent_id] = []
2125
+
2126
+ # Write content to agent's txt file
2127
+ self._write_to_agent_file(agent_id, content, content_type)
2128
+
2129
+ # Check if this is a status-changing content that should trigger web search truncation
2130
+ is_status_change = content_type in [
2131
+ "status",
2132
+ "presentation",
2133
+ "tool",
2134
+ ] or any(
2135
+ keyword in content.lower() for keyword in self._status_change_keywords
2136
+ )
2137
+
2138
+ # If status jump is enabled and this is a status change, truncate web search content
2139
+ if (
2140
+ self._status_jump_enabled
2141
+ and is_status_change
2142
+ and self._web_search_truncate_on_status_change
2143
+ and self.agent_outputs[agent_id]
2144
+ ):
2145
+
2146
+ self._truncate_web_search_content(agent_id)
2147
+
2148
+ # Enhanced filtering for web search content
2149
+ if self._should_filter_content(content, content_type):
2150
+ return
2151
+
2152
+ # Process content with buffering for smoother text display
2153
+ self._process_content_with_buffering(agent_id, content, content_type)
2154
+
2155
+ # Categorize updates by priority for layered refresh strategy
2156
+ self._categorize_update(agent_id, content_type, content)
2157
+
2158
+ # Schedule update based on priority
2159
+ is_critical = content_type in [
2160
+ "tool",
2161
+ "status",
2162
+ "presentation",
2163
+ "error",
2164
+ ] or any(
2165
+ keyword in content.lower() for keyword in self._status_change_keywords
2166
+ )
2167
+ self._schedule_layered_update(agent_id, is_critical)
2168
+
2169
+ def _process_content_with_buffering(
2170
+ self, agent_id: str, content: str, content_type: str
2171
+ ):
2172
+ """Process content with buffering to accumulate text chunks."""
2173
+ # Cancel any existing buffer timer
2174
+ if self._buffer_timers.get(agent_id):
2175
+ self._buffer_timers[agent_id].cancel()
2176
+ self._buffer_timers[agent_id] = None
2177
+
2178
+ # Special handling for content that should be displayed immediately
2179
+ if (
2180
+ content_type in ["tool", "status", "presentation", "error"]
2181
+ or "\n" in content
2182
+ ):
2183
+ # Flush any existing buffer first
2184
+ self._flush_buffer(agent_id)
2185
+
2186
+ # Process multi-line content line by line
2187
+ if "\n" in content:
2188
+ for line in content.splitlines():
2189
+ if line.strip() and not self._should_filter_line(line):
2190
+ self.agent_outputs[agent_id].append(line)
2191
+ else:
2192
+ # Add single-line important content directly
2193
+ if content.strip():
2194
+ self.agent_outputs[agent_id].append(content.strip())
2195
+ return
2196
+
2197
+ # Add content to buffer
2198
+ self._text_buffers[agent_id] += content
2199
+ buffer = self._text_buffers[agent_id]
2200
+
2201
+ # Simple buffer management - flush when buffer gets too long or after timeout
2202
+ if len(buffer) >= self._max_buffer_length:
2203
+ self._flush_buffer(agent_id)
2204
+ return
2205
+
2206
+ # Set a timer to flush the buffer if no more content arrives
2207
+ self._set_buffer_timer(agent_id)
2208
+
2209
+ def _flush_buffer(self, agent_id: str):
2210
+ """Flush the buffer for a specific agent."""
2211
+ if agent_id in self._text_buffers and self._text_buffers[agent_id]:
2212
+ buffer_content = self._text_buffers[agent_id].strip()
2213
+ if buffer_content:
2214
+ self.agent_outputs[agent_id].append(buffer_content)
2215
+ self._text_buffers[agent_id] = ""
2216
+
2217
+ # Cancel any existing timer
2218
+ if self._buffer_timers.get(agent_id):
2219
+ self._buffer_timers[agent_id].cancel()
2220
+ self._buffer_timers[agent_id] = None
2221
+
2222
+ def _set_buffer_timer(self, agent_id: str):
2223
+ """Set a timer to flush the buffer after a timeout."""
2224
+ if self._shutdown_flag:
2225
+ return
2226
+
2227
+ # Cancel existing timer if any
2228
+ if self._buffer_timers.get(agent_id):
2229
+ self._buffer_timers[agent_id].cancel()
2230
+
2231
+ def timeout_flush():
2232
+ with self._lock:
2233
+ if agent_id in self._text_buffers and self._text_buffers[agent_id]:
2234
+ self._flush_buffer(agent_id)
2235
+ # Trigger display update
2236
+ self._pending_updates.add(agent_id)
2237
+ self._schedule_async_update(force_update=True)
2238
+
2239
+ self._buffer_timers[agent_id] = threading.Timer(
2240
+ self._buffer_timeout, timeout_flush
2241
+ )
2242
+ self._buffer_timers[agent_id].start()
2243
+
2244
+ def _write_to_agent_file(self, agent_id: str, content: str, content_type: str):
2245
+ """Write content to agent's individual txt file."""
2246
+ if agent_id not in self.agent_files:
2247
+ return
2248
+
2249
+ try:
2250
+ file_path = self.agent_files[agent_id]
2251
+ timestamp = time.strftime("%H:%M:%S")
2252
+
2253
+ # Check if content contains emojis
2254
+ has_emoji = any(
2255
+ ord(char) > 127
2256
+ and ord(char) in range(0x1F600, 0x1F64F)
2257
+ or ord(char) in range(0x1F300, 0x1F5FF)
2258
+ or ord(char) in range(0x1F680, 0x1F6FF)
2259
+ or ord(char) in range(0x2600, 0x26FF)
2260
+ or ord(char) in range(0x2700, 0x27BF)
2261
+ for char in content
2262
+ )
2263
+
2264
+ if has_emoji:
2265
+ # Format with newline and timestamp when emojis are present
2266
+ formatted_content = (
2267
+ f"\n[{timestamp}] [{content_type.upper()}] {content}\n"
2268
+ )
2269
+ else:
2270
+ # Regular format without extra newline
2271
+ formatted_content = f"{content}"
2272
+
2273
+ # Append to file
2274
+ with open(file_path, "a", encoding="utf-8") as f:
2275
+ f.write(formatted_content)
2276
+
2277
+ except Exception as e:
2278
+ # Handle file write errors gracefully
2279
+ pass
2280
+
2281
+ def _write_system_status(self):
2282
+ """Write current system status to system status file - shows orchestrator events chronologically by time."""
2283
+ if not self.system_status_file:
2284
+ return
2285
+
2286
+ try:
2287
+ # Clear file and write all orchestrator events chronologically
2288
+ with open(self.system_status_file, "w", encoding="utf-8") as f:
2289
+ f.write("=== SYSTEM STATUS LOG ===\n\n")
2290
+
2291
+ # Show all orchestrator events in chronological order by time
2292
+ if self.orchestrator_events:
2293
+ for event in self.orchestrator_events:
2294
+ f.write(f" • {event}\n")
2295
+ else:
2296
+ f.write(" • No orchestrator events yet\n")
2297
+
2298
+ f.write("\n")
2299
+
2300
+ except Exception as e:
2301
+ # Handle file write errors gracefully
2302
+ pass
2303
+
2304
+ def update_agent_status(self, agent_id: str, status: str):
2305
+ """Update status for a specific agent with rich indicators."""
2306
+ if agent_id not in self.agent_ids:
2307
+ return
2308
+
2309
+ with self._lock:
2310
+ old_status = self.agent_status.get(agent_id, "waiting")
2311
+ last_tracked_status = self._last_agent_status.get(agent_id, "waiting")
2312
+
2313
+ # Check if this is a vote-related status change
2314
+ current_activity = self.agent_activity.get(agent_id, "")
2315
+ is_vote_status = (
2316
+ "voted" in status.lower() or "voted" in current_activity.lower()
2317
+ )
2318
+
2319
+ # Force update for vote statuses or actual status changes
2320
+ should_update = (
2321
+ old_status != status and last_tracked_status != status
2322
+ ) or is_vote_status
2323
+
2324
+ if should_update:
2325
+ # Truncate web search content when status changes for immediate focus on new status
2326
+ if (
2327
+ self._status_jump_enabled
2328
+ and self._web_search_truncate_on_status_change
2329
+ and old_status != status
2330
+ and agent_id in self.agent_outputs
2331
+ and self.agent_outputs[agent_id]
2332
+ ):
2333
+
2334
+ self._truncate_web_search_content(agent_id)
2335
+
2336
+ super().update_agent_status(agent_id, status)
2337
+ self._last_agent_status[agent_id] = status
2338
+
2339
+ # Mark for priority update - status changes get highest priority
2340
+ self._priority_updates.add(agent_id)
2341
+ self._pending_updates.add(agent_id)
2342
+ self._pending_updates.add("footer")
2343
+ self._schedule_priority_update(agent_id)
2344
+ self._schedule_async_update(force_update=True)
2345
+
2346
+ # Write system status update
2347
+ self._write_system_status()
2348
+ elif old_status != status:
2349
+ # Update the internal status but don't refresh display if already tracked
2350
+ super().update_agent_status(agent_id, status)
2351
+
2352
+ def add_orchestrator_event(self, event: str):
2353
+ """Add an orchestrator coordination event with timestamp."""
2354
+ with self._lock:
2355
+ if self.show_timestamps:
2356
+ timestamp = time.strftime("%H:%M:%S")
2357
+ formatted_event = f"[{timestamp}] {event}"
2358
+ else:
2359
+ formatted_event = event
2360
+
2361
+ # Check for duplicate events
2362
+ if (
2363
+ hasattr(self, "orchestrator_events")
2364
+ and self.orchestrator_events
2365
+ and self.orchestrator_events[-1] == formatted_event
2366
+ ):
2367
+ return # Skip duplicate events
2368
+
2369
+ super().add_orchestrator_event(formatted_event)
2370
+
2371
+ # Only update footer for important events that indicate real status changes
2372
+ if any(
2373
+ keyword in event.lower() for keyword in self._important_event_keywords
2374
+ ):
2375
+ # Mark footer for async update
2376
+ self._pending_updates.add("footer")
2377
+ self._schedule_async_update(force_update=True)
2378
+ # Write system status update for important events
2379
+ self._write_system_status()
2380
+
2381
+ def display_vote_results(self, vote_results: Dict[str, Any]):
2382
+ """Display voting results in a formatted rich panel."""
2383
+ if not vote_results or not vote_results.get("vote_counts"):
2384
+ return
2385
+
2386
+ # Stop live display temporarily for clean voting results output
2387
+ was_live = self.live is not None
2388
+ if self.live:
2389
+ self.live.stop()
2390
+ self.live = None
2391
+
2392
+ vote_counts = vote_results.get("vote_counts", {})
2393
+ voter_details = vote_results.get("voter_details", {})
2394
+ winner = vote_results.get("winner")
2395
+ is_tie = vote_results.get("is_tie", False)
2396
+
2397
+ # Create voting results content
2398
+ vote_content = Text()
2399
+
2400
+ # Vote count section
2401
+ vote_content.append("📊 Vote Count:\n", style=self.colors["primary"])
2402
+ for agent_id, count in sorted(
2403
+ vote_counts.items(), key=lambda x: x[1], reverse=True
2404
+ ):
2405
+ winner_mark = "🏆" if agent_id == winner else " "
2406
+ tie_mark = " (tie-broken)" if is_tie and agent_id == winner else ""
2407
+ vote_content.append(
2408
+ f" {winner_mark} {agent_id}: {count} vote{'s' if count != 1 else ''}{tie_mark}\n",
2409
+ style=(
2410
+ self.colors["success"]
2411
+ if agent_id == winner
2412
+ else self.colors["text"]
2413
+ ),
2414
+ )
2415
+
2416
+ # Vote details section
2417
+ if voter_details:
2418
+ vote_content.append("\n🔍 Vote Details:\n", style=self.colors["primary"])
2419
+ for voted_for, voters in voter_details.items():
2420
+ vote_content.append(f" → {voted_for}:\n", style=self.colors["info"])
2421
+ for voter_info in voters:
2422
+ voter = voter_info["voter"]
2423
+ reason = voter_info["reason"]
2424
+ vote_content.append(
2425
+ f' • {voter}: "{reason}"\n', style=self.colors["text"]
2426
+ )
2427
+
2428
+ # Tie-breaking info
2429
+ if is_tie:
2430
+ vote_content.append(
2431
+ "\n⚖️ Tie broken by agent registration order\n",
2432
+ style=self.colors["warning"],
2433
+ )
2434
+
2435
+ # Summary stats
2436
+ total_votes = vote_results.get("total_votes", 0)
2437
+ agents_voted = vote_results.get("agents_voted", 0)
2438
+ vote_content.append(
2439
+ f"\n📈 Summary: {agents_voted}/{total_votes} agents voted",
2440
+ style=self.colors["info"],
2441
+ )
2442
+
2443
+ # Create and display the voting panel
2444
+ voting_panel = Panel(
2445
+ vote_content,
2446
+ title="[bold bright_cyan]🗳️ VOTING RESULTS[/bold bright_cyan]",
2447
+ border_style=self.colors["primary"],
2448
+ box=DOUBLE,
2449
+ expand=False,
2450
+ )
2451
+
2452
+ self.console.print(voting_panel)
2453
+ self.console.print()
2454
+
2455
+ # Restart live display if it was active
2456
+ if was_live:
2457
+ self.live = Live(
2458
+ self._create_layout(),
2459
+ console=self.console,
2460
+ refresh_per_second=self.refresh_rate,
2461
+ vertical_overflow="ellipsis",
2462
+ transient=False,
2463
+ )
2464
+ self.live.start()
2465
+
2466
+ async def display_final_presentation(
2467
+ self,
2468
+ selected_agent: str,
2469
+ presentation_stream,
2470
+ vote_results: Dict[str, Any] = None,
2471
+ ):
2472
+ """Display final presentation from winning agent with enhanced orchestrator query support."""
2473
+ if not selected_agent:
2474
+ return ""
2475
+
2476
+ # Stop live display for clean presentation output
2477
+ was_live = self.live is not None
2478
+ if self.live:
2479
+ self.live.stop()
2480
+ self.live = None
2481
+
2482
+ # Create presentation header with orchestrator context
2483
+ header_text = Text()
2484
+ header_text.append(
2485
+ f"🎤 Final Presentation from {selected_agent}",
2486
+ style=self.colors["header_style"],
2487
+ )
2488
+ if vote_results and vote_results.get("vote_counts"):
2489
+ vote_count = vote_results["vote_counts"].get(selected_agent, 0)
2490
+ header_text.append(
2491
+ f" (Selected with {vote_count} votes)", style=self.colors["info"]
2492
+ )
2493
+
2494
+ header_panel = Panel(
2495
+ Align.center(header_text),
2496
+ border_style=self.colors["success"],
2497
+ box=DOUBLE,
2498
+ title="[bold]Final Presentation[/bold]",
2499
+ )
2500
+
2501
+ self.console.print(header_panel)
2502
+ self.console.print("=" * 60)
2503
+
2504
+ presentation_content = ""
2505
+ chunk_count = 0
2506
+
2507
+ try:
2508
+ # Enhanced streaming with orchestrator query awareness
2509
+ async for chunk in presentation_stream:
2510
+ chunk_count += 1
2511
+ content = getattr(chunk, "content", "") or ""
2512
+ chunk_type = getattr(chunk, "type", "")
2513
+ source = getattr(chunk, "source", selected_agent)
2514
+
2515
+ if content:
2516
+ # Ensure content is a string
2517
+ if isinstance(content, list):
2518
+ content = " ".join(str(item) for item in content)
2519
+ elif not isinstance(content, str):
2520
+ content = str(content)
2521
+
2522
+ # Accumulate content
2523
+ presentation_content += content
2524
+
2525
+ # Enhanced formatting for orchestrator query responses
2526
+ if chunk_type == "status":
2527
+ # Status updates from orchestrator query
2528
+ status_text = Text(f"🔄 {content}", style=self.colors["info"])
2529
+ self.console.print(status_text)
2530
+ elif "error" in chunk_type:
2531
+ # Error handling in orchestrator query
2532
+ error_text = Text(f"❌ {content}", style=self.colors["error"])
2533
+ self.console.print(error_text)
2534
+ else:
2535
+ # Main presentation content with simple output
2536
+ self.console.print(content, end="", highlight=False)
2537
+
2538
+ # Handle orchestrator query completion signals
2539
+ if chunk_type == "done":
2540
+ completion_text = Text(
2541
+ f"\n✅ Presentation completed by {source}",
2542
+ style=self.colors["success"],
2543
+ )
2544
+ self.console.print(completion_text)
2545
+ break
2546
+
2547
+ except Exception as e:
2548
+ # Enhanced error handling for orchestrator queries
2549
+ error_text = Text(
2550
+ f"❌ Error during final presentation: {e}", style=self.colors["error"]
2551
+ )
2552
+ self.console.print(error_text)
2553
+
2554
+ # Fallback: try to get content from agent's stored answer
2555
+ if hasattr(self, "orchestrator") and self.orchestrator:
2556
+ try:
2557
+ status = self.orchestrator.get_status()
2558
+ if selected_agent in status.get("agent_states", {}):
2559
+ stored_answer = status["agent_states"][selected_agent].get(
2560
+ "answer", ""
2561
+ )
2562
+ if stored_answer:
2563
+ fallback_text = Text(
2564
+ f"\n📋 Fallback to stored answer:\n{stored_answer}",
2565
+ style=self.colors["text"],
2566
+ )
2567
+ self.console.print(fallback_text)
2568
+ presentation_content = stored_answer
2569
+ except Exception:
2570
+ pass
2571
+
2572
+ self.console.print("\n" + "=" * 60)
2573
+
2574
+ # Show presentation statistics
2575
+ if chunk_count > 0:
2576
+ stats_text = Text(
2577
+ f"📊 Presentation processed {chunk_count} chunks",
2578
+ style=self.colors["info"],
2579
+ )
2580
+ self.console.print(stats_text)
2581
+
2582
+ # Store the presentation content for later re-display
2583
+ if presentation_content:
2584
+ self._stored_final_presentation = presentation_content
2585
+ self._stored_presentation_agent = selected_agent
2586
+ self._stored_vote_results = vote_results
2587
+
2588
+ # Restart live display if needed
2589
+ if was_live:
2590
+ time.sleep(0.5) # Brief pause before restarting live display
2591
+
2592
+ return presentation_content
2593
+
2594
+ def show_final_answer(
2595
+ self,
2596
+ answer: str,
2597
+ vote_results: Dict[str, Any] = None,
2598
+ selected_agent: str = None,
2599
+ ):
2600
+ """Display the final coordinated answer prominently with voting results, final presentation, and agent selector."""
2601
+ # Flush all buffers before showing final answer
2602
+ with self._lock:
2603
+ self._flush_all_buffers()
2604
+
2605
+ # Stop live display first to ensure clean output
2606
+ if self.live:
2607
+ self.live.stop()
2608
+ self.live = None
2609
+
2610
+ # Auto-get vote results and selected agent from orchestrator if not provided
2611
+ if vote_results is None or selected_agent is None:
2612
+ try:
2613
+ if hasattr(self, "orchestrator") and self.orchestrator:
2614
+ status = self.orchestrator.get_status()
2615
+ vote_results = vote_results or status.get("vote_results", {})
2616
+ selected_agent = selected_agent or status.get("selected_agent")
2617
+ except:
2618
+ pass
2619
+
2620
+ # Force update all agent final statuses first (show voting results in agent panels)
2621
+ with self._lock:
2622
+ for agent_id in self.agent_ids:
2623
+ self._pending_updates.add(agent_id)
2624
+ self._pending_updates.add("footer")
2625
+ self._schedule_async_update(force_update=True)
2626
+
2627
+ # Wait for agent status updates to complete
2628
+ time.sleep(0.5)
2629
+ self._force_display_final_vote_statuses()
2630
+ time.sleep(0.5)
2631
+
2632
+ # Display voting results first if available
2633
+ if vote_results and vote_results.get("vote_counts"):
2634
+ self.display_vote_results(vote_results)
2635
+ time.sleep(1.0) # Allow time for voting results to be visible
2636
+
2637
+ # Now display only the selected agent instead of the full answer
2638
+ if selected_agent:
2639
+ selected_agent_text = Text(
2640
+ f"🏆 Selected agent: {selected_agent}", style=self.colors["success"]
2641
+ )
2642
+ else:
2643
+ selected_agent_text = Text(
2644
+ "No agent selected", style=self.colors["warning"]
2645
+ )
2646
+
2647
+ final_panel = Panel(
2648
+ Align.center(selected_agent_text),
2649
+ title="[bold bright_green]🎯 FINAL COORDINATED ANSWER[/bold bright_green]",
2650
+ border_style=self.colors["success"],
2651
+ box=DOUBLE,
2652
+ expand=False,
2653
+ )
2654
+
2655
+ self.console.print("\n")
2656
+ self.console.print(final_panel)
2657
+
2658
+ # Show which agent was selected
2659
+ if selected_agent:
2660
+ selection_text = Text()
2661
+ selection_text.append(
2662
+ f"✅ Selected by: {selected_agent}", style=self.colors["success"]
2663
+ )
2664
+ if vote_results and vote_results.get("vote_counts"):
2665
+ vote_summary = ", ".join(
2666
+ [
2667
+ f"{agent}: {count}"
2668
+ for agent, count in vote_results["vote_counts"].items()
2669
+ ]
2670
+ )
2671
+ selection_text.append(
2672
+ f"\n🗳️ Vote results: {vote_summary}", style=self.colors["info"]
2673
+ )
2674
+
2675
+ selection_panel = Panel(
2676
+ selection_text, border_style=self.colors["info"], box=ROUNDED
2677
+ )
2678
+ self.console.print(selection_panel)
2679
+
2680
+ self.console.print("\n")
2681
+
2682
+ # Display selected agent's final provided answer directly without flush
2683
+ # if selected_agent:
2684
+ # selected_agent_answer = self._get_selected_agent_final_answer(selected_agent)
2685
+ # if selected_agent_answer:
2686
+ # # Create header for the final answer
2687
+ # header_text = Text()
2688
+ # header_text.append(f"📝 {selected_agent}'s Final Provided Answer:", style=self.colors['primary'])
2689
+
2690
+ # header_panel = Panel(
2691
+ # header_text,
2692
+ # title=f"[bold]{selected_agent.upper()} Final Answer[/bold]",
2693
+ # border_style=self.colors['primary'],
2694
+ # box=ROUNDED
2695
+ # )
2696
+ # self.console.print(header_panel)
2697
+
2698
+ # # Display immediately without any flush effect
2699
+ # answer_panel = Panel(
2700
+ # Text(selected_agent_answer, style=self.colors['text']),
2701
+ # border_style=self.colors['border'],
2702
+ # box=ROUNDED
2703
+ # )
2704
+ # self.console.print(answer_panel)
2705
+ # self.console.print("\n")
2706
+
2707
+ # Display final presentation immediately after voting results
2708
+ if selected_agent and hasattr(self, "orchestrator") and self.orchestrator:
2709
+ try:
2710
+ self._show_orchestrator_final_presentation(selected_agent, vote_results)
2711
+ # Add a small delay to ensure presentation completes before agent selector
2712
+ time.sleep(1.0)
2713
+ except Exception as e:
2714
+ # Handle errors gracefully
2715
+ error_text = Text(
2716
+ f"❌ Error getting final presentation: {e}",
2717
+ style=self.colors["error"],
2718
+ )
2719
+ self.console.print(error_text)
2720
+
2721
+ # Show interactive options for viewing agent details (only if not in safe mode)
2722
+ if (
2723
+ self._keyboard_interactive_mode
2724
+ and hasattr(self, "_agent_keys")
2725
+ and not self._safe_keyboard_mode
2726
+ ):
2727
+ self.show_agent_selector()
2728
+
2729
+ def _display_answer_with_flush(self, answer: str):
2730
+ """Display answer with flush output effect - streaming character by character."""
2731
+ import time
2732
+ import sys
2733
+
2734
+ # Use configurable delays
2735
+ char_delay = self._flush_char_delay
2736
+ word_delay = self._flush_word_delay
2737
+ line_delay = 0.2 # Delay at line breaks
2738
+
2739
+ try:
2740
+ # Split answer into lines to handle multi-line text properly
2741
+ lines = answer.split("\n")
2742
+
2743
+ for line_idx, line in enumerate(lines):
2744
+ if not line.strip():
2745
+ # Empty line - just print newline and continue
2746
+ self.console.print()
2747
+ continue
2748
+
2749
+ # Display this line character by character
2750
+ for i, char in enumerate(line):
2751
+ # Print character with style, using end='' to stay on same line
2752
+ styled_char = Text(char, style=self.colors["text"])
2753
+ self.console.print(styled_char, end="", highlight=False)
2754
+
2755
+ # Flush immediately for real-time effect
2756
+ sys.stdout.flush()
2757
+
2758
+ # Add delays for natural reading rhythm
2759
+ if char in [" ", ",", ";"]:
2760
+ time.sleep(word_delay)
2761
+ elif char in [".", "!", "?", ":"]:
2762
+ time.sleep(word_delay * 2)
2763
+ else:
2764
+ time.sleep(char_delay)
2765
+
2766
+ # Add newline at end of line (except for last line which might not need it)
2767
+ if line_idx < len(lines) - 1:
2768
+ self.console.print() # Newline
2769
+ time.sleep(line_delay)
2770
+
2771
+ # Final newline
2772
+ self.console.print()
2773
+
2774
+ except KeyboardInterrupt:
2775
+ # If user interrupts, show the complete answer immediately
2776
+ self.console.print(f"\n{Text(answer, style=self.colors['text'])}")
2777
+ except Exception:
2778
+ # On any error, fallback to immediate display
2779
+ self.console.print(Text(answer, style=self.colors["text"]))
2780
+
2781
+ def _get_selected_agent_final_answer(self, selected_agent: str) -> str:
2782
+ """Get the final provided answer from the selected agent."""
2783
+ if not selected_agent:
2784
+ return ""
2785
+
2786
+ # First, try to get the answer from orchestrator's stored state
2787
+ try:
2788
+ if hasattr(self, "orchestrator") and self.orchestrator:
2789
+ status = self.orchestrator.get_status()
2790
+ if (
2791
+ hasattr(self.orchestrator, "agent_states")
2792
+ and selected_agent in self.orchestrator.agent_states
2793
+ ):
2794
+ stored_answer = self.orchestrator.agent_states[
2795
+ selected_agent
2796
+ ].answer
2797
+ if stored_answer:
2798
+ # Clean up the stored answer
2799
+ return (
2800
+ stored_answer.replace("\\", "\n").replace("**", "").strip()
2801
+ )
2802
+
2803
+ # Alternative: try getting from status
2804
+ if (
2805
+ "agent_states" in status
2806
+ and selected_agent in status["agent_states"]
2807
+ ):
2808
+ agent_state = status["agent_states"][selected_agent]
2809
+ if hasattr(agent_state, "answer") and agent_state.answer:
2810
+ return (
2811
+ agent_state.answer.replace("\\", "\n")
2812
+ .replace("**", "")
2813
+ .strip()
2814
+ )
2815
+ elif isinstance(agent_state, dict) and "answer" in agent_state:
2816
+ return (
2817
+ agent_state["answer"]
2818
+ .replace("\\", "\n")
2819
+ .replace("**", "")
2820
+ .strip()
2821
+ )
2822
+ except:
2823
+ pass
2824
+
2825
+ # Fallback: extract from agent outputs
2826
+ if selected_agent not in self.agent_outputs:
2827
+ return ""
2828
+
2829
+ agent_output = self.agent_outputs[selected_agent]
2830
+ if not agent_output:
2831
+ return ""
2832
+
2833
+ # Look for the most recent meaningful answer content
2834
+ answer_lines = []
2835
+
2836
+ # Scan backwards through the output to find the most recent answer
2837
+ for line in reversed(agent_output):
2838
+ line = line.strip()
2839
+ if not line:
2840
+ continue
2841
+
2842
+ # Skip status indicators and tool outputs
2843
+ if any(
2844
+ marker in line
2845
+ for marker in ["⚡", "🔄", "✅", "🗳️", "❌", "voted", "🔧", "status"]
2846
+ ):
2847
+ continue
2848
+
2849
+ # Stop at voting/coordination markers - we want the answer before voting
2850
+ if any(
2851
+ marker in line.lower()
2852
+ for marker in ["final coordinated", "coordination", "voting"]
2853
+ ):
2854
+ break
2855
+
2856
+ # Collect meaningful content
2857
+ answer_lines.insert(0, line)
2858
+
2859
+ # Stop when we have enough content or hit a natural break
2860
+ if len(answer_lines) >= 10 or len("\n".join(answer_lines)) > 500:
2861
+ break
2862
+
2863
+ # Clean and return the answer
2864
+ if answer_lines:
2865
+ answer = "\n".join(answer_lines).strip()
2866
+ # Remove common formatting artifacts
2867
+ answer = answer.replace("**", "").replace("##", "").strip()
2868
+ return answer
2869
+
2870
+ return ""
2871
+
2872
+ def _extract_presentation_content(self, selected_agent: str) -> str:
2873
+ """Extract presentation content from the selected agent's output."""
2874
+ if selected_agent not in self.agent_outputs:
2875
+ return ""
2876
+
2877
+ agent_output = self.agent_outputs[selected_agent]
2878
+ presentation_lines = []
2879
+
2880
+ # Look for presentation content - typically comes after voting/status completion
2881
+ # and may be marked with 🎤 or similar presentation indicators
2882
+ collecting_presentation = False
2883
+
2884
+ for line in agent_output:
2885
+ # Start collecting when we see presentation indicators
2886
+ if "🎤" in line or "presentation" in line.lower():
2887
+ collecting_presentation = True
2888
+ continue
2889
+
2890
+ # Skip empty lines and status updates
2891
+ if not line.strip() or line.startswith("⚡") or line.startswith("🔄"):
2892
+ continue
2893
+
2894
+ # Collect meaningful content that appears to be presentation material
2895
+ if collecting_presentation and line.strip():
2896
+ # Stop if we hit another status indicator or coordination marker
2897
+ if any(
2898
+ marker in line
2899
+ for marker in [
2900
+ "✅",
2901
+ "🗳️",
2902
+ "🔄",
2903
+ "❌",
2904
+ "voted",
2905
+ "Final",
2906
+ "coordination",
2907
+ ]
2908
+ ):
2909
+ break
2910
+ presentation_lines.append(line.strip())
2911
+
2912
+ # If no specific presentation content found, get the most recent meaningful content
2913
+ if not presentation_lines and agent_output:
2914
+ # Get the last few non-status lines as potential presentation content
2915
+ for line in reversed(agent_output[-10:]): # Look at last 10 lines
2916
+ if (
2917
+ line.strip()
2918
+ and not line.startswith("⚡")
2919
+ and not line.startswith("🔄")
2920
+ and not any(
2921
+ marker in line for marker in ["voted", "🗳️", "✅", "status"]
2922
+ )
2923
+ ):
2924
+ presentation_lines.insert(0, line.strip())
2925
+ if len(presentation_lines) >= 5: # Limit to reasonable amount
2926
+ break
2927
+
2928
+ return "\n".join(presentation_lines) if presentation_lines else ""
2929
+
2930
+ def _display_final_presentation_content(
2931
+ self, selected_agent: str, presentation_content: str
2932
+ ):
2933
+ """Display the final presentation content in a formatted panel with orchestrator query enhancements."""
2934
+ if not presentation_content.strip():
2935
+ return
2936
+
2937
+ # Store the presentation content for later re-display
2938
+ self._stored_final_presentation = presentation_content
2939
+ self._stored_presentation_agent = selected_agent
2940
+
2941
+ # Create presentation header with orchestrator context
2942
+ header_text = Text()
2943
+ header_text.append(
2944
+ f"🎤 Final Presentation from {selected_agent}",
2945
+ style=self.colors["header_style"],
2946
+ )
2947
+
2948
+ header_panel = Panel(
2949
+ Align.center(header_text),
2950
+ border_style=self.colors["success"],
2951
+ box=DOUBLE,
2952
+ title="[bold]Final Presentation[/bold]",
2953
+ )
2954
+
2955
+ self.console.print(header_panel)
2956
+ self.console.print("=" * 60)
2957
+
2958
+ # Enhanced content formatting for orchestrator responses
2959
+ content_text = Text()
2960
+
2961
+ # Use the enhanced presentation content formatter
2962
+ formatted_content = self._format_presentation_content(presentation_content)
2963
+ content_text.append(formatted_content)
2964
+
2965
+ # Create content panel with orchestrator-specific styling
2966
+ content_panel = Panel(
2967
+ content_text,
2968
+ title=f"[bold]{selected_agent.upper()} Final Presentation[/bold]",
2969
+ border_style=self.colors["primary"],
2970
+ box=ROUNDED,
2971
+ subtitle=f"[italic]Final presentation content[/italic]",
2972
+ )
2973
+
2974
+ self.console.print(content_panel)
2975
+ self.console.print("=" * 60)
2976
+
2977
+ # Add presentation completion indicator
2978
+ completion_text = Text()
2979
+ completion_text.append(
2980
+ "✅ Final presentation completed successfully", style=self.colors["success"]
2981
+ )
2982
+ completion_panel = Panel(
2983
+ Align.center(completion_text),
2984
+ border_style=self.colors["success"],
2985
+ box=ROUNDED,
2986
+ )
2987
+ self.console.print(completion_panel)
2988
+
2989
+ def _show_orchestrator_final_presentation(
2990
+ self, selected_agent: str, vote_results: Dict[str, Any] = None
2991
+ ):
2992
+ """Show the final presentation from the orchestrator for the selected agent."""
2993
+ import time
2994
+ import traceback
2995
+
2996
+ try:
2997
+
2998
+ if not hasattr(self, "orchestrator") or not self.orchestrator:
2999
+ return
3000
+
3001
+ # Get the final presentation from the orchestrator
3002
+ if hasattr(self.orchestrator, "get_final_presentation"):
3003
+ import asyncio
3004
+
3005
+ async def _get_and_display_presentation():
3006
+ """Helper to get and display presentation asynchronously."""
3007
+ try:
3008
+ presentation_stream = self.orchestrator.get_final_presentation(
3009
+ selected_agent, vote_results
3010
+ )
3011
+
3012
+ # Display the presentation
3013
+ await self.display_final_presentation(
3014
+ selected_agent, presentation_stream, vote_results
3015
+ )
3016
+ except Exception as e:
3017
+ raise
3018
+
3019
+ # Run the async function
3020
+ import nest_asyncio
3021
+
3022
+ nest_asyncio.apply()
3023
+
3024
+ try:
3025
+ # Create new event loop if needed
3026
+ try:
3027
+ loop = asyncio.get_running_loop()
3028
+ except RuntimeError:
3029
+ loop = asyncio.new_event_loop()
3030
+ asyncio.set_event_loop(loop)
3031
+
3032
+ # Run the coroutine and ensure it completes
3033
+ loop.run_until_complete(_get_and_display_presentation())
3034
+ # Add explicit wait to ensure presentation is fully displayed
3035
+ time.sleep(0.5)
3036
+ except Exception as e:
3037
+ # If all else fails, try asyncio.run
3038
+ try:
3039
+ asyncio.run(_get_and_display_presentation())
3040
+ # Add explicit wait to ensure presentation is fully displayed
3041
+ time.sleep(0.5)
3042
+ except Exception as e2:
3043
+ # Last resort: show stored content
3044
+ self._display_final_presentation_content(
3045
+ selected_agent, "Unable to retrieve live presentation."
3046
+ )
3047
+ else:
3048
+ # Fallback: try to get stored presentation content
3049
+ status = self.orchestrator.get_status()
3050
+ if selected_agent in status.get("agent_states", {}):
3051
+ stored_answer = status["agent_states"][selected_agent].get(
3052
+ "answer", ""
3053
+ )
3054
+ if stored_answer:
3055
+ self._display_final_presentation_content(
3056
+ selected_agent, stored_answer
3057
+ )
3058
+ else:
3059
+ print("DEBUG: No stored answer found")
3060
+ else:
3061
+ print(f"DEBUG: Agent {selected_agent} not found in agent_states")
3062
+ except Exception as e:
3063
+ # Handle errors gracefully
3064
+ error_text = Text(
3065
+ f"❌ Error in final presentation: {e}", style=self.colors["error"]
3066
+ )
3067
+ self.console.print(error_text)
3068
+
3069
+ # except Exception as e:
3070
+ # # Handle errors gracefully - show a simple message
3071
+ # error_text = Text(f"Unable to retrieve final presentation: {str(e)}", style=self.colors['warning'])
3072
+ # self.console.print(error_text)
3073
+
3074
+ def _force_display_final_vote_statuses(self):
3075
+ """Force display update to show all agents' final vote statuses."""
3076
+ with self._lock:
3077
+ # Mark all agents for update to ensure final vote status is shown
3078
+ for agent_id in self.agent_ids:
3079
+ self._pending_updates.add(agent_id)
3080
+ self._pending_updates.add("footer")
3081
+
3082
+ # Force immediate update with final status display
3083
+ self._schedule_async_update(force_update=True)
3084
+
3085
+ # Wait longer to ensure all updates are processed and displayed
3086
+ import time
3087
+
3088
+ time.sleep(0.3) # Increased wait to ensure all vote statuses are displayed
3089
+
3090
+ def _flush_all_buffers(self):
3091
+ """Flush all text buffers to ensure no content is lost."""
3092
+ for agent_id in self.agent_ids:
3093
+ if agent_id in self._text_buffers and self._text_buffers[agent_id]:
3094
+ buffer_content = self._text_buffers[agent_id].strip()
3095
+ if buffer_content:
3096
+ self.agent_outputs[agent_id].append(buffer_content)
3097
+ self._text_buffers[agent_id] = ""
3098
+
3099
+ def cleanup(self):
3100
+ """Clean up display resources."""
3101
+ with self._lock:
3102
+ # Flush any remaining buffered content
3103
+ self._flush_all_buffers()
3104
+
3105
+ # Stop live display with proper error handling
3106
+ if self.live:
3107
+ try:
3108
+ self.live.stop()
3109
+ except Exception:
3110
+ # Ignore any errors during stop
3111
+ pass
3112
+ finally:
3113
+ self.live = None
3114
+
3115
+ # Stop input thread if active
3116
+ self._stop_input_thread = True
3117
+ if self._input_thread and self._input_thread.is_alive():
3118
+ try:
3119
+ self._input_thread.join(timeout=1.0)
3120
+ except:
3121
+ pass
3122
+
3123
+ # Restore terminal settings
3124
+ try:
3125
+ self._restore_terminal_settings()
3126
+ except:
3127
+ # Ignore errors during terminal restoration
3128
+ pass
3129
+
3130
+ # Reset all state flags
3131
+ self._agent_selector_active = False
3132
+ self._final_answer_shown = False
3133
+
3134
+ # Remove resize signal handler
3135
+ try:
3136
+ signal.signal(signal.SIGWINCH, signal.SIG_DFL)
3137
+ except (AttributeError, OSError):
3138
+ pass
3139
+
3140
+ # Stop keyboard handler if active
3141
+ if self._key_handler:
3142
+ try:
3143
+ self._key_handler.stop()
3144
+ except:
3145
+ pass
3146
+
3147
+ # Set shutdown flag to prevent new timers
3148
+ self._shutdown_flag = True
3149
+
3150
+ # Cancel all debounce timers
3151
+ for timer in self._debounce_timers.values():
3152
+ timer.cancel()
3153
+ self._debounce_timers.clear()
3154
+
3155
+ # Cancel all buffer timers
3156
+ for timer in self._buffer_timers.values():
3157
+ if timer:
3158
+ timer.cancel()
3159
+ self._buffer_timers.clear()
3160
+
3161
+ # Cancel batch timer
3162
+ if self._batch_timer:
3163
+ self._batch_timer.cancel()
3164
+ self._batch_timer = None
3165
+
3166
+ # Shutdown executors
3167
+ if hasattr(self, "_refresh_executor"):
3168
+ self._refresh_executor.shutdown(wait=True)
3169
+ if hasattr(self, "_status_update_executor"):
3170
+ self._status_update_executor.shutdown(wait=True)
3171
+
3172
+ # Close agent files gracefully
3173
+ try:
3174
+ for agent_id, file_path in self.agent_files.items():
3175
+ if file_path.exists():
3176
+ with open(file_path, "a", encoding="utf-8") as f:
3177
+ f.write(
3178
+ f"\n=== SESSION ENDED at {time.strftime('%Y-%m-%d %H:%M:%S')} ===\n"
3179
+ )
3180
+ except:
3181
+ pass
3182
+
3183
+ def _schedule_priority_update(self, agent_id: str):
3184
+ """Schedule immediate priority update for critical agent status changes."""
3185
+ if self._shutdown_flag:
3186
+ return
3187
+
3188
+ def priority_update():
3189
+ try:
3190
+ # Update the specific agent panel immediately
3191
+ self._update_agent_panel_cache(agent_id)
3192
+ # Trigger immediate display update
3193
+ self._update_display_safe()
3194
+ except Exception:
3195
+ pass
3196
+
3197
+ self._status_update_executor.submit(priority_update)
3198
+
3199
+ def _categorize_update(self, agent_id: str, content_type: str, content: str):
3200
+ """Categorize update by priority for layered refresh strategy."""
3201
+ if content_type in ["status", "error", "tool"] or any(
3202
+ keyword in content.lower()
3203
+ for keyword in ["error", "failed", "completed", "voted"]
3204
+ ):
3205
+ self._critical_updates.add(agent_id)
3206
+ # Remove from other categories to avoid duplicate processing
3207
+ self._normal_updates.discard(agent_id)
3208
+ self._decorative_updates.discard(agent_id)
3209
+ elif content_type in ["thinking", "presentation"]:
3210
+ if agent_id not in self._critical_updates:
3211
+ self._normal_updates.add(agent_id)
3212
+ self._decorative_updates.discard(agent_id)
3213
+ else:
3214
+ # Decorative updates (progress, timestamps, etc.)
3215
+ if (
3216
+ agent_id not in self._critical_updates
3217
+ and agent_id not in self._normal_updates
3218
+ ):
3219
+ self._decorative_updates.add(agent_id)
3220
+
3221
+ def _schedule_layered_update(self, agent_id: str, is_critical: bool = False):
3222
+ """Schedule update using layered refresh strategy with intelligent batching."""
3223
+ if is_critical:
3224
+ # Critical updates: immediate processing, flush any pending batch
3225
+ self._flush_update_batch()
3226
+ self._pending_updates.add(agent_id)
3227
+ self._schedule_async_update(force_update=True)
3228
+ else:
3229
+ # Normal updates: intelligent batching based on terminal performance
3230
+ perf_tier = self._terminal_performance["performance_tier"]
3231
+
3232
+ if perf_tier == "high":
3233
+ # High performance: process immediately
3234
+ self._pending_updates.add(agent_id)
3235
+ self._schedule_async_update(force_update=False)
3236
+ else:
3237
+ # Lower performance: use batching
3238
+ self._add_to_update_batch(agent_id)
3239
+
3240
+ def _schedule_delayed_update(self):
3241
+ """Schedule delayed update for non-critical content."""
3242
+ delay = self._debounce_delay * 2 # Double delay for non-critical updates
3243
+
3244
+ def delayed_update():
3245
+ if self._pending_updates:
3246
+ self._schedule_async_update(force_update=False)
3247
+
3248
+ # Cancel existing delayed timer
3249
+ if "delayed" in self._debounce_timers:
3250
+ self._debounce_timers["delayed"].cancel()
3251
+
3252
+ self._debounce_timers["delayed"] = threading.Timer(delay, delayed_update)
3253
+ self._debounce_timers["delayed"].start()
3254
+
3255
+ def _add_to_update_batch(self, agent_id: str):
3256
+ """Add update to batch for efficient processing."""
3257
+ self._update_batch.add(agent_id)
3258
+
3259
+ # Cancel existing batch timer
3260
+ if self._batch_timer:
3261
+ self._batch_timer.cancel()
3262
+
3263
+ # Set new batch timer
3264
+ self._batch_timer = threading.Timer(
3265
+ self._batch_timeout, self._process_update_batch
3266
+ )
3267
+ self._batch_timer.start()
3268
+
3269
+ def _process_update_batch(self):
3270
+ """Process accumulated batch of updates."""
3271
+ if self._update_batch:
3272
+ # Move batch to pending updates
3273
+ self._pending_updates.update(self._update_batch)
3274
+ self._update_batch.clear()
3275
+
3276
+ # Process batch
3277
+ self._schedule_async_update(force_update=False)
3278
+
3279
+ def _flush_update_batch(self):
3280
+ """Immediately flush any pending batch updates."""
3281
+ if self._batch_timer:
3282
+ self._batch_timer.cancel()
3283
+ self._batch_timer = None
3284
+
3285
+ if self._update_batch:
3286
+ self._pending_updates.update(self._update_batch)
3287
+ self._update_batch.clear()
3288
+
3289
+ def _schedule_async_update(self, force_update: bool = False):
3290
+ """Schedule asynchronous update with debouncing to prevent jitter."""
3291
+ current_time = time.time()
3292
+
3293
+ # Frame skipping: if the terminal is struggling, skip updates more aggressively
3294
+ if not force_update and self._should_skip_frame():
3295
+ return
3296
+
3297
+ # Check if we need a full refresh - less frequent for performance
3298
+ if (current_time - self._last_full_refresh) > self._full_refresh_interval:
3299
+ with self._lock:
3300
+ self._pending_updates.add("header")
3301
+ self._pending_updates.add("footer")
3302
+ self._pending_updates.update(self.agent_ids)
3303
+ self._last_full_refresh = current_time
3304
+
3305
+ # For force updates (status changes, tool content), bypass debouncing completely
3306
+ if force_update:
3307
+ self._last_update = current_time
3308
+ # Submit multiple update tasks for even faster processing
3309
+ self._refresh_executor.submit(self._async_update_components)
3310
+ return
3311
+
3312
+ # Cancel existing debounce timer if any
3313
+ if "main" in self._debounce_timers:
3314
+ self._debounce_timers["main"].cancel()
3315
+
3316
+ # Create new debounce timer
3317
+ def debounced_update():
3318
+ current_time = time.time()
3319
+ time_since_last_update = current_time - self._last_update
3320
+
3321
+ if time_since_last_update >= self._update_interval:
3322
+ self._last_update = current_time
3323
+ self._refresh_executor.submit(self._async_update_components)
3324
+
3325
+ self._debounce_timers["main"] = threading.Timer(
3326
+ self._debounce_delay, debounced_update
3327
+ )
3328
+ self._debounce_timers["main"].start()
3329
+
3330
+ def _should_skip_frame(self):
3331
+ """Determine if we should skip this frame update to maintain stability."""
3332
+ # Skip frames more aggressively for macOS terminals
3333
+ term_type = self._terminal_performance["type"]
3334
+ if term_type in ["iterm", "macos_terminal"]:
3335
+ # Skip if we have too many dropped frames
3336
+ if self._dropped_frames > 1:
3337
+ return True
3338
+ # Skip if refresh executor is overloaded
3339
+ if (
3340
+ hasattr(self._refresh_executor, "_work_queue")
3341
+ and self._refresh_executor._work_queue.qsize() > 2
3342
+ ):
3343
+ return True
3344
+
3345
+ return False
3346
+
3347
+ def _async_update_components(self):
3348
+ """Asynchronously update only the components that have changed."""
3349
+ start_time = time.time()
3350
+
3351
+ try:
3352
+ updates_to_process = None
3353
+
3354
+ with self._lock:
3355
+ if self._pending_updates:
3356
+ updates_to_process = self._pending_updates.copy()
3357
+ self._pending_updates.clear()
3358
+
3359
+ if not updates_to_process:
3360
+ return
3361
+
3362
+ # Update components in parallel
3363
+ futures = []
3364
+
3365
+ for update_id in updates_to_process:
3366
+ if update_id == "header":
3367
+ future = self._refresh_executor.submit(self._update_header_cache)
3368
+ futures.append(future)
3369
+ elif update_id == "footer":
3370
+ future = self._refresh_executor.submit(self._update_footer_cache)
3371
+ futures.append(future)
3372
+ elif update_id in self.agent_ids:
3373
+ future = self._refresh_executor.submit(
3374
+ self._update_agent_panel_cache, update_id
3375
+ )
3376
+ futures.append(future)
3377
+
3378
+ # Wait for all updates to complete
3379
+ for future in futures:
3380
+ future.result()
3381
+
3382
+ # Update display with new layout
3383
+ self._update_display_safe()
3384
+
3385
+ except Exception:
3386
+ # Silently handle errors to avoid disrupting display
3387
+ pass
3388
+ finally:
3389
+ # Performance monitoring
3390
+ refresh_time = time.time() - start_time
3391
+ self._refresh_times.append(refresh_time)
3392
+ self._monitor_performance()
3393
+
3394
+ def _update_header_cache(self):
3395
+ """Update the cached header panel."""
3396
+ try:
3397
+ self._header_cache = self._create_header()
3398
+ except:
3399
+ pass
3400
+
3401
+ def _update_footer_cache(self):
3402
+ """Update the cached footer panel."""
3403
+ try:
3404
+ self._footer_cache = self._create_footer()
3405
+ except:
3406
+ pass
3407
+
3408
+ def _update_agent_panel_cache(self, agent_id: str):
3409
+ """Update the cached panel for a specific agent."""
3410
+ try:
3411
+ self._agent_panels_cache[agent_id] = self._create_agent_panel(agent_id)
3412
+ except:
3413
+ pass
3414
+
3415
+ def _refresh_display(self):
3416
+ """Override parent's refresh method to use async updates."""
3417
+ # Only refresh if there are actual pending updates
3418
+ # This prevents unnecessary full refreshes
3419
+ if self._pending_updates:
3420
+ self._schedule_async_update()
3421
+
3422
+ def _is_content_important(self, content: str, content_type: str) -> bool:
3423
+ """Determine if content is important enough to trigger a display update."""
3424
+ # Always important content types
3425
+ if content_type in self._important_content_types:
3426
+ return True
3427
+
3428
+ # Check for status change indicators in content
3429
+ if any(keyword in content.lower() for keyword in self._status_change_keywords):
3430
+ return True
3431
+
3432
+ # Check for error indicators
3433
+ if any(
3434
+ keyword in content.lower()
3435
+ for keyword in ["error", "exception", "failed", "timeout"]
3436
+ ):
3437
+ return True
3438
+
3439
+ return False
3440
+
3441
+ def set_status_jump_enabled(self, enabled: bool):
3442
+ """Enable or disable status jumping functionality.
3443
+
3444
+ Args:
3445
+ enabled: Whether to enable status jumping
3446
+ """
3447
+ with self._lock:
3448
+ self._status_jump_enabled = enabled
3449
+
3450
+ def set_web_search_truncation(self, enabled: bool, max_lines: int = 3):
3451
+ """Configure web search content truncation on status changes.
3452
+
3453
+ Args:
3454
+ enabled: Whether to enable web search truncation
3455
+ max_lines: Maximum web search lines to keep when truncating
3456
+ """
3457
+ with self._lock:
3458
+ self._web_search_truncate_on_status_change = enabled
3459
+ self._max_web_search_lines = max_lines
3460
+
3461
+ def set_flush_output(
3462
+ self, enabled: bool, char_delay: float = 0.03, word_delay: float = 0.08
3463
+ ):
3464
+ """Configure flush output settings for final answer display.
3465
+
3466
+ Args:
3467
+ enabled: Whether to enable flush output effect
3468
+ char_delay: Delay between characters in seconds
3469
+ word_delay: Extra delay after punctuation in seconds
3470
+ """
3471
+ with self._lock:
3472
+ self._enable_flush_output = enabled
3473
+ self._flush_char_delay = char_delay
3474
+ self._flush_word_delay = word_delay
3475
+
3476
+
3477
+ # Convenience function to check Rich availability
3478
+ def is_rich_available() -> bool:
3479
+ """Check if Rich library is available."""
3480
+ return RICH_AVAILABLE
3481
+
3482
+
3483
+ # Factory function for creating display
3484
+ def create_rich_display(agent_ids: List[str], **kwargs) -> RichTerminalDisplay:
3485
+ """Create a RichTerminalDisplay instance.
3486
+
3487
+ Args:
3488
+ agent_ids: List of agent IDs to display
3489
+ **kwargs: Configuration options for RichTerminalDisplay
3490
+
3491
+ Returns:
3492
+ RichTerminalDisplay instance
3493
+
3494
+ Raises:
3495
+ ImportError: If Rich library is not available
3496
+ """
3497
+ return RichTerminalDisplay(agent_ids, **kwargs)