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.
Files changed (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,734 @@
1
+ """
2
+ CLI Approval Dialog Formatter for Omnimancer.
3
+
4
+ This module provides terminal UI formatting for approval requests from
5
+ EnhancedApprovalManager, converting them into rich, readable terminal output
6
+ with proper formatting for action context, impact assessment, and risk scores.
7
+ """
8
+
9
+ import json
10
+ import logging
11
+ from pathlib import Path
12
+ from typing import Any, Dict, Optional
13
+
14
+ from rich.console import Console, Group
15
+ from rich.panel import Panel
16
+ from rich.progress import BarColumn, Progress, TextColumn
17
+ from rich.rule import Rule
18
+ from rich.syntax import Syntax
19
+ from rich.table import Table
20
+ from rich.text import Text
21
+
22
+ from ..core.agent.approval_manager import (
23
+ BatchApprovalRequest,
24
+ ChangePreview,
25
+ ChangeType,
26
+ )
27
+ from ..core.security.approval_workflow import (
28
+ ApprovalRequest,
29
+ RiskLevel,
30
+ )
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class CLIApprovalFormatter:
36
+ """
37
+ Terminal UI formatter for approval requests.
38
+
39
+ Converts ApprovalRequest and ChangePreview objects into rich,
40
+ readable terminal output with color coding, visual indicators,
41
+ and structured data display.
42
+ """
43
+
44
+ def __init__(self, console: Optional[Console] = None):
45
+ """
46
+ Initialize the approval formatter.
47
+
48
+ Args:
49
+ console: Rich Console instance (creates new if None)
50
+ """
51
+ self.console = console or Console()
52
+
53
+ # Risk level color mapping
54
+ self.risk_colors = {
55
+ RiskLevel.LOW: "green",
56
+ RiskLevel.MEDIUM: "yellow",
57
+ RiskLevel.HIGH: "red",
58
+ RiskLevel.CRITICAL: "bold red",
59
+ }
60
+
61
+ # Change type icons
62
+ self.change_type_icons = {
63
+ ChangeType.FILE_CREATE: "📄",
64
+ ChangeType.FILE_MODIFY: "✏️ ",
65
+ ChangeType.FILE_DELETE: "🗑️ ",
66
+ ChangeType.DIRECTORY_CREATE: "📁",
67
+ ChangeType.DIRECTORY_DELETE: "📂",
68
+ ChangeType.COMMAND_EXECUTE: "⚡",
69
+ ChangeType.WEB_REQUEST: "🌐",
70
+ ChangeType.MCP_TOOL_CALL: "🔧",
71
+ }
72
+
73
+ # Risk level icons
74
+ self.risk_icons = {
75
+ RiskLevel.LOW: "✅",
76
+ RiskLevel.MEDIUM: "⚠️ ",
77
+ RiskLevel.HIGH: "⛔",
78
+ RiskLevel.CRITICAL: "🚨",
79
+ }
80
+
81
+ def format_approval_dialog(
82
+ self,
83
+ approval_request: ApprovalRequest,
84
+ preview: Optional[ChangePreview] = None,
85
+ operation_data: Optional[Dict[str, Any]] = None,
86
+ ) -> Group:
87
+ """
88
+ Format a complete approval dialog for terminal display.
89
+
90
+ Args:
91
+ approval_request: The approval request from workflow
92
+ preview: Optional change preview with diff information
93
+ operation_data: Additional operation data for context
94
+
95
+ Returns:
96
+ Rich Group with formatted approval dialog components
97
+ """
98
+ components = []
99
+
100
+ # Header with operation title and risk level
101
+ components.append(self._format_header(approval_request, preview))
102
+
103
+ # Separator
104
+ components.append(Rule(style="dim"))
105
+
106
+ # Operation details
107
+ components.append(
108
+ self._format_operation_details(approval_request, operation_data)
109
+ )
110
+
111
+ # Change preview and diff if available
112
+ if preview:
113
+ components.append(self._format_change_preview(preview))
114
+
115
+ # Show diff if available
116
+ if preview.diff:
117
+ components.append(self._format_diff_display(preview))
118
+
119
+ # Risk assessment
120
+ components.append(self._format_risk_assessment(approval_request))
121
+
122
+ # Impact assessment
123
+ if preview and preview.metadata:
124
+ components.append(self._format_impact_assessment(preview))
125
+
126
+ # Metadata information
127
+ if approval_request.metadata:
128
+ components.append(self._format_metadata(approval_request.metadata))
129
+
130
+ # Time information
131
+ components.append(self._format_time_info(approval_request))
132
+
133
+ # Separator before options
134
+ components.append(Rule(style="dim"))
135
+
136
+ # Available options
137
+ components.append(self._format_approval_options())
138
+
139
+ return Group(*components)
140
+
141
+ def format_batch_approval_dialog(
142
+ self, batch_request: BatchApprovalRequest
143
+ ) -> Group:
144
+ """
145
+ Format a batch approval dialog for multiple operations.
146
+
147
+ Args:
148
+ batch_request: BatchApprovalRequest containing multiple operations
149
+
150
+ Returns:
151
+ Rich Group with formatted batch approval dialog
152
+ """
153
+ components = []
154
+
155
+ # Batch header
156
+ components.append(self._format_batch_header(batch_request))
157
+ components.append(Rule(style="dim"))
158
+
159
+ # Summary table
160
+ components.append(self._format_batch_summary(batch_request))
161
+
162
+ # Individual operations
163
+ components.append(Text("Operations:", style="bold"))
164
+ for i, (operation, preview) in enumerate(
165
+ zip(batch_request.operations, batch_request.previews)
166
+ ):
167
+ # Create a mini approval request for consistent formatting
168
+ mini_request = ApprovalRequest(
169
+ operation_type=operation.type.value,
170
+ description=operation.description,
171
+ metadata=operation.data,
172
+ )
173
+
174
+ operation_panel = Panel(
175
+ self._format_single_batch_operation(mini_request, preview, i + 1),
176
+ title=f"Operation {i + 1}",
177
+ border_style="dim",
178
+ )
179
+ components.append(operation_panel)
180
+
181
+ # Batch approval options
182
+ components.append(Rule(style="dim"))
183
+ components.append(
184
+ self._format_batch_approval_options(len(batch_request.operations))
185
+ )
186
+
187
+ return Group(*components)
188
+
189
+ def _format_header(
190
+ self,
191
+ approval_request: ApprovalRequest,
192
+ preview: Optional[ChangePreview],
193
+ ) -> Panel:
194
+ """Format the approval dialog header."""
195
+ # Get icon for change type
196
+ icon = ""
197
+ if preview:
198
+ icon = self.change_type_icons.get(preview.change_type, "❓")
199
+
200
+ # Risk level with color and icon
201
+ risk_icon = self.risk_icons.get(approval_request.risk_level, "❓")
202
+ risk_color = self.risk_colors.get(approval_request.risk_level, "white")
203
+
204
+ title = f"{icon} {approval_request.operation_type.replace('_', ' ').title()}"
205
+ risk_text = Text(
206
+ f"{risk_icon} Risk Level: {approval_request.risk_level.value.upper()}",
207
+ style=risk_color,
208
+ )
209
+
210
+ header_content = Group(
211
+ Text(title, style="bold white"),
212
+ Text(approval_request.description, style="dim white"),
213
+ Text(),
214
+ risk_text,
215
+ )
216
+
217
+ return Panel(
218
+ header_content,
219
+ title="🔐 Approval Required",
220
+ title_align="left",
221
+ border_style=risk_color,
222
+ )
223
+
224
+ def _format_operation_details(
225
+ self,
226
+ approval_request: ApprovalRequest,
227
+ operation_data: Optional[Dict[str, Any]],
228
+ ) -> Panel:
229
+ """Format operation details table."""
230
+ table = Table(show_header=False, box=None, pad_edge=False)
231
+ table.add_column("Field", style="cyan", min_width=15)
232
+ table.add_column("Value", style="white")
233
+
234
+ # Basic operation info
235
+ table.add_row("Operation Type", approval_request.operation_type)
236
+ table.add_row("Requested By", approval_request.requested_by)
237
+ table.add_row(
238
+ "Request Time", approval_request.requested_at.strftime("%H:%M:%S")
239
+ )
240
+
241
+ # Operation-specific details from metadata or operation data
242
+ data_source = operation_data or approval_request.metadata
243
+ if data_source:
244
+ # Common operation details
245
+ if "path" in data_source:
246
+ table.add_row("Target Path", str(data_source["path"]))
247
+
248
+ if "command" in data_source:
249
+ table.add_row("Command", str(data_source["command"]))
250
+
251
+ if "url" in data_source:
252
+ table.add_row("URL", str(data_source["url"]))
253
+
254
+ if "content_length" in data_source:
255
+ size_kb = int(data_source["content_length"]) / 1024
256
+ table.add_row("Content Size", f"{size_kb:.1f} KB")
257
+
258
+ return Panel(
259
+ table,
260
+ title="Operation Details",
261
+ title_align="left",
262
+ border_style="blue",
263
+ )
264
+
265
+ def _format_change_preview(self, preview: ChangePreview) -> Panel:
266
+ """Format change preview information."""
267
+ preview_content = []
268
+
269
+ # Change description
270
+ preview_content.append(Text(preview.description, style="white"))
271
+ preview_content.append(Text())
272
+
273
+ # Key attributes
274
+ attrs_table = Table(show_header=False, box=None, pad_edge=False)
275
+ attrs_table.add_column("Attribute", style="cyan", min_width=12)
276
+ attrs_table.add_column("Value", style="white")
277
+
278
+ attrs_table.add_row("Reversible", "✅ Yes" if preview.reversible else "❌ No")
279
+
280
+ if preview.risk_assessment:
281
+ attrs_table.add_row("Risk", preview.risk_assessment)
282
+
283
+ # File-specific information
284
+ if preview.change_type in [
285
+ ChangeType.FILE_CREATE,
286
+ ChangeType.FILE_MODIFY,
287
+ ]:
288
+ if preview.proposed_state:
289
+ lines = len(preview.proposed_state.splitlines())
290
+ attrs_table.add_row("New Lines", str(lines))
291
+
292
+ if preview.change_type == ChangeType.FILE_DELETE:
293
+ if preview.current_state:
294
+ lines = len(preview.current_state.splitlines())
295
+ attrs_table.add_row("Lines Lost", str(lines))
296
+
297
+ # Directory-specific information
298
+ if preview.change_type == ChangeType.DIRECTORY_DELETE:
299
+ item_count = preview.metadata.get("item_count", 0)
300
+ attrs_table.add_row("Items Inside", str(item_count))
301
+
302
+ preview_content.append(attrs_table)
303
+
304
+ return Panel(
305
+ Group(*preview_content),
306
+ title=f"{self.change_type_icons.get(preview.change_type, '❓')} Change Preview",
307
+ title_align="left",
308
+ border_style="yellow",
309
+ )
310
+
311
+ def _format_diff_display(self, preview: ChangePreview) -> Panel:
312
+ """Format diff visualization with syntax highlighting."""
313
+ if not preview.diff:
314
+ return Panel(
315
+ Text("No diff available", style="dim"),
316
+ title="📋 Changes",
317
+ border_style="dim",
318
+ )
319
+
320
+ # Try to detect file type for syntax highlighting
321
+ if "path" in preview.metadata:
322
+ path = Path(preview.metadata["path"])
323
+ path.suffix.lstrip(".")
324
+
325
+ # Limit diff length to prevent terminal overflow
326
+ diff_text = preview.diff
327
+ if len(diff_text) > 2000:
328
+ lines = diff_text.splitlines()
329
+ if len(lines) > 50:
330
+ truncated_lines = (
331
+ lines[:25]
332
+ + ["...", "# Diff truncated - too long to display", "..."]
333
+ + lines[-25:]
334
+ )
335
+ diff_text = "\n".join(truncated_lines)
336
+
337
+ try:
338
+ # Use syntax highlighting for diff
339
+ syntax = Syntax(
340
+ diff_text,
341
+ "diff",
342
+ theme="monokai",
343
+ line_numbers=False,
344
+ word_wrap=False,
345
+ )
346
+ except Exception:
347
+ # Fallback to plain text if syntax highlighting fails
348
+ syntax = Text(diff_text, style="white")
349
+
350
+ return Panel(
351
+ syntax,
352
+ title="📋 Changes (Diff)",
353
+ title_align="left",
354
+ border_style="magenta",
355
+ )
356
+
357
+ def _format_risk_assessment(self, approval_request: ApprovalRequest) -> Panel:
358
+ """Format risk assessment with visual indicators."""
359
+ risk_level = approval_request.risk_level
360
+ risk_color = self.risk_colors[risk_level]
361
+ risk_icon = self.risk_icons[risk_level]
362
+
363
+ # Risk level bar
364
+ risk_value = {
365
+ RiskLevel.LOW: 25,
366
+ RiskLevel.MEDIUM: 50,
367
+ RiskLevel.HIGH: 75,
368
+ RiskLevel.CRITICAL: 100,
369
+ }[risk_level]
370
+
371
+ # Create visual risk meter
372
+ progress = Progress(
373
+ TextColumn("[bold blue]Risk Level"),
374
+ BarColumn(bar_width=20),
375
+ TextColumn(f"{risk_icon} {risk_level.value.upper()}"),
376
+ expand=False,
377
+ )
378
+
379
+ progress.add_task("risk", total=100, completed=risk_value)
380
+
381
+ # Risk description
382
+ risk_descriptions = {
383
+ RiskLevel.LOW: "Low risk operation. Minimal security impact expected.",
384
+ RiskLevel.MEDIUM: "Medium risk operation. Review recommended before approval.",
385
+ RiskLevel.HIGH: "High risk operation. Careful consideration required.",
386
+ RiskLevel.CRITICAL: "Critical risk operation. High security impact possible.",
387
+ }
388
+
389
+ description = Text(risk_descriptions[risk_level], style=risk_color)
390
+
391
+ risk_content = Group(progress, Text(), description)
392
+
393
+ return Panel(
394
+ risk_content,
395
+ title=f"{risk_icon} Risk Assessment",
396
+ title_align="left",
397
+ border_style=risk_color,
398
+ )
399
+
400
+ def _format_impact_assessment(self, preview: ChangePreview) -> Panel:
401
+ """Format impact assessment from preview metadata."""
402
+ impact_items = []
403
+
404
+ # File system impact
405
+ if preview.change_type in [
406
+ ChangeType.FILE_CREATE,
407
+ ChangeType.FILE_MODIFY,
408
+ ChangeType.FILE_DELETE,
409
+ ]:
410
+ if "path" in preview.metadata:
411
+ path = preview.metadata["path"]
412
+ impact_items.append(f"• File system: {path}")
413
+
414
+ if preview.change_type in [
415
+ ChangeType.DIRECTORY_CREATE,
416
+ ChangeType.DIRECTORY_DELETE,
417
+ ]:
418
+ if "path" in preview.metadata:
419
+ path = preview.metadata["path"]
420
+ item_count = preview.metadata.get("item_count", 0)
421
+ impact_items.append(f"• Directory: {path} ({item_count} items)")
422
+
423
+ # Command execution impact
424
+ if preview.change_type == ChangeType.COMMAND_EXECUTE:
425
+ if "command" in preview.metadata:
426
+ command = preview.metadata["command"]
427
+ impact_items.append(f"• System command: {command}")
428
+
429
+ # Network impact
430
+ if preview.change_type == ChangeType.WEB_REQUEST:
431
+ if "url" in preview.metadata:
432
+ url = preview.metadata["url"]
433
+ method = preview.metadata.get("method", "GET")
434
+ impact_items.append(f"• Network {method}: {url}")
435
+
436
+ # Reversibility impact
437
+ reversible_text = "✅ Reversible" if preview.reversible else "❌ Not reversible"
438
+ impact_items.append(f"• Recovery: {reversible_text}")
439
+
440
+ if not impact_items:
441
+ impact_items.append("• No specific impacts identified")
442
+
443
+ impact_text = Text("\n".join(impact_items), style="white")
444
+
445
+ return Panel(
446
+ impact_text,
447
+ title="🎯 Impact Assessment",
448
+ title_align="left",
449
+ border_style="cyan",
450
+ )
451
+
452
+ def _format_metadata(self, metadata: Dict[str, Any]) -> Panel:
453
+ """Format metadata information in a readable way."""
454
+ # Filter out large or sensitive metadata
455
+ filtered_metadata = {}
456
+ for key, value in metadata.items():
457
+ if key in ["preview", "operation_data"]:
458
+ continue # Skip large nested objects
459
+
460
+ # Limit string length
461
+ if isinstance(value, str) and len(value) > 100:
462
+ filtered_metadata[key] = value[:100] + "..."
463
+ else:
464
+ filtered_metadata[key] = value
465
+
466
+ if not filtered_metadata:
467
+ return Panel(
468
+ Text("No additional metadata", style="dim"),
469
+ title="📊 Additional Information",
470
+ border_style="dim",
471
+ )
472
+
473
+ try:
474
+ # Pretty print metadata as JSON
475
+ metadata_json = json.dumps(filtered_metadata, indent=2, default=str)
476
+ syntax = Syntax(metadata_json, "json", theme="monokai", line_numbers=False)
477
+ except Exception:
478
+ # Fallback to simple text display
479
+ lines = [f"{k}: {v}" for k, v in filtered_metadata.items()]
480
+ syntax = Text("\n".join(lines), style="white")
481
+
482
+ return Panel(
483
+ syntax,
484
+ title="📊 Additional Information",
485
+ title_align="left",
486
+ border_style="dim",
487
+ )
488
+
489
+ def _format_time_info(self, approval_request: ApprovalRequest) -> Panel:
490
+ """Format timing information."""
491
+ time_content = []
492
+
493
+ # Request time
494
+ request_time = approval_request.requested_at.strftime("%Y-%m-%d %H:%M:%S")
495
+ time_content.append(Text(f"Requested: {request_time}", style="white"))
496
+
497
+ # Expiration time
498
+ if approval_request.expires_at:
499
+ expires_time = approval_request.expires_at.strftime("%Y-%m-%d %H:%M:%S")
500
+ time_remaining = approval_request.time_remaining()
501
+
502
+ if time_remaining and time_remaining.total_seconds() > 0:
503
+ minutes_left = int(time_remaining.total_seconds() / 60)
504
+ time_content.append(
505
+ Text(
506
+ f"Expires: {expires_time} ({minutes_left}m remaining)",
507
+ style="yellow",
508
+ )
509
+ )
510
+ else:
511
+ time_content.append(
512
+ Text(f"Expires: {expires_time} (EXPIRED)", style="red")
513
+ )
514
+
515
+ return Panel(
516
+ Group(*time_content),
517
+ title="⏰ Timing",
518
+ title_align="left",
519
+ border_style="blue",
520
+ )
521
+
522
+ def _format_approval_options(self) -> Panel:
523
+ """Format available approval options."""
524
+ options_text = [
525
+ Text("Available Options:", style="bold white"),
526
+ Text(),
527
+ Text(
528
+ "• [bold green]y[/bold green] or [bold green]yes[/bold green] - Approve this operation",
529
+ style="green",
530
+ ),
531
+ Text(
532
+ "• [bold blue]r[/bold blue] or [bold blue]remember[/bold blue] - Approve and remember for similar operations",
533
+ style="blue",
534
+ ),
535
+ Text(
536
+ "• [bold red]n[/bold red] or [bold red]no[/bold red] - Deny this operation",
537
+ style="red",
538
+ ),
539
+ Text(
540
+ "• [bold yellow]q[/bold yellow] or [bold yellow]quit[/bold yellow] - Cancel and exit",
541
+ style="yellow",
542
+ ),
543
+ Text(),
544
+ Text("Press Ctrl+C to cancel at any time.", style="dim"),
545
+ ]
546
+
547
+ return Panel(
548
+ Group(*options_text),
549
+ title="⌨️ Options",
550
+ title_align="left",
551
+ border_style="white",
552
+ )
553
+
554
+ def _format_batch_header(self, batch_request: BatchApprovalRequest) -> Panel:
555
+ """Format batch approval header."""
556
+ summary = batch_request.get_approval_summary()
557
+
558
+ header_content = Group(
559
+ Text("Batch Operation Approval", style="bold white"),
560
+ Text(
561
+ f"Multiple operations require approval ({summary['total_operations']} operations)",
562
+ style="dim white",
563
+ ),
564
+ Text(),
565
+ Text(f"🔢 Operations: {summary['total_operations']}", style="blue"),
566
+ Text(
567
+ f"⏰ Created: {batch_request.created_at.strftime('%H:%M:%S')}",
568
+ style="white",
569
+ ),
570
+ )
571
+
572
+ return Panel(
573
+ header_content,
574
+ title="🔐 Batch Approval Required",
575
+ title_align="left",
576
+ border_style="blue",
577
+ )
578
+
579
+ def _format_batch_summary(self, batch_request: BatchApprovalRequest) -> Panel:
580
+ """Format batch operations summary table."""
581
+ table = Table(show_header=True, header_style="bold cyan")
582
+ table.add_column("Op", width=3)
583
+ table.add_column("Type", width=15)
584
+ table.add_column("Description", width=40)
585
+ table.add_column("Risk", width=8)
586
+
587
+ for i, (operation, preview) in enumerate(
588
+ zip(batch_request.operations, batch_request.previews)
589
+ ):
590
+ # Determine risk level from preview or use medium as default
591
+ risk_level = RiskLevel.MEDIUM
592
+ if preview.risk_assessment:
593
+ if "high" in preview.risk_assessment.lower():
594
+ risk_level = RiskLevel.HIGH
595
+ elif "low" in preview.risk_assessment.lower():
596
+ risk_level = RiskLevel.LOW
597
+ elif "critical" in preview.risk_assessment.lower():
598
+ risk_level = RiskLevel.CRITICAL
599
+
600
+ risk_color = self.risk_colors[risk_level]
601
+ risk_icon = self.risk_icons[risk_level]
602
+
603
+ # Truncate description if too long
604
+ desc = operation.description
605
+ if len(desc) > 37:
606
+ desc = desc[:37] + "..."
607
+
608
+ table.add_row(
609
+ str(i + 1),
610
+ operation.type.value.replace("_", " ").title(),
611
+ desc,
612
+ Text(f"{risk_icon} {risk_level.value}", style=risk_color),
613
+ )
614
+
615
+ return Panel(
616
+ table,
617
+ title="📋 Operations Summary",
618
+ title_align="left",
619
+ border_style="cyan",
620
+ )
621
+
622
+ def _format_single_batch_operation(
623
+ self,
624
+ approval_request: ApprovalRequest,
625
+ preview: ChangePreview,
626
+ operation_number: int,
627
+ ) -> Group:
628
+ """Format a single operation within a batch."""
629
+ components = []
630
+
631
+ # Operation header
632
+ icon = self.change_type_icons.get(preview.change_type, "❓")
633
+ components.append(
634
+ Text(f"{icon} {approval_request.description}", style="bold white")
635
+ )
636
+
637
+ # Key details in compact format
638
+ details_text = []
639
+ if "path" in approval_request.metadata:
640
+ details_text.append(f"Path: {approval_request.metadata['path']}")
641
+
642
+ if preview.risk_assessment:
643
+ details_text.append(f"Risk: {preview.risk_assessment}")
644
+
645
+ if details_text:
646
+ components.append(Text(" | ".join(details_text), style="dim white"))
647
+
648
+ return Group(*components)
649
+
650
+ def _format_batch_approval_options(self, operation_count: int) -> Panel:
651
+ """Format batch approval options."""
652
+ options_text = [
653
+ Text("Batch Approval Options:", style="bold white"),
654
+ Text(),
655
+ Text(
656
+ "• [bold green]all[/bold green] - Approve all operations",
657
+ style="green",
658
+ ),
659
+ Text(
660
+ "• [bold red]none[/bold red] - Deny all operations",
661
+ style="red",
662
+ ),
663
+ Text(
664
+ f"• [bold blue]select[/bold blue] - Choose specific operations (1-{operation_count})",
665
+ style="blue",
666
+ ),
667
+ Text(
668
+ "• [bold yellow]individual[/bold yellow] - Review each operation separately",
669
+ style="yellow",
670
+ ),
671
+ Text(
672
+ "• [bold yellow]q[/bold yellow] or [bold yellow]quit[/bold yellow] - Cancel batch",
673
+ style="yellow",
674
+ ),
675
+ Text(),
676
+ Text("Press Ctrl+C to cancel at any time.", style="dim"),
677
+ ]
678
+
679
+ return Panel(
680
+ Group(*options_text),
681
+ title="⌨️ Batch Options",
682
+ title_align="left",
683
+ border_style="white",
684
+ )
685
+
686
+ def format_approval_denied(
687
+ self, approval_request: ApprovalRequest, reason: str = ""
688
+ ) -> Panel:
689
+ """Format approval denied message."""
690
+ content = [
691
+ Text("❌ Operation Denied", style="bold red"),
692
+ Text(),
693
+ Text(f"Operation: {approval_request.description}", style="white"),
694
+ Text(f"Type: {approval_request.operation_type}", style="dim"),
695
+ ]
696
+
697
+ if reason:
698
+ content.extend([Text(), Text(f"Reason: {reason}", style="yellow")])
699
+
700
+ return Panel(
701
+ Group(*content),
702
+ title="🚫 Approval Denied",
703
+ title_align="left",
704
+ border_style="red",
705
+ )
706
+
707
+ def format_approval_approved(
708
+ self, approval_request: ApprovalRequest, remember: bool = False
709
+ ) -> Panel:
710
+ """Format approval approved message."""
711
+ content = [
712
+ Text("✅ Operation Approved", style="bold green"),
713
+ Text(),
714
+ Text(f"Operation: {approval_request.description}", style="white"),
715
+ Text(f"Type: {approval_request.operation_type}", style="dim"),
716
+ ]
717
+
718
+ if remember:
719
+ content.extend(
720
+ [
721
+ Text(),
722
+ Text(
723
+ "🧠 Similar operations will be auto-approved in the future",
724
+ style="blue",
725
+ ),
726
+ ]
727
+ )
728
+
729
+ return Panel(
730
+ Group(*content),
731
+ title="✅ Approval Granted",
732
+ title_align="left",
733
+ border_style="green",
734
+ )