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,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