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,647 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User interface for read-before-write functionality.
|
|
3
|
+
|
|
4
|
+
This module provides UI components for presenting file modification previews
|
|
5
|
+
to users and collecting their approval/modification decisions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.prompt import Confirm, Prompt
|
|
15
|
+
from rich.syntax import Syntax
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
from rich.text import Text
|
|
18
|
+
|
|
19
|
+
from ...ui.cancellation_handler import get_active_cancellation_handler
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ReadBeforeWriteUI:
|
|
25
|
+
"""
|
|
26
|
+
User interface for read-before-write operations.
|
|
27
|
+
|
|
28
|
+
Provides rich terminal UI for displaying file modification previews,
|
|
29
|
+
diffs, and collecting user decisions.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, console: Optional[Console] = None):
|
|
33
|
+
"""
|
|
34
|
+
Initialize the read-before-write UI.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
console: Optional Rich console instance
|
|
38
|
+
"""
|
|
39
|
+
self.console = console or Console()
|
|
40
|
+
|
|
41
|
+
async def review_file_modification(
|
|
42
|
+
self, review_data: Dict[str, Any]
|
|
43
|
+
) -> Dict[str, Any]:
|
|
44
|
+
"""
|
|
45
|
+
Present file modification review to user and collect decision.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
review_data: Dictionary containing:
|
|
49
|
+
- file_path: Path to the file
|
|
50
|
+
- file_exists: Whether file currently exists
|
|
51
|
+
- current_content: Current file content (if exists)
|
|
52
|
+
- new_content: Proposed new content
|
|
53
|
+
- diff: Unified diff between current and new content
|
|
54
|
+
- operation: 'create' or 'modify'
|
|
55
|
+
- encoding: File encoding
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dictionary with user decision:
|
|
59
|
+
- approved: Boolean indicating if change is approved
|
|
60
|
+
- modified_content: Optional modified content from user
|
|
61
|
+
- reason: Optional reason for decision
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
# Ensure we start on a new line, clearing any spinner artifacts
|
|
65
|
+
self.console.print()
|
|
66
|
+
|
|
67
|
+
# Display file modification preview
|
|
68
|
+
self._display_modification_header(review_data)
|
|
69
|
+
|
|
70
|
+
# Show content preview based on operation type
|
|
71
|
+
if review_data["operation"] == "create":
|
|
72
|
+
self._display_new_file_preview(review_data)
|
|
73
|
+
else:
|
|
74
|
+
self._display_modification_preview(review_data)
|
|
75
|
+
|
|
76
|
+
# Get user decision
|
|
77
|
+
decision = await self._get_user_decision(review_data)
|
|
78
|
+
|
|
79
|
+
return decision
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"Error in file modification review: {e}")
|
|
83
|
+
return {"approved": False, "reason": f"UI error: {str(e)}"}
|
|
84
|
+
|
|
85
|
+
def _display_modification_header(self, review_data: Dict[str, Any]):
|
|
86
|
+
"""Display header information about the file modification."""
|
|
87
|
+
file_path = review_data["file_path"]
|
|
88
|
+
operation = review_data["operation"]
|
|
89
|
+
|
|
90
|
+
# Create header panel
|
|
91
|
+
header_text = Text()
|
|
92
|
+
header_text.append("File Modification Review\n", style="bold cyan")
|
|
93
|
+
header_text.append(f"Operation: ", style="bold")
|
|
94
|
+
header_text.append(f"{operation.title()}\n", style="yellow")
|
|
95
|
+
header_text.append(f"File: ", style="bold")
|
|
96
|
+
header_text.append(f"{file_path}", style="green")
|
|
97
|
+
|
|
98
|
+
header_panel = Panel(
|
|
99
|
+
header_text,
|
|
100
|
+
title="[bold blue]Read-Before-Write Review[/bold blue]",
|
|
101
|
+
border_style="blue",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
self.console.print(header_panel)
|
|
105
|
+
|
|
106
|
+
def _display_new_file_preview(self, review_data: Dict[str, Any]):
|
|
107
|
+
"""Display preview for new file creation."""
|
|
108
|
+
new_content = review_data["new_content"]
|
|
109
|
+
file_path = review_data["file_path"]
|
|
110
|
+
|
|
111
|
+
# Determine file type for syntax highlighting
|
|
112
|
+
file_extension = Path(file_path).suffix.lower()
|
|
113
|
+
lexer = self._get_lexer_for_extension(file_extension)
|
|
114
|
+
|
|
115
|
+
# Show new content with syntax highlighting
|
|
116
|
+
if len(new_content) > 2000: # Truncate very long content
|
|
117
|
+
preview_content = new_content[:2000] + "\n... (content truncated)"
|
|
118
|
+
else:
|
|
119
|
+
preview_content = new_content
|
|
120
|
+
|
|
121
|
+
syntax = Syntax(
|
|
122
|
+
preview_content,
|
|
123
|
+
lexer,
|
|
124
|
+
theme="monokai",
|
|
125
|
+
line_numbers=True,
|
|
126
|
+
word_wrap=True,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
content_panel = Panel(
|
|
130
|
+
syntax,
|
|
131
|
+
title="[bold green]New File Content[/bold green]",
|
|
132
|
+
border_style="green",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
self.console.print(content_panel)
|
|
136
|
+
|
|
137
|
+
# Show file stats
|
|
138
|
+
self._display_content_stats(new_content, is_new=True)
|
|
139
|
+
|
|
140
|
+
def _display_modification_preview(self, review_data: Dict[str, Any]):
|
|
141
|
+
"""Display preview for file modification with diff."""
|
|
142
|
+
current_content = review_data.get("current_content", "")
|
|
143
|
+
new_content = review_data["new_content"]
|
|
144
|
+
diff = review_data.get("diff", "")
|
|
145
|
+
|
|
146
|
+
# Display current content summary
|
|
147
|
+
if current_content:
|
|
148
|
+
self._display_current_content_summary(current_content)
|
|
149
|
+
|
|
150
|
+
# Display diff if available
|
|
151
|
+
if diff:
|
|
152
|
+
self._display_diff(diff)
|
|
153
|
+
else:
|
|
154
|
+
# Fallback to side-by-side preview for very different content
|
|
155
|
+
self._display_side_by_side_preview(current_content, new_content)
|
|
156
|
+
|
|
157
|
+
# Show content statistics
|
|
158
|
+
self._display_content_comparison_stats(current_content, new_content)
|
|
159
|
+
|
|
160
|
+
def _display_current_content_summary(self, current_content: str):
|
|
161
|
+
"""Display summary of current file content."""
|
|
162
|
+
lines = current_content.split("\n")
|
|
163
|
+
total_lines = len(lines)
|
|
164
|
+
|
|
165
|
+
# Show first few and last few lines
|
|
166
|
+
preview_lines = []
|
|
167
|
+
if total_lines <= 20:
|
|
168
|
+
preview_lines = lines
|
|
169
|
+
else:
|
|
170
|
+
preview_lines = (
|
|
171
|
+
lines[:10]
|
|
172
|
+
+ [f"... ({total_lines - 20} lines omitted) ..."]
|
|
173
|
+
+ lines[-10:]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
preview_text = "\n".join(preview_lines)
|
|
177
|
+
|
|
178
|
+
current_panel = Panel(
|
|
179
|
+
preview_text,
|
|
180
|
+
title="[bold yellow]Current File Content (Preview)[/bold yellow]",
|
|
181
|
+
border_style="yellow",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
self.console.print(current_panel)
|
|
185
|
+
|
|
186
|
+
def _display_diff(self, diff: str):
|
|
187
|
+
"""Display unified diff with syntax highlighting."""
|
|
188
|
+
# Color code the diff
|
|
189
|
+
diff_text = Text()
|
|
190
|
+
|
|
191
|
+
for line in diff.split("\n"):
|
|
192
|
+
if line.startswith("+++") or line.startswith("---"):
|
|
193
|
+
diff_text.append(line + "\n", style="bold white")
|
|
194
|
+
elif line.startswith("@@"):
|
|
195
|
+
diff_text.append(line + "\n", style="bold cyan")
|
|
196
|
+
elif line.startswith("+"):
|
|
197
|
+
diff_text.append(line + "\n", style="bold green")
|
|
198
|
+
elif line.startswith("-"):
|
|
199
|
+
diff_text.append(line + "\n", style="bold red")
|
|
200
|
+
else:
|
|
201
|
+
diff_text.append(line + "\n", style="white")
|
|
202
|
+
|
|
203
|
+
diff_panel = Panel(
|
|
204
|
+
diff_text,
|
|
205
|
+
title="[bold magenta]Changes (Unified Diff)[/bold magenta]",
|
|
206
|
+
border_style="magenta",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
self.console.print(diff_panel)
|
|
210
|
+
|
|
211
|
+
def _display_side_by_side_preview(self, current_content: str, new_content: str):
|
|
212
|
+
"""Display side-by-side preview when diff is not available."""
|
|
213
|
+
# Create table for side-by-side view
|
|
214
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
215
|
+
table.add_column("Current Content", style="yellow", width=50)
|
|
216
|
+
table.add_column("New Content", style="green", width=50)
|
|
217
|
+
|
|
218
|
+
# Truncate content for display
|
|
219
|
+
current_preview = (
|
|
220
|
+
current_content[:1000] + "..."
|
|
221
|
+
if len(current_content) > 1000
|
|
222
|
+
else current_content
|
|
223
|
+
)
|
|
224
|
+
new_preview = (
|
|
225
|
+
new_content[:1000] + "..." if len(new_content) > 1000 else new_content
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
table.add_row(current_preview, new_preview)
|
|
229
|
+
|
|
230
|
+
preview_panel = Panel(
|
|
231
|
+
table,
|
|
232
|
+
title="[bold magenta]Content Comparison[/bold magenta]",
|
|
233
|
+
border_style="magenta",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
self.console.print(preview_panel)
|
|
237
|
+
|
|
238
|
+
def _display_content_stats(self, content: str, is_new: bool = False):
|
|
239
|
+
"""Display content statistics."""
|
|
240
|
+
lines = content.split("\n")
|
|
241
|
+
chars = len(content)
|
|
242
|
+
words = len(content.split())
|
|
243
|
+
|
|
244
|
+
stats_text = Text()
|
|
245
|
+
stats_text.append("Content Statistics:\n", style="bold")
|
|
246
|
+
stats_text.append(f"Lines: {len(lines)}\n")
|
|
247
|
+
stats_text.append(f"Characters: {chars}\n")
|
|
248
|
+
stats_text.append(f"Words: {words}")
|
|
249
|
+
|
|
250
|
+
title = (
|
|
251
|
+
"[bold blue]New File Statistics[/bold blue]"
|
|
252
|
+
if is_new
|
|
253
|
+
else "[bold blue]Content Statistics[/bold blue]"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
stats_panel = Panel(stats_text, title=title, border_style="blue", width=40)
|
|
257
|
+
|
|
258
|
+
self.console.print(stats_panel)
|
|
259
|
+
|
|
260
|
+
def _display_content_comparison_stats(self, current_content: str, new_content: str):
|
|
261
|
+
"""Display comparison statistics between current and new content."""
|
|
262
|
+
current_lines = len(current_content.split("\n")) if current_content else 0
|
|
263
|
+
new_lines = len(new_content.split("\n"))
|
|
264
|
+
current_chars = len(current_content) if current_content else 0
|
|
265
|
+
new_chars = len(new_content)
|
|
266
|
+
|
|
267
|
+
# Create comparison table
|
|
268
|
+
table = Table(show_header=True, header_style="bold blue")
|
|
269
|
+
table.add_column("Metric", style="cyan")
|
|
270
|
+
table.add_column("Current", style="yellow")
|
|
271
|
+
table.add_column("New", style="green")
|
|
272
|
+
table.add_column("Change", style="magenta")
|
|
273
|
+
|
|
274
|
+
line_change = new_lines - current_lines
|
|
275
|
+
char_change = new_chars - current_chars
|
|
276
|
+
|
|
277
|
+
table.add_row(
|
|
278
|
+
"Lines",
|
|
279
|
+
str(current_lines),
|
|
280
|
+
str(new_lines),
|
|
281
|
+
f"{'+' if line_change >= 0 else ''}{line_change}",
|
|
282
|
+
)
|
|
283
|
+
table.add_row(
|
|
284
|
+
"Characters",
|
|
285
|
+
str(current_chars),
|
|
286
|
+
str(new_chars),
|
|
287
|
+
f"{'+' if char_change >= 0 else ''}{char_change}",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
stats_panel = Panel(
|
|
291
|
+
table,
|
|
292
|
+
title="[bold blue]Content Comparison[/bold blue]",
|
|
293
|
+
border_style="blue",
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
self.console.print(stats_panel)
|
|
297
|
+
|
|
298
|
+
async def _get_user_decision(self, review_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
299
|
+
"""Get user decision about the file modification."""
|
|
300
|
+
self.console.print() # Add some spacing
|
|
301
|
+
|
|
302
|
+
# Present options to user
|
|
303
|
+
options_text = Text()
|
|
304
|
+
options_text.append("Options:\n", style="bold cyan")
|
|
305
|
+
options_text.append("1. ", style="bold green")
|
|
306
|
+
options_text.append("Approve changes\n", style="green")
|
|
307
|
+
options_text.append("2. ", style="bold yellow")
|
|
308
|
+
options_text.append("Edit content before writing\n", style="yellow")
|
|
309
|
+
options_text.append("3. ", style="bold red")
|
|
310
|
+
options_text.append("Reject changes\n", style="red")
|
|
311
|
+
|
|
312
|
+
options_panel = Panel(
|
|
313
|
+
options_text,
|
|
314
|
+
title="[bold cyan]Choose Action[/bold cyan]",
|
|
315
|
+
border_style="cyan",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
self.console.print(options_panel)
|
|
319
|
+
|
|
320
|
+
# Pause the spinner during user interaction
|
|
321
|
+
cancellation_handler = get_active_cancellation_handler()
|
|
322
|
+
if cancellation_handler:
|
|
323
|
+
cancellation_handler.pause_status_display()
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
# Get user choice with Rich prompt (now that spinner is paused)
|
|
327
|
+
choice = Prompt.ask(
|
|
328
|
+
"[bold cyan]Your choice[/bold cyan]",
|
|
329
|
+
choices=["1", "2", "3", "approve", "edit", "reject"],
|
|
330
|
+
default="1",
|
|
331
|
+
)
|
|
332
|
+
except (EOFError, KeyboardInterrupt):
|
|
333
|
+
return {"approved": False, "reason": "Operation cancelled by user"}
|
|
334
|
+
finally:
|
|
335
|
+
# Resume the spinner after user interaction
|
|
336
|
+
if cancellation_handler:
|
|
337
|
+
cancellation_handler.resume_status_display()
|
|
338
|
+
|
|
339
|
+
# Process the user's choice
|
|
340
|
+
if choice in ["1", "approve"]:
|
|
341
|
+
return {"approved": True, "reason": "User approved changes"}
|
|
342
|
+
elif choice in ["2", "edit"]:
|
|
343
|
+
return await self._handle_edit_content(review_data)
|
|
344
|
+
elif choice in ["3", "reject"]:
|
|
345
|
+
reason = Prompt.ask(
|
|
346
|
+
"[yellow]Reason for rejection (optional)[/yellow]",
|
|
347
|
+
default="User rejected changes",
|
|
348
|
+
)
|
|
349
|
+
return {"approved": False, "reason": reason}
|
|
350
|
+
|
|
351
|
+
async def _handle_edit_content(self, review_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
352
|
+
"""Handle user editing of content before writing."""
|
|
353
|
+
self.console.print("\n[yellow]Content editing functionality:[/yellow]")
|
|
354
|
+
self.console.print("You can:")
|
|
355
|
+
self.console.print("1. Provide replacement content inline")
|
|
356
|
+
self.console.print("2. Open external editor (if configured)")
|
|
357
|
+
self.console.print("3. Go back to original content")
|
|
358
|
+
|
|
359
|
+
edit_choice = Prompt.ask(
|
|
360
|
+
"Edit method",
|
|
361
|
+
choices=["1", "2", "3", "inline", "editor", "back"],
|
|
362
|
+
default="1",
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
if edit_choice in ["1", "inline"]:
|
|
366
|
+
return await self._inline_content_edit(review_data)
|
|
367
|
+
elif edit_choice in ["2", "editor"]:
|
|
368
|
+
return await self._external_editor_edit(review_data)
|
|
369
|
+
else:
|
|
370
|
+
# Go back to original decision
|
|
371
|
+
return await self._get_user_decision(review_data)
|
|
372
|
+
|
|
373
|
+
async def _inline_content_edit(self, review_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
374
|
+
"""Handle inline content editing."""
|
|
375
|
+
self.console.print("\n[yellow]Inline content editing:[/yellow]")
|
|
376
|
+
self.console.print(
|
|
377
|
+
"Enter new content (type END_CONTENT on a new line to finish):"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
lines = []
|
|
381
|
+
while True:
|
|
382
|
+
try:
|
|
383
|
+
line = input()
|
|
384
|
+
if line.strip() == "END_CONTENT":
|
|
385
|
+
break
|
|
386
|
+
lines.append(line)
|
|
387
|
+
except EOFError:
|
|
388
|
+
break
|
|
389
|
+
except KeyboardInterrupt:
|
|
390
|
+
self.console.print("\n[red]Edit cancelled[/red]")
|
|
391
|
+
return {"approved": False, "reason": "Edit cancelled by user"}
|
|
392
|
+
|
|
393
|
+
modified_content = "\n".join(lines)
|
|
394
|
+
|
|
395
|
+
# Confirm the edit
|
|
396
|
+
self.console.print(
|
|
397
|
+
f"\n[cyan]Modified content ({len(modified_content)} characters):[/cyan]"
|
|
398
|
+
)
|
|
399
|
+
preview = (
|
|
400
|
+
modified_content[:500] + "..."
|
|
401
|
+
if len(modified_content) > 500
|
|
402
|
+
else modified_content
|
|
403
|
+
)
|
|
404
|
+
self.console.print(Panel(preview, title="Modified Content Preview"))
|
|
405
|
+
|
|
406
|
+
confirm = Confirm.ask("Use this modified content?", default=True)
|
|
407
|
+
|
|
408
|
+
if confirm:
|
|
409
|
+
return {
|
|
410
|
+
"approved": True,
|
|
411
|
+
"modified_content": modified_content,
|
|
412
|
+
"reason": "User provided modified content",
|
|
413
|
+
}
|
|
414
|
+
else:
|
|
415
|
+
return await self._get_user_decision(review_data)
|
|
416
|
+
|
|
417
|
+
async def _external_editor_edit(
|
|
418
|
+
self, review_data: Dict[str, Any]
|
|
419
|
+
) -> Dict[str, Any]:
|
|
420
|
+
"""Handle external editor content editing."""
|
|
421
|
+
self.console.print(
|
|
422
|
+
"[yellow]External editor functionality not yet implemented[/yellow]"
|
|
423
|
+
)
|
|
424
|
+
self.console.print("[yellow]Falling back to inline editing...[/yellow]")
|
|
425
|
+
return await self._inline_content_edit(review_data)
|
|
426
|
+
|
|
427
|
+
async def confirm_file_overwrite(self, file_info: Dict[str, Any]) -> Dict[str, Any]:
|
|
428
|
+
"""
|
|
429
|
+
Prompt user for confirmation when a file already exists.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
file_info: Dictionary containing file information from check_file_exists():
|
|
433
|
+
- path: File path
|
|
434
|
+
- exists: Whether file exists
|
|
435
|
+
- is_file: Whether it's a file
|
|
436
|
+
- is_directory: Whether it's a directory
|
|
437
|
+
- size: File size in bytes (if applicable)
|
|
438
|
+
- modified_time: Last modified time
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Dictionary with user decision:
|
|
442
|
+
- confirmed: Boolean indicating if user confirmed overwrite
|
|
443
|
+
- action: 'overwrite', 'cancel', or 'backup'
|
|
444
|
+
- reason: Optional reason for decision
|
|
445
|
+
"""
|
|
446
|
+
try:
|
|
447
|
+
if not file_info.get("exists", False):
|
|
448
|
+
# File doesn't exist, no confirmation needed
|
|
449
|
+
return {
|
|
450
|
+
"confirmed": True,
|
|
451
|
+
"action": "create",
|
|
452
|
+
"reason": "File does not exist, proceeding with creation",
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
# Ensure we start on a new line, clearing any spinner artifacts
|
|
456
|
+
self.console.print()
|
|
457
|
+
|
|
458
|
+
# Display file existence warning
|
|
459
|
+
self._display_file_exists_warning(file_info)
|
|
460
|
+
|
|
461
|
+
# Get user confirmation
|
|
462
|
+
return await self._get_overwrite_decision(file_info)
|
|
463
|
+
|
|
464
|
+
except Exception as e:
|
|
465
|
+
logger.error(f"Error in file overwrite confirmation: {e}")
|
|
466
|
+
return {
|
|
467
|
+
"confirmed": False,
|
|
468
|
+
"action": "cancel",
|
|
469
|
+
"reason": f"Confirmation error: {str(e)}",
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
def _display_file_exists_warning(self, file_info: Dict[str, Any]):
|
|
473
|
+
"""Display warning about existing file."""
|
|
474
|
+
file_path = file_info["path"]
|
|
475
|
+
file_type = "directory" if file_info.get("is_directory") else "file"
|
|
476
|
+
|
|
477
|
+
# Create warning header
|
|
478
|
+
warning_text = Text()
|
|
479
|
+
warning_text.append("⚠️ File Exists Warning\n", style="bold yellow")
|
|
480
|
+
warning_text.append(f"The {file_type} already exists:\n", style="yellow")
|
|
481
|
+
warning_text.append(f"{file_path}\n\n", style="cyan")
|
|
482
|
+
|
|
483
|
+
# Add file details if available
|
|
484
|
+
if file_info.get("size") is not None:
|
|
485
|
+
size_mb = file_info["size"] / (1024 * 1024)
|
|
486
|
+
if size_mb >= 1:
|
|
487
|
+
warning_text.append(f"Size: {size_mb:.2f} MB\n", style="white")
|
|
488
|
+
else:
|
|
489
|
+
size_kb = file_info["size"] / 1024
|
|
490
|
+
warning_text.append(f"Size: {size_kb:.2f} KB\n", style="white")
|
|
491
|
+
|
|
492
|
+
if file_info.get("modified_time"):
|
|
493
|
+
warning_text.append(
|
|
494
|
+
f"Last modified: {file_info['modified_time']}\n", style="white"
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
if file_info.get("is_symlink"):
|
|
498
|
+
warning_text.append("Type: Symbolic link\n", style="magenta")
|
|
499
|
+
|
|
500
|
+
warning_panel = Panel(
|
|
501
|
+
warning_text,
|
|
502
|
+
title="[bold red]File Already Exists[/bold red]",
|
|
503
|
+
border_style="red",
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
self.console.print(warning_panel)
|
|
507
|
+
|
|
508
|
+
async def _get_overwrite_decision(
|
|
509
|
+
self, file_info: Dict[str, Any]
|
|
510
|
+
) -> Dict[str, Any]:
|
|
511
|
+
"""Get user decision about overwriting existing file."""
|
|
512
|
+
self.console.print() # Add some spacing
|
|
513
|
+
|
|
514
|
+
# Present options to user
|
|
515
|
+
options_text = Text()
|
|
516
|
+
options_text.append("What would you like to do?\n\n", style="bold cyan")
|
|
517
|
+
options_text.append("1. ", style="bold green")
|
|
518
|
+
options_text.append("Overwrite the existing file\n", style="green")
|
|
519
|
+
options_text.append("2. ", style="bold yellow")
|
|
520
|
+
options_text.append("Create backup and overwrite\n", style="yellow")
|
|
521
|
+
options_text.append("3. ", style="bold red")
|
|
522
|
+
options_text.append("Cancel operation\n", style="red")
|
|
523
|
+
|
|
524
|
+
options_panel = Panel(
|
|
525
|
+
options_text,
|
|
526
|
+
title="[bold cyan]Choose Action[/bold cyan]",
|
|
527
|
+
border_style="cyan",
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
self.console.print(options_panel)
|
|
531
|
+
|
|
532
|
+
# Pause the spinner during user interaction
|
|
533
|
+
cancellation_handler = get_active_cancellation_handler()
|
|
534
|
+
if cancellation_handler:
|
|
535
|
+
cancellation_handler.pause_status_display()
|
|
536
|
+
|
|
537
|
+
try:
|
|
538
|
+
# Get user choice with Rich prompt (now that spinner is paused)
|
|
539
|
+
choice = Prompt.ask(
|
|
540
|
+
"[bold cyan]Your choice[/bold cyan]",
|
|
541
|
+
choices=["1", "2", "3", "overwrite", "backup", "cancel"],
|
|
542
|
+
default="3", # Default to cancel for safety
|
|
543
|
+
)
|
|
544
|
+
except (EOFError, KeyboardInterrupt):
|
|
545
|
+
return {
|
|
546
|
+
"confirmed": False,
|
|
547
|
+
"action": "cancel",
|
|
548
|
+
"reason": "Operation cancelled by user",
|
|
549
|
+
}
|
|
550
|
+
finally:
|
|
551
|
+
# Resume the spinner after user interaction
|
|
552
|
+
if cancellation_handler:
|
|
553
|
+
cancellation_handler.resume_status_display()
|
|
554
|
+
|
|
555
|
+
# Process the user's choice
|
|
556
|
+
if choice in ["1", "overwrite"]:
|
|
557
|
+
confirm = Confirm.ask(
|
|
558
|
+
"[yellow]Are you sure you want to overwrite the existing file?[/yellow]",
|
|
559
|
+
default=False,
|
|
560
|
+
)
|
|
561
|
+
if confirm:
|
|
562
|
+
return {
|
|
563
|
+
"confirmed": True,
|
|
564
|
+
"action": "overwrite",
|
|
565
|
+
"reason": "User confirmed overwrite",
|
|
566
|
+
}
|
|
567
|
+
else:
|
|
568
|
+
return await self._get_overwrite_decision(file_info) # Ask again
|
|
569
|
+
|
|
570
|
+
elif choice in ["2", "backup"]:
|
|
571
|
+
return {
|
|
572
|
+
"confirmed": True,
|
|
573
|
+
"action": "backup",
|
|
574
|
+
"reason": "User chose to create backup before overwriting",
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
elif choice in ["3", "cancel"]:
|
|
578
|
+
reason = Prompt.ask(
|
|
579
|
+
"[yellow]Reason for cancellation (optional)[/yellow]",
|
|
580
|
+
default="User chose to cancel operation",
|
|
581
|
+
)
|
|
582
|
+
return {"confirmed": False, "action": "cancel", "reason": reason}
|
|
583
|
+
|
|
584
|
+
def _get_lexer_for_extension(self, extension: str) -> str:
|
|
585
|
+
"""Get appropriate lexer for file extension."""
|
|
586
|
+
extension_map = {
|
|
587
|
+
".py": "python",
|
|
588
|
+
".js": "javascript",
|
|
589
|
+
".ts": "typescript",
|
|
590
|
+
".html": "html",
|
|
591
|
+
".css": "css",
|
|
592
|
+
".json": "json",
|
|
593
|
+
".yaml": "yaml",
|
|
594
|
+
".yml": "yaml",
|
|
595
|
+
".xml": "xml",
|
|
596
|
+
".md": "markdown",
|
|
597
|
+
".sh": "bash",
|
|
598
|
+
".sql": "sql",
|
|
599
|
+
".cpp": "cpp",
|
|
600
|
+
".c": "c",
|
|
601
|
+
".java": "java",
|
|
602
|
+
".go": "go",
|
|
603
|
+
".rs": "rust",
|
|
604
|
+
".php": "php",
|
|
605
|
+
".rb": "ruby",
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return extension_map.get(extension, "text")
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
# Convenience functions for creating UI callbacks
|
|
612
|
+
def create_review_callback(console: Optional[Console] = None):
|
|
613
|
+
"""
|
|
614
|
+
Create a review callback function for use with FileSystemManager.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
console: Optional Rich console instance
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
Async callback function for file modification review
|
|
621
|
+
"""
|
|
622
|
+
ui = ReadBeforeWriteUI(console)
|
|
623
|
+
|
|
624
|
+
async def review_callback(review_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
625
|
+
return await ui.review_file_modification(review_data)
|
|
626
|
+
|
|
627
|
+
return review_callback
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def create_confirmation_callback(console: Optional[Console] = None):
|
|
631
|
+
"""
|
|
632
|
+
Create a confirmation callback function for file existence checks.
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
console: Optional Rich console instance
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
Async callback function for file overwrite confirmation
|
|
639
|
+
"""
|
|
640
|
+
ui = ReadBeforeWriteUI(console)
|
|
641
|
+
|
|
642
|
+
async def confirmation_callback(
|
|
643
|
+
file_info: Dict[str, Any],
|
|
644
|
+
) -> Dict[str, Any]:
|
|
645
|
+
return await ui.confirm_file_overwrite(file_info)
|
|
646
|
+
|
|
647
|
+
return confirmation_callback
|