omnimancer-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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")
|