omnimancer-cli 0.1.0__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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Progress UI Components for Omnimancer.
|
|
3
|
+
|
|
4
|
+
This module provides Rich-based UI components for displaying agent mode
|
|
5
|
+
progress, operation status, and real-time updates.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from typing import Dict, Optional
|
|
13
|
+
|
|
14
|
+
from rich.align import Align
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.layout import Layout
|
|
17
|
+
from rich.live import Live
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.progress import (
|
|
20
|
+
BarColumn,
|
|
21
|
+
Progress,
|
|
22
|
+
SpinnerColumn,
|
|
23
|
+
TaskID,
|
|
24
|
+
TextColumn,
|
|
25
|
+
TimeRemainingColumn,
|
|
26
|
+
)
|
|
27
|
+
from rich.table import Table
|
|
28
|
+
from rich.text import Text
|
|
29
|
+
|
|
30
|
+
from .agent_mode_manager import (
|
|
31
|
+
AgentMode,
|
|
32
|
+
AgentModeManager,
|
|
33
|
+
AgentOperation,
|
|
34
|
+
AgentOperationStatus,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class OperationProgress:
|
|
40
|
+
"""Tracks progress of an individual operation."""
|
|
41
|
+
|
|
42
|
+
operation_id: str
|
|
43
|
+
task_id: Optional[TaskID] = None
|
|
44
|
+
progress: float = 0.0
|
|
45
|
+
status_text: str = "Queued"
|
|
46
|
+
started_at: Optional[datetime] = None
|
|
47
|
+
estimated_duration: Optional[float] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AgentProgressUI:
|
|
51
|
+
"""
|
|
52
|
+
Rich-based UI for displaying agent mode progress and status.
|
|
53
|
+
|
|
54
|
+
Provides real-time updates of:
|
|
55
|
+
- Agent mode status
|
|
56
|
+
- Operation queue status
|
|
57
|
+
- Individual operation progress
|
|
58
|
+
- System metrics
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
agent_manager: AgentModeManager,
|
|
64
|
+
console: Optional[Console] = None,
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Initialize the progress UI.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
agent_manager: Agent mode manager instance
|
|
71
|
+
console: Rich console instance (creates new if None)
|
|
72
|
+
"""
|
|
73
|
+
self.agent_manager = agent_manager
|
|
74
|
+
self.console = console or Console()
|
|
75
|
+
|
|
76
|
+
# Progress tracking
|
|
77
|
+
self.progress = Progress(
|
|
78
|
+
SpinnerColumn(),
|
|
79
|
+
TextColumn("[progress.description]{task.description}"),
|
|
80
|
+
BarColumn(),
|
|
81
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
82
|
+
TimeRemainingColumn(),
|
|
83
|
+
console=self.console,
|
|
84
|
+
transient=True,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Operation tracking
|
|
88
|
+
self.operation_progress: Dict[str, OperationProgress] = {}
|
|
89
|
+
self.operation_task_ids: Dict[str, TaskID] = {}
|
|
90
|
+
|
|
91
|
+
# UI state
|
|
92
|
+
self.live_display: Optional[Live] = None
|
|
93
|
+
self.update_interval = 0.5 # seconds
|
|
94
|
+
self._update_task: Optional[asyncio.Task] = None
|
|
95
|
+
self._stop_event = asyncio.Event()
|
|
96
|
+
|
|
97
|
+
# Register callbacks
|
|
98
|
+
self.agent_manager.add_operation_callback(self._on_operation_change)
|
|
99
|
+
self.agent_manager.add_mode_change_callback(self._on_mode_change)
|
|
100
|
+
|
|
101
|
+
def start_monitoring(self):
|
|
102
|
+
"""Start real-time monitoring display."""
|
|
103
|
+
if self._update_task and not self._update_task.done():
|
|
104
|
+
return # Already running
|
|
105
|
+
|
|
106
|
+
self._stop_event.clear()
|
|
107
|
+
self._update_task = asyncio.create_task(self._monitoring_loop())
|
|
108
|
+
|
|
109
|
+
def stop_monitoring(self):
|
|
110
|
+
"""Stop real-time monitoring display."""
|
|
111
|
+
self._stop_event.set()
|
|
112
|
+
if self._update_task and not self._update_task.done():
|
|
113
|
+
self._update_task.cancel()
|
|
114
|
+
|
|
115
|
+
@asynccontextmanager
|
|
116
|
+
async def live_status_display(self):
|
|
117
|
+
"""Context manager for live status display."""
|
|
118
|
+
layout = self._create_status_layout()
|
|
119
|
+
|
|
120
|
+
with Live(layout, console=self.console, refresh_per_second=2) as live:
|
|
121
|
+
self.live_display = live
|
|
122
|
+
try:
|
|
123
|
+
yield live
|
|
124
|
+
finally:
|
|
125
|
+
self.live_display = None
|
|
126
|
+
|
|
127
|
+
def show_status_panel(self) -> Panel:
|
|
128
|
+
"""
|
|
129
|
+
Create and return a status panel.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Rich Panel with current status
|
|
133
|
+
"""
|
|
134
|
+
status_info = self.agent_manager.get_status()
|
|
135
|
+
|
|
136
|
+
# Mode status
|
|
137
|
+
mode_color = {"off": "red", "on": "green", "paused": "yellow"}.get(
|
|
138
|
+
status_info["mode"], "white"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
mode_text = Text(f"Agent Mode: ", style="bold")
|
|
142
|
+
mode_text.append(status_info["mode"].upper(), style=f"bold {mode_color}")
|
|
143
|
+
|
|
144
|
+
# Operation counts
|
|
145
|
+
ops = status_info["operations"]
|
|
146
|
+
ops_text = f"""
|
|
147
|
+
Queued: {ops['queued']}
|
|
148
|
+
In Progress: {ops['in_progress']}
|
|
149
|
+
Completed: {ops['completed']}
|
|
150
|
+
Failed: {ops['failed']}
|
|
151
|
+
Requires Approval: {ops['requires_approval']}"""
|
|
152
|
+
|
|
153
|
+
# Settings info
|
|
154
|
+
settings = status_info["settings"]
|
|
155
|
+
settings_text = f"""
|
|
156
|
+
Auto-approve Low Risk: {'Yes' if settings['auto_approve_low_risk'] else 'No'}
|
|
157
|
+
Auto-approve Read-only: {'Yes' if settings['auto_approve_read_only'] else 'No'}
|
|
158
|
+
Max Concurrent: {settings['max_concurrent_operations']}
|
|
159
|
+
Batch Approval: {'Enabled' if settings['enable_batch_approval'] else 'Disabled'}"""
|
|
160
|
+
|
|
161
|
+
status_content = (
|
|
162
|
+
f"{mode_text}\n\nOperations:{ops_text}\n\nSettings:{settings_text}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return Panel(
|
|
166
|
+
status_content,
|
|
167
|
+
title="Agent Status",
|
|
168
|
+
border_style="blue",
|
|
169
|
+
padding=(1, 2),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def show_operations_table(self, limit: int = 10) -> Table:
|
|
173
|
+
"""
|
|
174
|
+
Create and return an operations table.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
limit: Maximum number of operations to show
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Rich Table with operation history
|
|
181
|
+
"""
|
|
182
|
+
table = Table(
|
|
183
|
+
title=f"Recent Operations (Last {limit})",
|
|
184
|
+
show_header=True,
|
|
185
|
+
header_style="bold magenta",
|
|
186
|
+
border_style="blue",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
table.add_column("ID", style="dim", width=12)
|
|
190
|
+
table.add_column("Type", style="cyan", width=15)
|
|
191
|
+
table.add_column("Description", width=30)
|
|
192
|
+
table.add_column("Status", width=12)
|
|
193
|
+
table.add_column("Duration", width=10)
|
|
194
|
+
table.add_column("Retries", width=8)
|
|
195
|
+
|
|
196
|
+
history = self.agent_manager.get_operation_history(limit)
|
|
197
|
+
|
|
198
|
+
for op in history:
|
|
199
|
+
# Format status with color
|
|
200
|
+
status = op["status"]
|
|
201
|
+
status_color = {
|
|
202
|
+
"queued": "yellow",
|
|
203
|
+
"in_progress": "blue",
|
|
204
|
+
"completed": "green",
|
|
205
|
+
"failed": "red",
|
|
206
|
+
"cancelled": "red",
|
|
207
|
+
"requires_approval": "magenta",
|
|
208
|
+
}.get(status, "white")
|
|
209
|
+
|
|
210
|
+
status_text = Text(status.replace("_", " ").title(), style=status_color)
|
|
211
|
+
|
|
212
|
+
# Format duration
|
|
213
|
+
duration = op.get("duration")
|
|
214
|
+
if duration:
|
|
215
|
+
if duration < 60:
|
|
216
|
+
duration_str = f"{duration:.1f}s"
|
|
217
|
+
else:
|
|
218
|
+
duration_str = f"{duration/60:.1f}m"
|
|
219
|
+
else:
|
|
220
|
+
duration_str = "-"
|
|
221
|
+
|
|
222
|
+
# Format retry count
|
|
223
|
+
retry_count = op.get("retry_count", 0)
|
|
224
|
+
retry_str = str(retry_count) if retry_count > 0 else "-"
|
|
225
|
+
|
|
226
|
+
table.add_row(
|
|
227
|
+
op["id"][-8:], # Show last 8 chars of ID
|
|
228
|
+
op["type"],
|
|
229
|
+
(
|
|
230
|
+
op["description"][:28] + "..."
|
|
231
|
+
if len(op["description"]) > 28
|
|
232
|
+
else op["description"]
|
|
233
|
+
),
|
|
234
|
+
status_text,
|
|
235
|
+
duration_str,
|
|
236
|
+
retry_str,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if not history:
|
|
240
|
+
table.add_row("", "", "No operations yet", "", "", "")
|
|
241
|
+
|
|
242
|
+
return table
|
|
243
|
+
|
|
244
|
+
def show_progress_panel(self) -> Panel:
|
|
245
|
+
"""
|
|
246
|
+
Create and return a progress panel for active operations.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Rich Panel with progress information
|
|
250
|
+
"""
|
|
251
|
+
active_ops = list(self.agent_manager.active_operations.values())
|
|
252
|
+
|
|
253
|
+
if not active_ops:
|
|
254
|
+
content = Text("No operations currently running", style="dim italic")
|
|
255
|
+
return Panel(
|
|
256
|
+
Align.center(content),
|
|
257
|
+
title="Active Operations",
|
|
258
|
+
border_style="blue",
|
|
259
|
+
height=5,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Create progress display
|
|
263
|
+
progress_text = []
|
|
264
|
+
for op in active_ops:
|
|
265
|
+
# Calculate elapsed time
|
|
266
|
+
elapsed = datetime.now() - op.started_at if op.started_at else timedelta(0)
|
|
267
|
+
elapsed_str = f"{elapsed.total_seconds():.1f}s"
|
|
268
|
+
|
|
269
|
+
# Get progress if available
|
|
270
|
+
op_progress = self.operation_progress.get(op.id)
|
|
271
|
+
if op_progress and op_progress.progress > 0:
|
|
272
|
+
progress_str = f"[{int(op_progress.progress)}%]"
|
|
273
|
+
status = op_progress.status_text
|
|
274
|
+
else:
|
|
275
|
+
progress_str = "[---]"
|
|
276
|
+
status = op.status.value.replace("_", " ").title()
|
|
277
|
+
|
|
278
|
+
line = f"• {op.operation.type.value}: {op.operation.description[:30]}"
|
|
279
|
+
if len(op.operation.description) > 30:
|
|
280
|
+
line += "..."
|
|
281
|
+
line += f" {progress_str} ({elapsed_str}) - {status}"
|
|
282
|
+
|
|
283
|
+
progress_text.append(line)
|
|
284
|
+
|
|
285
|
+
content = "\n".join(progress_text)
|
|
286
|
+
|
|
287
|
+
return Panel(
|
|
288
|
+
content,
|
|
289
|
+
title="Active Operations",
|
|
290
|
+
border_style="green",
|
|
291
|
+
padding=(1, 2),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def show_approval_queue_panel(self) -> Panel:
|
|
295
|
+
"""
|
|
296
|
+
Create and return a panel showing operations requiring approval.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Rich Panel with approval queue
|
|
300
|
+
"""
|
|
301
|
+
approval_ops = [
|
|
302
|
+
op
|
|
303
|
+
for op in self.agent_manager.operation_queue
|
|
304
|
+
if op.status == AgentOperationStatus.REQUIRES_APPROVAL
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
if not approval_ops:
|
|
308
|
+
content = Text("No operations waiting for approval", style="dim italic")
|
|
309
|
+
return Panel(
|
|
310
|
+
Align.center(content),
|
|
311
|
+
title="Approval Queue",
|
|
312
|
+
border_style="yellow",
|
|
313
|
+
height=5,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
approval_text = []
|
|
317
|
+
for i, op in enumerate(approval_ops[:5], 1): # Show up to 5
|
|
318
|
+
risk = op.operation.data.get("risk_level", "medium")
|
|
319
|
+
risk_color = {
|
|
320
|
+
"low": "green",
|
|
321
|
+
"medium": "yellow",
|
|
322
|
+
"high": "red",
|
|
323
|
+
"critical": "magenta",
|
|
324
|
+
}.get(risk.lower(), "white")
|
|
325
|
+
|
|
326
|
+
line = f"{i}. {op.operation.description[:40]}"
|
|
327
|
+
if len(op.operation.description) > 40:
|
|
328
|
+
line += "..."
|
|
329
|
+
line += f" [Risk: {risk}]"
|
|
330
|
+
|
|
331
|
+
# Create colored text
|
|
332
|
+
text = Text(line)
|
|
333
|
+
text.stylize(risk_color, len(line) - len(f"[Risk: {risk}]"), len(line))
|
|
334
|
+
approval_text.append(text)
|
|
335
|
+
|
|
336
|
+
if len(approval_ops) > 5:
|
|
337
|
+
approval_text.append(
|
|
338
|
+
Text(f"... and {len(approval_ops) - 5} more", style="dim")
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
content = "\n".join(str(text) for text in approval_text)
|
|
342
|
+
|
|
343
|
+
return Panel(
|
|
344
|
+
content,
|
|
345
|
+
title=f"Approval Queue ({len(approval_ops)})",
|
|
346
|
+
border_style="yellow",
|
|
347
|
+
padding=(1, 2),
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def show_agent_dashboard(self) -> Layout:
|
|
351
|
+
"""
|
|
352
|
+
Create a comprehensive agent dashboard layout.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Rich Layout with full dashboard
|
|
356
|
+
"""
|
|
357
|
+
layout = Layout()
|
|
358
|
+
|
|
359
|
+
# Split into sections
|
|
360
|
+
layout.split(
|
|
361
|
+
Layout(name="header", size=8),
|
|
362
|
+
Layout(name="body"),
|
|
363
|
+
Layout(name="footer", size=4),
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Header: Status and settings
|
|
367
|
+
layout["header"].split_row(
|
|
368
|
+
Layout(self.show_status_panel(), name="status"),
|
|
369
|
+
Layout(self.show_approval_queue_panel(), name="approvals"),
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Body: Operations and progress
|
|
373
|
+
layout["body"].split_row(
|
|
374
|
+
Layout(self.show_operations_table(8), name="operations"),
|
|
375
|
+
Layout(self.show_progress_panel(), name="progress"),
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Footer: Help text
|
|
379
|
+
help_text = Text(
|
|
380
|
+
"Commands: /agent on|off|status • Ctrl+C to exit dashboard • Use /agent off to disable agent mode",
|
|
381
|
+
style="dim",
|
|
382
|
+
)
|
|
383
|
+
layout["footer"] = Panel(Align.center(help_text), style="dim")
|
|
384
|
+
|
|
385
|
+
return layout
|
|
386
|
+
|
|
387
|
+
def update_operation_progress(
|
|
388
|
+
self, operation_id: str, progress: float, status_text: str = ""
|
|
389
|
+
):
|
|
390
|
+
"""
|
|
391
|
+
Update progress for a specific operation.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
operation_id: ID of the operation
|
|
395
|
+
progress: Progress percentage (0-100)
|
|
396
|
+
status_text: Current status description
|
|
397
|
+
"""
|
|
398
|
+
if operation_id not in self.operation_progress:
|
|
399
|
+
self.operation_progress[operation_id] = OperationProgress(
|
|
400
|
+
operation_id=operation_id, started_at=datetime.now()
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
op_progress = self.operation_progress[operation_id]
|
|
404
|
+
op_progress.progress = progress
|
|
405
|
+
if status_text:
|
|
406
|
+
op_progress.status_text = status_text
|
|
407
|
+
|
|
408
|
+
# Update Rich progress bar if exists
|
|
409
|
+
if operation_id in self.operation_task_ids:
|
|
410
|
+
task_id = self.operation_task_ids[operation_id]
|
|
411
|
+
self.progress.update(task_id, completed=progress, description=status_text)
|
|
412
|
+
|
|
413
|
+
def _create_status_layout(self) -> Layout:
|
|
414
|
+
"""Create the main status layout for live display."""
|
|
415
|
+
return self.show_agent_dashboard()
|
|
416
|
+
|
|
417
|
+
async def _monitoring_loop(self):
|
|
418
|
+
"""Main monitoring loop for real-time updates."""
|
|
419
|
+
try:
|
|
420
|
+
while not self._stop_event.is_set():
|
|
421
|
+
# Update live display if active
|
|
422
|
+
if self.live_display:
|
|
423
|
+
self.live_display.update(self._create_status_layout())
|
|
424
|
+
|
|
425
|
+
# Wait for next update
|
|
426
|
+
try:
|
|
427
|
+
await asyncio.wait_for(
|
|
428
|
+
self._stop_event.wait(), timeout=self.update_interval
|
|
429
|
+
)
|
|
430
|
+
break # Stop event was set
|
|
431
|
+
except asyncio.TimeoutError:
|
|
432
|
+
continue # Normal timeout, continue monitoring
|
|
433
|
+
|
|
434
|
+
except asyncio.CancelledError:
|
|
435
|
+
pass
|
|
436
|
+
except Exception as e:
|
|
437
|
+
self.console.print(f"[red]Error in monitoring loop: {e}[/red]")
|
|
438
|
+
|
|
439
|
+
def _on_operation_change(self, operation: AgentOperation):
|
|
440
|
+
"""Handle operation state changes."""
|
|
441
|
+
# Update progress tracking
|
|
442
|
+
if operation.status == AgentOperationStatus.IN_PROGRESS:
|
|
443
|
+
# Start tracking progress
|
|
444
|
+
if operation.id not in self.operation_progress:
|
|
445
|
+
self.operation_progress[operation.id] = OperationProgress(
|
|
446
|
+
operation_id=operation.id,
|
|
447
|
+
started_at=operation.started_at,
|
|
448
|
+
status_text="Starting...",
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
elif operation.status in [
|
|
452
|
+
AgentOperationStatus.COMPLETED,
|
|
453
|
+
AgentOperationStatus.FAILED,
|
|
454
|
+
AgentOperationStatus.CANCELLED,
|
|
455
|
+
]:
|
|
456
|
+
# Update final progress
|
|
457
|
+
if operation.id in self.operation_progress:
|
|
458
|
+
op_progress = self.operation_progress[operation.id]
|
|
459
|
+
op_progress.progress = (
|
|
460
|
+
100.0 if operation.status == AgentOperationStatus.COMPLETED else 0.0
|
|
461
|
+
)
|
|
462
|
+
op_progress.status_text = operation.status.value.replace(
|
|
463
|
+
"_", " "
|
|
464
|
+
).title()
|
|
465
|
+
|
|
466
|
+
# Clean up progress tracking after delay
|
|
467
|
+
asyncio.create_task(self._cleanup_operation_progress(operation.id))
|
|
468
|
+
|
|
469
|
+
def _on_mode_change(self, old_mode: AgentMode, new_mode: AgentMode):
|
|
470
|
+
"""Handle agent mode changes."""
|
|
471
|
+
if new_mode == AgentMode.ON:
|
|
472
|
+
# Start monitoring if not already running
|
|
473
|
+
if not self._update_task or self._update_task.done():
|
|
474
|
+
self.start_monitoring()
|
|
475
|
+
elif new_mode == AgentMode.OFF:
|
|
476
|
+
# Stop monitoring
|
|
477
|
+
self.stop_monitoring()
|
|
478
|
+
|
|
479
|
+
async def _cleanup_operation_progress(self, operation_id: str, delay: float = 5.0):
|
|
480
|
+
"""Clean up operation progress tracking after delay."""
|
|
481
|
+
await asyncio.sleep(delay)
|
|
482
|
+
|
|
483
|
+
# Remove from tracking
|
|
484
|
+
self.operation_progress.pop(operation_id, None)
|
|
485
|
+
|
|
486
|
+
# Remove from Rich progress
|
|
487
|
+
if operation_id in self.operation_task_ids:
|
|
488
|
+
task_id = self.operation_task_ids.pop(operation_id)
|
|
489
|
+
try:
|
|
490
|
+
self.progress.remove_task(task_id)
|
|
491
|
+
except KeyError:
|
|
492
|
+
pass # Task already removed
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class AgentStatusIndicator:
|
|
496
|
+
"""Simple status indicator for embedding in other UI components."""
|
|
497
|
+
|
|
498
|
+
def __init__(self, agent_manager: AgentModeManager):
|
|
499
|
+
"""
|
|
500
|
+
Initialize status indicator.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
agent_manager: Agent mode manager instance
|
|
504
|
+
"""
|
|
505
|
+
self.agent_manager = agent_manager
|
|
506
|
+
|
|
507
|
+
def get_status_badge(self) -> Text:
|
|
508
|
+
"""
|
|
509
|
+
Get a small status badge for display.
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
Rich Text object with status badge
|
|
513
|
+
"""
|
|
514
|
+
status = self.agent_manager.get_status()
|
|
515
|
+
mode = status["mode"]
|
|
516
|
+
|
|
517
|
+
if mode == "on":
|
|
518
|
+
active_count = status["operations"]["in_progress"]
|
|
519
|
+
if active_count > 0:
|
|
520
|
+
return Text(f"🤖 AGENT[{active_count}]", style="bold green")
|
|
521
|
+
else:
|
|
522
|
+
return Text("🤖 AGENT", style="bold green")
|
|
523
|
+
elif mode == "paused":
|
|
524
|
+
return Text("🤖 PAUSED", style="bold yellow")
|
|
525
|
+
else:
|
|
526
|
+
return Text("🤖 OFF", style="dim red")
|
|
527
|
+
|
|
528
|
+
def get_mini_status(self) -> str:
|
|
529
|
+
"""
|
|
530
|
+
Get minimal status string.
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
Short status string
|
|
534
|
+
"""
|
|
535
|
+
status = self.agent_manager.get_status()
|
|
536
|
+
mode = status["mode"]
|
|
537
|
+
|
|
538
|
+
if mode == "on":
|
|
539
|
+
active = status["operations"]["in_progress"]
|
|
540
|
+
queued = status["operations"]["queued"]
|
|
541
|
+
return f"Agent: ON ({active} active, {queued} queued)"
|
|
542
|
+
elif mode == "paused":
|
|
543
|
+
return "Agent: PAUSED"
|
|
544
|
+
else:
|
|
545
|
+
return "Agent: OFF"
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chat manager for Omnimancer.
|
|
3
|
+
|
|
4
|
+
This module handles conversation management, context tracking,
|
|
5
|
+
and message history for chat sessions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from .models import ChatContext, ChatMessage, MessageRole
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ChatManager:
|
|
16
|
+
"""
|
|
17
|
+
Manages chat conversations and context.
|
|
18
|
+
|
|
19
|
+
This class handles the conversation state, message history,
|
|
20
|
+
and context management for chat sessions.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
"""Initialize the chat manager."""
|
|
25
|
+
self.current_context: Optional[ChatContext] = None
|
|
26
|
+
self._initialize_context()
|
|
27
|
+
|
|
28
|
+
def _initialize_context(self) -> None:
|
|
29
|
+
"""Initialize a new chat context."""
|
|
30
|
+
self.current_context = ChatContext(
|
|
31
|
+
messages=[],
|
|
32
|
+
current_model="",
|
|
33
|
+
session_id=str(uuid.uuid4()),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def get_current_context(self) -> ChatContext:
|
|
37
|
+
"""
|
|
38
|
+
Get the current chat context.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Current ChatContext instance
|
|
42
|
+
"""
|
|
43
|
+
if self.current_context is None:
|
|
44
|
+
self._initialize_context()
|
|
45
|
+
return self.current_context
|
|
46
|
+
|
|
47
|
+
def set_current_model(self, model: str) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Set the current model being used.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
model: Model name
|
|
53
|
+
"""
|
|
54
|
+
context = self.get_current_context()
|
|
55
|
+
context.current_model = model
|
|
56
|
+
|
|
57
|
+
def add_user_message(self, content: str) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Add a user message to the current context.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
content: Message content
|
|
63
|
+
"""
|
|
64
|
+
context = self.get_current_context()
|
|
65
|
+
message = ChatMessage(
|
|
66
|
+
role=MessageRole.USER,
|
|
67
|
+
content=content,
|
|
68
|
+
timestamp=datetime.now(),
|
|
69
|
+
model_used=context.current_model,
|
|
70
|
+
)
|
|
71
|
+
context.add_message(message)
|
|
72
|
+
|
|
73
|
+
def add_assistant_message(self, content: str, model_used: str) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Add an assistant message to the current context.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
content: Message content
|
|
79
|
+
model_used: Model that generated the message
|
|
80
|
+
"""
|
|
81
|
+
context = self.get_current_context()
|
|
82
|
+
message = ChatMessage(
|
|
83
|
+
role=MessageRole.ASSISTANT,
|
|
84
|
+
content=content,
|
|
85
|
+
timestamp=datetime.now(),
|
|
86
|
+
model_used=model_used,
|
|
87
|
+
)
|
|
88
|
+
context.add_message(message)
|
|
89
|
+
|
|
90
|
+
def add_system_message(self, content: str) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Add a system message to the current context.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
content: Message content
|
|
96
|
+
"""
|
|
97
|
+
context = self.get_current_context()
|
|
98
|
+
message = ChatMessage(
|
|
99
|
+
role=MessageRole.SYSTEM,
|
|
100
|
+
content=content,
|
|
101
|
+
timestamp=datetime.now(),
|
|
102
|
+
model_used=context.current_model,
|
|
103
|
+
)
|
|
104
|
+
context.add_message(message)
|
|
105
|
+
|
|
106
|
+
def clear_context(self) -> None:
|
|
107
|
+
"""Clear the current conversation context."""
|
|
108
|
+
if self.current_context:
|
|
109
|
+
self.current_context.clear()
|
|
110
|
+
|
|
111
|
+
def start_new_session(self) -> str:
|
|
112
|
+
"""
|
|
113
|
+
Start a new chat session.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
New session ID
|
|
117
|
+
"""
|
|
118
|
+
self._initialize_context()
|
|
119
|
+
return self.current_context.session_id
|
|
120
|
+
|
|
121
|
+
def get_message_count(self) -> int:
|
|
122
|
+
"""
|
|
123
|
+
Get the number of messages in the current context.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Message count
|
|
127
|
+
"""
|
|
128
|
+
context = self.get_current_context()
|
|
129
|
+
return len(context.messages)
|
|
130
|
+
|
|
131
|
+
def get_last_message(self) -> Optional[ChatMessage]:
|
|
132
|
+
"""
|
|
133
|
+
Get the last message in the conversation.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Last ChatMessage or None if no messages
|
|
137
|
+
"""
|
|
138
|
+
context = self.get_current_context()
|
|
139
|
+
if context.messages:
|
|
140
|
+
return context.messages[-1]
|
|
141
|
+
return None
|