jarvis-ai-assistant 0.1.98__py3-none-any.whl → 0.1.99__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 jarvis-ai-assistant might be problematic. Click here for more details.

Files changed (40) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/agent.py +199 -157
  3. jarvis/jarvis_code_agent/__init__.py +0 -0
  4. jarvis/jarvis_code_agent/main.py +203 -0
  5. jarvis/jarvis_codebase/main.py +412 -284
  6. jarvis/jarvis_coder/file_select.py +209 -0
  7. jarvis/jarvis_coder/git_utils.py +64 -2
  8. jarvis/jarvis_coder/main.py +11 -389
  9. jarvis/jarvis_coder/patch_handler.py +84 -14
  10. jarvis/jarvis_coder/plan_generator.py +49 -7
  11. jarvis/jarvis_rag/main.py +9 -9
  12. jarvis/jarvis_smart_shell/main.py +5 -7
  13. jarvis/models/base.py +6 -1
  14. jarvis/models/ollama.py +2 -2
  15. jarvis/models/registry.py +3 -6
  16. jarvis/tools/ask_user.py +6 -6
  17. jarvis/tools/codebase_qa.py +5 -7
  18. jarvis/tools/create_code_sub_agent.py +55 -0
  19. jarvis/tools/{sub_agent.py → create_sub_agent.py} +4 -1
  20. jarvis/tools/execute_code_modification.py +72 -0
  21. jarvis/tools/{file_ops.py → file_operation.py} +13 -14
  22. jarvis/tools/find_related_files.py +86 -0
  23. jarvis/tools/methodology.py +25 -25
  24. jarvis/tools/rag.py +32 -32
  25. jarvis/tools/registry.py +72 -36
  26. jarvis/tools/search.py +1 -1
  27. jarvis/tools/select_code_files.py +64 -0
  28. jarvis/utils.py +153 -49
  29. {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/METADATA +1 -1
  30. jarvis_ai_assistant-0.1.99.dist-info/RECORD +52 -0
  31. {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/entry_points.txt +2 -1
  32. jarvis/main.py +0 -155
  33. jarvis/tools/coder.py +0 -69
  34. jarvis_ai_assistant-0.1.98.dist-info/RECORD +0 -47
  35. /jarvis/tools/{shell.py → execute_shell.py} +0 -0
  36. /jarvis/tools/{generator.py → generate_tool.py} +0 -0
  37. /jarvis/tools/{webpage.py → read_webpage.py} +0 -0
  38. {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/LICENSE +0 -0
  39. {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/WHEEL +0 -0
  40. {jarvis_ai_assistant-0.1.98.dist-info → jarvis_ai_assistant-0.1.99.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,7 @@ import threading
3
3
  from typing import Dict, Any, List, Optional
4
4
  import re
5
5
 
6
+ from jarvis.jarvis_coder.file_select import select_files
6
7
  from jarvis.utils import OutputType, PrettyOutput, find_git_root, get_max_context_length, is_long_context, load_env_from_file, while_success
7
8
  from jarvis.models.registry import PlatformRegistry
8
9
  from jarvis.jarvis_codebase.main import CodeBase
@@ -12,7 +13,7 @@ from prompt_toolkit.formatted_text import FormattedText
12
13
  from prompt_toolkit.styles import Style
13
14
  import fnmatch
14
15
  from .patch_handler import PatchHandler
15
- from .git_utils import generate_commit_message, save_edit_record
16
+ from .git_utils import generate_commit_message, init_git_repo, save_edit_record
16
17
  from .plan_generator import PlanGenerator
17
18
 
18
19
  # 全局锁对象
@@ -29,98 +30,14 @@ class JarvisCoder:
29
30
  def _init_directories(self):
30
31
  """Initialize directories"""
31
32
  self.max_context_length = get_max_context_length()
32
-
33
- root_dir = find_git_root(self.root_dir)
34
- if not root_dir:
35
- root_dir = self.root_dir
36
-
37
- self.root_dir = root_dir
38
-
39
- PrettyOutput.print(f"Git root directory: {self.root_dir}", OutputType.INFO)
40
-
41
- # 1. Check if the code repository path exists, if it does not exist, create it
42
- if not os.path.exists(self.root_dir):
43
- PrettyOutput.print(
44
- "Root directory does not exist, creating...", OutputType.INFO)
45
- os.makedirs(self.root_dir)
46
-
47
- os.chdir(self.root_dir)
48
-
49
- # 2. Create .jarvis-coder directory
50
- self.jarvis_dir = os.path.join(self.root_dir, ".jarvis-coder")
51
- if not os.path.exists(self.jarvis_dir):
52
- os.makedirs(self.jarvis_dir)
53
-
54
- self.record_dir = os.path.join(self.jarvis_dir, "record")
55
- if not os.path.exists(self.record_dir):
56
- os.makedirs(self.record_dir)
57
-
58
- # 3. Process .gitignore file
59
- gitignore_path = os.path.join(self.root_dir, ".gitignore")
60
- gitignore_modified = False
61
- jarvis_ignore_pattern = ".jarvis-*"
62
-
63
- # 3.1 If .gitignore does not exist, create it
64
- if not os.path.exists(gitignore_path):
65
- PrettyOutput.print("Create .gitignore file", OutputType.INFO)
66
- with open(gitignore_path, "w", encoding="utf-8") as f:
67
- f.write(f"{jarvis_ignore_pattern}\n")
68
- gitignore_modified = True
69
- else:
70
- # 3.2 Check if it already contains the .jarvis-* pattern
71
- with open(gitignore_path, "r", encoding="utf-8") as f:
72
- content = f.read()
73
-
74
- # 3.2 Check if it already contains the .jarvis-* pattern
75
- if jarvis_ignore_pattern not in content.split("\n"):
76
- PrettyOutput.print("Add .jarvis-* to .gitignore", OutputType.INFO)
77
- with open(gitignore_path, "a", encoding="utf-8") as f:
78
- # Ensure the file ends with a newline
79
- if not content.endswith("\n"):
80
- f.write("\n")
81
- f.write(f"{jarvis_ignore_pattern}\n")
82
- gitignore_modified = True
83
-
84
- # 4. Check if the code repository is a git repository, if not, initialize the git repository
85
- if not os.path.exists(os.path.join(self.root_dir, ".git")):
86
- PrettyOutput.print("Initialize Git repository", OutputType.INFO)
87
- os.system("git init")
88
- os.system("git add .")
89
- os.system("git commit -m 'Initial commit'")
90
- # 5. If .gitignore is modified, commit the changes
91
- elif gitignore_modified:
92
- PrettyOutput.print("Commit .gitignore changes", OutputType.INFO)
93
- os.system("git add .gitignore")
94
- os.system("git commit -m 'chore: update .gitignore to exclude .jarvis-* files'")
95
- # 6. Check if there are uncommitted files in the code repository, if there are, commit once
96
- elif self._has_uncommitted_files():
97
- PrettyOutput.print("Commit uncommitted changes", OutputType.INFO)
98
- os.system("git add .")
99
- git_diff = os.popen("git diff --cached").read()
100
- commit_message = generate_commit_message(git_diff)
101
- os.system(f"git commit -m '{commit_message}'")
33
+ self.root_dir = init_git_repo(self.root_dir)
102
34
 
103
35
  def _init_codebase(self):
104
36
  """Initialize codebase"""
105
37
  self._codebase = CodeBase(self.root_dir)
106
38
 
107
- def _has_uncommitted_files(self) -> bool:
108
- """Check if there are uncommitted files in the code repository"""
109
- # Get unstaged modifications
110
- unstaged = os.popen("git diff --name-only").read()
111
- # Get staged but uncommitted modifications
112
- staged = os.popen("git diff --cached --name-only").read()
113
- # Get untracked files
114
- untracked = os.popen("git ls-files --others --exclude-standard").read()
115
-
116
- return bool(unstaged or staged or untracked)
117
-
118
- def _prepare_execution(self) -> None:
119
- """Prepare execution environment"""
120
- self._codebase.generate_codebase()
121
-
122
39
 
123
- def _load_related_files(self, feature: str) -> List[Dict]:
40
+ def _load_related_files(self, feature: str) -> List[str]:
124
41
  """Load related file content"""
125
42
  ret = []
126
43
  # Ensure the index database is generated
@@ -129,300 +46,23 @@ class JarvisCoder:
129
46
  self._codebase.generate_codebase()
130
47
 
131
48
  related_files = self._codebase.search_similar(feature)
132
- for file, score in related_files:
133
- PrettyOutput.print(f"Related file: {file} (score: {score:.3f})", OutputType.SUCCESS)
134
- content = open(file, "r", encoding="utf-8").read()
135
- ret.append({"file_path": file, "file_content": content})
49
+ for file in related_files:
50
+ PrettyOutput.print(f"Related file: {file}", OutputType.SUCCESS)
51
+ ret.append(file)
136
52
  return ret
137
53
 
138
- def _parse_file_selection(self, input_str: str, max_index: int) -> List[int]:
139
- """Parse file selection expression
140
-
141
- Supported formats:
142
- - Single number: "1"
143
- - Comma-separated: "1,3,5"
144
- - Range: "1-5"
145
- - Combination: "1,3-5,7"
146
-
147
- Args:
148
- input_str: User input selection expression
149
- max_index: Maximum selectable index
150
-
151
- Returns:
152
- List[int]: Selected index list (starting from 0)
153
- """
154
- selected = set()
155
-
156
- # Remove all whitespace characters
157
- input_str = "".join(input_str.split())
158
-
159
- # Process comma-separated parts
160
- for part in input_str.split(","):
161
- if not part:
162
- continue
163
-
164
- # Process range (e.g.: 3-6)
165
- if "-" in part:
166
- try:
167
- start, end = map(int, part.split("-"))
168
- # Convert to index starting from 0
169
- start = max(0, start - 1)
170
- end = min(max_index, end - 1)
171
- if start <= end:
172
- selected.update(range(start, end + 1))
173
- except ValueError:
174
- PrettyOutput.print(f"Ignore invalid range expression: {part}", OutputType.WARNING)
175
- # Process single number
176
- else:
177
- try:
178
- index = int(part) - 1 # Convert to index starting from 0
179
- if 0 <= index < max_index:
180
- selected.add(index)
181
- else:
182
- PrettyOutput.print(f"Ignore index out of range: {part}", OutputType.WARNING)
183
- except ValueError:
184
- PrettyOutput.print(f"Ignore invalid number: {part}", OutputType.WARNING)
185
-
186
- return sorted(list(selected))
187
-
188
- def _get_file_completer(self) -> Completer:
189
- """Create file path completer"""
190
- class FileCompleter(Completer):
191
- def __init__(self, root_dir: str):
192
- self.root_dir = root_dir
193
-
194
- def get_completions(self, document, complete_event):
195
- # Get the text of the current input
196
- text = document.text_before_cursor
197
-
198
- # If the input is empty, return all files in the root directory
199
- if not text:
200
- for path in self._list_files(""):
201
- yield Completion(path, start_position=0)
202
- return
203
-
204
- # Get the current directory and partial file name
205
- current_dir = os.path.dirname(text)
206
- file_prefix = os.path.basename(text)
207
-
208
- # List matching files
209
- search_dir = os.path.join(self.root_dir, current_dir) if current_dir else self.root_dir
210
- if os.path.isdir(search_dir):
211
- for path in self._list_files(current_dir):
212
- if path.startswith(text):
213
- yield Completion(path, start_position=-len(text))
214
-
215
- def _list_files(self, current_dir: str) -> List[str]:
216
- """List all files in the specified directory (recursively)"""
217
- files = []
218
- search_dir = os.path.join(self.root_dir, current_dir)
219
-
220
- for root, _, filenames in os.walk(search_dir):
221
- for filename in filenames:
222
- full_path = os.path.join(root, filename)
223
- rel_path = os.path.relpath(full_path, self.root_dir)
224
- # Ignore .git directory and other hidden files
225
- if not any(part.startswith('.') for part in rel_path.split(os.sep)):
226
- files.append(rel_path)
227
-
228
- return sorted(files)
229
-
230
- return FileCompleter(self.root_dir)
231
-
232
- def _fuzzy_match_files(self, pattern: str) -> List[str]:
233
- """Fuzzy match file path
234
-
235
- Args:
236
- pattern: Matching pattern
237
-
238
- Returns:
239
- List[str]: List of matching file paths
240
- """
241
- matches = []
242
-
243
- # 将模式转换为正则表达式
244
- pattern = pattern.replace('.', r'\.').replace('*', '.*').replace('?', '.')
245
- pattern = f".*{pattern}.*" # 允许部分匹配
246
- regex = re.compile(pattern, re.IGNORECASE)
247
-
248
- # 遍历所有文件
249
- for root, _, files in os.walk(self.root_dir):
250
- for file in files:
251
- full_path = os.path.join(root, file)
252
- rel_path = os.path.relpath(full_path, self.root_dir)
253
- # 忽略 .git 目录和其他隐藏文件
254
- if not any(part.startswith('.') for part in rel_path.split(os.sep)):
255
- if regex.match(rel_path):
256
- matches.append(rel_path)
257
-
258
- return sorted(matches)
259
-
260
- def _select_files(self, related_files: List[Dict]) -> List[Dict]:
261
- """Let the user select and supplement related files"""
262
- PrettyOutput.section("Related files", OutputType.INFO)
263
-
264
- # Display found files
265
- selected_files = list(related_files) # Default select all
266
- for i, file in enumerate(related_files, 1):
267
- PrettyOutput.print(f"[{i}] {file['file_path']}", OutputType.INFO)
268
-
269
- # Ask the user if they need to adjust the file list
270
- user_input = input("\nDo you need to adjust the file list? (y/n) [n]: ").strip().lower() or 'n'
271
- if user_input == 'y':
272
- # Let the user select files
273
- PrettyOutput.print("\nPlease enter the file numbers to include (support: 1,3-6 format, press Enter to keep the current selection):", OutputType.INFO)
274
- numbers = input(">>> ").strip()
275
- if numbers:
276
- selected_indices = self._parse_file_selection(numbers, len(related_files))
277
- if selected_indices:
278
- selected_files = [related_files[i] for i in selected_indices]
279
- else:
280
- PrettyOutput.print("No valid files selected, keep the current selection", OutputType.WARNING)
281
-
282
- # Ask if they need to supplement files
283
- user_input = input("\nDo you need to supplement other files? (y/n) [n]: ").strip().lower() or 'n'
284
- if user_input == 'y':
285
- # Create file completion session
286
- session = PromptSession(
287
- completer=self._get_file_completer(),
288
- complete_while_typing=True
289
- )
290
-
291
- while True:
292
- PrettyOutput.print("\nPlease enter the file path to supplement (support Tab completion and *? wildcard, input empty line to end):", OutputType.INFO)
293
- try:
294
- file_path = session.prompt(">>> ").strip()
295
- except KeyboardInterrupt:
296
- break
297
-
298
- if not file_path:
299
- break
300
-
301
- # Process wildcard matching
302
- if '*' in file_path or '?' in file_path:
303
- matches = self._fuzzy_match_files(file_path)
304
- if not matches:
305
- PrettyOutput.print("No matching files found", OutputType.WARNING)
306
- continue
307
-
308
- # Display matching files
309
- PrettyOutput.print("\nFound the following matching files:", OutputType.INFO)
310
- for i, path in enumerate(matches, 1):
311
- PrettyOutput.print(f"[{i}] {path}", OutputType.INFO)
312
-
313
- # Let the user select
314
- numbers = input("\nPlease select the file numbers to add (support: 1,3-6 format, press Enter to select all): ").strip()
315
- if numbers:
316
- indices = self._parse_file_selection(numbers, len(matches))
317
- if not indices:
318
- continue
319
- paths_to_add = [matches[i] for i in indices]
320
- else:
321
- paths_to_add = matches
322
- else:
323
- paths_to_add = [file_path]
324
-
325
- # Add selected files
326
- for path in paths_to_add:
327
- full_path = os.path.join(self.root_dir, path)
328
- if not os.path.isfile(full_path):
329
- PrettyOutput.print(f"File does not exist: {path}", OutputType.ERROR)
330
- continue
331
-
332
- try:
333
- with open(full_path, "r", encoding="utf-8") as f:
334
- content = f.read()
335
- selected_files.append({
336
- "file_path": path,
337
- "file_content": content
338
- })
339
- PrettyOutput.print(f"File added: {path}", OutputType.SUCCESS)
340
- except Exception as e:
341
- PrettyOutput.print(f"Failed to read file: {str(e)}", OutputType.ERROR)
342
-
343
- return selected_files
344
54
 
345
- def _finalize_changes(self, feature: str) -> None:
346
- """Complete changes and commit"""
347
- PrettyOutput.print("Modification confirmed, committing...", OutputType.INFO)
348
-
349
- # Add only modified files under git control
350
- os.system("git add -u")
351
-
352
- # Then get git diff
353
- git_diff = os.popen("git diff --cached").read()
354
-
355
- # Automatically generate commit information, pass in feature
356
- commit_message = generate_commit_message(git_diff)
357
-
358
- # Display and confirm commit information
359
- PrettyOutput.print(f"Automatically generated commit information: {commit_message}", OutputType.INFO)
360
- user_confirm = input("Use this commit information? (y/n) [y]: ") or "y"
361
-
362
- if user_confirm.lower() != "y":
363
- commit_message = input("Please enter a new commit information: ")
364
-
365
- # No need to git add again, it has already been added
366
- os.system(f"git commit -m '{commit_message}'")
367
- save_edit_record(self.record_dir, commit_message, git_diff)
368
-
369
- def _revert_changes(self) -> None:
370
- """Revert all changes"""
371
- PrettyOutput.print("Modification cancelled, reverting changes", OutputType.INFO)
372
- os.system(f"git reset --hard")
373
- os.system(f"git clean -df")
374
-
375
- def get_key_code(self, files: List[Dict], feature: str):
376
- """Extract relevant key code snippets from files"""
377
- for file_info in files:
378
- PrettyOutput.print(f"Analyzing file: {file_info['file_path']}", OutputType.INFO)
379
- model = PlatformRegistry.get_global_platform_registry().get_codegen_platform()
380
- model.set_suppress_output(True)
381
- file_path = file_info["file_path"]
382
- content = file_info["file_content"]
383
-
384
- try:
385
- prompt = f"""You are a code analysis expert who can extract relevant snippets from code.
386
- Please return in the following format:
387
- <PART>
388
- content
389
- </PART>
390
-
391
- Multiple snippets can be returned. If the file content is not relevant to the requirement, return empty.
392
-
393
- Requirement: {feature}
394
- File path: {file_path}
395
- Code content:
396
- {content}
397
- """
398
-
399
- # 调用大模型进行分析
400
- response = model.chat_until_success(prompt)
401
-
402
- parts = re.findall(r'<PART>\n(.*?)\n</PART>', response, re.DOTALL)
403
- file_info["parts"] = parts
404
- except Exception as e:
405
- PrettyOutput.print(f"Failed to analyze file: {str(e)}", OutputType.ERROR)
406
55
 
407
56
  def execute(self, feature: str) -> Dict[str, Any]:
408
57
  """Execute code modification"""
409
58
  try:
410
- self._prepare_execution()
411
-
412
59
  # Get and select related files
413
60
  initial_files = self._load_related_files(feature)
414
- selected_files = self._select_files(initial_files)
61
+ selected_files = select_files(initial_files, self.root_dir)
415
62
 
416
- # Whether it is a long context
417
- if is_long_context([file['file_path'] for file in selected_files]):
418
- self.get_key_code(selected_files, feature)
419
- else:
420
- for file in selected_files:
421
- file["parts"] = [file["file_content"]]
422
-
423
63
  # Get modification plan
424
- raw_plan, structed_plan = PlanGenerator().generate_plan(feature, selected_files)
425
- if not raw_plan or not structed_plan:
64
+ structed_plan = PlanGenerator().generate_plan(feature, selected_files)
65
+ if not structed_plan:
426
66
  return {
427
67
  "success": False,
428
68
  "stdout": "",
@@ -430,15 +70,13 @@ Code content:
430
70
  }
431
71
 
432
72
  # Execute modification
433
- if PatchHandler().handle_patch_application(feature ,raw_plan, structed_plan):
434
- self._finalize_changes(feature)
73
+ if PatchHandler().handle_patch_application(feature, structed_plan):
435
74
  return {
436
75
  "success": True,
437
76
  "stdout": "Code modification successful",
438
77
  "stderr": "",
439
78
  }
440
79
  else:
441
- self._revert_changes()
442
80
  return {
443
81
  "success": False,
444
82
  "stdout": "",
@@ -446,7 +84,6 @@ Code content:
446
84
  }
447
85
 
448
86
  except Exception as e:
449
- self._revert_changes()
450
87
  return {
451
88
  "success": False,
452
89
  "stdout": "",
@@ -557,21 +194,6 @@ class FilePathCompleter(Completer):
557
194
  # Calculate the correct start_position
558
195
  yield Completion(path, start_position=-(len(search)))
559
196
 
560
- class SmartCompleter(Completer):
561
- """Smart auto-completer, combine word and file path completion"""
562
-
563
- def __init__(self, word_completer: WordCompleter, file_completer: FilePathCompleter):
564
- self.word_completer = word_completer
565
- self.file_completer = file_completer
566
-
567
- def get_completions(self, document, complete_event):
568
- """Get completion suggestions"""
569
- # If the current line ends with @, use file completion
570
- if document.text_before_cursor.strip().endswith('@'):
571
- yield from self.file_completer.get_completions(document, complete_event)
572
- else:
573
- # Otherwise, use word completion
574
- yield from self.word_completer.get_completions(document, complete_event)
575
197
 
576
198
  def get_multiline_input(prompt_text: str, root_dir: Optional[str] = ".") -> str:
577
199
  """Get multi-line input, support file path auto-completion function
@@ -1,10 +1,13 @@
1
1
  import re
2
2
  import os
3
3
  from typing import List, Tuple, Dict
4
+ import yaml
5
+ from pathlib import Path
4
6
 
7
+ from jarvis.jarvis_coder.git_utils import generate_commit_message, init_git_repo, save_edit_record
5
8
  from jarvis.models.base import BasePlatform
6
9
  from jarvis.models.registry import PlatformRegistry
7
- from jarvis.utils import OutputType, PrettyOutput, get_multiline_input, while_success
10
+ from jarvis.utils import OutputType, PrettyOutput, get_multiline_input, get_single_line_input, while_success
8
11
 
9
12
  class Patch:
10
13
  def __init__(self, old_code: str, new_code: str):
@@ -12,7 +15,32 @@ class Patch:
12
15
  self.new_code = new_code
13
16
 
14
17
  class PatchHandler:
18
+ def __init__(self):
19
+ self.prompt_file = Path.home() / ".jarvis-coder-patch-prompt"
20
+ self.additional_info = self._load_additional_info()
21
+ self.root_dir = init_git_repo(os.getcwd())
22
+ self.record_dir = os.path.join(self.root_dir, ".jarvis-coder", "record")
23
+ if not os.path.exists(self.record_dir):
24
+ os.makedirs(self.record_dir)
25
+ def _load_additional_info(self) -> str:
26
+ """Load saved additional info from prompt file"""
27
+ if not self.prompt_file.exists():
28
+ return ""
29
+ try:
30
+ with open(self.prompt_file, 'r') as f:
31
+ data = yaml.safe_load(f)
32
+ return data.get('additional_info', '') if data else ''
33
+ except Exception as e:
34
+ PrettyOutput.print(f"Failed to load additional info: {e}", OutputType.WARNING)
35
+ return ""
15
36
 
37
+ def _save_additional_info(self, info: str):
38
+ """Save additional info to prompt file"""
39
+ try:
40
+ with open(self.prompt_file, 'w') as f:
41
+ yaml.dump({'additional_info': info}, f)
42
+ except Exception as e:
43
+ PrettyOutput.print(f"Failed to save additional info: {e}", OutputType.WARNING)
16
44
 
17
45
  def _extract_patches(self, response: str) -> List[Patch]:
18
46
  """Extract patches from response
@@ -34,7 +62,7 @@ class PatchHandler:
34
62
  def _confirm_and_apply_changes(self, file_path: str) -> bool:
35
63
  """Confirm and apply changes"""
36
64
  os.system(f"git diff --cached {file_path}")
37
- confirm = input(f"\nAccept {file_path} changes? (y/n) [y]: ").lower() or "y"
65
+ confirm = get_single_line_input(f"Accept {file_path} changes? (y/n) [y]").lower() or "y"
38
66
  if confirm == "y":
39
67
  return True
40
68
  else:
@@ -43,6 +71,38 @@ class PatchHandler:
43
71
  os.system(f"git checkout -- {file_path}")
44
72
  PrettyOutput.print(f"Changes to {file_path} have been rolled back", OutputType.WARNING)
45
73
  return False
74
+
75
+
76
+
77
+ def _finalize_changes(self) -> None:
78
+ """Complete changes and commit"""
79
+ PrettyOutput.print("Modification confirmed, committing...", OutputType.INFO)
80
+
81
+ # Add only modified files under git control
82
+ os.system("git add -u")
83
+
84
+ # Then get git diff
85
+ git_diff = os.popen("git diff --cached").read()
86
+
87
+ # Automatically generate commit information, pass in feature
88
+ commit_message = generate_commit_message(git_diff)
89
+
90
+ # Display and confirm commit information
91
+ PrettyOutput.print(f"Automatically generated commit information: {commit_message}", OutputType.INFO)
92
+ user_confirm = get_single_line_input("Use this commit information? (y/n) [y]").lower() or "y"
93
+
94
+ if user_confirm.lower() != "y":
95
+ commit_message = get_single_line_input("Please enter a new commit information")
96
+
97
+ # No need to git add again, it has already been added
98
+ os.system(f"git commit -m '{commit_message}'")
99
+ save_edit_record(self.record_dir, commit_message, git_diff)
100
+
101
+ def _revert_changes(self) -> None:
102
+ """Revert all changes"""
103
+ PrettyOutput.print("Modification cancelled, reverting changes", OutputType.INFO)
104
+ os.system(f"git reset --hard")
105
+ os.system(f"git clean -df")
46
106
 
47
107
 
48
108
  def apply_file_patch(self, file_path: str, patches: List[Patch]) -> bool:
@@ -79,18 +139,26 @@ class PatchHandler:
79
139
  return True
80
140
 
81
141
 
82
- def retry_comfirm(self) -> Tuple[str, str]:# Restore user selection logic
83
- choice = input("\nPlease choose an action: (1) Retry (2) Skip (3) Completely stop [1]: ") or "1"
142
+ def retry_comfirm(self) -> Tuple[str, str]:
143
+ choice = get_single_line_input("\nPlease choose an action: (1) Retry (2) Skip (3) Completely stop [1]: ") or "1"
84
144
  if choice == "2":
85
145
  return "skip", ""
86
146
  if choice == "3":
87
147
  return "break", ""
88
- return "continue", get_multiline_input("Please enter additional information and requirements:")
148
+
149
+ feedback = get_multiline_input("Please enter additional information and requirements:")
150
+ if feedback:
151
+ save_prompt = get_single_line_input("Would you like to save this as general feedback for future patches? (y/n) [n]: ").lower() or "n"
152
+ if save_prompt == "y":
153
+ self._save_additional_info(feedback)
154
+ PrettyOutput.print("Feedback saved for future use", OutputType.SUCCESS)
155
+
156
+ return "continue", feedback
89
157
 
90
- def apply_patch(self, feature: str, raw_plan: str, structed_plan: Dict[str, str]) -> Tuple[bool, str]:
158
+ def apply_patch(self, feature: str, structed_plan: Dict[str, str]) -> bool:
91
159
  """Apply patch (main entry)"""
92
160
  for file_path, current_plan in structed_plan.items():
93
- additional_info = ""
161
+ additional_info = self.additional_info # Initialize with saved info
94
162
  while True:
95
163
 
96
164
  if os.path.exists(file_path):
@@ -114,16 +182,17 @@ class PatchHandler:
114
182
  2. old_code will be replaced with new_code, pay attention to context continuity
115
183
  3. Avoid breaking existing code logic when generating patches, e.g., don't insert function definitions inside existing function bodies
116
184
  4. Include sufficient context to avoid ambiguity
117
- 5. Patches will be merged using file_content.replace(patch.old_code, patch.new_code, 1), so old_code and new_code need to match exactly, including empty lines, line breaks, whitespace, tabs, and comments
185
+ 5. Patches will be merged using file_content.replace(patch.old_code, patch.new_code, 1), so old_code and new_code need to match exactly, including EMPTY LINES, LINE BREAKS, WHITESPACE, TABS, and COMMENTS
118
186
  6. Ensure generated code has correct format (syntax, indentation, line breaks)
119
187
  7. Ensure new_code's indentation and format matches old_code
120
188
  8. Ensure code is inserted in appropriate locations, e.g., code using variables should be after declarations/definitions
121
189
  9. Provide at least 3 lines of context before and after modified code for location
190
+ 10. Each patch should be no more than 20 lines of code, if it is more than 20 lines, split it into multiple patches
191
+ 11. old code's line breaks should be consistent with the original code
122
192
 
123
193
 
124
194
  """
125
195
  prompt += f"""# Original requirement: {feature}
126
- # Complete modification plan: {raw_plan}
127
196
  # Current file path: {file_path}
128
197
  # Current file content:
129
198
  <CONTENT>
@@ -146,7 +215,7 @@ class PatchHandler:
146
215
  act, msg = self.retry_comfirm()
147
216
  if act == "break":
148
217
  PrettyOutput.print("Terminate patch application", OutputType.WARNING)
149
- return False, msg
218
+ return False
150
219
  if act == "skip":
151
220
  PrettyOutput.print(f"Skip file {file_path}", OutputType.WARNING)
152
221
  break
@@ -154,13 +223,14 @@ class PatchHandler:
154
223
  additional_info += msg + "\n"
155
224
  continue
156
225
  else:
226
+ self._finalize_changes()
157
227
  break
158
228
 
159
- return True, ""
229
+ return True
160
230
 
161
231
 
162
232
 
163
- def handle_patch_application(self, feature: str, raw_plan: str, structed_plan: Dict[str,str]) -> bool:
233
+ def handle_patch_application(self, feature: str, structed_plan: Dict[str,str]) -> bool:
164
234
  """Process patch application process
165
235
 
166
236
  Args:
@@ -176,13 +246,13 @@ class PatchHandler:
176
246
  PrettyOutput.print(f"\nFile: {file_path}", OutputType.INFO)
177
247
  PrettyOutput.print(f"Modification plan: \n{patches_code}", OutputType.INFO)
178
248
  # 3. Apply patches
179
- success, error_msg = self.apply_patch(feature, raw_plan, structed_plan)
249
+ success = self.apply_patch(feature, structed_plan)
180
250
  if not success:
181
251
  os.system("git reset --hard")
182
252
  return False
183
253
  # 6. Apply successfully, let user confirm changes
184
254
  PrettyOutput.print("\nPatches applied, please check the modification effect.", OutputType.SUCCESS)
185
- confirm = input("\nKeep these changes? (y/n) [y]: ").lower() or "y"
255
+ confirm = get_single_line_input("\nKeep these changes? (y/n) [y]: ").lower() or "y"
186
256
  if confirm != "y":
187
257
  PrettyOutput.print("User cancelled changes, rolling back", OutputType.WARNING)
188
258
  os.system("git reset --hard") # Rollback all changes