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,902 @@
1
+ """
2
+ Structured Approval Dialog Layout for Omnimancer Agent Operations.
3
+
4
+ This module provides the main approval dialog interface that combines
5
+ rich text rendering, diff display, and interactive controls for
6
+ operation approval workflows.
7
+ """
8
+
9
+ import logging
10
+ from dataclasses import dataclass
11
+ from datetime import datetime
12
+ from enum import Enum
13
+ from typing import Dict, Optional, Union
14
+
15
+ from rich.align import Align
16
+ from rich.box import MINIMAL, ROUNDED, SIMPLE
17
+ from rich.console import Console
18
+ from rich.layout import Layout
19
+ from rich.live import Live
20
+ from rich.panel import Panel
21
+ from rich.rule import Rule
22
+ from rich.table import Table
23
+ from rich.text import Text
24
+
25
+ from .approval_context import (
26
+ ApprovalContext,
27
+ ApprovalDecision,
28
+ OperationDetails,
29
+ )
30
+ from .diff_renderer import DiffType, EnhancedDiffRenderer, create_diff_renderer
31
+ from .input_handler import (
32
+ InputState,
33
+ InteractiveInputHandler,
34
+ KeyAction,
35
+ create_input_handler,
36
+ )
37
+ from .rich_renderer import (
38
+ RichTextRenderer,
39
+ RiskLevel,
40
+ create_renderer,
41
+ )
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+
46
+ class DialogState(Enum):
47
+ """States of the approval dialog."""
48
+
49
+ INITIALIZING = "initializing"
50
+ DISPLAYING = "displaying"
51
+ WAITING_INPUT = "waiting_input"
52
+ PROCESSING = "processing"
53
+ COMPLETED = "completed"
54
+ CANCELLED = "cancelled"
55
+
56
+
57
+ class DialogSection(Enum):
58
+ """Sections of the approval dialog."""
59
+
60
+ HEADER = "header"
61
+ OPERATION = "operation"
62
+ DIFF = "diff"
63
+ RISK = "risk"
64
+ CONTROLS = "controls"
65
+ STATUS = "status"
66
+
67
+
68
+ @dataclass
69
+ class DialogOptions:
70
+ """Configuration options for the approval dialog."""
71
+
72
+ # Display options
73
+ show_diff: bool = True
74
+ show_risk_assessment: bool = True
75
+ show_file_tree: bool = True
76
+ show_operation_details: bool = True
77
+
78
+ # Layout options
79
+ max_height: int = 40
80
+ max_width: int = 120
81
+ compact_mode: bool = False
82
+
83
+ # Interactive options
84
+ auto_scroll: bool = True
85
+ enable_shortcuts: bool = True
86
+ timeout_seconds: Optional[int] = None
87
+
88
+ # Rendering options
89
+ diff_type: DiffType = DiffType.UNIFIED
90
+ syntax_highlighting: bool = True
91
+ show_line_numbers: bool = True
92
+
93
+
94
+ class ApprovalDialog:
95
+ """
96
+ Structured approval dialog for agent operations.
97
+
98
+ Provides a comprehensive interface for reviewing and approving
99
+ agent operations with rich formatting, diff display, and
100
+ interactive controls.
101
+ """
102
+
103
+ def __init__(
104
+ self,
105
+ renderer: Optional[RichTextRenderer] = None,
106
+ diff_renderer: Optional[EnhancedDiffRenderer] = None,
107
+ console: Optional[Console] = None,
108
+ options: Optional[DialogOptions] = None,
109
+ input_handler: Optional[InteractiveInputHandler] = None,
110
+ ):
111
+ """
112
+ Initialize the approval dialog.
113
+
114
+ Args:
115
+ renderer: Rich text renderer
116
+ diff_renderer: Enhanced diff renderer
117
+ console: Rich console for output
118
+ options: Dialog configuration options
119
+ input_handler: Interactive input handler for keyboard controls
120
+ """
121
+ self.renderer = renderer or create_renderer()
122
+ self.diff_renderer = diff_renderer or create_diff_renderer(self.renderer)
123
+ self.console = console or self.renderer.console
124
+ self.options = options or DialogOptions()
125
+
126
+ # Interactive input handling
127
+ self.input_handler = input_handler or create_input_handler(
128
+ timeout_seconds=self.options.timeout_seconds
129
+ )
130
+ self.input_handler.set_callbacks(
131
+ approval_callback=self._handle_user_approval,
132
+ denial_callback=self._handle_user_denial,
133
+ quit_callback=self._handle_user_quit,
134
+ display_update_callback=self._handle_display_update,
135
+ )
136
+
137
+ # Dialog state
138
+ self.state = DialogState.INITIALIZING
139
+ self.current_context: Optional[ApprovalContext] = None
140
+ self.decision: Optional[ApprovalDecision] = None
141
+
142
+ # Layout components
143
+ self.layout = Layout()
144
+ self.sections: Dict[str, Layout] = {}
145
+
146
+ # Legacy shortcuts for compatibility
147
+ self.shortcuts: Dict[str, str] = {
148
+ "y": "Approve",
149
+ "n": "Deny",
150
+ "d": "Show Details",
151
+ "f": "Show Full Diff",
152
+ "s": "Show Summary",
153
+ "q": "Quit",
154
+ "h": "Help",
155
+ }
156
+
157
+ # Initialize layout
158
+ self._setup_layout()
159
+
160
+ def _setup_layout(self) -> None:
161
+ """Set up the dialog layout structure."""
162
+ # Calculate responsive dimensions
163
+ self.renderer.get_terminal_width()
164
+ term_height = self.renderer.get_terminal_height()
165
+
166
+ # Initialize sections dictionary
167
+ self.sections = {}
168
+
169
+ # Adjust for compact mode
170
+ if self.options.compact_mode or term_height < 25:
171
+ self.layout.split(
172
+ Layout(name="header", size=3),
173
+ Layout(name="main", ratio=1),
174
+ Layout(name="controls", size=4),
175
+ )
176
+ # Store basic sections
177
+ self.sections.update(
178
+ {
179
+ "header": self.layout["header"],
180
+ "main": self.layout["main"],
181
+ "controls": self.layout["controls"],
182
+ }
183
+ )
184
+ else:
185
+ self.layout.split(
186
+ Layout(name="header", size=3),
187
+ Layout(name="operation", size=6),
188
+ Layout(name="main", ratio=1),
189
+ Layout(name="risk", size=5),
190
+ Layout(name="controls", size=4),
191
+ )
192
+ # Store all sections including operation and risk
193
+ self.sections.update(
194
+ {
195
+ "header": self.layout["header"],
196
+ "operation": self.layout["operation"],
197
+ "main": self.layout["main"],
198
+ "risk": self.layout["risk"],
199
+ "controls": self.layout["controls"],
200
+ }
201
+ )
202
+
203
+ # Set up main content area
204
+ if not self.options.compact_mode:
205
+ self.layout["main"].split_row(
206
+ Layout(name="diff", ratio=2), Layout(name="details", ratio=1)
207
+ )
208
+ else:
209
+ # Single column for compact mode
210
+ self.layout["main"].split(
211
+ Layout(name="diff", ratio=2), Layout(name="details", ratio=1)
212
+ )
213
+
214
+ # Add main content sections
215
+ self.sections.update(
216
+ {
217
+ "diff": self.layout["main"]["diff"],
218
+ "details": self.layout["main"]["details"],
219
+ }
220
+ )
221
+
222
+ def _render_header(self, context: ApprovalContext) -> Panel:
223
+ """Render the dialog header."""
224
+ title = Text()
225
+ title.append("🔒 ", style="warning")
226
+ title.append("Operation Approval Required", style="header")
227
+
228
+ subtitle = Text()
229
+ subtitle.append(f"Agent: ", style="label")
230
+ subtitle.append(context.agent_name, style="value")
231
+ subtitle.append(" | ", style="dim")
232
+ subtitle.append(f"Time: ", style="label")
233
+ subtitle.append(context.timestamp.strftime("%H:%M:%S"), style="value")
234
+
235
+ return Panel(
236
+ Align.center(subtitle),
237
+ title=title,
238
+ border_style="cyan",
239
+ box=ROUNDED,
240
+ )
241
+
242
+ def _render_operation_summary(self, operation: OperationDetails) -> Panel:
243
+ """Render operation summary information."""
244
+ # Create summary table
245
+ table = Table(show_header=False, box=None, padding=0)
246
+ table.add_column("Field", style="label", width=15)
247
+ table.add_column("Value", style="value")
248
+
249
+ # Add operation details
250
+ table.add_row("Operation", operation.operation_type)
251
+ table.add_row("Target", operation.target or "N/A")
252
+ table.add_row("Risk Level", self._format_risk_level(operation.risk_level))
253
+
254
+ if operation.estimated_time:
255
+ table.add_row("Est. Time", f"{operation.estimated_time}s")
256
+
257
+ if operation.files_affected:
258
+ table.add_row("Files", str(len(operation.files_affected)))
259
+
260
+ # Add description if available
261
+ if operation.description:
262
+ description = Text(operation.description)
263
+ if len(operation.description) > 60:
264
+ description = Text(operation.description[:57] + "...")
265
+ table.add_row("Description", description)
266
+
267
+ return Panel(
268
+ table,
269
+ title="📋 Operation Summary",
270
+ border_style="blue",
271
+ box=SIMPLE,
272
+ )
273
+
274
+ def _render_risk_assessment(self, operation: OperationDetails) -> Panel:
275
+ """Render risk assessment panel."""
276
+ # Risk indicator
277
+ risk_indicator = self.renderer.render_risk_indicator(
278
+ operation.risk_level, show_label=True, show_bar=True
279
+ )
280
+
281
+ # Risk factors
282
+ factors = []
283
+ if operation.risk_factors:
284
+ for factor in operation.risk_factors[:3]: # Show top 3
285
+ factors.append(f"• {factor}")
286
+
287
+ if len(operation.risk_factors) > 3:
288
+ factors.append(f"• ... and {len(operation.risk_factors) - 3} more")
289
+
290
+ content = []
291
+ content.append(risk_indicator)
292
+
293
+ if factors:
294
+ content.append(Rule(style="dim"))
295
+ content.append(Text("\n".join(factors), style="warning"))
296
+
297
+ return Panel(
298
+ Align.left("\n".join([str(c) for c in content])),
299
+ title="⚠️ Risk Assessment",
300
+ border_style=self._get_risk_border_color(operation.risk_level),
301
+ box=ROUNDED,
302
+ )
303
+
304
+ def _render_diff_panel(self, context: ApprovalContext) -> Panel:
305
+ """Render file differences panel."""
306
+ if not context.diff_content:
307
+ return Panel(
308
+ Align.center("No file changes to display"),
309
+ title="📄 Changes",
310
+ border_style="dim",
311
+ box=SIMPLE,
312
+ )
313
+
314
+ # Render diff using enhanced diff renderer
315
+ renderables = self.diff_renderer.render_diff_set(
316
+ context.diff_content,
317
+ diff_type=self.options.diff_type,
318
+ show_summary=not self.options.compact_mode,
319
+ show_file_tree=self.options.show_file_tree,
320
+ )
321
+
322
+ # Combine renderables
323
+ if len(renderables) == 1:
324
+ content = renderables[0]
325
+ else:
326
+ # Join with rules between sections
327
+ combined = []
328
+ for i, renderable in enumerate(renderables):
329
+ if i > 0:
330
+ combined.append(Rule(style="dim"))
331
+ combined.append(renderable)
332
+ content = "\n".join([str(r) for r in combined])
333
+
334
+ return Panel(
335
+ content,
336
+ title="📄 File Changes",
337
+ border_style="yellow",
338
+ box=SIMPLE,
339
+ expand=True,
340
+ )
341
+
342
+ def _render_operation_details(self, operation: OperationDetails) -> Panel:
343
+ """Render detailed operation information."""
344
+ sections = []
345
+
346
+ # Command/Script details
347
+ if operation.command:
348
+ sections.append(self._render_command_details(operation))
349
+
350
+ # File operations
351
+ if operation.files_affected:
352
+ sections.append(self._render_file_details(operation))
353
+
354
+ # Environment/Context
355
+ if operation.environment_vars or operation.working_directory:
356
+ sections.append(self._render_environment_details(operation))
357
+
358
+ # Metadata
359
+ if operation.metadata:
360
+ sections.append(self._render_metadata(operation))
361
+
362
+ if not sections:
363
+ content = Align.center("No additional details available")
364
+ else:
365
+ content = "\n".join([str(s) for s in sections])
366
+
367
+ return Panel(
368
+ content,
369
+ title="🔍 Details",
370
+ border_style="cyan",
371
+ box=SIMPLE,
372
+ expand=True,
373
+ )
374
+
375
+ def _render_command_details(self, operation: OperationDetails) -> Table:
376
+ """Render command execution details."""
377
+ table = Table(show_header=True, box=MINIMAL)
378
+ table.add_column("Property", style="label")
379
+ table.add_column("Value", style="value")
380
+
381
+ table.add_row("Command", operation.command)
382
+ if operation.arguments:
383
+ table.add_row("Arguments", " ".join(operation.arguments))
384
+ if operation.working_directory:
385
+ table.add_row("Working Dir", operation.working_directory)
386
+
387
+ return table
388
+
389
+ def _render_file_details(self, operation: OperationDetails) -> Table:
390
+ """Render file operation details."""
391
+ table = Table(show_header=True, box=MINIMAL)
392
+ table.add_column("File", style="value", width=30)
393
+ table.add_column("Operation", style="label")
394
+ table.add_column("Size", style="dim", justify="right")
395
+
396
+ for file_path in operation.files_affected[:5]: # Show first 5
397
+ # Determine operation type from file path context
398
+ op_type = "modify" # Default
399
+ size = "N/A"
400
+
401
+ table.add_row(str(file_path), op_type, size)
402
+
403
+ if len(operation.files_affected) > 5:
404
+ table.add_row(
405
+ f"... and {len(operation.files_affected) - 5} more files",
406
+ "",
407
+ "",
408
+ )
409
+
410
+ return table
411
+
412
+ def _render_environment_details(self, operation: OperationDetails) -> Table:
413
+ """Render environment and context details."""
414
+ table = Table(show_header=True, box=MINIMAL)
415
+ table.add_column("Variable", style="label")
416
+ table.add_column("Value", style="value")
417
+
418
+ if operation.working_directory:
419
+ table.add_row("PWD", operation.working_directory)
420
+
421
+ if operation.environment_vars:
422
+ for key, value in list(operation.environment_vars.items())[:3]:
423
+ # Mask sensitive values
424
+ if any(
425
+ sensitive in key.lower()
426
+ for sensitive in ["key", "token", "password", "secret"]
427
+ ):
428
+ value = "***"
429
+ elif len(str(value)) > 40:
430
+ value = str(value)[:37] + "..."
431
+
432
+ table.add_row(key, str(value))
433
+
434
+ return table
435
+
436
+ def _render_metadata(self, operation: OperationDetails) -> Table:
437
+ """Render operation metadata."""
438
+ table = Table(show_header=True, box=MINIMAL)
439
+ table.add_column("Key", style="label")
440
+ table.add_column("Value", style="value")
441
+
442
+ for key, value in list(operation.metadata.items())[:3]:
443
+ if len(str(value)) > 40:
444
+ value = str(value)[:37] + "..."
445
+ table.add_row(key, str(value))
446
+
447
+ return table
448
+
449
+ def _render_controls(self) -> Panel:
450
+ """Render control buttons and shortcuts."""
451
+ if self.options.enable_shortcuts:
452
+ shortcuts_help = self.renderer.render_shortcuts_help(
453
+ self.shortcuts,
454
+ columns=2 if self.renderer.get_terminal_width() > 80 else 1,
455
+ )
456
+ content = shortcuts_help
457
+ else:
458
+ # Simple button layout
459
+ content = Text("Use 'y' to approve, 'n' to deny, 'q' to quit")
460
+
461
+ return Panel(content, title="⌨️ Controls", border_style="green", box=SIMPLE)
462
+
463
+ def _format_risk_level(self, risk_level: Union[RiskLevel, int, str]) -> Text:
464
+ """Format risk level with appropriate styling."""
465
+ if isinstance(risk_level, int):
466
+ risk_text = f"Level {risk_level}"
467
+ if risk_level <= 3:
468
+ style = "green"
469
+ elif risk_level <= 6:
470
+ style = "yellow"
471
+ else:
472
+ style = "red"
473
+ else:
474
+ risk_text = str(risk_level).upper()
475
+ style = self.renderer.get_risk_color(risk_level)
476
+
477
+ return Text(risk_text, style=style)
478
+
479
+ def _get_risk_border_color(self, risk_level: Union[RiskLevel, int, str]) -> str:
480
+ """Get appropriate border color for risk level."""
481
+ if isinstance(risk_level, int):
482
+ if risk_level <= 3:
483
+ return "green"
484
+ elif risk_level <= 6:
485
+ return "yellow"
486
+ else:
487
+ return "red"
488
+
489
+ risk_colors = {
490
+ RiskLevel.LOW: "green",
491
+ RiskLevel.MEDIUM: "yellow",
492
+ RiskLevel.HIGH: "red",
493
+ RiskLevel.CRITICAL: "bold red",
494
+ }
495
+
496
+ return risk_colors.get(risk_level, "yellow")
497
+
498
+ async def show_approval_dialog(self, context: ApprovalContext) -> ApprovalDecision:
499
+ """
500
+ Display the approval dialog and wait for user decision.
501
+
502
+ Args:
503
+ context: Approval context with operation details
504
+
505
+ Returns:
506
+ User's approval decision
507
+ """
508
+ try:
509
+ self.state = DialogState.DISPLAYING
510
+ self.current_context = context
511
+
512
+ # Render all dialog sections
513
+ self.sections["header"].update(self._render_header(context))
514
+
515
+ if not self.options.compact_mode and "operation" in self.sections:
516
+ self.sections["operation"].update(
517
+ self._render_operation_summary(context.operation_details)
518
+ )
519
+
520
+ if not self.options.compact_mode and "risk" in self.sections:
521
+ self.sections["risk"].update(
522
+ self._render_risk_assessment(context.operation_details)
523
+ )
524
+
525
+ self.sections["diff"].update(self._render_diff_panel(context))
526
+ self.sections["details"].update(
527
+ self._render_operation_details(context.operation_details)
528
+ )
529
+ self.sections["controls"].update(self._render_controls())
530
+
531
+ # Show the dialog with Live display
532
+ with Live(
533
+ self.layout,
534
+ console=self.console,
535
+ refresh_per_second=4,
536
+ auto_refresh=True,
537
+ ) as live:
538
+ self.state = DialogState.WAITING_INPUT
539
+ self.live_display = live # Store for display updates
540
+
541
+ # Wait for user input
542
+ decision = await self._wait_for_decision()
543
+
544
+ self.state = DialogState.COMPLETED
545
+ self.decision = decision
546
+
547
+ return decision
548
+
549
+ except KeyboardInterrupt:
550
+ self.state = DialogState.CANCELLED
551
+ return ApprovalDecision(
552
+ approved=False,
553
+ reason="Operation cancelled by user",
554
+ timestamp=datetime.now(),
555
+ )
556
+ except Exception as e:
557
+ logger.error(f"Dialog error: {e}", exc_info=True)
558
+ self.state = DialogState.CANCELLED
559
+ return ApprovalDecision(
560
+ approved=False,
561
+ reason=f"Dialog error: {e}",
562
+ timestamp=datetime.now(),
563
+ )
564
+
565
+ async def _wait_for_decision(self) -> ApprovalDecision:
566
+ """Wait for user input and return decision."""
567
+ try:
568
+ # Use the interactive input handler to get user decision
569
+ final_action = await self.input_handler.handle_input_loop()
570
+
571
+ # Create decision based on the action
572
+ if final_action == KeyAction.APPROVE:
573
+ return ApprovalDecision(
574
+ approved=True,
575
+ reason="Approved by user",
576
+ timestamp=datetime.now(),
577
+ additional_data={
578
+ "input_action": "approve",
579
+ "interactive": True,
580
+ },
581
+ )
582
+ elif final_action == KeyAction.DENY:
583
+ return ApprovalDecision(
584
+ approved=False,
585
+ reason="Denied by user",
586
+ timestamp=datetime.now(),
587
+ additional_data={
588
+ "input_action": "deny",
589
+ "interactive": True,
590
+ },
591
+ )
592
+ else: # QUIT or timeout
593
+ return ApprovalDecision(
594
+ approved=False,
595
+ reason="Operation cancelled or timed out",
596
+ timestamp=datetime.now(),
597
+ additional_data={
598
+ "input_action": "quit",
599
+ "interactive": True,
600
+ },
601
+ )
602
+
603
+ except Exception as e:
604
+ logger.error(f"Error in input handling: {e}")
605
+ return ApprovalDecision(
606
+ approved=False,
607
+ reason=f"Input handling error: {e}",
608
+ timestamp=datetime.now(),
609
+ additional_data={"input_error": str(e)},
610
+ )
611
+
612
+ def get_current_state(self) -> DialogState:
613
+ """Get current dialog state."""
614
+ return self.state
615
+
616
+ def get_decision(self) -> Optional[ApprovalDecision]:
617
+ """Get the approval decision if available."""
618
+ return self.decision
619
+
620
+ # Callback methods for input handler integration
621
+
622
+ async def _handle_user_approval(self) -> bool:
623
+ """Handle user approval action."""
624
+ logger.info("User approved operation")
625
+ return True
626
+
627
+ async def _handle_user_denial(self) -> bool:
628
+ """Handle user denial action."""
629
+ logger.info("User denied operation")
630
+ return True
631
+
632
+ async def _handle_user_quit(self) -> bool:
633
+ """Handle user quit action."""
634
+ logger.info("User quit approval dialog")
635
+ return True
636
+
637
+ async def _handle_display_update(self, input_state: InputState) -> None:
638
+ """Handle display updates based on input state changes."""
639
+ try:
640
+ # Update dialog based on input state
641
+ if hasattr(self, "live_display") and self.current_context:
642
+
643
+ # Update sections based on input state
644
+ if input_state.help_visible:
645
+ # Show help overlay
646
+ help_panel = Panel(
647
+ self.input_handler.get_help_text(),
648
+ title="🔍 Keyboard Shortcuts",
649
+ border_style="cyan",
650
+ box=ROUNDED,
651
+ )
652
+
653
+ # Replace main content with help
654
+ if "main" in self.sections:
655
+ self.sections["main"].update(help_panel)
656
+
657
+ else:
658
+ # Show normal content - update diff section if focused
659
+ if (
660
+ input_state.current_section == "diff"
661
+ and "diff" in self.sections
662
+ ):
663
+ # Potentially update diff rendering based on expanded state
664
+ diff_panel = self._render_diff_panel(
665
+ self.current_context,
666
+ expanded=input_state.diff_expanded,
667
+ )
668
+ self.sections["diff"].update(diff_panel)
669
+
670
+ # Update details section if focused
671
+ elif (
672
+ input_state.current_section == "details"
673
+ and "details" in self.sections
674
+ ):
675
+ details_panel = self._render_operation_details(
676
+ self.current_context.operation_details,
677
+ expanded=input_state.details_expanded,
678
+ )
679
+ self.sections["details"].update(details_panel)
680
+
681
+ # Update status/controls with current input state
682
+ if "controls" in self.sections:
683
+ controls_panel = self._render_controls_with_state(input_state)
684
+ self.sections["controls"].update(controls_panel)
685
+
686
+ # Refresh the display
687
+ self.live_display.refresh()
688
+
689
+ except Exception as e:
690
+ logger.error(f"Error updating display: {e}")
691
+
692
+ def _render_diff_panel(
693
+ self, context: ApprovalContext, expanded: bool = False
694
+ ) -> Panel:
695
+ """Render file differences panel with optional expansion."""
696
+ if not context.diff_content:
697
+ return Panel(
698
+ Align.center("No file changes to display"),
699
+ title="📄 Changes",
700
+ border_style="dim",
701
+ box=SIMPLE,
702
+ )
703
+
704
+ # Render diff using enhanced diff renderer
705
+ renderables = self.diff_renderer.render_diff_set(
706
+ context.diff_content,
707
+ diff_type=(DiffType.SIDE_BY_SIDE if expanded else self.options.diff_type),
708
+ show_summary=not self.options.compact_mode or expanded,
709
+ show_file_tree=self.options.show_file_tree,
710
+ )
711
+
712
+ # Combine renderables
713
+ if len(renderables) == 1:
714
+ content = renderables[0]
715
+ else:
716
+ # Join with rules between sections
717
+ combined = []
718
+ for i, renderable in enumerate(renderables):
719
+ if i > 0:
720
+ combined.append(Rule(style="dim"))
721
+ combined.append(renderable)
722
+ content = "\n".join([str(r) for r in combined])
723
+
724
+ title = "📄 File Changes (Expanded)" if expanded else "📄 File Changes"
725
+
726
+ return Panel(
727
+ content,
728
+ title=title,
729
+ border_style="yellow" if expanded else "blue",
730
+ box=SIMPLE,
731
+ expand=True,
732
+ )
733
+
734
+ def _render_operation_details(
735
+ self, operation: OperationDetails, expanded: bool = False
736
+ ) -> Panel:
737
+ """Render detailed operation information with optional expansion."""
738
+ sections = []
739
+
740
+ # Command/Script details
741
+ if operation.command:
742
+ sections.append(self._render_command_details(operation))
743
+
744
+ # File operations
745
+ if operation.files_affected:
746
+ sections.append(self._render_file_details(operation, show_all=expanded))
747
+
748
+ # Environment/Context
749
+ if operation.environment_vars or operation.working_directory:
750
+ sections.append(
751
+ self._render_environment_details(operation, show_all=expanded)
752
+ )
753
+
754
+ # Metadata
755
+ if operation.metadata:
756
+ sections.append(self._render_metadata(operation, show_all=expanded))
757
+
758
+ if not sections:
759
+ content = Align.center("No additional details available")
760
+ else:
761
+ content = "\n".join([str(s) for s in sections])
762
+
763
+ title = "🔍 Details (Expanded)" if expanded else "🔍 Details"
764
+
765
+ return Panel(
766
+ content,
767
+ title=title,
768
+ border_style="cyan" if expanded else "green",
769
+ box=SIMPLE,
770
+ expand=True,
771
+ )
772
+
773
+ def _render_file_details(
774
+ self, operation: OperationDetails, show_all: bool = False
775
+ ) -> Table:
776
+ """Render file operation details with optional full listing."""
777
+ table = Table(show_header=True, box=MINIMAL)
778
+ table.add_column("File", style="value", width=30)
779
+ table.add_column("Operation", style="label")
780
+ table.add_column("Size", style="dim", justify="right")
781
+
782
+ file_limit = len(operation.files_affected) if show_all else 5
783
+
784
+ for file_path in operation.files_affected[:file_limit]:
785
+ # Determine operation type from file path context
786
+ op_type = "modify" # Default
787
+ size = "N/A"
788
+
789
+ table.add_row(str(file_path), op_type, size)
790
+
791
+ if not show_all and len(operation.files_affected) > 5:
792
+ table.add_row(
793
+ f"... and {len(operation.files_affected) - 5} more files",
794
+ "",
795
+ "",
796
+ )
797
+
798
+ return table
799
+
800
+ def _render_environment_details(
801
+ self, operation: OperationDetails, show_all: bool = False
802
+ ) -> Table:
803
+ """Render environment and context details with optional full listing."""
804
+ table = Table(show_header=True, box=MINIMAL)
805
+ table.add_column("Variable", style="label")
806
+ table.add_column("Value", style="value")
807
+
808
+ if operation.working_directory:
809
+ table.add_row("PWD", operation.working_directory)
810
+
811
+ if operation.environment_vars:
812
+ var_limit = len(operation.environment_vars) if show_all else 3
813
+ for key, value in list(operation.environment_vars.items())[:var_limit]:
814
+ # Mask sensitive values
815
+ if any(
816
+ sensitive in key.lower()
817
+ for sensitive in ["key", "token", "password", "secret"]
818
+ ):
819
+ value = "***"
820
+ elif len(str(value)) > 40:
821
+ value = str(value)[:37] + "..."
822
+
823
+ table.add_row(key, str(value))
824
+
825
+ return table
826
+
827
+ def _render_metadata(
828
+ self, operation: OperationDetails, show_all: bool = False
829
+ ) -> Table:
830
+ """Render operation metadata with optional full listing."""
831
+ table = Table(show_header=True, box=MINIMAL)
832
+ table.add_column("Key", style="label")
833
+ table.add_column("Value", style="value")
834
+
835
+ item_limit = len(operation.metadata) if show_all else 3
836
+ for key, value in list(operation.metadata.items())[:item_limit]:
837
+ if len(str(value)) > 40:
838
+ value = str(value)[:37] + "..."
839
+ table.add_row(key, str(value))
840
+
841
+ return table
842
+
843
+ def _render_controls_with_state(self, input_state: InputState) -> Panel:
844
+ """Render control buttons with current state information."""
845
+ if self.options.enable_shortcuts:
846
+ # Show shortcuts with current state
847
+ shortcuts_text = self.renderer.render_shortcuts_help(
848
+ self.shortcuts,
849
+ columns=2 if self.renderer.get_terminal_width() > 80 else 1,
850
+ )
851
+
852
+ # Add state information
853
+ state_info = Text()
854
+ state_info.append(f"Mode: {input_state.mode.value} | ", style="dim")
855
+ state_info.append(f"Section: {input_state.current_section} | ", style="dim")
856
+ state_info.append(f"Scroll: {input_state.scroll_position}", style="dim")
857
+
858
+ content = f"{shortcuts_text}\n{state_info}"
859
+ else:
860
+ # Simple button layout with state
861
+ content = Text(
862
+ f"Use 'y' to approve, 'n' to deny, 'q' to quit | Mode: {input_state.mode.value}"
863
+ )
864
+
865
+ return Panel(content, title="⌨️ Controls", border_style="green", box=SIMPLE)
866
+
867
+
868
+ # Utility functions
869
+
870
+
871
+ def create_approval_dialog(
872
+ renderer: Optional[RichTextRenderer] = None,
873
+ options: Optional[DialogOptions] = None,
874
+ ) -> ApprovalDialog:
875
+ """Create an approval dialog with default configuration."""
876
+ return ApprovalDialog(renderer=renderer, options=options)
877
+
878
+
879
+ async def show_quick_approval(
880
+ operation_type: str,
881
+ target: str,
882
+ risk_level: Union[RiskLevel, int] = RiskLevel.MEDIUM,
883
+ console: Optional[Console] = None,
884
+ ) -> bool:
885
+ """Quick approval dialog for simple operations."""
886
+ context = ApprovalContext(
887
+ agent_name="Omnimancer",
888
+ operation_details=OperationDetails(
889
+ operation_type=operation_type, target=target, risk_level=risk_level
890
+ ),
891
+ timestamp=datetime.now(),
892
+ )
893
+
894
+ # Use compact dialog for quick approval
895
+ options = DialogOptions(compact_mode=True, show_file_tree=False, timeout_seconds=30)
896
+ dialog = create_approval_dialog(options=options)
897
+
898
+ if console:
899
+ dialog.console = console
900
+
901
+ decision = await dialog.show_approval_dialog(context)
902
+ return decision.approved