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,706 @@
1
+ """
2
+ Enhanced Diff Display and Code Highlighting for Omnimancer Approval Interface.
3
+
4
+ This module provides sophisticated diff visualization with syntax highlighting,
5
+ side-by-side comparisons, and enhanced formatting for code changes in the
6
+ approval workflow.
7
+ """
8
+
9
+ import difflib
10
+ import re
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+ from pathlib import Path
14
+ from typing import Dict, List, Optional, Tuple
15
+
16
+ from rich.box import ROUNDED, SIMPLE
17
+ from rich.console import Console, RenderableType
18
+ from rich.panel import Panel
19
+ from rich.rule import Rule
20
+ from rich.table import Table
21
+ from rich.text import Text
22
+ from rich.tree import Tree
23
+
24
+ from .rich_renderer import RichTextRenderer, create_renderer
25
+
26
+
27
+ class DiffType(Enum):
28
+ """Types of diff display formats."""
29
+
30
+ UNIFIED = "unified"
31
+ SIDE_BY_SIDE = "side_by_side"
32
+ INLINE = "inline"
33
+ CONTEXT = "context"
34
+
35
+
36
+ class FileChangeType(Enum):
37
+ """Types of file changes."""
38
+
39
+ ADDED = "added"
40
+ MODIFIED = "modified"
41
+ DELETED = "deleted"
42
+ RENAMED = "renamed"
43
+ COPIED = "copied"
44
+
45
+
46
+ @dataclass
47
+ class FileChange:
48
+ """Represents a single file change in a diff."""
49
+
50
+ file_path: str
51
+ change_type: FileChangeType
52
+ old_path: Optional[str] = None
53
+ new_path: Optional[str] = None
54
+ old_content: Optional[str] = None
55
+ new_content: Optional[str] = None
56
+ diff_text: Optional[str] = None
57
+ language: Optional[str] = None
58
+ lines_added: int = 0
59
+ lines_removed: int = 0
60
+
61
+
62
+ @dataclass
63
+ class DiffChunk:
64
+ """Represents a chunk of changes within a file."""
65
+
66
+ old_start: int
67
+ old_count: int
68
+ new_start: int
69
+ new_count: int
70
+ context_before: List[str]
71
+ changes: List[Tuple[str, str]] # (change_type, line_content)
72
+ context_after: List[str]
73
+
74
+
75
+ class EnhancedDiffRenderer:
76
+ """
77
+ Enhanced diff renderer with syntax highlighting and multiple display formats.
78
+
79
+ Provides sophisticated diff visualization capabilities for the approval interface,
80
+ including side-by-side comparisons, syntax highlighting, and file tree views.
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ renderer: Optional[RichTextRenderer] = None,
86
+ console: Optional[Console] = None,
87
+ ):
88
+ """
89
+ Initialize the enhanced diff renderer.
90
+
91
+ Args:
92
+ renderer: Rich text renderer instance
93
+ console: Rich console instance
94
+ """
95
+ self.renderer = renderer or create_renderer()
96
+ self.console = console or self.renderer.console
97
+
98
+ # Diff rendering configuration
99
+ self.max_line_length = 120
100
+ self.context_lines = 3
101
+ self.show_line_numbers = True
102
+ self.highlight_syntax = True
103
+ self.word_level_diff = True
104
+
105
+ # Language detection patterns
106
+ self.language_patterns = {
107
+ r"\.py$": "python",
108
+ r"\.js$": "javascript",
109
+ r"\.ts$": "typescript",
110
+ r"\.jsx$": "jsx",
111
+ r"\.tsx$": "tsx",
112
+ r"\.json$": "json",
113
+ r"\.yaml$|\.yml$": "yaml",
114
+ r"\.md$": "markdown",
115
+ r"\.sh$": "bash",
116
+ r"\.sql$": "sql",
117
+ r"\.css$": "css",
118
+ r"\.html$": "html",
119
+ r"\.xml$": "xml",
120
+ r"\.go$": "go",
121
+ r"\.rs$": "rust",
122
+ r"\.java$": "java",
123
+ r"\.cpp$|\.cc$|\.cxx$": "cpp",
124
+ r"\.c$": "c",
125
+ r"\.h$": "c",
126
+ r"\.php$": "php",
127
+ r"\.rb$": "ruby",
128
+ }
129
+
130
+ def detect_language(self, file_path: str) -> Optional[str]:
131
+ """
132
+ Detect programming language from file path.
133
+
134
+ Args:
135
+ file_path: Path to the file
136
+
137
+ Returns:
138
+ Detected language or None
139
+ """
140
+ for pattern, language in self.language_patterns.items():
141
+ if re.search(pattern, file_path, re.IGNORECASE):
142
+ return language
143
+ return None
144
+
145
+ def parse_unified_diff(self, diff_text: str) -> List[FileChange]:
146
+ """
147
+ Parse unified diff format into structured file changes.
148
+
149
+ Args:
150
+ diff_text: Raw unified diff text
151
+
152
+ Returns:
153
+ List of parsed file changes
154
+ """
155
+ file_changes = []
156
+ current_change = None
157
+
158
+ lines = diff_text.split("\n")
159
+ i = 0
160
+
161
+ while i < len(lines):
162
+ line = lines[i]
163
+
164
+ # File header detection
165
+ if line.startswith("diff --git"):
166
+ if current_change:
167
+ file_changes.append(current_change)
168
+
169
+ # Parse git diff header
170
+ parts = line.split()
171
+ if len(parts) >= 4:
172
+ old_file = parts[2][2:] # Remove 'a/' prefix
173
+ new_file = parts[3][2:] # Remove 'b/' prefix
174
+
175
+ current_change = FileChange(
176
+ file_path=new_file,
177
+ change_type=FileChangeType.MODIFIED,
178
+ old_path=old_file,
179
+ new_path=new_file,
180
+ language=self.detect_language(new_file),
181
+ )
182
+
183
+ elif line.startswith("new file mode"):
184
+ if current_change:
185
+ current_change.change_type = FileChangeType.ADDED
186
+
187
+ elif line.startswith("deleted file mode"):
188
+ if current_change:
189
+ current_change.change_type = FileChangeType.DELETED
190
+
191
+ elif line.startswith("rename from"):
192
+ if current_change:
193
+ current_change.change_type = FileChangeType.RENAMED
194
+ current_change.old_path = line[12:] # Remove 'rename from '
195
+
196
+ elif line.startswith("rename to"):
197
+ if current_change:
198
+ current_change.new_path = line[10:] # Remove 'rename to '
199
+ current_change.file_path = current_change.new_path
200
+
201
+ elif line.startswith("@@"):
202
+ # Hunk header - extract line numbers and changes
203
+ if current_change:
204
+ # Collect diff content for this hunk
205
+ diff_lines = [line]
206
+ i += 1
207
+
208
+ while (
209
+ i < len(lines)
210
+ and not lines[i].startswith("@@")
211
+ and not lines[i].startswith("diff")
212
+ ):
213
+ diff_line = lines[i]
214
+ diff_lines.append(diff_line)
215
+
216
+ # Count additions and deletions
217
+ if diff_line.startswith("+") and not diff_line.startswith(
218
+ "+++"
219
+ ):
220
+ current_change.lines_added += 1
221
+ elif diff_line.startswith("-") and not diff_line.startswith(
222
+ "---"
223
+ ):
224
+ current_change.lines_removed += 1
225
+
226
+ i += 1
227
+
228
+ # Store diff text for this file
229
+ if current_change.diff_text is None:
230
+ current_change.diff_text = "\n".join(diff_lines)
231
+ else:
232
+ current_change.diff_text += "\n" + "\n".join(diff_lines)
233
+
234
+ i -= 1 # Back up one line since we'll increment at end of loop
235
+
236
+ i += 1
237
+
238
+ # Add the last file change
239
+ if current_change:
240
+ file_changes.append(current_change)
241
+
242
+ return file_changes
243
+
244
+ def render_file_tree(self, file_changes: List[FileChange]) -> Tree:
245
+ """
246
+ Render a tree view of all changed files.
247
+
248
+ Args:
249
+ file_changes: List of file changes to display
250
+
251
+ Returns:
252
+ Rich Tree object with file changes
253
+ """
254
+ tree = Tree("📁 Changed Files", style="bold blue")
255
+
256
+ # Group files by directory
257
+ file_groups: Dict[str, List[FileChange]] = {}
258
+
259
+ for change in file_changes:
260
+ directory = str(Path(change.file_path).parent)
261
+ if directory == ".":
262
+ directory = "root"
263
+
264
+ if directory not in file_groups:
265
+ file_groups[directory] = []
266
+ file_groups[directory].append(change)
267
+
268
+ # Add each directory and its files
269
+ for directory, changes in sorted(file_groups.items()):
270
+ if len(file_groups) > 1:
271
+ dir_node = tree.add(f"📂 {directory}", style="cyan")
272
+ else:
273
+ dir_node = tree
274
+
275
+ for change in sorted(changes, key=lambda x: x.file_path):
276
+ # Choose icon based on change type
277
+ icon_map = {
278
+ FileChangeType.ADDED: "🆕",
279
+ FileChangeType.MODIFIED: "📝",
280
+ FileChangeType.DELETED: "🗑️",
281
+ FileChangeType.RENAMED: "🔄",
282
+ FileChangeType.COPIED: "📋",
283
+ }
284
+
285
+ icon = icon_map.get(change.change_type, "📄")
286
+
287
+ # Color based on change type
288
+ color_map = {
289
+ FileChangeType.ADDED: "green",
290
+ FileChangeType.MODIFIED: "yellow",
291
+ FileChangeType.DELETED: "red",
292
+ FileChangeType.RENAMED: "blue",
293
+ FileChangeType.COPIED: "cyan",
294
+ }
295
+
296
+ color = color_map.get(change.change_type, "white")
297
+
298
+ # File name with stats
299
+ file_name = Path(change.file_path).name
300
+ stats = (
301
+ f"(+{change.lines_added}/-{change.lines_removed})"
302
+ if change.lines_added or change.lines_removed
303
+ else ""
304
+ )
305
+
306
+ dir_node.add(f"{icon} {file_name} {stats}", style=color)
307
+
308
+ return tree
309
+
310
+ def render_unified_diff(self, file_change: FileChange) -> Panel:
311
+ """
312
+ Render a unified diff view for a single file.
313
+
314
+ Args:
315
+ file_change: File change to render
316
+
317
+ Returns:
318
+ Rich Panel containing the diff
319
+ """
320
+ if not file_change.diff_text:
321
+ return Panel("No changes to display", title=file_change.file_path)
322
+
323
+ # Apply syntax highlighting and diff coloring
324
+ diff_content = self.renderer.apply_diff_highlighting(
325
+ file_change.diff_text, file_type=file_change.language
326
+ )
327
+
328
+ # Create title with change type and stats
329
+ change_type_colors = {
330
+ FileChangeType.ADDED: "green",
331
+ FileChangeType.MODIFIED: "yellow",
332
+ FileChangeType.DELETED: "red",
333
+ FileChangeType.RENAMED: "blue",
334
+ FileChangeType.COPIED: "cyan",
335
+ }
336
+
337
+ title_color = change_type_colors.get(file_change.change_type, "white")
338
+ stats = f"(+{file_change.lines_added}/-{file_change.lines_removed})"
339
+
340
+ title = Text()
341
+ title.append(file_change.file_path, style=f"bold {title_color}")
342
+ title.append(f" {stats}", style="dim")
343
+
344
+ return Panel(diff_content, title=title, border_style=title_color, expand=False)
345
+
346
+ def render_side_by_side_diff(self, file_change: FileChange) -> Panel:
347
+ """
348
+ Render a side-by-side diff view for a single file.
349
+
350
+ Args:
351
+ file_change: File change to render
352
+
353
+ Returns:
354
+ Rich Panel containing the side-by-side diff
355
+ """
356
+ if not file_change.old_content or not file_change.new_content:
357
+ return self.render_unified_diff(file_change)
358
+
359
+ # Split content into lines
360
+ old_lines = file_change.old_content.split("\n")
361
+ new_lines = file_change.new_content.split("\n")
362
+
363
+ # Create side-by-side comparison
364
+ table = Table(
365
+ show_header=True,
366
+ header_style="bold",
367
+ box=SIMPLE,
368
+ expand=True,
369
+ min_width=self.renderer.get_terminal_width() - 4,
370
+ )
371
+
372
+ table.add_column("Old", style="red", width=50)
373
+ table.add_column("New", style="green", width=50)
374
+
375
+ # Use difflib for intelligent line matching
376
+ differ = difflib.SequenceMatcher(None, old_lines, new_lines)
377
+
378
+ for tag, i1, i2, j1, j2 in differ.get_opcodes():
379
+ if tag == "equal":
380
+ # Lines are identical
381
+ for i in range(i2 - i1):
382
+ old_line = old_lines[i1 + i] if i1 + i < len(old_lines) else ""
383
+ new_line = new_lines[j1 + i] if j1 + i < len(new_lines) else ""
384
+
385
+ table.add_row(
386
+ Text(old_line, style="dim"),
387
+ Text(new_line, style="dim"),
388
+ )
389
+
390
+ elif tag == "delete":
391
+ # Lines deleted from old
392
+ for i in range(i1, i2):
393
+ table.add_row(
394
+ Text(old_lines[i], style="red"), Text("", style="dim")
395
+ )
396
+
397
+ elif tag == "insert":
398
+ # Lines added to new
399
+ for j in range(j1, j2):
400
+ table.add_row(
401
+ Text("", style="dim"),
402
+ Text(new_lines[j], style="green"),
403
+ )
404
+
405
+ elif tag == "replace":
406
+ # Lines changed
407
+ old_chunk = old_lines[i1:i2]
408
+ new_chunk = new_lines[j1:j2]
409
+
410
+ # Pad shorter chunk with empty lines
411
+ max_len = max(len(old_chunk), len(new_chunk))
412
+
413
+ for i in range(max_len):
414
+ old_line = old_chunk[i] if i < len(old_chunk) else ""
415
+ new_line = new_chunk[i] if i < len(new_chunk) else ""
416
+
417
+ table.add_row(
418
+ Text(old_line, style="red" if old_line else "dim"),
419
+ Text(new_line, style="green" if new_line else "dim"),
420
+ )
421
+
422
+ # Create title
423
+ title = Text()
424
+ title.append(file_change.file_path, style="bold yellow")
425
+ title.append(
426
+ f" (+{file_change.lines_added}/-{file_change.lines_removed})",
427
+ style="dim",
428
+ )
429
+
430
+ return Panel(table, title=title, border_style="yellow", expand=False)
431
+
432
+ def render_word_diff(self, old_line: str, new_line: str) -> Tuple[Text, Text]:
433
+ """
434
+ Generate word-level diff highlighting for two lines.
435
+
436
+ Args:
437
+ old_line: Original line content
438
+ new_line: New line content
439
+
440
+ Returns:
441
+ Tuple of (old_formatted, new_formatted) Text objects
442
+ """
443
+ if not self.word_level_diff:
444
+ return Text(old_line), Text(new_line)
445
+
446
+ # Split into words for better diffing
447
+ old_words = re.findall(r"\S+|\s+", old_line)
448
+ new_words = re.findall(r"\S+|\s+", new_line)
449
+
450
+ differ = difflib.SequenceMatcher(None, old_words, new_words)
451
+
452
+ old_formatted = Text()
453
+ new_formatted = Text()
454
+
455
+ for tag, i1, i2, j1, j2 in differ.get_opcodes():
456
+ if tag == "equal":
457
+ # Words are identical
458
+ for i in range(i2 - i1):
459
+ word = old_words[i1 + i]
460
+ old_formatted.append(word)
461
+ new_formatted.append(word)
462
+
463
+ elif tag == "delete":
464
+ # Words deleted from old
465
+ deleted_text = "".join(old_words[i1:i2])
466
+ old_formatted.append(deleted_text, style="bold red on white")
467
+
468
+ elif tag == "insert":
469
+ # Words added to new
470
+ added_text = "".join(new_words[j1:j2])
471
+ new_formatted.append(added_text, style="bold green on white")
472
+
473
+ elif tag == "replace":
474
+ # Words changed
475
+ old_text = "".join(old_words[i1:i2])
476
+ new_text = "".join(new_words[j1:j2])
477
+
478
+ old_formatted.append(old_text, style="bold red on white")
479
+ new_formatted.append(new_text, style="bold green on white")
480
+
481
+ return old_formatted, new_formatted
482
+
483
+ def render_diff_summary(self, file_changes: List[FileChange]) -> Table:
484
+ """
485
+ Render a summary table of all file changes.
486
+
487
+ Args:
488
+ file_changes: List of file changes
489
+
490
+ Returns:
491
+ Rich Table with summary information
492
+ """
493
+ table = Table(
494
+ title="📊 Diff Summary",
495
+ show_header=True,
496
+ header_style="bold cyan",
497
+ box=ROUNDED,
498
+ )
499
+
500
+ table.add_column("File", style="white", width=40)
501
+ table.add_column("Change", style="yellow", justify="center")
502
+ table.add_column("Added", style="green", justify="right")
503
+ table.add_column("Removed", style="red", justify="right")
504
+ table.add_column("Language", style="blue", justify="center")
505
+
506
+ total_added = 0
507
+ total_removed = 0
508
+
509
+ for change in file_changes:
510
+ # Change type with icon
511
+ change_icons = {
512
+ FileChangeType.ADDED: "🆕 Added",
513
+ FileChangeType.MODIFIED: "📝 Modified",
514
+ FileChangeType.DELETED: "🗑️ Deleted",
515
+ FileChangeType.RENAMED: "🔄 Renamed",
516
+ FileChangeType.COPIED: "📋 Copied",
517
+ }
518
+
519
+ change_text = change_icons.get(change.change_type, "❓ Unknown")
520
+
521
+ table.add_row(
522
+ change.file_path,
523
+ change_text,
524
+ str(change.lines_added),
525
+ str(change.lines_removed),
526
+ change.language or "—",
527
+ )
528
+
529
+ total_added += change.lines_added
530
+ total_removed += change.lines_removed
531
+
532
+ # Add totals row
533
+ table.add_row("—" * 20, "—" * 10, "—" * 5, "—" * 5, "—" * 8, style="dim")
534
+
535
+ table.add_row(
536
+ Text("TOTALS", style="bold"),
537
+ f"{len(file_changes)} files",
538
+ Text(str(total_added), style="bold green"),
539
+ Text(str(total_removed), style="bold red"),
540
+ "—",
541
+ )
542
+
543
+ return table
544
+
545
+ def render_diff_set(
546
+ self,
547
+ diff_text: str,
548
+ diff_type: DiffType = DiffType.UNIFIED,
549
+ show_summary: bool = True,
550
+ show_file_tree: bool = True,
551
+ ) -> List[RenderableType]:
552
+ """
553
+ Render a complete diff set with multiple files.
554
+
555
+ Args:
556
+ diff_text: Raw diff text (unified diff format)
557
+ diff_type: Type of diff display to use
558
+ show_summary: Whether to show summary table
559
+ show_file_tree: Whether to show file tree
560
+
561
+ Returns:
562
+ List of Rich renderables
563
+ """
564
+ # Parse the diff
565
+ file_changes = self.parse_unified_diff(diff_text)
566
+
567
+ if not file_changes:
568
+ return [Panel("No changes detected", style="yellow")]
569
+
570
+ renderables = []
571
+
572
+ # Add summary table
573
+ if show_summary:
574
+ renderables.append(self.render_diff_summary(file_changes))
575
+ renderables.append(Rule(style="dim"))
576
+
577
+ # Add file tree
578
+ if show_file_tree:
579
+ renderables.append(self.render_file_tree(file_changes))
580
+ renderables.append(Rule(style="dim"))
581
+
582
+ # Render each file's diff
583
+ for change in file_changes:
584
+ if diff_type == DiffType.SIDE_BY_SIDE:
585
+ renderables.append(self.render_side_by_side_diff(change))
586
+ else: # Default to unified
587
+ renderables.append(self.render_unified_diff(change))
588
+
589
+ renderables.append(Rule(style="dim"))
590
+
591
+ return renderables
592
+
593
+ def render_file_content_comparison(
594
+ self,
595
+ old_content: str,
596
+ new_content: str,
597
+ file_path: str,
598
+ language: Optional[str] = None,
599
+ ) -> Panel:
600
+ """
601
+ Render a comparison between two complete file contents.
602
+
603
+ Args:
604
+ old_content: Original file content
605
+ new_content: New file content
606
+ file_path: Path to the file
607
+ language: Programming language for syntax highlighting
608
+
609
+ Returns:
610
+ Rich Panel with file comparison
611
+ """
612
+ # Auto-detect language if not provided
613
+ if not language:
614
+ language = self.detect_language(file_path)
615
+
616
+ # Create unified diff
617
+ diff_lines = list(
618
+ difflib.unified_diff(
619
+ old_content.split("\n"),
620
+ new_content.split("\n"),
621
+ fromfile=f"a/{file_path}",
622
+ tofile=f"b/{file_path}",
623
+ lineterm="",
624
+ )
625
+ )
626
+
627
+ diff_text = "\n".join(diff_lines)
628
+
629
+ # Apply syntax highlighting
630
+ highlighted_diff = self.renderer.apply_diff_highlighting(
631
+ diff_text, file_type=language
632
+ )
633
+
634
+ return Panel(
635
+ highlighted_diff,
636
+ title=f"📄 {file_path}",
637
+ border_style="blue",
638
+ expand=False,
639
+ )
640
+
641
+
642
+ # Utility functions for easy integration
643
+
644
+
645
+ def create_diff_renderer(
646
+ renderer: Optional[RichTextRenderer] = None,
647
+ ) -> EnhancedDiffRenderer:
648
+ """Create an enhanced diff renderer with default configuration."""
649
+ return EnhancedDiffRenderer(renderer=renderer)
650
+
651
+
652
+ def render_git_diff(
653
+ diff_output: str,
654
+ diff_type: DiffType = DiffType.UNIFIED,
655
+ console: Optional[Console] = None,
656
+ ) -> None:
657
+ """
658
+ Quick function to render git diff output.
659
+
660
+ Args:
661
+ diff_output: Raw git diff output
662
+ diff_type: Type of diff display
663
+ console: Rich console for output
664
+ """
665
+ renderer = create_diff_renderer()
666
+ console = console or renderer.console
667
+
668
+ renderables = renderer.render_diff_set(diff_output, diff_type)
669
+
670
+ for renderable in renderables:
671
+ console.print(renderable)
672
+
673
+
674
+ def compare_files(
675
+ old_file: str,
676
+ new_file: str,
677
+ file_path: str,
678
+ console: Optional[Console] = None,
679
+ ) -> None:
680
+ """
681
+ Compare two file contents and display the diff.
682
+
683
+ Args:
684
+ old_file: Path to old file or content string
685
+ new_file: Path to new file or content string
686
+ file_path: Display path for the file
687
+ console: Rich console for output
688
+ """
689
+ renderer = create_diff_renderer()
690
+ console = console or renderer.console
691
+
692
+ # Read file contents if they're paths
693
+ old_content = old_file
694
+ new_content = new_file
695
+
696
+ if Path(old_file).exists():
697
+ old_content = Path(old_file).read_text()
698
+
699
+ if Path(new_file).exists():
700
+ new_content = Path(new_file).read_text()
701
+
702
+ comparison = renderer.render_file_content_comparison(
703
+ old_content, new_content, file_path
704
+ )
705
+
706
+ console.print(comparison)