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,617 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Batch Approval Display Components for Omnimancer CLI.
|
|
3
|
+
|
|
4
|
+
This module provides Rich-based UI components for displaying batch approval
|
|
5
|
+
information including action lists, summaries, and risk assessments.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from rich.console import Console, Group
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.progress import BarColumn, Progress, TextColumn, TimeElapsedColumn
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
|
|
20
|
+
from ..core.agent.approval_manager import BatchApprovalRequest, ChangePreview
|
|
21
|
+
from ..core.agent.types import Operation, OperationType
|
|
22
|
+
from ..core.security.approval_workflow import ApprovalStatus
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DisplayMode(Enum):
|
|
28
|
+
"""Display modes for batch approval interface."""
|
|
29
|
+
|
|
30
|
+
COMPACT = "compact"
|
|
31
|
+
DETAILED = "detailed"
|
|
32
|
+
SUMMARY_ONLY = "summary"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class BatchDisplayConfig:
|
|
37
|
+
"""Configuration for batch approval display."""
|
|
38
|
+
|
|
39
|
+
mode: DisplayMode = DisplayMode.DETAILED
|
|
40
|
+
show_risk_colors: bool = True
|
|
41
|
+
show_line_numbers: bool = True
|
|
42
|
+
max_preview_length: int = 100
|
|
43
|
+
items_per_page: int = 10
|
|
44
|
+
show_timestamps: bool = True
|
|
45
|
+
compact_operation_names: bool = True
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BatchApprovalPanel:
|
|
49
|
+
"""
|
|
50
|
+
Rich-based UI components for displaying batch approval information.
|
|
51
|
+
|
|
52
|
+
Provides structured display of batch approval requests including
|
|
53
|
+
action lists, summaries, risk assessments, and interactive elements.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
console: Optional[Console] = None,
|
|
59
|
+
config: Optional[BatchDisplayConfig] = None,
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Initialize batch approval panel.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
console: Rich console for output
|
|
66
|
+
config: Display configuration settings
|
|
67
|
+
"""
|
|
68
|
+
self.console = console or Console()
|
|
69
|
+
self.config = config or BatchDisplayConfig()
|
|
70
|
+
|
|
71
|
+
def render_batch_overview(self, batch_request: BatchApprovalRequest) -> Panel:
|
|
72
|
+
"""
|
|
73
|
+
Render overview panel for batch approval request.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
batch_request: Batch request to display
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Rich Panel containing batch overview
|
|
80
|
+
"""
|
|
81
|
+
# Calculate batch statistics
|
|
82
|
+
total_operations = len(batch_request.operations)
|
|
83
|
+
approved_count = len(batch_request.approved_operations)
|
|
84
|
+
pending_count = total_operations - approved_count
|
|
85
|
+
|
|
86
|
+
# Risk level distribution
|
|
87
|
+
risk_counts = self._calculate_risk_distribution(batch_request.previews)
|
|
88
|
+
|
|
89
|
+
# Create overview table
|
|
90
|
+
overview_table = Table.grid(padding=1)
|
|
91
|
+
overview_table.add_column("Label", style="bold cyan")
|
|
92
|
+
overview_table.add_column("Value", style="bright_white")
|
|
93
|
+
|
|
94
|
+
# Basic info
|
|
95
|
+
overview_table.add_row("Batch ID:", batch_request.id[:8] + "...")
|
|
96
|
+
overview_table.add_row(
|
|
97
|
+
"Created:", batch_request.created_at.strftime("%Y-%m-%d %H:%M:%S")
|
|
98
|
+
)
|
|
99
|
+
overview_table.add_row("Status:", self._format_status(batch_request.status))
|
|
100
|
+
|
|
101
|
+
if batch_request.expires_at:
|
|
102
|
+
time_remaining = batch_request.expires_at - datetime.now()
|
|
103
|
+
if time_remaining.total_seconds() > 0:
|
|
104
|
+
overview_table.add_row(
|
|
105
|
+
"Expires:", f"{time_remaining.seconds // 60} minutes"
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
overview_table.add_row("Expires:", "[red]EXPIRED[/red]")
|
|
109
|
+
|
|
110
|
+
# Operation counts
|
|
111
|
+
overview_table.add_row("Total Operations:", str(total_operations))
|
|
112
|
+
if approved_count > 0:
|
|
113
|
+
overview_table.add_row("Approved:", f"[green]{approved_count}[/green]")
|
|
114
|
+
if pending_count > 0:
|
|
115
|
+
overview_table.add_row("Pending:", f"[yellow]{pending_count}[/yellow]")
|
|
116
|
+
|
|
117
|
+
# Risk distribution
|
|
118
|
+
if risk_counts:
|
|
119
|
+
overview_table.add_row("", "") # Separator
|
|
120
|
+
overview_table.add_row("Risk Distribution:", "")
|
|
121
|
+
for risk_level, count in risk_counts.items():
|
|
122
|
+
color = self._get_risk_color(risk_level)
|
|
123
|
+
overview_table.add_row(
|
|
124
|
+
f" {risk_level.title()}:", f"[{color}]{count}[/{color}]"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return Panel(
|
|
128
|
+
overview_table,
|
|
129
|
+
title="[bold]Batch Approval Overview[/bold]",
|
|
130
|
+
title_align="left",
|
|
131
|
+
border_style="blue",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def render_operations_table(
|
|
135
|
+
self,
|
|
136
|
+
batch_request: BatchApprovalRequest,
|
|
137
|
+
page: int = 0,
|
|
138
|
+
show_approved: bool = True,
|
|
139
|
+
) -> Panel:
|
|
140
|
+
"""
|
|
141
|
+
Render table of operations in the batch.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
batch_request: Batch request to display
|
|
145
|
+
page: Page number for pagination
|
|
146
|
+
show_approved: Whether to show already approved operations
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Rich Panel containing operations table
|
|
150
|
+
"""
|
|
151
|
+
operations = batch_request.operations
|
|
152
|
+
previews = batch_request.previews
|
|
153
|
+
|
|
154
|
+
# Filter operations if needed
|
|
155
|
+
if not show_approved:
|
|
156
|
+
filtered_ops = []
|
|
157
|
+
filtered_previews = []
|
|
158
|
+
for i, (op, preview) in enumerate(zip(operations, previews)):
|
|
159
|
+
if i not in batch_request.approved_operations:
|
|
160
|
+
filtered_ops.append(op)
|
|
161
|
+
filtered_previews.append(preview)
|
|
162
|
+
operations = filtered_ops
|
|
163
|
+
previews = filtered_previews
|
|
164
|
+
|
|
165
|
+
# Pagination
|
|
166
|
+
start_idx = page * self.config.items_per_page
|
|
167
|
+
end_idx = min(start_idx + self.config.items_per_page, len(operations))
|
|
168
|
+
|
|
169
|
+
if start_idx >= len(operations):
|
|
170
|
+
return Panel(
|
|
171
|
+
"[yellow]No operations to display on this page.[/yellow]",
|
|
172
|
+
title="Operations",
|
|
173
|
+
border_style="yellow",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Create operations table
|
|
177
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
178
|
+
|
|
179
|
+
if self.config.show_line_numbers:
|
|
180
|
+
table.add_column("#", style="dim", width=4)
|
|
181
|
+
|
|
182
|
+
table.add_column("Type", style="cyan", width=15)
|
|
183
|
+
table.add_column("Target", style="bright_white", width=30)
|
|
184
|
+
table.add_column("Risk", width=8, justify="center")
|
|
185
|
+
table.add_column("Status", width=10, justify="center")
|
|
186
|
+
|
|
187
|
+
if self.config.mode == DisplayMode.DETAILED:
|
|
188
|
+
table.add_column("Description", style="dim", width=40)
|
|
189
|
+
|
|
190
|
+
# Add rows
|
|
191
|
+
for i in range(start_idx, end_idx):
|
|
192
|
+
operation = operations[i]
|
|
193
|
+
preview = previews[i] if i < len(previews) else None
|
|
194
|
+
|
|
195
|
+
# Determine status
|
|
196
|
+
original_idx = (
|
|
197
|
+
i if show_approved else self._get_original_index(batch_request, i)
|
|
198
|
+
)
|
|
199
|
+
is_approved = original_idx in batch_request.approved_operations
|
|
200
|
+
status = (
|
|
201
|
+
"[green]✓ Approved[/green]"
|
|
202
|
+
if is_approved
|
|
203
|
+
else "[yellow]○ Pending[/yellow]"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Format operation type
|
|
207
|
+
op_type = self._format_operation_type(operation.type)
|
|
208
|
+
|
|
209
|
+
# Format target
|
|
210
|
+
target = self._format_target(operation)
|
|
211
|
+
|
|
212
|
+
# Risk assessment
|
|
213
|
+
risk_display = (
|
|
214
|
+
self._format_risk_assessment(preview) if preview else "Unknown"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Build row
|
|
218
|
+
row = []
|
|
219
|
+
if self.config.show_line_numbers:
|
|
220
|
+
row.append(str(i + 1))
|
|
221
|
+
|
|
222
|
+
row.extend([op_type, target, risk_display, status])
|
|
223
|
+
|
|
224
|
+
if self.config.mode == DisplayMode.DETAILED:
|
|
225
|
+
description = operation.description or "No description"
|
|
226
|
+
if len(description) > self.config.max_preview_length:
|
|
227
|
+
description = description[: self.config.max_preview_length] + "..."
|
|
228
|
+
row.append(description)
|
|
229
|
+
|
|
230
|
+
table.add_row(*row)
|
|
231
|
+
|
|
232
|
+
# Panel title with pagination info
|
|
233
|
+
total_ops = len(operations)
|
|
234
|
+
page_info = (
|
|
235
|
+
f"Page {page + 1} of {(total_ops - 1) // self.config.items_per_page + 1}"
|
|
236
|
+
if total_ops > self.config.items_per_page
|
|
237
|
+
else ""
|
|
238
|
+
)
|
|
239
|
+
title = f"[bold]Operations ({start_idx + 1}-{end_idx} of {total_ops})[/bold]"
|
|
240
|
+
if page_info:
|
|
241
|
+
title += f" - {page_info}"
|
|
242
|
+
|
|
243
|
+
return Panel(
|
|
244
|
+
table,
|
|
245
|
+
title=title,
|
|
246
|
+
title_align="left",
|
|
247
|
+
border_style="green" if show_approved else "yellow",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def render_action_details(
|
|
251
|
+
self, operation: Operation, preview: Optional[ChangePreview] = None
|
|
252
|
+
) -> Panel:
|
|
253
|
+
"""
|
|
254
|
+
Render detailed view of a single operation.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
operation: Operation to display
|
|
258
|
+
preview: Optional change preview
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Rich Panel with operation details
|
|
262
|
+
"""
|
|
263
|
+
details_group = []
|
|
264
|
+
|
|
265
|
+
# Operation information
|
|
266
|
+
info_table = Table.grid(padding=1)
|
|
267
|
+
info_table.add_column("Label", style="bold cyan")
|
|
268
|
+
info_table.add_column("Value", style="bright_white")
|
|
269
|
+
|
|
270
|
+
info_table.add_row("Type:", self._format_operation_type(operation.type))
|
|
271
|
+
info_table.add_row("Description:", operation.description or "No description")
|
|
272
|
+
info_table.add_row(
|
|
273
|
+
"Requires Approval:",
|
|
274
|
+
"Yes" if operation.requires_approval else "No",
|
|
275
|
+
)
|
|
276
|
+
info_table.add_row("Reversible:", "Yes" if operation.reversible else "No")
|
|
277
|
+
|
|
278
|
+
if operation.data:
|
|
279
|
+
info_table.add_row("", "") # Separator
|
|
280
|
+
info_table.add_row("Operation Data:", "")
|
|
281
|
+
for key, value in operation.data.items():
|
|
282
|
+
display_value = str(value)
|
|
283
|
+
if len(display_value) > 100:
|
|
284
|
+
display_value = display_value[:100] + "..."
|
|
285
|
+
info_table.add_row(f" {key}:", display_value)
|
|
286
|
+
|
|
287
|
+
details_group.append(
|
|
288
|
+
Panel(info_table, title="Operation Details", border_style="blue")
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Preview information
|
|
292
|
+
if preview:
|
|
293
|
+
preview_table = Table.grid(padding=1)
|
|
294
|
+
preview_table.add_column("Label", style="bold cyan")
|
|
295
|
+
preview_table.add_column("Value", style="bright_white")
|
|
296
|
+
|
|
297
|
+
preview_table.add_row(
|
|
298
|
+
"Change Type:",
|
|
299
|
+
preview.change_type.value.replace("_", " ").title(),
|
|
300
|
+
)
|
|
301
|
+
preview_table.add_row(
|
|
302
|
+
"Risk Assessment:", self._format_risk_assessment(preview)
|
|
303
|
+
)
|
|
304
|
+
preview_table.add_row("Reversible:", "Yes" if preview.reversible else "No")
|
|
305
|
+
|
|
306
|
+
if preview.metadata:
|
|
307
|
+
preview_table.add_row("", "") # Separator
|
|
308
|
+
preview_table.add_row("Metadata:", "")
|
|
309
|
+
for key, value in preview.metadata.items():
|
|
310
|
+
preview_table.add_row(f" {key}:", str(value))
|
|
311
|
+
|
|
312
|
+
details_group.append(
|
|
313
|
+
Panel(
|
|
314
|
+
preview_table,
|
|
315
|
+
title="Change Preview",
|
|
316
|
+
border_style="yellow",
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Show diff if available
|
|
321
|
+
if preview.diff:
|
|
322
|
+
diff_text = preview.diff
|
|
323
|
+
if len(diff_text) > 500: # Limit diff display
|
|
324
|
+
diff_text = diff_text[:500] + "\n\n... (diff truncated)"
|
|
325
|
+
|
|
326
|
+
details_group.append(
|
|
327
|
+
Panel(
|
|
328
|
+
Text(diff_text, style="dim"),
|
|
329
|
+
title="Changes",
|
|
330
|
+
border_style="magenta",
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
return Panel(
|
|
335
|
+
Group(*details_group),
|
|
336
|
+
title=f"[bold]Action Details - {operation.type.value}[/bold]",
|
|
337
|
+
title_align="left",
|
|
338
|
+
border_style="white",
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def render_batch_summary(self, batch_request: BatchApprovalRequest) -> Panel:
|
|
342
|
+
"""
|
|
343
|
+
Render summary of batch operations and risk assessment.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
batch_request: Batch request to summarize
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Rich Panel with batch summary
|
|
350
|
+
"""
|
|
351
|
+
summary_parts = []
|
|
352
|
+
|
|
353
|
+
# Operation type breakdown
|
|
354
|
+
type_counts = {}
|
|
355
|
+
for operation in batch_request.operations:
|
|
356
|
+
type_name = operation.type.value
|
|
357
|
+
type_counts[type_name] = type_counts.get(type_name, 0) + 1
|
|
358
|
+
|
|
359
|
+
type_table = Table(show_header=True, header_style="bold")
|
|
360
|
+
type_table.add_column("Operation Type", style="cyan")
|
|
361
|
+
type_table.add_column("Count", style="green", justify="right")
|
|
362
|
+
|
|
363
|
+
for op_type, count in sorted(type_counts.items()):
|
|
364
|
+
formatted_type = op_type.replace("_", " ").title()
|
|
365
|
+
type_table.add_row(formatted_type, str(count))
|
|
366
|
+
|
|
367
|
+
summary_parts.append(
|
|
368
|
+
Panel(type_table, title="Operation Types", border_style="cyan")
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Risk assessment summary
|
|
372
|
+
risk_counts = self._calculate_risk_distribution(batch_request.previews)
|
|
373
|
+
overall_risk = self._assess_overall_risk(risk_counts)
|
|
374
|
+
|
|
375
|
+
risk_table = Table(show_header=True, header_style="bold")
|
|
376
|
+
risk_table.add_column("Risk Level", style="yellow")
|
|
377
|
+
risk_table.add_column("Count", style="green", justify="right")
|
|
378
|
+
risk_table.add_column("Percentage", style="blue", justify="right")
|
|
379
|
+
|
|
380
|
+
total_with_risk = sum(risk_counts.values())
|
|
381
|
+
for risk_level, count in risk_counts.items():
|
|
382
|
+
if count > 0:
|
|
383
|
+
percentage = (
|
|
384
|
+
(count / total_with_risk) * 100 if total_with_risk > 0 else 0
|
|
385
|
+
)
|
|
386
|
+
color = self._get_risk_color(risk_level)
|
|
387
|
+
risk_table.add_row(
|
|
388
|
+
f"[{color}]{risk_level.title()}[/{color}]",
|
|
389
|
+
str(count),
|
|
390
|
+
f"{percentage:.1f}%",
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
risk_panel = Panel(
|
|
394
|
+
risk_table,
|
|
395
|
+
title=f"Risk Assessment - Overall: {overall_risk}",
|
|
396
|
+
border_style="red",
|
|
397
|
+
)
|
|
398
|
+
summary_parts.append(risk_panel)
|
|
399
|
+
|
|
400
|
+
return Panel(
|
|
401
|
+
Group(*summary_parts),
|
|
402
|
+
title="[bold]Batch Summary[/bold]",
|
|
403
|
+
title_align="left",
|
|
404
|
+
border_style="blue",
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def render_progress_display(self, batch_request: BatchApprovalRequest) -> Panel:
|
|
408
|
+
"""
|
|
409
|
+
Render progress display for batch processing.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
batch_request: Batch request to show progress for
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Rich Panel with progress information
|
|
416
|
+
"""
|
|
417
|
+
total_operations = len(batch_request.operations)
|
|
418
|
+
approved_count = len(batch_request.approved_operations)
|
|
419
|
+
|
|
420
|
+
# Create progress bar
|
|
421
|
+
progress = Progress(
|
|
422
|
+
TextColumn("[bold blue]Processing batch...", justify="right"),
|
|
423
|
+
BarColumn(bar_width=None),
|
|
424
|
+
TextColumn("[progress.percentage]{task.percentage:>3.1f}%"),
|
|
425
|
+
TextColumn("•"),
|
|
426
|
+
TextColumn("{task.completed}/{task.total} operations"),
|
|
427
|
+
TimeElapsedColumn(),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
progress.add_task(
|
|
431
|
+
"batch_progress", total=total_operations, completed=approved_count
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
return Panel(
|
|
435
|
+
progress,
|
|
436
|
+
title=f"[bold]Batch Progress[/bold]",
|
|
437
|
+
title_align="left",
|
|
438
|
+
border_style="green",
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# Helper methods
|
|
442
|
+
|
|
443
|
+
def _calculate_risk_distribution(
|
|
444
|
+
self, previews: List[ChangePreview]
|
|
445
|
+
) -> Dict[str, int]:
|
|
446
|
+
"""Calculate distribution of risk levels in previews."""
|
|
447
|
+
risk_counts = {
|
|
448
|
+
"low": 0,
|
|
449
|
+
"medium": 0,
|
|
450
|
+
"high": 0,
|
|
451
|
+
"critical": 0,
|
|
452
|
+
"unknown": 0,
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
for preview in previews:
|
|
456
|
+
if preview.risk_assessment:
|
|
457
|
+
risk_level = self._extract_risk_level(preview.risk_assessment)
|
|
458
|
+
risk_counts[risk_level] = risk_counts.get(risk_level, 0) + 1
|
|
459
|
+
else:
|
|
460
|
+
risk_counts["unknown"] += 1
|
|
461
|
+
|
|
462
|
+
return risk_counts
|
|
463
|
+
|
|
464
|
+
def _extract_risk_level(self, risk_assessment: str) -> str:
|
|
465
|
+
"""Extract risk level from risk assessment string."""
|
|
466
|
+
risk_lower = risk_assessment.lower()
|
|
467
|
+
if "critical" in risk_lower:
|
|
468
|
+
return "critical"
|
|
469
|
+
elif "high" in risk_lower:
|
|
470
|
+
return "high"
|
|
471
|
+
elif "medium" in risk_lower:
|
|
472
|
+
return "medium"
|
|
473
|
+
elif "low" in risk_lower:
|
|
474
|
+
return "low"
|
|
475
|
+
else:
|
|
476
|
+
return "unknown"
|
|
477
|
+
|
|
478
|
+
def _assess_overall_risk(self, risk_counts: Dict[str, int]) -> str:
|
|
479
|
+
"""Assess overall risk level based on distribution."""
|
|
480
|
+
if risk_counts.get("critical", 0) > 0:
|
|
481
|
+
return "[red]CRITICAL[/red]"
|
|
482
|
+
elif risk_counts.get("high", 0) > 0:
|
|
483
|
+
return "[red]HIGH[/red]"
|
|
484
|
+
elif risk_counts.get("medium", 0) > 0:
|
|
485
|
+
return "[yellow]MEDIUM[/yellow]"
|
|
486
|
+
elif risk_counts.get("low", 0) > 0:
|
|
487
|
+
return "[green]LOW[/green]"
|
|
488
|
+
else:
|
|
489
|
+
return "[dim]UNKNOWN[/dim]"
|
|
490
|
+
|
|
491
|
+
def _get_risk_color(self, risk_level: str) -> str:
|
|
492
|
+
"""Get color for risk level."""
|
|
493
|
+
color_map = {
|
|
494
|
+
"low": "green",
|
|
495
|
+
"medium": "yellow",
|
|
496
|
+
"high": "red",
|
|
497
|
+
"critical": "bright_red",
|
|
498
|
+
"unknown": "dim",
|
|
499
|
+
}
|
|
500
|
+
return color_map.get(risk_level.lower(), "white")
|
|
501
|
+
|
|
502
|
+
def _format_status(self, status: ApprovalStatus) -> str:
|
|
503
|
+
"""Format approval status with colors."""
|
|
504
|
+
status_colors = {
|
|
505
|
+
ApprovalStatus.PENDING: "yellow",
|
|
506
|
+
ApprovalStatus.APPROVED: "green",
|
|
507
|
+
ApprovalStatus.DENIED: "red",
|
|
508
|
+
ApprovalStatus.EXPIRED: "dim",
|
|
509
|
+
}
|
|
510
|
+
color = status_colors.get(status, "white")
|
|
511
|
+
return f"[{color}]{status.value.title()}[/{color}]"
|
|
512
|
+
|
|
513
|
+
def _format_operation_type(self, op_type: OperationType) -> str:
|
|
514
|
+
"""Format operation type for display."""
|
|
515
|
+
if self.config.compact_operation_names:
|
|
516
|
+
# Use shorter names
|
|
517
|
+
type_map = {
|
|
518
|
+
OperationType.FILE_READ: "Read",
|
|
519
|
+
OperationType.FILE_WRITE: "Write",
|
|
520
|
+
OperationType.FILE_DELETE: "Delete",
|
|
521
|
+
OperationType.DIRECTORY_CREATE: "MkDir",
|
|
522
|
+
OperationType.DIRECTORY_DELETE: "RmDir",
|
|
523
|
+
OperationType.COMMAND_EXECUTE: "Exec",
|
|
524
|
+
OperationType.WEB_REQUEST: "Web",
|
|
525
|
+
OperationType.MCP_TOOL_CALL: "MCP",
|
|
526
|
+
}
|
|
527
|
+
return type_map.get(op_type, op_type.value)
|
|
528
|
+
else:
|
|
529
|
+
return op_type.value.replace("_", " ").title()
|
|
530
|
+
|
|
531
|
+
def _format_target(self, operation: Operation) -> str:
|
|
532
|
+
"""Format operation target for display."""
|
|
533
|
+
if operation.data:
|
|
534
|
+
# Extract meaningful target from operation data
|
|
535
|
+
if "path" in operation.data:
|
|
536
|
+
path = operation.data["path"]
|
|
537
|
+
if len(path) > 25:
|
|
538
|
+
return f"...{path[-25:]}"
|
|
539
|
+
return path
|
|
540
|
+
elif "url" in operation.data:
|
|
541
|
+
url = operation.data["url"]
|
|
542
|
+
if len(url) > 25:
|
|
543
|
+
return f"...{url[-25:]}"
|
|
544
|
+
return url
|
|
545
|
+
elif "command" in operation.data:
|
|
546
|
+
cmd = operation.data["command"]
|
|
547
|
+
if len(cmd) > 25:
|
|
548
|
+
return f"...{cmd[-25:]}"
|
|
549
|
+
return cmd
|
|
550
|
+
|
|
551
|
+
return (
|
|
552
|
+
operation.description[:25] + "..."
|
|
553
|
+
if len(operation.description or "") > 25
|
|
554
|
+
else (operation.description or "No target")
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
def _format_risk_assessment(self, preview: Optional[ChangePreview]) -> str:
|
|
558
|
+
"""Format risk assessment for display."""
|
|
559
|
+
if not preview or not preview.risk_assessment:
|
|
560
|
+
return "[dim]Unknown[/dim]"
|
|
561
|
+
|
|
562
|
+
risk_level = self._extract_risk_level(preview.risk_assessment)
|
|
563
|
+
color = self._get_risk_color(risk_level)
|
|
564
|
+
return f"[{color}]{risk_level.title()}[/{color}]"
|
|
565
|
+
|
|
566
|
+
def _get_original_index(
|
|
567
|
+
self, batch_request: BatchApprovalRequest, filtered_index: int
|
|
568
|
+
) -> int:
|
|
569
|
+
"""Get original index when operations are filtered."""
|
|
570
|
+
# This would need more sophisticated logic if we implement complex filtering
|
|
571
|
+
# For now, assume simple filtering where we just skip approved items
|
|
572
|
+
current_filtered = 0
|
|
573
|
+
for i, operation in enumerate(batch_request.operations):
|
|
574
|
+
if i not in batch_request.approved_operations:
|
|
575
|
+
if current_filtered == filtered_index:
|
|
576
|
+
return i
|
|
577
|
+
current_filtered += 1
|
|
578
|
+
return filtered_index
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def create_batch_approval_panel(
|
|
582
|
+
console: Optional[Console] = None,
|
|
583
|
+
config: Optional[BatchDisplayConfig] = None,
|
|
584
|
+
) -> BatchApprovalPanel:
|
|
585
|
+
"""
|
|
586
|
+
Create a batch approval panel with default configuration.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
console: Rich console for output
|
|
590
|
+
config: Display configuration settings
|
|
591
|
+
|
|
592
|
+
Returns:
|
|
593
|
+
Configured BatchApprovalPanel instance
|
|
594
|
+
"""
|
|
595
|
+
return BatchApprovalPanel(console=console, config=config)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def create_compact_batch_panel(
|
|
599
|
+
console: Optional[Console] = None,
|
|
600
|
+
) -> BatchApprovalPanel:
|
|
601
|
+
"""
|
|
602
|
+
Create a batch approval panel optimized for compact display.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
console: Rich console for output
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
BatchApprovalPanel configured for compact mode
|
|
609
|
+
"""
|
|
610
|
+
config = BatchDisplayConfig(
|
|
611
|
+
mode=DisplayMode.COMPACT,
|
|
612
|
+
show_line_numbers=False,
|
|
613
|
+
max_preview_length=50,
|
|
614
|
+
items_per_page=20,
|
|
615
|
+
compact_operation_names=True,
|
|
616
|
+
)
|
|
617
|
+
return BatchApprovalPanel(console=console, config=config)
|