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,663 @@
1
+ """
2
+ Proposed Changes Display Integration Module.
3
+
4
+ This module integrates the display of proposed changes with the file system manager
5
+ and approval workflow, providing a seamless interface for showing file modifications.
6
+ """
7
+
8
+ import difflib
9
+ import logging
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime
12
+ from enum import Enum
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ from rich.console import Console
17
+ from rich.panel import Panel
18
+ from rich.table import Table
19
+ from rich.text import Text
20
+
21
+ from ..security.approval_workflow import RiskLevel
22
+ from .approval_manager import (
23
+ ChangeType,
24
+ EnhancedApprovalManager,
25
+ )
26
+ from .diff_renderer import (
27
+ DiffType,
28
+ EnhancedDiffRenderer,
29
+ FileChange,
30
+ FileChangeType,
31
+ )
32
+ from .file_content_display import (
33
+ DisplayMode,
34
+ FileDisplayConfig,
35
+ UnifiedFileContentDisplay,
36
+ )
37
+ from .file_system_manager import FileSystemManager
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ class ChangeDisplayMode(Enum):
43
+ """Display modes for proposed changes."""
44
+
45
+ INLINE = "inline" # Changes shown inline with markers
46
+ SIDE_BY_SIDE = "side_by_side" # Original and modified side by side
47
+ UNIFIED = "unified" # Unified diff format
48
+ OVERLAY = "overlay" # Changes overlaid with transparency
49
+ SUMMARY = "summary" # Summary of changes only
50
+
51
+
52
+ @dataclass
53
+ class ProposedChange:
54
+ """Represents a proposed change to a file."""
55
+
56
+ file_path: str
57
+ operation_type: ChangeType
58
+ original_content: Optional[str] = None
59
+ modified_content: Optional[str] = None
60
+ metadata: Dict[str, Any] = field(default_factory=dict)
61
+ risk_level: RiskLevel = RiskLevel.LOW
62
+ change_summary: Optional[str] = None
63
+ line_changes: Optional[Dict[str, Any]] = None
64
+ timestamp: datetime = field(default_factory=datetime.now)
65
+
66
+
67
+ @dataclass
68
+ class ChangeSet:
69
+ """Collection of related proposed changes."""
70
+
71
+ id: str
72
+ description: str
73
+ changes: List[ProposedChange]
74
+ total_risk_score: float = 0.0
75
+ approved: bool = False
76
+ applied: bool = False
77
+ created_at: datetime = field(default_factory=datetime.now)
78
+
79
+
80
+ class ProposedChangesIntegration:
81
+ """
82
+ Integrates proposed changes display with file system operations.
83
+
84
+ Provides a cohesive interface for fetching, displaying, and managing
85
+ proposed changes with clear visual distinction from original content.
86
+ """
87
+
88
+ def __init__(
89
+ self,
90
+ file_system_manager: Optional[FileSystemManager] = None,
91
+ approval_manager: Optional[EnhancedApprovalManager] = None,
92
+ console: Optional[Console] = None,
93
+ ):
94
+ """
95
+ Initialize the proposed changes integration.
96
+
97
+ Args:
98
+ file_system_manager: File system manager instance
99
+ approval_manager: Approval manager instance
100
+ console: Rich console for output
101
+ """
102
+ self.console = console or Console()
103
+ self.file_system_manager = file_system_manager or FileSystemManager()
104
+ self.approval_manager = approval_manager or EnhancedApprovalManager()
105
+
106
+ # Initialize display components
107
+ self.display_config = FileDisplayConfig(
108
+ display_mode=DisplayMode.DIFF,
109
+ diff_type=DiffType.UNIFIED,
110
+ syntax_highlighting=True,
111
+ show_line_numbers=True,
112
+ )
113
+
114
+ self.unified_display = UnifiedFileContentDisplay(
115
+ console=self.console, config=self.display_config
116
+ )
117
+
118
+ self.diff_renderer = EnhancedDiffRenderer(console=self.console)
119
+
120
+ # Change tracking
121
+ self.pending_changes: Dict[str, ChangeSet] = {}
122
+ self.change_history: List[ChangeSet] = []
123
+
124
+ # Display preferences
125
+ self.color_scheme = {
126
+ "added": "green",
127
+ "removed": "red",
128
+ "modified": "yellow",
129
+ "unchanged": "dim white",
130
+ "context": "bright_black",
131
+ }
132
+
133
+ async def fetch_proposed_changes(
134
+ self, operation_id: str, file_paths: Optional[List[str]] = None
135
+ ) -> ChangeSet:
136
+ """
137
+ Fetch proposed changes for display.
138
+
139
+ Args:
140
+ operation_id: ID of the operation with proposed changes
141
+ file_paths: Optional list of specific file paths to fetch
142
+
143
+ Returns:
144
+ ChangeSet containing all proposed changes
145
+ """
146
+ try:
147
+ changes = []
148
+
149
+ # Get operations from approval manager
150
+ if hasattr(self.approval_manager, "get_pending_operations"):
151
+ operations = await self.approval_manager.get_pending_operations(
152
+ operation_id
153
+ )
154
+ else:
155
+ operations = []
156
+
157
+ for op in operations:
158
+ if file_paths and op.get("file_path") not in file_paths:
159
+ continue
160
+
161
+ # Create ProposedChange from operation
162
+ change = await self._create_proposed_change(op)
163
+ changes.append(change)
164
+
165
+ # Create change set
166
+ change_set = ChangeSet(
167
+ id=operation_id,
168
+ description=f"Changes for operation {operation_id}",
169
+ changes=changes,
170
+ )
171
+
172
+ # Calculate total risk
173
+ change_set.total_risk_score = self._calculate_total_risk(changes)
174
+
175
+ # Store in pending changes
176
+ self.pending_changes[operation_id] = change_set
177
+
178
+ return change_set
179
+
180
+ except Exception as e:
181
+ logger.error(f"Error fetching proposed changes: {e}")
182
+ return ChangeSet(id=operation_id, description="Error", changes=[])
183
+
184
+ async def display_proposed_changes(
185
+ self,
186
+ change_set: ChangeSet,
187
+ display_mode: ChangeDisplayMode = ChangeDisplayMode.UNIFIED,
188
+ interactive: bool = True,
189
+ ) -> Dict[str, Any]:
190
+ """
191
+ Display proposed changes with clear visual distinction.
192
+
193
+ Args:
194
+ change_set: Set of changes to display
195
+ display_mode: How to display the changes
196
+ interactive: Whether to allow user interaction
197
+
198
+ Returns:
199
+ Display result with user decisions if interactive
200
+ """
201
+ try:
202
+ # Display header
203
+ self._display_change_set_header(change_set)
204
+
205
+ # Display each change based on mode
206
+ if display_mode == ChangeDisplayMode.SUMMARY:
207
+ self._display_change_summary(change_set)
208
+ else:
209
+ for change in change_set.changes:
210
+ await self._display_single_change(change, display_mode)
211
+
212
+ # Get user decision if interactive
213
+ if interactive:
214
+ decision = await self._get_change_approval(change_set)
215
+ return decision
216
+
217
+ return {"displayed": True, "interactive": False}
218
+
219
+ except Exception as e:
220
+ logger.error(f"Error displaying proposed changes: {e}")
221
+ return {"error": str(e), "displayed": False}
222
+
223
+ async def display_inline_changes(
224
+ self, file_path: str, original_content: str, modified_content: str
225
+ ) -> None:
226
+ """
227
+ Display changes inline with color coding and annotations.
228
+
229
+ Args:
230
+ file_path: Path to the file
231
+ original_content: Original file content
232
+ modified_content: Modified file content
233
+ """
234
+ # Generate line-by-line diff
235
+ original_lines = original_content.splitlines(keepends=True)
236
+ modified_lines = modified_content.splitlines(keepends=True)
237
+
238
+ differ = difflib.unified_diff(
239
+ original_lines,
240
+ modified_lines,
241
+ fromfile=f"a/{file_path}",
242
+ tofile=f"b/{file_path}",
243
+ lineterm="",
244
+ )
245
+
246
+ # Create annotated display
247
+ annotated_text = Text()
248
+ line_num = 0
249
+
250
+ for line in differ:
251
+ line_num += 1
252
+ if line.startswith("+++") or line.startswith("---"):
253
+ continue
254
+ elif line.startswith("@@"):
255
+ annotated_text.append(f"\n{line}\n", style="bold cyan")
256
+ elif line.startswith("+"):
257
+ annotated_text.append(f"{line_num:4d} + {line[1:]}", style="green")
258
+ elif line.startswith("-"):
259
+ annotated_text.append(f"{line_num:4d} - {line[1:]}", style="red")
260
+ else:
261
+ annotated_text.append(f"{line_num:4d} {line}", style="dim white")
262
+
263
+ # Display in panel
264
+ panel = Panel(
265
+ annotated_text,
266
+ title=f"[bold]Inline Changes: {file_path}[/bold]",
267
+ border_style="yellow",
268
+ )
269
+
270
+ self.console.print(panel)
271
+
272
+ async def display_side_by_side_changes(
273
+ self, file_path: str, original_content: str, modified_content: str
274
+ ) -> None:
275
+ """
276
+ Display changes in side-by-side format.
277
+
278
+ Args:
279
+ file_path: Path to the file
280
+ original_content: Original file content
281
+ modified_content: Modified file content
282
+ """
283
+ # Create file change for diff renderer
284
+ file_change = FileChange(
285
+ file_path=file_path,
286
+ change_type=FileChangeType.MODIFIED,
287
+ old_content=original_content,
288
+ new_content=modified_content,
289
+ language=self._detect_language(file_path),
290
+ )
291
+
292
+ # Use diff renderer for side-by-side display
293
+ self.diff_renderer.render_side_by_side_diff(file_change)
294
+
295
+ async def apply_proposed_changes(
296
+ self, change_set_id: str, selected_changes: Optional[List[int]] = None
297
+ ) -> Dict[str, Any]:
298
+ """
299
+ Apply approved proposed changes to files.
300
+
301
+ Args:
302
+ change_set_id: ID of the change set to apply
303
+ selected_changes: Optional list of change indices to apply (None = all)
304
+
305
+ Returns:
306
+ Result of applying changes
307
+ """
308
+ try:
309
+ change_set = self.pending_changes.get(change_set_id)
310
+ if not change_set:
311
+ return {"success": False, "error": "Change set not found"}
312
+
313
+ if not change_set.approved:
314
+ return {"success": False, "error": "Changes not approved"}
315
+
316
+ applied = []
317
+ failed = []
318
+
319
+ # Determine which changes to apply
320
+ changes_to_apply = change_set.changes
321
+ if selected_changes is not None:
322
+ changes_to_apply = [
323
+ change_set.changes[i]
324
+ for i in selected_changes
325
+ if i < len(change_set.changes)
326
+ ]
327
+
328
+ # Apply each change
329
+ for change in changes_to_apply:
330
+ try:
331
+ result = await self._apply_single_change(change)
332
+ if result["success"]:
333
+ applied.append(change.file_path)
334
+ else:
335
+ failed.append((change.file_path, result.get("error")))
336
+ except Exception as e:
337
+ failed.append((change.file_path, str(e)))
338
+
339
+ # Update change set status
340
+ if not failed:
341
+ change_set.applied = True
342
+ self.change_history.append(change_set)
343
+ del self.pending_changes[change_set_id]
344
+
345
+ return {
346
+ "success": len(failed) == 0,
347
+ "applied": applied,
348
+ "failed": failed,
349
+ }
350
+
351
+ except Exception as e:
352
+ logger.error(f"Error applying proposed changes: {e}")
353
+ return {"success": False, "error": str(e)}
354
+
355
+ def get_change_statistics(self, change_set: ChangeSet) -> Dict[str, Any]:
356
+ """
357
+ Get statistics about a change set.
358
+
359
+ Args:
360
+ change_set: Change set to analyze
361
+
362
+ Returns:
363
+ Statistics dictionary
364
+ """
365
+ stats = {
366
+ "total_changes": len(change_set.changes),
367
+ "files_affected": len(set(c.file_path for c in change_set.changes)),
368
+ "operations": {},
369
+ "risk_distribution": {},
370
+ "total_lines_added": 0,
371
+ "total_lines_removed": 0,
372
+ }
373
+
374
+ for change in change_set.changes:
375
+ # Count operations
376
+ op_type = (
377
+ change.operation_type.value if change.operation_type else "unknown"
378
+ )
379
+ stats["operations"][op_type] = stats["operations"].get(op_type, 0) + 1
380
+
381
+ # Count risk levels
382
+ risk = change.risk_level.value if change.risk_level else "unknown"
383
+ stats["risk_distribution"][risk] = (
384
+ stats["risk_distribution"].get(risk, 0) + 1
385
+ )
386
+
387
+ # Count line changes
388
+ if change.line_changes:
389
+ stats["total_lines_added"] += change.line_changes.get("added", 0)
390
+ stats["total_lines_removed"] += change.line_changes.get("removed", 0)
391
+
392
+ return stats
393
+
394
+ async def _create_proposed_change(
395
+ self, operation: Dict[str, Any]
396
+ ) -> ProposedChange:
397
+ """Create a ProposedChange from an operation."""
398
+ file_path = operation.get("file_path", "")
399
+ op_type = operation.get("type", "unknown")
400
+
401
+ # Map operation type to ChangeType
402
+ change_type_map = {
403
+ "create": ChangeType.FILE_CREATE,
404
+ "modify": ChangeType.FILE_MODIFY,
405
+ "delete": ChangeType.FILE_DELETE,
406
+ "move": ChangeType.FILE_MODIFY,
407
+ "copy": ChangeType.FILE_CREATE,
408
+ }
409
+
410
+ change_type = change_type_map.get(op_type, ChangeType.FILE_MODIFY)
411
+
412
+ # Get content
413
+ original_content = operation.get("original_content")
414
+ modified_content = operation.get("modified_content")
415
+
416
+ # Calculate line changes if both contents available
417
+ line_changes = None
418
+ if original_content and modified_content:
419
+ line_changes = self._calculate_line_changes(
420
+ original_content, modified_content
421
+ )
422
+
423
+ return ProposedChange(
424
+ file_path=file_path,
425
+ operation_type=change_type,
426
+ original_content=original_content,
427
+ modified_content=modified_content,
428
+ metadata=operation.get("metadata", {}),
429
+ risk_level=RiskLevel(operation.get("risk_level", "low")),
430
+ change_summary=operation.get("summary"),
431
+ line_changes=line_changes,
432
+ )
433
+
434
+ def _calculate_line_changes(self, original: str, modified: str) -> Dict[str, Any]:
435
+ """Calculate line changes between two contents."""
436
+ original_lines = original.splitlines()
437
+ modified_lines = modified.splitlines()
438
+
439
+ matcher = difflib.SequenceMatcher(None, original_lines, modified_lines)
440
+
441
+ added = 0
442
+ removed = 0
443
+
444
+ for tag, i1, i2, j1, j2 in matcher.get_opcodes():
445
+ if tag == "delete":
446
+ removed += i2 - i1
447
+ elif tag == "insert":
448
+ added += j2 - j1
449
+ elif tag == "replace":
450
+ removed += i2 - i1
451
+ added += j2 - j1
452
+
453
+ return {
454
+ "added": added,
455
+ "removed": removed,
456
+ "modified": len(original_lines) - removed,
457
+ }
458
+
459
+ def _calculate_total_risk(self, changes: List[ProposedChange]) -> float:
460
+ """Calculate total risk score for a set of changes."""
461
+ risk_scores = {
462
+ RiskLevel.LOW: 1.0,
463
+ RiskLevel.MEDIUM: 3.0,
464
+ RiskLevel.HIGH: 7.0,
465
+ RiskLevel.CRITICAL: 10.0,
466
+ }
467
+
468
+ total = sum(risk_scores.get(change.risk_level, 1.0) for change in changes)
469
+ return total / len(changes) if changes else 0.0
470
+
471
+ def _display_change_set_header(self, change_set: ChangeSet):
472
+ """Display header for a change set."""
473
+ stats = self.get_change_statistics(change_set)
474
+
475
+ # Create header table
476
+ header_table = Table.grid(padding=1)
477
+ header_table.add_column("Label", style="bold cyan")
478
+ header_table.add_column("Value", style="bright_white")
479
+
480
+ header_table.add_row("Change Set ID:", change_set.id[:8] + "...")
481
+ header_table.add_row("Description:", change_set.description)
482
+ header_table.add_row("Total Changes:", str(stats["total_changes"]))
483
+ header_table.add_row("Files Affected:", str(stats["files_affected"]))
484
+ header_table.add_row("Risk Score:", f"{change_set.total_risk_score:.1f}")
485
+
486
+ # Create risk distribution bar
487
+ risk_bar = self._create_risk_distribution_bar(stats["risk_distribution"])
488
+ header_table.add_row("Risk Distribution:", risk_bar)
489
+
490
+ panel = Panel(
491
+ header_table,
492
+ title="[bold]Proposed Changes Overview[/bold]",
493
+ border_style="blue",
494
+ )
495
+
496
+ self.console.print(panel)
497
+
498
+ def _display_change_summary(self, change_set: ChangeSet):
499
+ """Display summary of changes."""
500
+ summary_table = Table(title="Changes Summary")
501
+ summary_table.add_column("#", style="dim", width=3)
502
+ summary_table.add_column("File", style="cyan")
503
+ summary_table.add_column("Operation", style="yellow")
504
+ summary_table.add_column("Risk", style="magenta")
505
+ summary_table.add_column("Summary", style="white")
506
+
507
+ for i, change in enumerate(change_set.changes, 1):
508
+ risk_color = self._get_risk_color(change.risk_level)
509
+ summary = change.change_summary or "No summary available"
510
+
511
+ summary_table.add_row(
512
+ str(i),
513
+ change.file_path,
514
+ (change.operation_type.value if change.operation_type else "unknown"),
515
+ f"[{risk_color}]{change.risk_level.value if change.risk_level else 'unknown'}[/{risk_color}]",
516
+ summary[:50] + "..." if len(summary) > 50 else summary,
517
+ )
518
+
519
+ self.console.print(summary_table)
520
+
521
+ async def _display_single_change(
522
+ self, change: ProposedChange, display_mode: ChangeDisplayMode
523
+ ):
524
+ """Display a single change based on display mode."""
525
+ if display_mode == ChangeDisplayMode.INLINE:
526
+ if change.original_content and change.modified_content:
527
+ await self.display_inline_changes(
528
+ change.file_path,
529
+ change.original_content,
530
+ change.modified_content,
531
+ )
532
+ elif display_mode == ChangeDisplayMode.SIDE_BY_SIDE:
533
+ if change.original_content and change.modified_content:
534
+ await self.display_side_by_side_changes(
535
+ change.file_path,
536
+ change.original_content,
537
+ change.modified_content,
538
+ )
539
+ else: # UNIFIED
540
+ if change.modified_content:
541
+ await self.unified_display.display_file_modification(
542
+ change.file_path,
543
+ change.original_content or "",
544
+ change.modified_content,
545
+ {"interactive": False},
546
+ )
547
+
548
+ async def _get_change_approval(self, change_set: ChangeSet) -> Dict[str, Any]:
549
+ """Get user approval for changes."""
550
+ from rich.prompt import Prompt
551
+
552
+ # Show options
553
+ self.console.print("\n[bold]Options:[/bold]")
554
+ self.console.print(" [green]A[/green] - Approve all changes")
555
+ self.console.print(" [yellow]S[/yellow] - Select specific changes")
556
+ self.console.print(" [red]D[/red] - Deny all changes")
557
+ self.console.print(" [cyan]V[/cyan] - View detailed changes")
558
+
559
+ choice = Prompt.ask("Your choice", choices=["A", "S", "D", "V"], default="V")
560
+
561
+ if choice == "A":
562
+ change_set.approved = True
563
+ return {"approved": True, "all_changes": True}
564
+ elif choice == "S":
565
+ selected = await self._select_specific_changes(change_set)
566
+ change_set.approved = len(selected) > 0
567
+ return {
568
+ "approved": len(selected) > 0,
569
+ "selected_changes": selected,
570
+ }
571
+ elif choice == "D":
572
+ return {"approved": False}
573
+ else: # V
574
+ # Show detailed view and ask again
575
+ for change in change_set.changes:
576
+ await self._display_single_change(change, ChangeDisplayMode.UNIFIED)
577
+ return await self._get_change_approval(change_set)
578
+
579
+ async def _select_specific_changes(self, change_set: ChangeSet) -> List[int]:
580
+ """Let user select specific changes to apply."""
581
+ from rich.prompt import Prompt
582
+
583
+ self._display_change_summary(change_set)
584
+
585
+ selection = Prompt.ask(
586
+ "Enter change numbers to approve (comma-separated)", default=""
587
+ )
588
+
589
+ if not selection:
590
+ return []
591
+
592
+ try:
593
+ indices = [int(x.strip()) - 1 for x in selection.split(",")]
594
+ return [i for i in indices if 0 <= i < len(change_set.changes)]
595
+ except ValueError:
596
+ self.console.print("[red]Invalid selection[/red]")
597
+ return []
598
+
599
+ async def _apply_single_change(self, change: ProposedChange) -> Dict[str, Any]:
600
+ """Apply a single change to the file system."""
601
+ try:
602
+ if change.operation_type == ChangeType.FILE_CREATE:
603
+ return await self.file_system_manager.create_file(
604
+ change.file_path, change.modified_content or ""
605
+ )
606
+ elif change.operation_type == ChangeType.FILE_MODIFY:
607
+ return await self.file_system_manager.modify_file(
608
+ change.file_path, change.modified_content or ""
609
+ )
610
+ elif change.operation_type == ChangeType.FILE_DELETE:
611
+ return await self.file_system_manager.delete_file(change.file_path)
612
+ else:
613
+ return {
614
+ "success": False,
615
+ "error": f"Unsupported operation: {change.operation_type}",
616
+ }
617
+ except Exception as e:
618
+ return {"success": False, "error": str(e)}
619
+
620
+ def _create_risk_distribution_bar(self, risk_distribution: Dict[str, int]) -> str:
621
+ """Create a visual risk distribution bar."""
622
+ total = sum(risk_distribution.values())
623
+ if total == 0:
624
+ return "No risks"
625
+
626
+ bar_parts = []
627
+ for risk in ["LOW", "MEDIUM", "HIGH", "CRITICAL"]:
628
+ count = risk_distribution.get(risk, 0)
629
+ if count > 0:
630
+ percentage = (count / total) * 100
631
+ color = self._get_risk_color(RiskLevel[risk])
632
+ bar_parts.append(
633
+ f"[{color}]{risk}: {count} ({percentage:.0f}%)[/{color}]"
634
+ )
635
+
636
+ return " | ".join(bar_parts)
637
+
638
+ def _get_risk_color(self, risk_level: RiskLevel) -> str:
639
+ """Get color for risk level."""
640
+ colors = {
641
+ RiskLevel.LOW: "green",
642
+ RiskLevel.MEDIUM: "yellow",
643
+ RiskLevel.HIGH: "orange1",
644
+ RiskLevel.CRITICAL: "red",
645
+ }
646
+ return colors.get(risk_level, "white")
647
+
648
+ def _detect_language(self, file_path: str) -> str:
649
+ """Detect programming language from file extension."""
650
+ extension = Path(file_path).suffix.lower()
651
+ language_map = {
652
+ ".py": "python",
653
+ ".js": "javascript",
654
+ ".ts": "typescript",
655
+ ".java": "java",
656
+ ".cpp": "cpp",
657
+ ".c": "c",
658
+ ".cs": "csharp",
659
+ ".rb": "ruby",
660
+ ".go": "go",
661
+ ".rs": "rust",
662
+ }
663
+ return language_map.get(extension, "text")