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.
- massgen/__init__.py +94 -0
- massgen/agent_config.py +507 -0
- massgen/backend/CLAUDE_API_RESEARCH.md +266 -0
- massgen/backend/Function calling openai responses.md +1161 -0
- massgen/backend/GEMINI_API_DOCUMENTATION.md +410 -0
- massgen/backend/OPENAI_RESPONSES_API_FORMAT.md +65 -0
- massgen/backend/__init__.py +25 -0
- massgen/backend/base.py +180 -0
- massgen/backend/chat_completions.py +228 -0
- massgen/backend/claude.py +661 -0
- massgen/backend/gemini.py +652 -0
- massgen/backend/grok.py +187 -0
- massgen/backend/response.py +397 -0
- massgen/chat_agent.py +440 -0
- massgen/cli.py +686 -0
- massgen/configs/README.md +293 -0
- massgen/configs/creative_team.yaml +53 -0
- massgen/configs/gemini_4o_claude.yaml +31 -0
- massgen/configs/news_analysis.yaml +51 -0
- massgen/configs/research_team.yaml +51 -0
- massgen/configs/single_agent.yaml +18 -0
- massgen/configs/single_flash2.5.yaml +44 -0
- massgen/configs/technical_analysis.yaml +51 -0
- massgen/configs/three_agents_default.yaml +31 -0
- massgen/configs/travel_planning.yaml +51 -0
- massgen/configs/two_agents.yaml +39 -0
- massgen/frontend/__init__.py +20 -0
- massgen/frontend/coordination_ui.py +945 -0
- massgen/frontend/displays/__init__.py +24 -0
- massgen/frontend/displays/base_display.py +83 -0
- massgen/frontend/displays/rich_terminal_display.py +3497 -0
- massgen/frontend/displays/simple_display.py +93 -0
- massgen/frontend/displays/terminal_display.py +381 -0
- massgen/frontend/logging/__init__.py +9 -0
- massgen/frontend/logging/realtime_logger.py +197 -0
- massgen/message_templates.py +431 -0
- massgen/orchestrator.py +1222 -0
- massgen/tests/__init__.py +10 -0
- massgen/tests/multi_turn_conversation_design.md +214 -0
- massgen/tests/multiturn_llm_input_analysis.md +189 -0
- massgen/tests/test_case_studies.md +113 -0
- massgen/tests/test_claude_backend.py +310 -0
- massgen/tests/test_grok_backend.py +160 -0
- massgen/tests/test_message_context_building.py +293 -0
- massgen/tests/test_rich_terminal_display.py +378 -0
- massgen/tests/test_v3_3agents.py +117 -0
- massgen/tests/test_v3_simple.py +216 -0
- massgen/tests/test_v3_three_agents.py +272 -0
- massgen/tests/test_v3_two_agents.py +176 -0
- massgen/utils.py +79 -0
- massgen/v1/README.md +330 -0
- massgen/v1/__init__.py +91 -0
- massgen/v1/agent.py +605 -0
- massgen/v1/agents.py +330 -0
- massgen/v1/backends/gemini.py +584 -0
- massgen/v1/backends/grok.py +410 -0
- massgen/v1/backends/oai.py +571 -0
- massgen/v1/cli.py +351 -0
- massgen/v1/config.py +169 -0
- massgen/v1/examples/fast-4o-mini-config.yaml +44 -0
- massgen/v1/examples/fast_config.yaml +44 -0
- massgen/v1/examples/production.yaml +70 -0
- massgen/v1/examples/single_agent.yaml +39 -0
- massgen/v1/logging.py +974 -0
- massgen/v1/main.py +368 -0
- massgen/v1/orchestrator.py +1138 -0
- massgen/v1/streaming_display.py +1190 -0
- massgen/v1/tools.py +160 -0
- massgen/v1/types.py +245 -0
- massgen/v1/utils.py +199 -0
- massgen-0.0.3.dist-info/METADATA +568 -0
- massgen-0.0.3.dist-info/RECORD +76 -0
- massgen-0.0.3.dist-info/WHEEL +5 -0
- massgen-0.0.3.dist-info/entry_points.txt +2 -0
- massgen-0.0.3.dist-info/licenses/LICENSE +204 -0
- 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)
|