cognify-code 0.2.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 (55) hide show
  1. ai_code_assistant/__init__.py +14 -0
  2. ai_code_assistant/agent/__init__.py +63 -0
  3. ai_code_assistant/agent/code_agent.py +461 -0
  4. ai_code_assistant/agent/code_generator.py +388 -0
  5. ai_code_assistant/agent/code_reviewer.py +365 -0
  6. ai_code_assistant/agent/diff_engine.py +308 -0
  7. ai_code_assistant/agent/file_manager.py +300 -0
  8. ai_code_assistant/agent/intent_classifier.py +284 -0
  9. ai_code_assistant/chat/__init__.py +11 -0
  10. ai_code_assistant/chat/agent_session.py +156 -0
  11. ai_code_assistant/chat/session.py +165 -0
  12. ai_code_assistant/cli.py +1571 -0
  13. ai_code_assistant/config.py +149 -0
  14. ai_code_assistant/editor/__init__.py +8 -0
  15. ai_code_assistant/editor/diff_handler.py +270 -0
  16. ai_code_assistant/editor/file_editor.py +350 -0
  17. ai_code_assistant/editor/prompts.py +146 -0
  18. ai_code_assistant/generator/__init__.py +7 -0
  19. ai_code_assistant/generator/code_gen.py +265 -0
  20. ai_code_assistant/generator/prompts.py +114 -0
  21. ai_code_assistant/git/__init__.py +6 -0
  22. ai_code_assistant/git/commit_generator.py +130 -0
  23. ai_code_assistant/git/manager.py +203 -0
  24. ai_code_assistant/llm.py +111 -0
  25. ai_code_assistant/providers/__init__.py +23 -0
  26. ai_code_assistant/providers/base.py +124 -0
  27. ai_code_assistant/providers/cerebras.py +97 -0
  28. ai_code_assistant/providers/factory.py +148 -0
  29. ai_code_assistant/providers/google.py +103 -0
  30. ai_code_assistant/providers/groq.py +111 -0
  31. ai_code_assistant/providers/ollama.py +86 -0
  32. ai_code_assistant/providers/openai.py +114 -0
  33. ai_code_assistant/providers/openrouter.py +130 -0
  34. ai_code_assistant/py.typed +0 -0
  35. ai_code_assistant/refactor/__init__.py +20 -0
  36. ai_code_assistant/refactor/analyzer.py +189 -0
  37. ai_code_assistant/refactor/change_plan.py +172 -0
  38. ai_code_assistant/refactor/multi_file_editor.py +346 -0
  39. ai_code_assistant/refactor/prompts.py +175 -0
  40. ai_code_assistant/retrieval/__init__.py +19 -0
  41. ai_code_assistant/retrieval/chunker.py +215 -0
  42. ai_code_assistant/retrieval/indexer.py +236 -0
  43. ai_code_assistant/retrieval/search.py +239 -0
  44. ai_code_assistant/reviewer/__init__.py +7 -0
  45. ai_code_assistant/reviewer/analyzer.py +278 -0
  46. ai_code_assistant/reviewer/prompts.py +113 -0
  47. ai_code_assistant/utils/__init__.py +18 -0
  48. ai_code_assistant/utils/file_handler.py +155 -0
  49. ai_code_assistant/utils/formatters.py +259 -0
  50. cognify_code-0.2.0.dist-info/METADATA +383 -0
  51. cognify_code-0.2.0.dist-info/RECORD +55 -0
  52. cognify_code-0.2.0.dist-info/WHEEL +5 -0
  53. cognify_code-0.2.0.dist-info/entry_points.txt +3 -0
  54. cognify_code-0.2.0.dist-info/licenses/LICENSE +22 -0
  55. cognify_code-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,365 @@
1
+ """Code Reviewer for AI-powered code analysis."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import List, Optional
6
+
7
+ from ai_code_assistant.agent.file_manager import FileContextManager
8
+
9
+
10
+ class IssueSeverity(Enum):
11
+ """Severity levels for code issues."""
12
+ CRITICAL = "critical"
13
+ HIGH = "high"
14
+ MEDIUM = "medium"
15
+ LOW = "low"
16
+ INFO = "info"
17
+
18
+
19
+ class IssueCategory(Enum):
20
+ """Categories of code issues."""
21
+ SECURITY = "security"
22
+ BUG = "bug"
23
+ PERFORMANCE = "performance"
24
+ STYLE = "style"
25
+ MAINTAINABILITY = "maintainability"
26
+ BEST_PRACTICE = "best_practice"
27
+ ERROR_HANDLING = "error_handling"
28
+ DOCUMENTATION = "documentation"
29
+
30
+
31
+ @dataclass
32
+ class CodeIssue:
33
+ """Represents a code issue found during review."""
34
+ severity: IssueSeverity
35
+ category: IssueCategory
36
+ message: str
37
+ line_number: Optional[int] = None
38
+ line_content: str = ""
39
+ suggestion: str = ""
40
+ fixed_code: str = ""
41
+
42
+ @property
43
+ def severity_icon(self) -> str:
44
+ icons = {
45
+ IssueSeverity.CRITICAL: "🔴",
46
+ IssueSeverity.HIGH: "🟠",
47
+ IssueSeverity.MEDIUM: "��",
48
+ IssueSeverity.LOW: "🔵",
49
+ IssueSeverity.INFO: "⚪",
50
+ }
51
+ return icons.get(self.severity, "⚪")
52
+
53
+ def format(self) -> str:
54
+ """Format issue for display."""
55
+ lines = [
56
+ f"{self.severity_icon} [{self.severity.value.upper()}] {self.message}"
57
+ ]
58
+ if self.line_number:
59
+ lines.append(f" Line {self.line_number}: {self.line_content[:60]}")
60
+ if self.suggestion:
61
+ lines.append(f" → {self.suggestion}")
62
+ return "\n".join(lines)
63
+
64
+
65
+ @dataclass
66
+ class ReviewResult:
67
+ """Result of a code review."""
68
+ file_path: str
69
+ issues: List[CodeIssue] = field(default_factory=list)
70
+ summary: str = ""
71
+ score: int = 100 # 0-100 quality score
72
+ reviewed_lines: int = 0
73
+
74
+ @property
75
+ def critical_count(self) -> int:
76
+ return sum(1 for i in self.issues if i.severity == IssueSeverity.CRITICAL)
77
+
78
+ @property
79
+ def high_count(self) -> int:
80
+ return sum(1 for i in self.issues if i.severity == IssueSeverity.HIGH)
81
+
82
+ @property
83
+ def medium_count(self) -> int:
84
+ return sum(1 for i in self.issues if i.severity == IssueSeverity.MEDIUM)
85
+
86
+ @property
87
+ def low_count(self) -> int:
88
+ return sum(1 for i in self.issues if i.severity == IssueSeverity.LOW)
89
+
90
+ @property
91
+ def has_critical_issues(self) -> bool:
92
+ return self.critical_count > 0
93
+
94
+ def format_summary(self) -> str:
95
+ """Format review summary."""
96
+ lines = [
97
+ f"📋 Code Review: {self.file_path}",
98
+ f" Score: {self.score}/100",
99
+ f" Issues: 🔴{self.critical_count} 🟠{self.high_count} 🟡{self.medium_count} 🔵{self.low_count}",
100
+ "",
101
+ ]
102
+
103
+ if self.summary:
104
+ lines.append(f"Summary: {self.summary}")
105
+ lines.append("")
106
+
107
+ return "\n".join(lines)
108
+
109
+
110
+ REVIEW_PROMPT = '''You are an expert code reviewer. Analyze the following code and identify issues.
111
+
112
+ ## Code to Review
113
+ File: {file_path}
114
+ Language: {language}
115
+
116
+ ```{language}
117
+ {code}
118
+ ```
119
+
120
+ ## Review Focus Areas
121
+ 1. **Security**: SQL injection, XSS, authentication issues, secrets exposure
122
+ 2. **Bugs**: Logic errors, null references, race conditions, edge cases
123
+ 3. **Performance**: Inefficient algorithms, memory leaks, N+1 queries
124
+ 4. **Error Handling**: Missing try/catch, unhandled exceptions
125
+ 5. **Best Practices**: Code style, naming conventions, SOLID principles
126
+ 6. **Maintainability**: Code complexity, duplication, documentation
127
+
128
+ ## Output Format
129
+ For each issue found, provide:
130
+ - SEVERITY: critical/high/medium/low/info
131
+ - CATEGORY: security/bug/performance/style/maintainability/best_practice/error_handling/documentation
132
+ - LINE: line number (if applicable)
133
+ - MESSAGE: brief description of the issue
134
+ - SUGGESTION: how to fix it
135
+ - FIXED_CODE: corrected code snippet (if applicable)
136
+
137
+ Format each issue as:
138
+ ```
139
+ ISSUE:
140
+ SEVERITY: <severity>
141
+ CATEGORY: <category>
142
+ LINE: <number or "N/A">
143
+ MESSAGE: <description>
144
+ SUGGESTION: <fix recommendation>
145
+ FIXED_CODE:
146
+ <code if applicable>
147
+ END_ISSUE
148
+ ```
149
+
150
+ After all issues, provide:
151
+ ```
152
+ SUMMARY: <overall assessment>
153
+ SCORE: <0-100>
154
+ ```
155
+
156
+ If no issues found, respond with:
157
+ ```
158
+ SUMMARY: Code looks good! No significant issues found.
159
+ SCORE: 95
160
+ ```
161
+ '''
162
+
163
+
164
+ class CodeReviewer:
165
+ """Reviews code for issues and suggests improvements."""
166
+
167
+ def __init__(self, llm_manager, file_manager: Optional[FileContextManager] = None):
168
+ self.llm = llm_manager
169
+ self.file_manager = file_manager or FileContextManager()
170
+
171
+ def review_file(self, file_path: str,
172
+ focus_areas: Optional[List[IssueCategory]] = None) -> ReviewResult:
173
+ """Review a file for issues."""
174
+ content = self.file_manager.read_file(file_path)
175
+
176
+ if not content:
177
+ raise ValueError(f"Cannot read file: {file_path}")
178
+
179
+ return self.review_code(content, file_path, focus_areas)
180
+
181
+ def review_code(self, code: str, file_path: str = "code",
182
+ focus_areas: Optional[List[IssueCategory]] = None) -> ReviewResult:
183
+ """Review code content for issues."""
184
+ # Detect language
185
+ language = self._detect_language(file_path)
186
+
187
+ # Build prompt
188
+ prompt = REVIEW_PROMPT.format(
189
+ file_path=file_path,
190
+ language=language,
191
+ code=code[:8000], # Limit code size
192
+ )
193
+
194
+ # Get review from LLM
195
+ response = self.llm.invoke(prompt)
196
+
197
+ # Parse response
198
+ result = self._parse_review_response(response, file_path, code)
199
+
200
+ # Filter by focus areas if specified
201
+ if focus_areas:
202
+ result.issues = [i for i in result.issues if i.category in focus_areas]
203
+
204
+ return result
205
+
206
+ def quick_review(self, file_path: str) -> str:
207
+ """Get a quick review summary."""
208
+ result = self.review_file(file_path)
209
+ return result.format_summary()
210
+
211
+ def security_review(self, file_path: str) -> ReviewResult:
212
+ """Focus on security issues only."""
213
+ return self.review_file(file_path, focus_areas=[IssueCategory.SECURITY])
214
+
215
+ def get_fix_suggestions(self, file_path: str) -> List[dict]:
216
+ """Get actionable fix suggestions for a file."""
217
+ result = self.review_file(file_path)
218
+
219
+ suggestions = []
220
+ for issue in result.issues:
221
+ if issue.fixed_code:
222
+ suggestions.append({
223
+ "line": issue.line_number,
224
+ "original": issue.line_content,
225
+ "fixed": issue.fixed_code,
226
+ "reason": issue.message,
227
+ })
228
+
229
+ return suggestions
230
+
231
+ def _detect_language(self, file_path: str) -> str:
232
+ """Detect language from file extension."""
233
+ ext_map = {
234
+ ".py": "python",
235
+ ".js": "javascript",
236
+ ".ts": "typescript",
237
+ ".jsx": "javascript",
238
+ ".tsx": "typescript",
239
+ ".java": "java",
240
+ ".go": "go",
241
+ ".rs": "rust",
242
+ ".rb": "ruby",
243
+ ".php": "php",
244
+ ".c": "c",
245
+ ".cpp": "cpp",
246
+ ".cs": "csharp",
247
+ }
248
+
249
+ for ext, lang in ext_map.items():
250
+ if file_path.endswith(ext):
251
+ return lang
252
+
253
+ return "text"
254
+
255
+ def _parse_review_response(self, response: str, file_path: str, code: str) -> ReviewResult:
256
+ """Parse LLM response into ReviewResult."""
257
+ result = ReviewResult(
258
+ file_path=file_path,
259
+ reviewed_lines=code.count("\n") + 1,
260
+ )
261
+
262
+ # Parse issues
263
+ import re
264
+
265
+ issue_pattern = r"ISSUE:\s*\n(.*?)END_ISSUE"
266
+ issue_matches = re.findall(issue_pattern, response, re.DOTALL)
267
+
268
+ for issue_text in issue_matches:
269
+ issue = self._parse_issue(issue_text, code)
270
+ if issue:
271
+ result.issues.append(issue)
272
+
273
+ # Parse summary and score
274
+ summary_match = re.search(r"SUMMARY:\s*(.+?)(?:\n|$)", response)
275
+ if summary_match:
276
+ result.summary = summary_match.group(1).strip()
277
+
278
+ score_match = re.search(r"SCORE:\s*(\d+)", response)
279
+ if score_match:
280
+ result.score = min(100, max(0, int(score_match.group(1))))
281
+ else:
282
+ # Calculate score based on issues
283
+ result.score = self._calculate_score(result.issues)
284
+
285
+ return result
286
+
287
+ def _parse_issue(self, issue_text: str, code: str) -> Optional[CodeIssue]:
288
+ """Parse a single issue from text."""
289
+ import re
290
+
291
+ # Extract fields
292
+ severity_match = re.search(r"SEVERITY:\s*(\w+)", issue_text, re.IGNORECASE)
293
+ category_match = re.search(r"CATEGORY:\s*(\w+)", issue_text, re.IGNORECASE)
294
+ line_match = re.search(r"LINE:\s*(\d+|N/A)", issue_text, re.IGNORECASE)
295
+ message_match = re.search(r"MESSAGE:\s*(.+?)(?:\n|SUGGESTION)", issue_text, re.DOTALL)
296
+ suggestion_match = re.search(r"SUGGESTION:\s*(.+?)(?:\n|FIXED_CODE|$)", issue_text, re.DOTALL)
297
+ fixed_match = re.search(r"FIXED_CODE:\s*\n?(.*?)(?:END_ISSUE|$)", issue_text, re.DOTALL)
298
+
299
+ if not message_match:
300
+ return None
301
+
302
+ # Map severity
303
+ severity_map = {
304
+ "critical": IssueSeverity.CRITICAL,
305
+ "high": IssueSeverity.HIGH,
306
+ "medium": IssueSeverity.MEDIUM,
307
+ "low": IssueSeverity.LOW,
308
+ "info": IssueSeverity.INFO,
309
+ }
310
+ severity = IssueSeverity.MEDIUM
311
+ if severity_match:
312
+ severity = severity_map.get(severity_match.group(1).lower(), IssueSeverity.MEDIUM)
313
+
314
+ # Map category
315
+ category_map = {
316
+ "security": IssueCategory.SECURITY,
317
+ "bug": IssueCategory.BUG,
318
+ "performance": IssueCategory.PERFORMANCE,
319
+ "style": IssueCategory.STYLE,
320
+ "maintainability": IssueCategory.MAINTAINABILITY,
321
+ "best_practice": IssueCategory.BEST_PRACTICE,
322
+ "error_handling": IssueCategory.ERROR_HANDLING,
323
+ "documentation": IssueCategory.DOCUMENTATION,
324
+ }
325
+ category = IssueCategory.BEST_PRACTICE
326
+ if category_match:
327
+ category = category_map.get(category_match.group(1).lower(), IssueCategory.BEST_PRACTICE)
328
+
329
+ # Get line number and content
330
+ line_number = None
331
+ line_content = ""
332
+ if line_match and line_match.group(1) != "N/A":
333
+ try:
334
+ line_number = int(line_match.group(1))
335
+ code_lines = code.split("\n")
336
+ if 0 < line_number <= len(code_lines):
337
+ line_content = code_lines[line_number - 1].strip()
338
+ except ValueError:
339
+ pass
340
+
341
+ return CodeIssue(
342
+ severity=severity,
343
+ category=category,
344
+ message=message_match.group(1).strip() if message_match else "",
345
+ line_number=line_number,
346
+ line_content=line_content,
347
+ suggestion=suggestion_match.group(1).strip() if suggestion_match else "",
348
+ fixed_code=fixed_match.group(1).strip() if fixed_match else "",
349
+ )
350
+
351
+ def _calculate_score(self, issues: List[CodeIssue]) -> int:
352
+ """Calculate quality score based on issues."""
353
+ score = 100
354
+
355
+ for issue in issues:
356
+ if issue.severity == IssueSeverity.CRITICAL:
357
+ score -= 25
358
+ elif issue.severity == IssueSeverity.HIGH:
359
+ score -= 15
360
+ elif issue.severity == IssueSeverity.MEDIUM:
361
+ score -= 8
362
+ elif issue.severity == IssueSeverity.LOW:
363
+ score -= 3
364
+
365
+ return max(0, score)
@@ -0,0 +1,308 @@
1
+ """Diff Engine for showing and applying code changes."""
2
+
3
+ import difflib
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+ from typing import List, Optional, Tuple
7
+
8
+
9
+ class ChangeType(Enum):
10
+ """Type of file change."""
11
+ CREATE = "create"
12
+ MODIFY = "modify"
13
+ DELETE = "delete"
14
+ RENAME = "rename"
15
+
16
+
17
+ @dataclass
18
+ class FileDiff:
19
+ """Represents a diff for a single file."""
20
+ file_path: str
21
+ change_type: ChangeType
22
+ original_content: str = ""
23
+ new_content: str = ""
24
+ diff_lines: List[str] = field(default_factory=list)
25
+ additions: int = 0
26
+ deletions: int = 0
27
+
28
+ @property
29
+ def has_changes(self) -> bool:
30
+ return self.original_content != self.new_content
31
+
32
+ @property
33
+ def summary(self) -> str:
34
+ if self.change_type == ChangeType.CREATE:
35
+ return f"📄 {self.file_path} (new file, +{self.additions} lines)"
36
+ elif self.change_type == ChangeType.DELETE:
37
+ return f"🗑️ {self.file_path} (deleted, -{self.deletions} lines)"
38
+ else:
39
+ return f"📝 {self.file_path} (+{self.additions}/-{self.deletions})"
40
+
41
+
42
+ @dataclass
43
+ class ChangeSet:
44
+ """A set of file changes to apply."""
45
+ description: str
46
+ diffs: List[FileDiff] = field(default_factory=list)
47
+
48
+ @property
49
+ def total_additions(self) -> int:
50
+ return sum(d.additions for d in self.diffs)
51
+
52
+ @property
53
+ def total_deletions(self) -> int:
54
+ return sum(d.deletions for d in self.diffs)
55
+
56
+ @property
57
+ def files_changed(self) -> int:
58
+ return len(self.diffs)
59
+
60
+ def add_file_change(self, file_path: str, new_content: str,
61
+ original_content: str = "") -> FileDiff:
62
+ """Add a file change to the set."""
63
+ if not original_content and new_content:
64
+ change_type = ChangeType.CREATE
65
+ elif original_content and not new_content:
66
+ change_type = ChangeType.DELETE
67
+ else:
68
+ change_type = ChangeType.MODIFY
69
+
70
+ diff = FileDiff(
71
+ file_path=file_path,
72
+ change_type=change_type,
73
+ original_content=original_content,
74
+ new_content=new_content,
75
+ )
76
+
77
+ self.diffs.append(diff)
78
+ return diff
79
+
80
+
81
+ class DiffEngine:
82
+ """Engine for creating and applying diffs."""
83
+
84
+ def __init__(self, file_manager=None):
85
+ self.file_manager = file_manager
86
+
87
+ def create_diff(self, original: str, modified: str,
88
+ file_path: str = "file") -> FileDiff:
89
+ """Create a diff between original and modified content."""
90
+ original_lines = original.splitlines(keepends=True)
91
+ modified_lines = modified.splitlines(keepends=True)
92
+
93
+ # Generate unified diff
94
+ diff_lines = list(difflib.unified_diff(
95
+ original_lines,
96
+ modified_lines,
97
+ fromfile=f"a/{file_path}",
98
+ tofile=f"b/{file_path}",
99
+ lineterm="",
100
+ ))
101
+
102
+ # Count additions and deletions
103
+ additions = sum(1 for line in diff_lines if line.startswith("+") and not line.startswith("+++"))
104
+ deletions = sum(1 for line in diff_lines if line.startswith("-") and not line.startswith("---"))
105
+
106
+ # Determine change type
107
+ if not original and modified:
108
+ change_type = ChangeType.CREATE
109
+ elif original and not modified:
110
+ change_type = ChangeType.DELETE
111
+ else:
112
+ change_type = ChangeType.MODIFY
113
+
114
+ return FileDiff(
115
+ file_path=file_path,
116
+ change_type=change_type,
117
+ original_content=original,
118
+ new_content=modified,
119
+ diff_lines=diff_lines,
120
+ additions=additions,
121
+ deletions=deletions,
122
+ )
123
+
124
+ def create_file_diff(self, file_path: str, new_content: str) -> FileDiff:
125
+ """Create a diff for a file, reading original from disk."""
126
+ original = ""
127
+ if self.file_manager:
128
+ original = self.file_manager.read_file(file_path) or ""
129
+
130
+ return self.create_diff(original, new_content, file_path)
131
+
132
+ def format_diff(self, diff: FileDiff, color: bool = True,
133
+ context_lines: int = 3) -> str:
134
+ """Format a diff for display."""
135
+ if not diff.diff_lines:
136
+ # Generate diff lines if not present
137
+ diff = self.create_diff(
138
+ diff.original_content,
139
+ diff.new_content,
140
+ diff.file_path
141
+ )
142
+
143
+ lines = []
144
+
145
+ # Header
146
+ if diff.change_type == ChangeType.CREATE:
147
+ header = f"📄 New file: {diff.file_path}"
148
+ elif diff.change_type == ChangeType.DELETE:
149
+ header = f"🗑️ Delete: {diff.file_path}"
150
+ else:
151
+ header = f"📝 Modified: {diff.file_path}"
152
+
153
+ lines.append(header)
154
+ lines.append("─" * 60)
155
+
156
+ # Diff content
157
+ for line in diff.diff_lines:
158
+ if color:
159
+ if line.startswith("+") and not line.startswith("+++"):
160
+ lines.append(f"[green]{line}[/green]")
161
+ elif line.startswith("-") and not line.startswith("---"):
162
+ lines.append(f"[red]{line}[/red]")
163
+ elif line.startswith("@@"):
164
+ lines.append(f"[cyan]{line}[/cyan]")
165
+ else:
166
+ lines.append(line)
167
+ else:
168
+ lines.append(line)
169
+
170
+ # Summary
171
+ lines.append("─" * 60)
172
+ lines.append(f"+{diff.additions} additions, -{diff.deletions} deletions")
173
+
174
+ return "\n".join(lines)
175
+
176
+ def format_diff_simple(self, diff: FileDiff) -> str:
177
+ """Format diff in a simple, readable way."""
178
+ lines = []
179
+
180
+ if diff.change_type == ChangeType.CREATE:
181
+ lines.append(f"📄 {diff.file_path} (new file)")
182
+ lines.append("┌" + "─" * 58 + "┐")
183
+ for i, line in enumerate(diff.new_content.split("\n")[:30]):
184
+ lines.append(f"│ {line[:56]:<56} │")
185
+ if diff.new_content.count("\n") > 30:
186
+ lines.append(f"│ {'... (truncated)':<56} │")
187
+ lines.append("└" + "─" * 58 + "┘")
188
+ else:
189
+ lines.append(f"📝 {diff.file_path}")
190
+ lines.append("─" * 60)
191
+
192
+ # Show only changed sections
193
+ for line in diff.diff_lines[:50]:
194
+ if line.startswith("+") and not line.startswith("+++"):
195
+ lines.append(f"[green]+ {line[1:]}[/green]")
196
+ elif line.startswith("-") and not line.startswith("---"):
197
+ lines.append(f"[red]- {line[1:]}[/red]")
198
+ elif line.startswith("@@"):
199
+ lines.append(f"[dim]{line}[/dim]")
200
+
201
+ if len(diff.diff_lines) > 50:
202
+ lines.append("[dim]... (more changes)[/dim]")
203
+
204
+ return "\n".join(lines)
205
+
206
+ def format_changeset(self, changeset: ChangeSet, detailed: bool = False) -> str:
207
+ """Format a complete changeset for display."""
208
+ lines = [
209
+ f"📦 {changeset.description}",
210
+ f" {changeset.files_changed} file(s), "
211
+ f"+{changeset.total_additions}/-{changeset.total_deletions}",
212
+ "",
213
+ ]
214
+
215
+ for diff in changeset.diffs:
216
+ if detailed:
217
+ lines.append(self.format_diff_simple(diff))
218
+ else:
219
+ lines.append(f" {diff.summary}")
220
+ lines.append("")
221
+
222
+ return "\n".join(lines)
223
+
224
+ def apply_diff(self, diff: FileDiff) -> bool:
225
+ """Apply a diff to the filesystem."""
226
+ if not self.file_manager:
227
+ raise RuntimeError("No file manager configured")
228
+
229
+ if diff.change_type == ChangeType.DELETE:
230
+ return self.file_manager.delete_file(diff.file_path)
231
+ else:
232
+ return self.file_manager.write_file(diff.file_path, diff.new_content)
233
+
234
+ def apply_changeset(self, changeset: ChangeSet) -> Tuple[int, int]:
235
+ """Apply all changes in a changeset.
236
+
237
+ Returns:
238
+ Tuple of (successful, failed) counts
239
+ """
240
+ successful = 0
241
+ failed = 0
242
+
243
+ for diff in changeset.diffs:
244
+ try:
245
+ if self.apply_diff(diff):
246
+ successful += 1
247
+ else:
248
+ failed += 1
249
+ except Exception:
250
+ failed += 1
251
+
252
+ return successful, failed
253
+
254
+ def preview_changes(self, changeset: ChangeSet) -> str:
255
+ """Generate a preview of changes to be applied."""
256
+ lines = [
257
+ "┌" + "─" * 58 + "┐",
258
+ f"│ {'CHANGE PREVIEW':^56} │",
259
+ "├" + "─" * 58 + "┤",
260
+ ]
261
+
262
+ for diff in changeset.diffs:
263
+ icon = {
264
+ ChangeType.CREATE: "📄",
265
+ ChangeType.MODIFY: "📝",
266
+ ChangeType.DELETE: "🗑️ ",
267
+ }.get(diff.change_type, "📋")
268
+
269
+ action = {
270
+ ChangeType.CREATE: "CREATE",
271
+ ChangeType.MODIFY: "MODIFY",
272
+ ChangeType.DELETE: "DELETE",
273
+ }.get(diff.change_type, "CHANGE")
274
+
275
+ path_display = diff.file_path[:40]
276
+ if len(diff.file_path) > 40:
277
+ path_display = "..." + diff.file_path[-37:]
278
+
279
+ lines.append(f"│ {icon} {action:<8} {path_display:<44} │")
280
+ lines.append(f"│ {'+' + str(diff.additions):<6} {'-' + str(diff.deletions):<6} {'':38} │")
281
+
282
+ lines.append("├" + "─" * 58 + "┤")
283
+ lines.append(f"│ Total: {changeset.files_changed} files, "
284
+ f"+{changeset.total_additions}/-{changeset.total_deletions}{'':20} │")
285
+ lines.append("└" + "─" * 58 + "┘")
286
+
287
+ return "\n".join(lines)
288
+
289
+ def get_inline_diff(self, original: str, modified: str) -> List[Tuple[str, str]]:
290
+ """Get inline diff showing word-level changes.
291
+
292
+ Returns list of (type, text) tuples where type is 'equal', 'insert', or 'delete'.
293
+ """
294
+ matcher = difflib.SequenceMatcher(None, original.split(), modified.split())
295
+ result = []
296
+
297
+ for tag, i1, i2, j1, j2 in matcher.get_opcodes():
298
+ if tag == "equal":
299
+ result.append(("equal", " ".join(original.split()[i1:i2])))
300
+ elif tag == "replace":
301
+ result.append(("delete", " ".join(original.split()[i1:i2])))
302
+ result.append(("insert", " ".join(modified.split()[j1:j2])))
303
+ elif tag == "delete":
304
+ result.append(("delete", " ".join(original.split()[i1:i2])))
305
+ elif tag == "insert":
306
+ result.append(("insert", " ".join(modified.split()[j1:j2])))
307
+
308
+ return result