mirror of
https://github.com/Aider-AI/aider.git
synced 2025-06-12 15:45:00 +00:00
Granular tool-call based editing
This commit is contained in:
parent
a310df3df3
commit
7f0ef1a04a
4 changed files with 1384 additions and 12 deletions
118
aider/change_tracker.py
Normal file
118
aider/change_tracker.py
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class ChangeTracker:
|
||||||
|
"""
|
||||||
|
Tracks changes made to files for the undo functionality.
|
||||||
|
This enables granular editing operations with the ability to undo specific changes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.changes = {} # change_id -> change_info
|
||||||
|
self.files_changed = defaultdict(list) # file_path -> [change_ids]
|
||||||
|
|
||||||
|
def track_change(self, file_path, change_type, original_content, new_content,
|
||||||
|
metadata=None, change_id=None):
|
||||||
|
"""
|
||||||
|
Record a change to enable future undo operations.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- file_path: Path to the file that was changed
|
||||||
|
- change_type: Type of change (e.g., 'replacetext', 'insertlines')
|
||||||
|
- original_content: Original content before the change
|
||||||
|
- new_content: New content after the change
|
||||||
|
- metadata: Additional information about the change (line numbers, positions, etc.)
|
||||||
|
- change_id: Optional custom ID for the change (if None, one will be generated)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- change_id: Unique identifier for the change
|
||||||
|
"""
|
||||||
|
if change_id is None:
|
||||||
|
change_id = self._generate_change_id()
|
||||||
|
|
||||||
|
change = {
|
||||||
|
'id': change_id,
|
||||||
|
'file_path': file_path,
|
||||||
|
'type': change_type,
|
||||||
|
'original': original_content,
|
||||||
|
'new': new_content,
|
||||||
|
'metadata': metadata or {},
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.changes[change_id] = change
|
||||||
|
self.files_changed[file_path].append(change_id)
|
||||||
|
return change_id
|
||||||
|
|
||||||
|
def undo_change(self, change_id):
|
||||||
|
"""
|
||||||
|
Get information needed to reverse a specific change by ID.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- change_id: ID of the change to undo
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- (success, message, change_info): Tuple with success flag, message, and change information
|
||||||
|
"""
|
||||||
|
if change_id not in self.changes:
|
||||||
|
return False, f"Change ID {change_id} not found", None
|
||||||
|
|
||||||
|
change = self.changes[change_id]
|
||||||
|
|
||||||
|
# Mark this change as undone by removing it from the tracking dictionaries
|
||||||
|
self.files_changed[change['file_path']].remove(change_id)
|
||||||
|
if not self.files_changed[change['file_path']]:
|
||||||
|
del self.files_changed[change['file_path']]
|
||||||
|
|
||||||
|
# Keep the change in the changes dict but mark it as undone
|
||||||
|
change['undone'] = True
|
||||||
|
change['undone_at'] = time.time()
|
||||||
|
|
||||||
|
return True, f"Undid change {change_id} in {change['file_path']}", change
|
||||||
|
|
||||||
|
def get_last_change(self, file_path):
|
||||||
|
"""
|
||||||
|
Get the most recent change for a specific file.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- file_path: Path to the file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- change_id or None if no changes found
|
||||||
|
"""
|
||||||
|
changes = self.files_changed.get(file_path, [])
|
||||||
|
if not changes:
|
||||||
|
return None
|
||||||
|
return changes[-1]
|
||||||
|
|
||||||
|
def list_changes(self, file_path=None, limit=10):
|
||||||
|
"""
|
||||||
|
List recent changes, optionally filtered by file.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- file_path: Optional path to filter changes by file
|
||||||
|
- limit: Maximum number of changes to list
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- List of change dictionaries
|
||||||
|
"""
|
||||||
|
if file_path:
|
||||||
|
# Get changes only for the specified file
|
||||||
|
change_ids = self.files_changed.get(file_path, [])
|
||||||
|
changes = [self.changes[cid] for cid in change_ids if cid in self.changes]
|
||||||
|
else:
|
||||||
|
# Get all changes
|
||||||
|
changes = list(self.changes.values())
|
||||||
|
|
||||||
|
# Filter out undone changes and sort by timestamp (most recent first)
|
||||||
|
changes = [c for c in changes if not c.get('undone', False)]
|
||||||
|
changes = sorted(changes, key=lambda c: c['timestamp'], reverse=True)
|
||||||
|
|
||||||
|
# Apply limit
|
||||||
|
return changes[:limit]
|
||||||
|
|
||||||
|
def _generate_change_id(self):
|
||||||
|
"""Generate a unique ID for a change."""
|
||||||
|
return str(uuid.uuid4())[:8] # Short, readable ID
|
|
@ -43,10 +43,6 @@ from ..dump import dump # noqa: F401
|
||||||
from .chat_chunks import ChatChunks
|
from .chat_chunks import ChatChunks
|
||||||
|
|
||||||
|
|
||||||
# Pattern to detect fenced search/replace blocks
|
|
||||||
SEARCH_REPLACE_FENCE = re.compile(r"```search_replace\n", re.MULTILINE)
|
|
||||||
|
|
||||||
|
|
||||||
class UnknownEditFormat(ValueError):
|
class UnknownEditFormat(ValueError):
|
||||||
def __init__(self, edit_format, valid_formats):
|
def __init__(self, edit_format, valid_formats):
|
||||||
self.edit_format = edit_format
|
self.edit_format = edit_format
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -12,7 +12,7 @@ class NavigatorPrompts(CoderPrompts):
|
||||||
LLM to manage its own context by adding/removing files and executing commands.
|
LLM to manage its own context by adding/removing files and executing commands.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
main_system = """<context name="session_config">
|
main_system = r'''<context name="session_config">
|
||||||
## Role and Purpose
|
## Role and Purpose
|
||||||
Act as an expert software engineer with the ability to autonomously navigate and modify a codebase.
|
Act as an expert software engineer with the ability to autonomously navigate and modify a codebase.
|
||||||
|
|
||||||
|
@ -60,10 +60,50 @@ Act as an expert software engineer with the ability to autonomously navigate and
|
||||||
- **MakeReadonly**: `[tool_call(MakeReadonly, file_path="src/main.py")]`
|
- **MakeReadonly**: `[tool_call(MakeReadonly, file_path="src/main.py")]`
|
||||||
Convert an editable file back to read-only status.
|
Convert an editable file back to read-only status.
|
||||||
|
|
||||||
|
### Granular Editing Tools
|
||||||
|
- **ReplaceText**: `[tool_call(ReplaceText, file_path="path/to/file.py", find_text="old text", replace_text="new text", near_context="unique nearby text", occurrence=1)]`
|
||||||
|
Replace specific text with new text. Use near_context to disambiguate between multiple occurrences.
|
||||||
|
Set occurrence to -1 for the last occurrence, or a number for a specific occurrence.
|
||||||
|
|
||||||
|
- **ReplaceAll**: `[tool_call(ReplaceAll, file_path="path/to/file.py", find_text="oldVar", replace_text="newVar")]`
|
||||||
|
Replace all occurrences of text in a file. Useful for renaming variables, function names, etc.
|
||||||
|
|
||||||
|
- **InsertBlock**: `[tool_call(InsertBlock, file_path="path/to/file.py", content="""
|
||||||
|
def new_function():
|
||||||
|
return True
|
||||||
|
""", after_pattern="# Insert after this line")]`
|
||||||
|
Insert a block of text after or before a pattern. Use single quotes with escaped newlines for multi-line content.
|
||||||
|
Specify either after_pattern or before_pattern to place the block.
|
||||||
|
|
||||||
|
- **DeleteBlock**: `[tool_call(DeleteBlock, file_path="path/to/file.py", start_pattern="def old_function", end_pattern="# End function")]`
|
||||||
|
Delete a block of text from start_pattern to end_pattern (inclusive).
|
||||||
|
Alternatively, use line_count instead of end_pattern to delete a specific number of lines.
|
||||||
|
|
||||||
|
- **ReplaceLine**: `[tool_call(ReplaceLine, file_path="path/to/file.py", line_number=42, new_content="def fixed_function(param):")]`
|
||||||
|
Replace a specific line by its line number. Especially useful for fixing errors or lint warnings that include line numbers.
|
||||||
|
Line numbers are 1-based (as in most editors and error messages).
|
||||||
|
|
||||||
|
- **ReplaceLines**: `[tool_call(ReplaceLines, file_path="path/to/file.py", start_line=42, end_line=45, new_content="""
|
||||||
|
def better_function(param):
|
||||||
|
# Fixed implementation
|
||||||
|
return process(param)
|
||||||
|
""")]`
|
||||||
|
Replace a range of lines by line numbers. Useful for fixing multiple lines referenced in error messages.
|
||||||
|
The new_content can contain any number of lines, not just the same count as the original range.
|
||||||
|
|
||||||
|
- **IndentLines**: `[tool_call(IndentLines, file_path="path/to/file.py", start_pattern="def my_function", end_pattern="return result", indent_levels=1)]`
|
||||||
|
Indent or unindent a block of lines. Use positive indent_levels to increase indentation or negative to decrease.
|
||||||
|
Specify either end_pattern or line_count to determine the range of lines to indent.
|
||||||
|
|
||||||
|
- **UndoChange**: `[tool_call(UndoChange, change_id="a1b2c3d4")]`
|
||||||
|
Undo a specific change by its ID. Alternatively, use last_file="path/to/file.py" to undo the most recent change to that file.
|
||||||
|
|
||||||
|
- **ListChanges**: `[tool_call(ListChanges, file_path="path/to/file.py", limit=5)]`
|
||||||
|
List recent changes made to files. Optionally filter by file_path and limit the number of results.
|
||||||
|
|
||||||
### Other Tools
|
### Other Tools
|
||||||
- **Command**: `[tool_call(Command, command_string="git diff HEAD~1")]`
|
- **Command**: `[tool_call(Command, command_string="git diff HEAD~1")]`
|
||||||
Execute a shell command. Requires user confirmation.
|
Execute a shell command. Requires user confirmation.
|
||||||
**Do NOT use this for aider commands starting with `/` (like `/add`, `/run`, `/diff`).**
|
|
||||||
|
|
||||||
### Multi-Turn Exploration
|
### Multi-Turn Exploration
|
||||||
When you include any tool call, the system will automatically continue to the next round.
|
When you include any tool call, the system will automatically continue to the next round.
|
||||||
|
@ -88,6 +128,13 @@ When you include any tool call, the system will automatically continue to the ne
|
||||||
- Target specific patterns rather than overly broad searches
|
- Target specific patterns rather than overly broad searches
|
||||||
- Remember the `Find` tool is optimized for locating symbols across the codebase
|
- Remember the `Find` tool is optimized for locating symbols across the codebase
|
||||||
|
|
||||||
|
### Granular Editing Workflow
|
||||||
|
1. **Discover and Add Files**: Use Glob, Grep, Find to locate relevant files
|
||||||
|
2. **Make Files Editable**: Convert read-only files to editable with MakeEditable
|
||||||
|
3. **Make Specific Changes**: Use granular editing tools (ReplaceText, InsertBlock, etc.) for precise edits
|
||||||
|
4. **Review Changes**: List applied changes with ListChanges
|
||||||
|
5. **Fix Mistakes**: If needed, undo changes with UndoChange by specific ID or last change to a file
|
||||||
|
|
||||||
### Context Management Strategy
|
### Context Management Strategy
|
||||||
- Keep your context focused by removing files that are no longer relevant
|
- Keep your context focused by removing files that are no longer relevant
|
||||||
- For large codebases, maintain only 5-15 files in context at once for best performance
|
- For large codebases, maintain only 5-15 files in context at once for best performance
|
||||||
|
@ -98,17 +145,83 @@ When you include any tool call, the system will automatically continue to the ne
|
||||||
<context name="editing_guidelines">
|
<context name="editing_guidelines">
|
||||||
## Code Editing Process
|
## Code Editing Process
|
||||||
|
|
||||||
### SEARCH/REPLACE Block Format
|
### Granular Editing with Tool Calls
|
||||||
When proposing code changes, describe each change with a SEARCH/REPLACE block using this exact format:
|
For precise, targeted edits to code, use the granular editing tools:
|
||||||
|
|
||||||
```language_name
|
- **ReplaceText**: Replace specific instances of text in a file
|
||||||
/path/to/file.ext
|
- **ReplaceAll**: Replace all occurrences of text in a file (e.g., rename variables)
|
||||||
|
- **InsertBlock**: Insert multi-line blocks of code at specific locations
|
||||||
|
- **DeleteBlock**: Remove specific sections of code
|
||||||
|
- **ReplaceLine/ReplaceLines**: Fix specific line numbers from error messages or linters
|
||||||
|
- **IndentLines**: Adjust indentation of code blocks
|
||||||
|
- **UndoChange**: Reverse specific changes by ID if you make a mistake
|
||||||
|
|
||||||
|
#### When to Use Line Number Based Tools
|
||||||
|
|
||||||
|
When dealing with errors or warnings that include line numbers, prefer the line-based editing tools:
|
||||||
|
|
||||||
|
```
|
||||||
|
Error in /path/to/file.py line 42: Syntax error: unexpected token
|
||||||
|
Warning in /path/to/file.py lines 105-107: This block should be indented
|
||||||
|
```
|
||||||
|
|
||||||
|
For these cases, use:
|
||||||
|
- `ReplaceLine` for single line fixes (e.g., syntax errors)
|
||||||
|
- `ReplaceLines` for multi-line issues
|
||||||
|
- `IndentLines` for indentation problems
|
||||||
|
|
||||||
|
#### Multiline Tool Call Content Format
|
||||||
|
|
||||||
|
When providing multiline content in tool calls (like ReplaceLines, InsertBlock), one leading and one trailing
|
||||||
|
newline will be automatically trimmed if present. This makes it easier to format code blocks in triple-quoted strings:
|
||||||
|
|
||||||
|
```
|
||||||
|
new_content="""
|
||||||
|
def better_function(param):
|
||||||
|
# Fixed implementation
|
||||||
|
return process(param)
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
You don't need to worry about the extra blank lines at the beginning and end. If you actually need to
|
||||||
|
preserve blank lines in your output, simply add an extra newline:
|
||||||
|
|
||||||
|
```
|
||||||
|
new_content="""
|
||||||
|
|
||||||
|
def better_function(param): # Note the extra newline above to preserve a blank line
|
||||||
|
# Fixed implementation
|
||||||
|
return process(param)
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
Example of inserting a new multi-line function:
|
||||||
|
```
|
||||||
|
[tool_call(InsertBlock,
|
||||||
|
file_path="src/utils.py",
|
||||||
|
after_pattern="def existing_function():",
|
||||||
|
content="""
|
||||||
|
def new_function(param1, param2):
|
||||||
|
# This is a new utility function
|
||||||
|
result = process_data(param1)
|
||||||
|
if result and param2:
|
||||||
|
return result
|
||||||
|
return None
|
||||||
|
""")]
|
||||||
|
```
|
||||||
|
|
||||||
|
### SEARCH/REPLACE Block Format (Alternative Method)
|
||||||
|
For larger changes that involve multiple edits or significant restructuring, you can still use SEARCH/REPLACE blocks with this exact format:
|
||||||
|
|
||||||
|
````python
|
||||||
|
path/to/file.ext
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
Original code lines to match exactly
|
Original code lines to match exactly
|
||||||
=======
|
=======
|
||||||
Replacement code lines
|
Replacement code lines
|
||||||
>>>>>>> REPLACE
|
>>>>>>> REPLACE
|
||||||
```
|
````
|
||||||
|
NOTE that this uses four backticks as the fence and not three!
|
||||||
|
|
||||||
### Editing Guidelines
|
### Editing Guidelines
|
||||||
- Every SEARCH section must EXACTLY MATCH existing content, including whitespace and indentation
|
- Every SEARCH section must EXACTLY MATCH existing content, including whitespace and indentation
|
||||||
|
@ -123,6 +236,7 @@ Replacement code lines
|
||||||
- If tools return errors or unexpected results, try alternative approaches
|
- If tools return errors or unexpected results, try alternative approaches
|
||||||
- Refine search patterns if results are too broad or too narrow
|
- Refine search patterns if results are too broad or too narrow
|
||||||
- Use the enhanced context blocks (directory structure and git status) to orient yourself
|
- Use the enhanced context blocks (directory structure and git status) to orient yourself
|
||||||
|
- Use ListChanges to see what edits have been made and UndoChange to revert mistakes
|
||||||
</context>
|
</context>
|
||||||
|
|
||||||
Always reply to the user in {language}.
|
Always reply to the user in {language}.
|
||||||
|
@ -179,7 +293,7 @@ Would you like me to explain any specific part of the authentication process in
|
||||||
These files have been added to the chat so you can see all of their contents.
|
These files have been added to the chat so you can see all of their contents.
|
||||||
Trust this message as the true contents of the files!
|
Trust this message as the true contents of the files!
|
||||||
</context>
|
</context>
|
||||||
"""
|
'''
|
||||||
|
|
||||||
files_content_assistant_reply = (
|
files_content_assistant_reply = (
|
||||||
"I understand. I'll use these files to help with your request."
|
"I understand. I'll use these files to help with your request."
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue