auto-coder 0.1.316__py3-none-any.whl → 0.1.318__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.

Potentially problematic release.


This version of auto-coder might be problematic. Click here for more details.

Files changed (41) hide show
  1. {auto_coder-0.1.316.dist-info → auto_coder-0.1.318.dist-info}/METADATA +2 -2
  2. {auto_coder-0.1.316.dist-info → auto_coder-0.1.318.dist-info}/RECORD +41 -20
  3. autocoder/auto_coder_runner.py +1 -2
  4. autocoder/common/__init__.py +3 -0
  5. autocoder/common/auto_coder_lang.py +24 -0
  6. autocoder/common/code_auto_merge_editblock.py +2 -42
  7. autocoder/common/git_utils.py +2 -2
  8. autocoder/common/token_cost_caculate.py +103 -42
  9. autocoder/common/v2/__init__.py +0 -0
  10. autocoder/common/v2/code_auto_generate.py +199 -0
  11. autocoder/common/v2/code_auto_generate_diff.py +361 -0
  12. autocoder/common/v2/code_auto_generate_editblock.py +380 -0
  13. autocoder/common/v2/code_auto_generate_strict_diff.py +269 -0
  14. autocoder/common/v2/code_auto_merge.py +211 -0
  15. autocoder/common/v2/code_auto_merge_diff.py +354 -0
  16. autocoder/common/v2/code_auto_merge_editblock.py +523 -0
  17. autocoder/common/v2/code_auto_merge_strict_diff.py +259 -0
  18. autocoder/common/v2/code_diff_manager.py +266 -0
  19. autocoder/common/v2/code_editblock_manager.py +282 -0
  20. autocoder/common/v2/code_manager.py +238 -0
  21. autocoder/common/v2/code_strict_diff_manager.py +241 -0
  22. autocoder/dispacher/actions/action.py +16 -0
  23. autocoder/dispacher/actions/plugins/action_regex_project.py +6 -0
  24. autocoder/events/event_manager_singleton.py +2 -2
  25. autocoder/helper/__init__.py +0 -0
  26. autocoder/helper/project_creator.py +570 -0
  27. autocoder/linters/linter_factory.py +44 -25
  28. autocoder/linters/models.py +220 -0
  29. autocoder/linters/python_linter.py +1 -7
  30. autocoder/linters/reactjs_linter.py +580 -0
  31. autocoder/linters/shadow_linter.py +390 -0
  32. autocoder/linters/vue_linter.py +576 -0
  33. autocoder/memory/active_context_manager.py +0 -4
  34. autocoder/memory/active_package.py +12 -12
  35. autocoder/shadows/__init__.py +0 -0
  36. autocoder/shadows/shadow_manager.py +235 -0
  37. autocoder/version.py +1 -1
  38. {auto_coder-0.1.316.dist-info → auto_coder-0.1.318.dist-info}/LICENSE +0 -0
  39. {auto_coder-0.1.316.dist-info → auto_coder-0.1.318.dist-info}/WHEEL +0 -0
  40. {auto_coder-0.1.316.dist-info → auto_coder-0.1.318.dist-info}/entry_points.txt +0 -0
  41. {auto_coder-0.1.316.dist-info → auto_coder-0.1.318.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,211 @@
1
+ import os
2
+ from byzerllm.utils.client import code_utils
3
+ from autocoder.common import AutoCoderArgs, git_utils, SourceCodeList, SourceCode
4
+ from autocoder.common.action_yml_file_manager import ActionYmlFileManager
5
+ from autocoder.common.text import TextSimilarity
6
+ from autocoder.memory.active_context_manager import ActiveContextManager
7
+ import pydantic
8
+ import byzerllm
9
+
10
+ import hashlib
11
+ import subprocess
12
+ import tempfile
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+ from rich.syntax import Syntax
16
+ import json
17
+ from typing import Union, List, Tuple, Dict
18
+ from autocoder.common.types import CodeGenerateResult, MergeCodeWithoutEffect
19
+ from autocoder.common.code_modification_ranker import CodeModificationRanker
20
+ from autocoder.common import files as FileUtils
21
+ from autocoder.common.printer import Printer
22
+ from autocoder.shadows.shadow_manager import ShadowManager
23
+
24
+ class PathAndCode(pydantic.BaseModel):
25
+ path: str
26
+ content: str
27
+
28
+
29
+ class CodeAutoMerge:
30
+ def __init__(
31
+ self,
32
+ llm: byzerllm.ByzerLLM,
33
+ args: AutoCoderArgs,
34
+ ):
35
+ self.llm = llm
36
+ self.args = args
37
+ self.printer = Printer()
38
+
39
+ def run_pylint(self, code: str) -> tuple[bool, str]:
40
+ with tempfile.NamedTemporaryFile(
41
+ mode="w", suffix=".py", delete=False
42
+ ) as temp_file:
43
+ temp_file.write(code)
44
+ temp_file_path = temp_file.name
45
+
46
+ try:
47
+ result = subprocess.run(
48
+ [
49
+ "pylint",
50
+ "--disable=all",
51
+ "--enable=E0001,W0311,W0312",
52
+ temp_file_path,
53
+ ],
54
+ capture_output=True,
55
+ text=True,
56
+ check=False,
57
+ )
58
+ os.unlink(temp_file_path)
59
+ if result.returncode != 0:
60
+ error_message = result.stdout.strip() or result.stderr.strip()
61
+ self.printer.print_in_terminal("pylint_check_failed", error_message=error_message)
62
+ return False, error_message
63
+ return True, ""
64
+ except subprocess.CalledProcessError as e:
65
+ error_message = f"Error running pylint: {str(e)}"
66
+ self.printer.print_in_terminal("pylint_error", error_message=error_message)
67
+ os.unlink(temp_file_path)
68
+ return False, error_message
69
+
70
+ def get_source_code_list_from_shadow_files(self, shadow_files: Dict[str, str]) -> SourceCodeList:
71
+ """
72
+ 将影子文件转换为SourceCodeList对象
73
+
74
+ 参数:
75
+ shadow_files (Dict[str, str]): 映射 {影子文件路径: 内容}
76
+
77
+ 返回:
78
+ SourceCodeList: 包含原始路径和内容的SourceCodeList对象
79
+ """
80
+ sources = []
81
+ shadow_manager = ShadowManager(self.args.source_dir,event_file_id=self.args.event_file)
82
+ for shadow_path, content in shadow_files.items():
83
+ # 将影子路径转换回原始文件路径
84
+ file_path = shadow_manager.from_shadow_path(shadow_path)
85
+ # 创建SourceCode对象并添加到sources列表
86
+ source = SourceCode(module_name=file_path, source_code=content)
87
+ sources.append(source)
88
+
89
+ return SourceCodeList(sources)
90
+
91
+ def choose_best_choice(self, generate_result: CodeGenerateResult) -> CodeGenerateResult:
92
+ if len(generate_result.contents) == 1:
93
+ return generate_result
94
+
95
+ merge_results = []
96
+ for content,conversations in zip(generate_result.contents,generate_result.conversations):
97
+ merge_result = self._merge_code_without_effect(content)
98
+ merge_results.append(merge_result)
99
+
100
+ # If all merge results are None, return first one
101
+ if all(len(result.failed_blocks) != 0 for result in merge_results):
102
+ self.printer.print_in_terminal("all_merge_results_failed")
103
+ return CodeGenerateResult(contents=[generate_result.contents[0]], conversations=[generate_result.conversations[0]])
104
+
105
+ # If only one merge result is not None, return that one
106
+ not_none_indices = [i for i, result in enumerate(merge_results) if len(result.failed_blocks) == 0]
107
+ if len(not_none_indices) == 1:
108
+ idx = not_none_indices[0]
109
+ self.printer.print_in_terminal("only_one_merge_result_success")
110
+ return CodeGenerateResult(contents=[generate_result.contents[idx]], conversations=[generate_result.conversations[idx]])
111
+
112
+ # 最后,如果有多个,那么根据质量排序再返回
113
+ ranker = CodeModificationRanker(self.llm, self.args)
114
+ ranked_result = ranker.rank_modifications(generate_result,merge_results)
115
+
116
+ ## 得到的结果,再做一次合并,第一个通过的返回 , 返回做合并有点重复低效,未来修改。
117
+ for content,conversations in zip(ranked_result.contents,ranked_result.conversations):
118
+ merge_result = self._merge_code_without_effect(content)
119
+ if not merge_result.failed_blocks:
120
+ return CodeGenerateResult(contents=[content], conversations=[conversations])
121
+
122
+ # 最后保底,但实际不会出现
123
+ return CodeGenerateResult(contents=[ranked_result.contents[0]], conversations=[ranked_result.conversations[0]])
124
+
125
+ @byzerllm.prompt(render="jinja2")
126
+ def git_require_msg(self,source_dir:str,error:str)->str:
127
+ '''
128
+ auto_merge only works for git repositories.
129
+
130
+ Try to use git init in the source directory.
131
+
132
+ ```shell
133
+ cd {{ source_dir }}
134
+ git init .
135
+ ```
136
+
137
+ Then try to run auto-coder again.
138
+ Error: {{ error }}
139
+ '''
140
+
141
+ def _merge_code(self, content: str, force_skip_git: bool = False):
142
+ file_content = FileUtils.read_file(self.args.file)
143
+ md5 = hashlib.md5(file_content.encode("utf-8")).hexdigest()
144
+ file_name = os.path.basename(self.args.file)
145
+
146
+ if not force_skip_git and not self.args.skip_commit:
147
+ try:
148
+ git_utils.commit_changes(
149
+ self.args.source_dir, f"auto_coder_pre_{file_name}_{md5}"
150
+ )
151
+ except Exception as e:
152
+ self.printer.print_str_in_terminal(
153
+ self.git_require_msg(source_dir=self.args.source_dir, error=str(e)),
154
+ style="red"
155
+ )
156
+ return
157
+
158
+ merge_result = self._merge_code_without_effect(content)
159
+ if not merge_result.success_blocks:
160
+ self.printer.print_in_terminal("no_changes_to_merge")
161
+ return
162
+
163
+ # Apply changes
164
+ for file_path, new_content in merge_result.success_blocks:
165
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
166
+ with open(file_path, "w") as f:
167
+ f.write(new_content)
168
+
169
+ if not force_skip_git and not self.args.skip_commit:
170
+ try:
171
+ commit_result = git_utils.commit_changes(
172
+ self.args.source_dir,
173
+ f"{self.args.query}\nauto_coder_{file_name}",
174
+ )
175
+
176
+ action_yml_file_manager = ActionYmlFileManager(self.args.source_dir)
177
+ action_file_name = os.path.basename(self.args.file)
178
+ add_updated_urls = []
179
+ commit_result.changed_files
180
+ for file in commit_result.changed_files:
181
+ add_updated_urls.append(os.path.join(self.args.source_dir, file))
182
+
183
+ self.args.add_updated_urls = add_updated_urls
184
+ update_yaml_success = action_yml_file_manager.update_yaml_field(action_file_name, "add_updated_urls", add_updated_urls)
185
+ if not update_yaml_success:
186
+ self.printer.print_in_terminal("yaml_save_error", style="red", yaml_file=action_file_name)
187
+
188
+ if self.args.enable_active_context:
189
+ active_context_manager = ActiveContextManager(self.llm, self.args.source_dir)
190
+ task_id = active_context_manager.process_changes(self.args)
191
+ self.printer.print_in_terminal("active_context_background_task",
192
+ style="blue",
193
+ task_id=task_id)
194
+ git_utils.print_commit_info(commit_result=commit_result)
195
+ except Exception as e:
196
+ self.printer.print_str_in_terminal(
197
+ self.git_require_msg(source_dir=self.args.source_dir, error=str(e)),
198
+ style="red"
199
+ )
200
+ else:
201
+ self.print_merged_blocks(merge_result.success_blocks)
202
+
203
+ self.printer.print_in_terminal("merge_success",
204
+ num_files=len(merge_result.success_blocks),
205
+ num_changes=len(merge_result.success_blocks),
206
+ total_blocks=len(merge_result.success_blocks) + len(merge_result.failed_blocks))
207
+
208
+ def merge_code(self, generate_result: CodeGenerateResult, force_skip_git: bool = False):
209
+ result = self.choose_best_choice(generate_result)
210
+ self._merge_code(result.contents[0], force_skip_git)
211
+ return result
@@ -0,0 +1,354 @@
1
+ import os
2
+ from byzerllm.utils.client import code_utils
3
+ from autocoder.common import AutoCoderArgs, git_utils, SourceCodeList, SourceCode
4
+ from autocoder.common.action_yml_file_manager import ActionYmlFileManager
5
+ from autocoder.common.text import TextSimilarity
6
+ from autocoder.memory.active_context_manager import ActiveContextManager
7
+ import pydantic
8
+ import byzerllm
9
+
10
+ import hashlib
11
+ import subprocess
12
+ import tempfile
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+ from rich.syntax import Syntax
16
+ import json
17
+ from typing import Union, List, Tuple, Dict
18
+ from autocoder.common.types import CodeGenerateResult, MergeCodeWithoutEffect
19
+ from autocoder.common.code_modification_ranker import CodeModificationRanker
20
+ from autocoder.common import files as FileUtils
21
+ from autocoder.common.printer import Printer
22
+ from autocoder.shadows.shadow_manager import ShadowManager
23
+
24
+ class PathAndCode(pydantic.BaseModel):
25
+ path: str
26
+ content: str
27
+
28
+
29
+ class CodeAutoMergeDiff:
30
+ def __init__(
31
+ self,
32
+ llm: byzerllm.ByzerLLM,
33
+ args: AutoCoderArgs,
34
+ ):
35
+ self.llm = llm
36
+ self.args = args
37
+ self.printer = Printer()
38
+
39
+ def run_pylint(self, code: str) -> tuple[bool, str]:
40
+ with tempfile.NamedTemporaryFile(
41
+ mode="w", suffix=".py", delete=False
42
+ ) as temp_file:
43
+ temp_file.write(code)
44
+ temp_file_path = temp_file.name
45
+
46
+ try:
47
+ result = subprocess.run(
48
+ [
49
+ "pylint",
50
+ "--disable=all",
51
+ "--enable=E0001,W0311,W0312",
52
+ temp_file_path,
53
+ ],
54
+ capture_output=True,
55
+ text=True,
56
+ check=False,
57
+ )
58
+ os.unlink(temp_file_path)
59
+ if result.returncode != 0:
60
+ error_message = result.stdout.strip() or result.stderr.strip()
61
+ self.printer.print_in_terminal("pylint_check_failed", error_message=error_message)
62
+ return False, error_message
63
+ return True, ""
64
+ except subprocess.CalledProcessError as e:
65
+ error_message = f"Error running pylint: {str(e)}"
66
+ self.printer.print_in_terminal("pylint_error", error_message=error_message)
67
+ os.unlink(temp_file_path)
68
+ return False, error_message
69
+
70
+ def get_edits(self, content: str):
71
+ edits = self.parse_whole_text(content)
72
+ result = []
73
+ for edit in edits:
74
+ result.append((edit.path, edit.content))
75
+ return result
76
+
77
+ def get_source_code_list_from_shadow_files(self, shadow_files: Dict[str, str]) -> SourceCodeList:
78
+ """
79
+ 将影子文件转换为SourceCodeList对象
80
+
81
+ 参数:
82
+ shadow_files (Dict[str, str]): 映射 {影子文件路径: 内容}
83
+
84
+ 返回:
85
+ SourceCodeList: 包含原始路径和内容的SourceCodeList对象
86
+ """
87
+ sources = []
88
+ shadow_manager = ShadowManager(self.args.source_dir,event_file_id=self.args.event_file)
89
+ for shadow_path, content in shadow_files.items():
90
+ # 将影子路径转换回原始文件路径
91
+ file_path = shadow_manager.from_shadow_path(shadow_path)
92
+ # 创建SourceCode对象并添加到sources列表
93
+ source = SourceCode(module_name=file_path, source_code=content)
94
+ sources.append(source)
95
+
96
+ return SourceCodeList(sources)
97
+
98
+ def _merge_code_without_effect(self, content: str) -> MergeCodeWithoutEffect:
99
+ """Merge code without any side effects like git operations, linting or file writing.
100
+ Returns a tuple of:
101
+ - list of (file_path, new_content) tuples for successfully merged blocks
102
+ - list of (file_path, hunk) tuples for failed to merge blocks"""
103
+ edits = self.get_edits(content)
104
+ file_content_mapping = {}
105
+ failed_blocks = []
106
+
107
+ for path, hunk in edits:
108
+ if not os.path.exists(path):
109
+ file_content_mapping[path] = hunk
110
+ else:
111
+ if path not in file_content_mapping:
112
+ file_content_mapping[path] = FileUtils.read_file(path)
113
+ existing_content = file_content_mapping[path]
114
+
115
+ try:
116
+ new_content = self.apply_hunk(existing_content, hunk)
117
+ if new_content:
118
+ file_content_mapping[path] = new_content
119
+ else:
120
+ failed_blocks.append((path, hunk))
121
+ except Exception as e:
122
+ failed_blocks.append((path, hunk))
123
+
124
+ return MergeCodeWithoutEffect(
125
+ success_blocks=[(path, content)
126
+ for path, content in file_content_mapping.items()],
127
+ failed_blocks=failed_blocks
128
+ )
129
+
130
+ def apply_hunk(self, content: str, hunk: str) -> str:
131
+ """Apply a unified diff hunk to content"""
132
+ import difflib
133
+ import re
134
+
135
+ # Split hunk into lines
136
+ lines = hunk.splitlines()
137
+
138
+ # Extract file paths
139
+ old_path = lines[0].replace("--- ", "").strip()
140
+ new_path = lines[1].replace("+++ ", "").strip()
141
+
142
+ # Skip the file paths and the @@ line
143
+ lines = lines[2:]
144
+
145
+ # Find the @@ line
146
+ for i, line in enumerate(lines):
147
+ if line.startswith("@@ "):
148
+ lines = lines[i+1:]
149
+ break
150
+
151
+ # Create a patch
152
+ patch = "\n".join(lines)
153
+
154
+ # Apply the patch
155
+ try:
156
+ result = difflib.unified_diff(content.splitlines(keepends=True),
157
+ patch.splitlines(keepends=True),
158
+ fromfile=old_path,
159
+ tofile=new_path)
160
+ # Convert back to string
161
+ return "".join(result)
162
+ except Exception as e:
163
+ return None
164
+
165
+ def _merge_code(self, content: str, force_skip_git: bool = False):
166
+ file_content = FileUtils.read_file(self.args.file)
167
+ md5 = hashlib.md5(file_content.encode("utf-8")).hexdigest()
168
+ file_name = os.path.basename(self.args.file)
169
+
170
+ edits = self.get_edits(content)
171
+ changes_to_make = []
172
+ changes_made = False
173
+ unmerged_blocks = []
174
+ merged_blocks = []
175
+
176
+ # First, check if there are any changes to be made
177
+ file_content_mapping = {}
178
+ for path, hunk in edits:
179
+ if not os.path.exists(path):
180
+ changes_to_make.append((path, None, hunk))
181
+ file_content_mapping[path] = hunk
182
+ merged_blocks.append((path, "", hunk, 1))
183
+ changes_made = True
184
+ else:
185
+ if path not in file_content_mapping:
186
+ file_content_mapping[path] = FileUtils.read_file(path)
187
+ existing_content = file_content_mapping[path]
188
+ new_content = self.apply_hunk(existing_content, hunk)
189
+ if new_content:
190
+ changes_to_make.append(
191
+ (path, existing_content, new_content))
192
+ file_content_mapping[path] = new_content
193
+ merged_blocks.append((path, hunk, new_content, 1))
194
+ changes_made = True
195
+ else:
196
+ unmerged_blocks.append(
197
+ (path, hunk, existing_content, 0))
198
+
199
+ if unmerged_blocks:
200
+ if self.args.request_id and not self.args.skip_events:
201
+ # collect unmerged blocks
202
+ event_data = []
203
+ for file_path, head, update, similarity in unmerged_blocks:
204
+ event_data.append(
205
+ {
206
+ "file_path": file_path,
207
+ "head": head,
208
+ "update": update,
209
+ "similarity": similarity,
210
+ }
211
+ )
212
+ return
213
+
214
+ self.printer.print_in_terminal("unmerged_blocks_warning", num_blocks=len(unmerged_blocks))
215
+ self._print_unmerged_blocks(unmerged_blocks)
216
+ return
217
+
218
+ # lint check
219
+ for file_path, new_content in file_content_mapping.items():
220
+ if file_path.endswith(".py"):
221
+ pylint_passed, error_message = self.run_pylint(new_content)
222
+ if not pylint_passed:
223
+ self.printer.print_in_terminal("pylint_file_check_failed",
224
+ file_path=file_path,
225
+ error_message=error_message)
226
+
227
+ if changes_made and not force_skip_git and not self.args.skip_commit:
228
+ try:
229
+ git_utils.commit_changes(
230
+ self.args.source_dir, f"auto_coder_pre_{file_name}_{md5}"
231
+ )
232
+ except Exception as e:
233
+ self.printer.print_str_in_terminal(
234
+ self.git_require_msg(source_dir=self.args.source_dir, error=str(e)),
235
+ style="red"
236
+ )
237
+ return
238
+
239
+ # Now, apply the changes
240
+ for file_path, new_content in file_content_mapping.items():
241
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
242
+ with open(file_path, "w") as f:
243
+ f.write(new_content)
244
+
245
+ if self.args.request_id and not self.args.skip_events:
246
+ # collect modified files
247
+ event_data = []
248
+ for code in merged_blocks:
249
+ file_path, head, update, similarity = code
250
+ event_data.append(
251
+ {
252
+ "file_path": file_path,
253
+ "head": head,
254
+ "update": update,
255
+ "similarity": similarity,
256
+ }
257
+ )
258
+
259
+ if changes_made:
260
+ if not force_skip_git and not self.args.skip_commit:
261
+ try:
262
+ commit_result = git_utils.commit_changes(
263
+ self.args.source_dir,
264
+ f"{self.args.query}\nauto_coder_{file_name}",
265
+ )
266
+
267
+ action_yml_file_manager = ActionYmlFileManager(self.args.source_dir)
268
+ action_file_name = os.path.basename(self.args.file)
269
+ add_updated_urls = []
270
+ commit_result.changed_files
271
+ for file in commit_result.changed_files:
272
+ add_updated_urls.append(os.path.join(self.args.source_dir, file))
273
+
274
+ self.args.add_updated_urls = add_updated_urls
275
+ update_yaml_success = action_yml_file_manager.update_yaml_field(action_file_name, "add_updated_urls", add_updated_urls)
276
+ if not update_yaml_success:
277
+ self.printer.print_in_terminal("yaml_save_error", style="red", yaml_file=action_file_name)
278
+
279
+ if self.args.enable_active_context:
280
+ active_context_manager = ActiveContextManager(self.llm, self.args.source_dir)
281
+ task_id = active_context_manager.process_changes(self.args)
282
+ self.printer.print_in_terminal("active_context_background_task",
283
+ style="blue",
284
+ task_id=task_id)
285
+ git_utils.print_commit_info(commit_result=commit_result)
286
+ except Exception as e:
287
+ self.printer.print_str_in_terminal(
288
+ self.git_require_msg(source_dir=self.args.source_dir, error=str(e)),
289
+ style="red"
290
+ )
291
+ else:
292
+ self.print_merged_blocks(merged_blocks)
293
+
294
+ self.printer.print_in_terminal("merge_success",
295
+ num_files=len(file_content_mapping.keys()),
296
+ num_changes=len(changes_to_make),
297
+ total_blocks=len(edits))
298
+
299
+ else:
300
+ self.printer.print_in_terminal("no_changes_made")
301
+
302
+ def _print_unmerged_blocks(self, unmerged_blocks: List[tuple]):
303
+ self.printer.print_in_terminal("unmerged_blocks_title", style="bold red")
304
+ for file_path, head, update, similarity in unmerged_blocks:
305
+ self.printer.print_str_in_terminal(
306
+ f"\n{self.printer.get_message_from_key_with_format('unmerged_file_path',file_path=file_path)}",
307
+ style="bold blue"
308
+ )
309
+ self.printer.print_str_in_terminal(
310
+ f"\n{self.printer.get_message_from_key_with_format('unmerged_search_block',similarity=similarity)}",
311
+ style="bold green"
312
+ )
313
+ syntax = Syntax(head, "python", theme="monokai", line_numbers=True)
314
+ self.printer.console.print(Panel(syntax, expand=False))
315
+ self.printer.print_in_terminal("unmerged_replace_block", style="bold yellow")
316
+ syntax = Syntax(update, "python", theme="monokai", line_numbers=True)
317
+ self.printer.console.print(Panel(syntax, expand=False))
318
+ self.printer.print_in_terminal("unmerged_blocks_total", num_blocks=len(unmerged_blocks), style="bold red")
319
+
320
+
321
+ def print_merged_blocks(self, merged_blocks: List[tuple]):
322
+ """Print search/replace blocks for user review using rich library"""
323
+ from rich.syntax import Syntax
324
+ from rich.panel import Panel
325
+
326
+ # Group blocks by file path
327
+ file_blocks = {}
328
+ for file_path, head, update, similarity in merged_blocks:
329
+ if file_path not in file_blocks:
330
+ file_blocks[file_path] = []
331
+ file_blocks[file_path].append((head, update, similarity))
332
+
333
+ # Generate formatted text for each file
334
+ formatted_text = ""
335
+ for file_path, blocks in file_blocks.items():
336
+ formatted_text += f"##File: {file_path}\n"
337
+ for head, update, similarity in blocks:
338
+ formatted_text += "<<<<<<< SEARCH\n"
339
+ formatted_text += head + "\n"
340
+ formatted_text += "=======\n"
341
+ formatted_text += update + "\n"
342
+ formatted_text += ">>>>>>> REPLACE\n"
343
+ formatted_text += "\n"
344
+
345
+ # Print with rich panel
346
+ self.printer.print_in_terminal("merged_blocks_title", style="bold green")
347
+ self.printer.console.print(
348
+ Panel(
349
+ Syntax(formatted_text, "diff", theme="monokai"),
350
+ title="Merged Changes",
351
+ border_style="green",
352
+ expand=False
353
+ )
354
+ )