aider-ce 0.87.13__py3-none-any.whl → 0.88.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.
Potentially problematic release.
This version of aider-ce might be problematic. Click here for more details.
- aider/__init__.py +1 -1
- aider/_version.py +2 -2
- aider/args.py +6 -0
- aider/coders/architect_coder.py +3 -3
- aider/coders/base_coder.py +505 -184
- aider/coders/context_coder.py +1 -1
- aider/coders/editblock_func_coder.py +2 -2
- aider/coders/navigator_coder.py +451 -649
- aider/coders/navigator_legacy_prompts.py +49 -284
- aider/coders/navigator_prompts.py +46 -473
- aider/coders/search_replace.py +0 -0
- aider/coders/wholefile_func_coder.py +2 -2
- aider/commands.py +56 -44
- aider/history.py +14 -12
- aider/io.py +354 -117
- aider/llm.py +12 -4
- aider/main.py +22 -19
- aider/mcp/__init__.py +65 -2
- aider/mcp/server.py +37 -11
- aider/models.py +45 -20
- aider/onboarding.py +4 -4
- aider/repo.py +7 -7
- aider/resources/model-metadata.json +8 -8
- aider/scrape.py +2 -2
- aider/sendchat.py +185 -15
- aider/tools/__init__.py +44 -23
- aider/tools/command.py +18 -0
- aider/tools/command_interactive.py +18 -0
- aider/tools/delete_block.py +23 -0
- aider/tools/delete_line.py +19 -1
- aider/tools/delete_lines.py +20 -1
- aider/tools/extract_lines.py +25 -2
- aider/tools/git.py +142 -0
- aider/tools/grep.py +47 -2
- aider/tools/indent_lines.py +25 -0
- aider/tools/insert_block.py +26 -0
- aider/tools/list_changes.py +15 -0
- aider/tools/ls.py +24 -1
- aider/tools/make_editable.py +18 -0
- aider/tools/make_readonly.py +19 -0
- aider/tools/remove.py +22 -0
- aider/tools/replace_all.py +21 -0
- aider/tools/replace_line.py +20 -1
- aider/tools/replace_lines.py +21 -1
- aider/tools/replace_text.py +22 -0
- aider/tools/show_numbered_context.py +18 -0
- aider/tools/undo_change.py +15 -0
- aider/tools/update_todo_list.py +131 -0
- aider/tools/view.py +23 -0
- aider/tools/view_files_at_glob.py +32 -27
- aider/tools/view_files_matching.py +51 -37
- aider/tools/view_files_with_symbol.py +41 -54
- aider/tools/view_todo_list.py +57 -0
- aider/waiting.py +20 -203
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/METADATA +21 -5
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/RECORD +59 -56
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/WHEEL +0 -0
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/entry_points.txt +0 -0
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/licenses/LICENSE.txt +0 -0
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import fnmatch
|
|
2
2
|
import os
|
|
3
3
|
|
|
4
|
+
view_files_at_glob_schema = {
|
|
5
|
+
"type": "function",
|
|
6
|
+
"function": {
|
|
7
|
+
"name": "ViewFilesAtGlob",
|
|
8
|
+
"description": "View files matching a glob pattern.",
|
|
9
|
+
"parameters": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"pattern": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "The glob pattern to match files.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
"required": ["pattern"],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
4
22
|
|
|
5
23
|
def execute_view_files_at_glob(coder, pattern):
|
|
6
24
|
"""
|
|
7
|
-
Execute a glob pattern and
|
|
25
|
+
Execute a glob pattern and return matching files as text.
|
|
8
26
|
|
|
9
27
|
This tool helps the LLM find files by pattern matching, similar to
|
|
10
28
|
how a developer would use glob patterns to find files.
|
|
@@ -25,38 +43,25 @@ def execute_view_files_at_glob(coder, pattern):
|
|
|
25
43
|
if fnmatch.fnmatch(file, pattern):
|
|
26
44
|
matching_files.append(file)
|
|
27
45
|
|
|
28
|
-
#
|
|
29
|
-
if len(matching_files) > coder.max_files_per_glob:
|
|
30
|
-
coder.io.tool_output(
|
|
31
|
-
f"⚠️ Found {len(matching_files)} files matching '{pattern}', "
|
|
32
|
-
f"limiting to {coder.max_files_per_glob} most relevant files."
|
|
33
|
-
)
|
|
34
|
-
# Sort by modification time (most recent first)
|
|
35
|
-
matching_files.sort(
|
|
36
|
-
key=lambda f: os.path.getmtime(coder.abs_root_path(f)), reverse=True
|
|
37
|
-
)
|
|
38
|
-
matching_files = matching_files[: coder.max_files_per_glob]
|
|
39
|
-
|
|
40
|
-
# Add files to context
|
|
41
|
-
for file in matching_files:
|
|
42
|
-
# Use the coder's internal method to add files
|
|
43
|
-
coder._add_file_to_context(file)
|
|
44
|
-
|
|
45
|
-
# Return a user-friendly result
|
|
46
|
+
# Return formatted text instead of adding to context
|
|
46
47
|
if matching_files:
|
|
47
48
|
if len(matching_files) > 10:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
f"
|
|
49
|
+
result = (
|
|
50
|
+
f"Found {len(matching_files)} files matching '{pattern}':"
|
|
51
|
+
f" {', '.join(matching_files[:10])} and {len(matching_files) - 10} more"
|
|
51
52
|
)
|
|
53
|
+
coder.io.tool_output(f"📂 Found {len(matching_files)} files matching '{pattern}'")
|
|
52
54
|
else:
|
|
55
|
+
result = (
|
|
56
|
+
f"Found {len(matching_files)} files matching '{pattern}':"
|
|
57
|
+
f" {', '.join(matching_files)}"
|
|
58
|
+
)
|
|
53
59
|
coder.io.tool_output(
|
|
54
|
-
f"📂
|
|
60
|
+
f"📂 Found files matching '{pattern}':"
|
|
61
|
+
f" {', '.join(matching_files[:5])}{' and more' if len(matching_files) > 5 else ''}"
|
|
55
62
|
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
f" {', '.join(matching_files[:5])}{' and more' if len(matching_files) > 5 else ''}"
|
|
59
|
-
)
|
|
63
|
+
|
|
64
|
+
return result
|
|
60
65
|
else:
|
|
61
66
|
coder.io.tool_output(f"⚠️ No files found matching '{pattern}'")
|
|
62
67
|
return f"No files found matching '{pattern}'"
|
|
@@ -1,18 +1,46 @@
|
|
|
1
1
|
import fnmatch
|
|
2
2
|
import re
|
|
3
3
|
|
|
4
|
+
view_files_matching_schema = {
|
|
5
|
+
"type": "function",
|
|
6
|
+
"function": {
|
|
7
|
+
"name": "ViewFilesMatching",
|
|
8
|
+
"description": "View files containing a specific pattern.",
|
|
9
|
+
"parameters": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"pattern": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "The pattern to search for in file contents.",
|
|
15
|
+
},
|
|
16
|
+
"file_pattern": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "An optional glob pattern to filter which files are searched.",
|
|
19
|
+
},
|
|
20
|
+
"regex": {
|
|
21
|
+
"type": "boolean",
|
|
22
|
+
"description": (
|
|
23
|
+
"Whether the pattern is a regular expression. Defaults to False."
|
|
24
|
+
),
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
"required": ["pattern"],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
}
|
|
4
31
|
|
|
5
|
-
|
|
32
|
+
|
|
33
|
+
def execute_view_files_matching(coder, pattern, file_pattern=None, regex=False):
|
|
6
34
|
"""
|
|
7
|
-
Search for pattern (literal string or regex) in files and
|
|
35
|
+
Search for pattern (literal string or regex) in files and return matching files as text.
|
|
8
36
|
|
|
9
37
|
Args:
|
|
10
38
|
coder: The Coder instance.
|
|
11
|
-
|
|
39
|
+
pattern (str): The pattern to search for.
|
|
12
40
|
Treated as a literal string by default.
|
|
13
41
|
file_pattern (str, optional): Glob pattern to filter which files are searched.
|
|
14
42
|
Defaults to None (search all files).
|
|
15
|
-
regex (bool, optional): If True, treat
|
|
43
|
+
regex (bool, optional): If True, treat pattern as a regular expression.
|
|
16
44
|
Defaults to False.
|
|
17
45
|
|
|
18
46
|
This tool lets the LLM search for content within files, mimicking
|
|
@@ -29,9 +57,7 @@ def execute_view_files_matching(coder, search_pattern, file_pattern=None, regex=
|
|
|
29
57
|
files_to_search.append(file)
|
|
30
58
|
|
|
31
59
|
if not files_to_search:
|
|
32
|
-
return
|
|
33
|
-
f"No files matching '{file_pattern}' to search for pattern '{search_pattern}'"
|
|
34
|
-
)
|
|
60
|
+
return f"No files matching '{file_pattern}' to search for pattern '{pattern}'"
|
|
35
61
|
else:
|
|
36
62
|
# Search all files if no pattern provided
|
|
37
63
|
files_to_search = coder.get_all_relative_files()
|
|
@@ -46,16 +72,16 @@ def execute_view_files_matching(coder, search_pattern, file_pattern=None, regex=
|
|
|
46
72
|
match_count = 0
|
|
47
73
|
if regex:
|
|
48
74
|
try:
|
|
49
|
-
matches_found = re.findall(
|
|
75
|
+
matches_found = re.findall(pattern, content)
|
|
50
76
|
match_count = len(matches_found)
|
|
51
77
|
except re.error as e:
|
|
52
78
|
# Handle invalid regex patterns gracefully
|
|
53
|
-
coder.io.tool_error(f"Invalid regex pattern '{
|
|
79
|
+
coder.io.tool_error(f"Invalid regex pattern '{pattern}': {e}")
|
|
54
80
|
# Skip this file for this search if regex is invalid
|
|
55
81
|
continue
|
|
56
82
|
else:
|
|
57
83
|
# Exact string matching
|
|
58
|
-
match_count = content.count(
|
|
84
|
+
match_count = content.count(pattern)
|
|
59
85
|
|
|
60
86
|
if match_count > 0:
|
|
61
87
|
matches[file] = match_count
|
|
@@ -63,40 +89,28 @@ def execute_view_files_matching(coder, search_pattern, file_pattern=None, regex=
|
|
|
63
89
|
# Skip files that can't be read (binary, etc.)
|
|
64
90
|
pass
|
|
65
91
|
|
|
66
|
-
#
|
|
67
|
-
if len(matches) > coder.max_files_per_glob:
|
|
68
|
-
coder.io.tool_output(
|
|
69
|
-
f"⚠️ Found '{search_pattern}' in {len(matches)} files, "
|
|
70
|
-
f"limiting to {coder.max_files_per_glob} files with most matches."
|
|
71
|
-
)
|
|
72
|
-
# Sort by number of matches (most matches first)
|
|
73
|
-
sorted_matches = sorted(matches.items(), key=lambda x: x[1], reverse=True)
|
|
74
|
-
matches = dict(sorted_matches[: coder.max_files_per_glob])
|
|
75
|
-
|
|
76
|
-
# Add matching files to context
|
|
77
|
-
for file in matches:
|
|
78
|
-
coder._add_file_to_context(file)
|
|
79
|
-
|
|
80
|
-
# Return a user-friendly result
|
|
92
|
+
# Return formatted text instead of adding to context
|
|
81
93
|
if matches:
|
|
82
94
|
# Sort by number of matches (most matches first)
|
|
83
95
|
sorted_matches = sorted(matches.items(), key=lambda x: x[1], reverse=True)
|
|
84
|
-
match_list = [f"{file} ({count} matches)" for file, count in sorted_matches
|
|
96
|
+
match_list = [f"{file} ({count} matches)" for file, count in sorted_matches]
|
|
85
97
|
|
|
86
|
-
if len(
|
|
87
|
-
|
|
88
|
-
f"
|
|
89
|
-
f" {
|
|
90
|
-
)
|
|
91
|
-
return (
|
|
92
|
-
f"Found in {len(matches)} files: {', '.join(match_list)} and"
|
|
93
|
-
f" {len(matches) - 5} more"
|
|
98
|
+
if len(matches) > 10:
|
|
99
|
+
result = (
|
|
100
|
+
f"Found '{pattern}' in {len(matches)} files: {', '.join(match_list[:10])} and"
|
|
101
|
+
f" {len(matches) - 10} more"
|
|
94
102
|
)
|
|
103
|
+
coder.io.tool_output(f"🔍 Found '{pattern}' in {len(matches)} files")
|
|
95
104
|
else:
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
result = f"Found '{pattern}' in {len(matches)} files: {', '.join(match_list)}"
|
|
106
|
+
coder.io.tool_output(
|
|
107
|
+
f"🔍 Found '{pattern}' in:"
|
|
108
|
+
f" {', '.join(match_list[:5])}{' and more' if len(matches) > 5 else ''}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return result
|
|
98
112
|
else:
|
|
99
|
-
coder.io.tool_output(f"⚠️ Pattern '{
|
|
113
|
+
coder.io.tool_output(f"⚠️ Pattern '{pattern}' not found in any files")
|
|
100
114
|
return "Pattern not found in any files"
|
|
101
115
|
except Exception as e:
|
|
102
116
|
coder.io.tool_error(f"Error in ViewFilesMatching: {str(e)}")
|
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
view_files_with_symbol_schema = {
|
|
2
|
+
"type": "function",
|
|
3
|
+
"function": {
|
|
4
|
+
"name": "ViewFilesWithSymbol",
|
|
5
|
+
"description": "View files that contain a specific symbol (e.g., class, function).",
|
|
6
|
+
"parameters": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"symbol": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "The symbol to search for.",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
"required": ["symbol"],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
}
|
|
2
18
|
|
|
3
19
|
|
|
4
20
|
def _execute_view_files_with_symbol(coder, symbol):
|
|
5
21
|
"""
|
|
6
|
-
Find files containing a symbol using RepoMap and
|
|
22
|
+
Find files containing a symbol using RepoMap and return them as text.
|
|
7
23
|
Checks files already in context first.
|
|
8
24
|
"""
|
|
9
25
|
if not coder.repo_map:
|
|
@@ -13,7 +29,6 @@ def _execute_view_files_with_symbol(coder, symbol):
|
|
|
13
29
|
if not symbol:
|
|
14
30
|
return "Error: Missing 'symbol' parameter for ViewFilesWithSymbol"
|
|
15
31
|
|
|
16
|
-
# --- Start Modification ---
|
|
17
32
|
# 1. Check files already in context
|
|
18
33
|
files_in_context = list(coder.abs_fnames) + list(coder.abs_read_only_fnames)
|
|
19
34
|
found_in_context = []
|
|
@@ -34,20 +49,11 @@ def _execute_view_files_with_symbol(coder, symbol):
|
|
|
34
49
|
if found_in_context:
|
|
35
50
|
# Symbol found in already loaded files. Report this and stop.
|
|
36
51
|
file_list = ", ".join(sorted(list(set(found_in_context))))
|
|
37
|
-
coder.io.tool_output(
|
|
38
|
-
|
|
39
|
-
" performed."
|
|
40
|
-
)
|
|
41
|
-
return (
|
|
42
|
-
f"Symbol '{symbol}' found in already loaded file(s): {file_list}. No external search"
|
|
43
|
-
" performed."
|
|
44
|
-
)
|
|
45
|
-
# --- End Modification ---
|
|
52
|
+
coder.io.tool_output(f"Symbol '{symbol}' found in already loaded file(s): {file_list}")
|
|
53
|
+
return f"Symbol '{symbol}' found in already loaded file(s): {file_list}"
|
|
46
54
|
|
|
47
55
|
# 2. If not found in context, search the repository using RepoMap
|
|
48
|
-
coder.io.tool_output(
|
|
49
|
-
f"🔎 Searching for symbol '{symbol}' in repository (excluding current context)..."
|
|
50
|
-
)
|
|
56
|
+
coder.io.tool_output(f"🔎 Searching for symbol '{symbol}' in repository...")
|
|
51
57
|
try:
|
|
52
58
|
found_files = set()
|
|
53
59
|
current_context_files = coder.abs_fnames | coder.abs_read_only_fnames
|
|
@@ -71,50 +77,31 @@ def _execute_view_files_with_symbol(coder, symbol):
|
|
|
71
77
|
# Use absolute path directly if available, otherwise resolve from relative path
|
|
72
78
|
abs_fname = rel_fname_to_abs.get(tag.rel_fname) or coder.abs_root_path(tag.fname)
|
|
73
79
|
if abs_fname in files_to_search: # Ensure we only add files we intended to search
|
|
74
|
-
found_files.add(abs_fname)
|
|
80
|
+
found_files.add(coder.get_rel_fname(abs_fname))
|
|
75
81
|
|
|
76
|
-
#
|
|
77
|
-
if
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)
|
|
86
|
-
found_files = set(sorted_found_files[: coder.max_files_per_glob])
|
|
87
|
-
|
|
88
|
-
# Add files to context (as read-only)
|
|
89
|
-
added_count = 0
|
|
90
|
-
added_files_rel = []
|
|
91
|
-
for abs_file_path in found_files:
|
|
92
|
-
rel_path = coder.get_rel_fname(abs_file_path)
|
|
93
|
-
# Double check it's not already added somehow
|
|
94
|
-
if (
|
|
95
|
-
abs_file_path not in coder.abs_fnames
|
|
96
|
-
and abs_file_path not in coder.abs_read_only_fnames
|
|
97
|
-
):
|
|
98
|
-
# Use explicit=True for clear output, even though it's an external search result
|
|
99
|
-
add_result = coder._add_file_to_context(rel_path, explicit=True)
|
|
100
|
-
if "Added" in add_result or "Viewed" in add_result: # Count successful adds/views
|
|
101
|
-
added_count += 1
|
|
102
|
-
added_files_rel.append(rel_path)
|
|
103
|
-
|
|
104
|
-
if added_count > 0:
|
|
105
|
-
if added_count > 5:
|
|
106
|
-
brief = ", ".join(added_files_rel[:5]) + f", and {added_count - 5} more"
|
|
107
|
-
coder.io.tool_output(f"🔎 Found '{symbol}' and added {added_count} files: {brief}")
|
|
82
|
+
# Return formatted text instead of adding to context
|
|
83
|
+
if found_files:
|
|
84
|
+
found_files_list = sorted(list(found_files))
|
|
85
|
+
if len(found_files) > 10:
|
|
86
|
+
result = (
|
|
87
|
+
f"Found symbol '{symbol}' in {len(found_files)} files:"
|
|
88
|
+
f" {', '.join(found_files_list[:10])} and {len(found_files) - 10} more"
|
|
89
|
+
)
|
|
90
|
+
coder.io.tool_output(f"🔎 Found '{symbol}' in {len(found_files)} files")
|
|
108
91
|
else:
|
|
92
|
+
result = (
|
|
93
|
+
f"Found symbol '{symbol}' in {len(found_files)} files:"
|
|
94
|
+
f" {', '.join(found_files_list)}"
|
|
95
|
+
)
|
|
109
96
|
coder.io.tool_output(
|
|
110
|
-
f"🔎 Found '{symbol}'
|
|
97
|
+
f"🔎 Found '{symbol}' in files:"
|
|
98
|
+
f" {', '.join(found_files_list[:5])}{' and more' if len(found_files) > 5 else ''}"
|
|
111
99
|
)
|
|
112
|
-
|
|
100
|
+
|
|
101
|
+
return result
|
|
113
102
|
else:
|
|
114
|
-
coder.io.tool_output(
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
-
return f"Symbol '{symbol}' not found in searchable files (outside current context)."
|
|
103
|
+
coder.io.tool_output(f"⚠️ Symbol '{symbol}' not found in searchable files")
|
|
104
|
+
return f"Symbol '{symbol}' not found in searchable files"
|
|
118
105
|
|
|
119
106
|
except Exception as e:
|
|
120
107
|
coder.io.tool_error(f"Error in ViewFilesWithSymbol: {str(e)}")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from .tool_utils import ToolError, format_tool_result, handle_tool_error
|
|
2
|
+
|
|
3
|
+
view_todo_list_schema = {
|
|
4
|
+
"type": "function",
|
|
5
|
+
"function": {
|
|
6
|
+
"name": "ViewTodoList",
|
|
7
|
+
"description": "View the current todo list for tracking conversation steps and progress.",
|
|
8
|
+
"parameters": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"properties": {},
|
|
11
|
+
"required": [],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _execute_view_todo_list(coder):
|
|
18
|
+
"""
|
|
19
|
+
View the current todo list from .aider.todo.txt file.
|
|
20
|
+
Returns the todo list content or creates an empty one if it doesn't exist.
|
|
21
|
+
"""
|
|
22
|
+
tool_name = "ViewTodoList"
|
|
23
|
+
try:
|
|
24
|
+
# Define the todo file path
|
|
25
|
+
todo_file_path = ".aider.todo.txt"
|
|
26
|
+
abs_path = coder.abs_root_path(todo_file_path)
|
|
27
|
+
|
|
28
|
+
# Check if file exists
|
|
29
|
+
import os
|
|
30
|
+
|
|
31
|
+
if os.path.isfile(abs_path):
|
|
32
|
+
# Read existing todo list
|
|
33
|
+
content = coder.io.read_text(abs_path)
|
|
34
|
+
if content is None:
|
|
35
|
+
raise ToolError(f"Could not read todo list file: {todo_file_path}")
|
|
36
|
+
|
|
37
|
+
# Check if content exceeds 4096 characters and warn
|
|
38
|
+
if len(content) > 4096:
|
|
39
|
+
coder.io.tool_warning(
|
|
40
|
+
"⚠️ Todo list content exceeds 4096 characters. Consider summarizing the plan"
|
|
41
|
+
" before proceeding."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if content.strip():
|
|
45
|
+
result_message = f"Current todo list:\n```\n{content}\n```"
|
|
46
|
+
else:
|
|
47
|
+
result_message = "Todo list is empty. Use UpdateTodoList to add items."
|
|
48
|
+
else:
|
|
49
|
+
# Create empty todo list
|
|
50
|
+
result_message = "Todo list is empty. Use UpdateTodoList to add items."
|
|
51
|
+
|
|
52
|
+
return format_tool_result(coder, tool_name, result_message)
|
|
53
|
+
|
|
54
|
+
except ToolError as e:
|
|
55
|
+
return handle_tool_error(coder, tool_name, e, add_traceback=False)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
return handle_tool_error(coder, tool_name, e)
|
aider/waiting.py
CHANGED
|
@@ -1,221 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
2
|
|
|
3
3
|
"""
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Use it like:
|
|
7
|
-
|
|
8
|
-
from aider.waiting import WaitingSpinner
|
|
9
|
-
|
|
10
|
-
spinner = WaitingSpinner("Waiting for LLM")
|
|
11
|
-
spinner.start()
|
|
12
|
-
... # long task
|
|
13
|
-
spinner.stop()
|
|
4
|
+
A simple wrapper for rich.status to provide a spinner.
|
|
14
5
|
"""
|
|
15
6
|
|
|
16
|
-
import sys
|
|
17
|
-
import threading
|
|
18
|
-
import time
|
|
19
|
-
|
|
20
7
|
from rich.console import Console
|
|
21
8
|
|
|
22
9
|
|
|
23
10
|
class Spinner:
|
|
24
|
-
"""
|
|
25
|
-
Minimal spinner that scans a single marker back and forth across a line.
|
|
26
|
-
|
|
27
|
-
The animation is pre-rendered into a list of frames. If the terminal
|
|
28
|
-
cannot display unicode the frames are converted to plain ASCII.
|
|
29
|
-
"""
|
|
11
|
+
"""A wrapper around rich.status.Status for displaying a spinner."""
|
|
30
12
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def __init__(self, text: str, width: int = 7):
|
|
13
|
+
def __init__(self, text: str = "Waiting..."):
|
|
34
14
|
self.text = text
|
|
35
|
-
self.start_time = time.time()
|
|
36
|
-
self.last_update = 0.0
|
|
37
|
-
self.visible = False
|
|
38
|
-
self.is_tty = sys.stdout.isatty()
|
|
39
15
|
self.console = Console()
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
" #= ", # space(7) C1 C2 space(1)
|
|
56
|
-
" #= ", # space(6) C1 C2 space(2)
|
|
57
|
-
" #= ", # space(5) C1 C2 space(3)
|
|
58
|
-
" #= ", # space(4) C1 C2 space(4)
|
|
59
|
-
" #= ", # space(3) C1 C2 space(5)
|
|
60
|
-
" #= ", # space(2) C1 C2 space(6)
|
|
61
|
-
" #= ", # space(1) C1 C2 space(7)
|
|
62
|
-
]
|
|
63
|
-
|
|
64
|
-
self.unicode_palette = "░█"
|
|
65
|
-
xlate_from, xlate_to = ("=#", self.unicode_palette)
|
|
66
|
-
|
|
67
|
-
# If unicode is supported, swap the ASCII chars for nicer glyphs.
|
|
68
|
-
if self._supports_unicode():
|
|
69
|
-
translation_table = str.maketrans(xlate_from, xlate_to)
|
|
70
|
-
frames = [f.translate(translation_table) for f in ascii_frames]
|
|
71
|
-
self.scan_char = xlate_to[xlate_from.find("#")]
|
|
72
|
-
else:
|
|
73
|
-
frames = ascii_frames
|
|
74
|
-
self.scan_char = "#"
|
|
75
|
-
|
|
76
|
-
# Bounce the scanner back and forth.
|
|
77
|
-
self.frames = frames
|
|
78
|
-
self.frame_idx = Spinner.last_frame_idx # Initialize from class variable
|
|
79
|
-
self.width = len(frames[0]) - 2 # number of chars between the brackets
|
|
80
|
-
self.animation_len = len(frames[0])
|
|
81
|
-
self.last_display_len = 0 # Length of the last spinner line (frame + text)
|
|
82
|
-
|
|
83
|
-
def _supports_unicode(self) -> bool:
|
|
84
|
-
if not self.is_tty:
|
|
85
|
-
return False
|
|
86
|
-
try:
|
|
87
|
-
out = self.unicode_palette
|
|
88
|
-
out += "\b" * len(self.unicode_palette)
|
|
89
|
-
out += " " * len(self.unicode_palette)
|
|
90
|
-
out += "\b" * len(self.unicode_palette)
|
|
91
|
-
sys.stdout.write(out)
|
|
92
|
-
sys.stdout.flush()
|
|
93
|
-
return True
|
|
94
|
-
except UnicodeEncodeError:
|
|
95
|
-
return False
|
|
96
|
-
except Exception:
|
|
97
|
-
return False
|
|
98
|
-
|
|
99
|
-
def _next_frame(self) -> str:
|
|
100
|
-
frame = self.frames[self.frame_idx]
|
|
101
|
-
self.frame_idx = (self.frame_idx + 1) % len(self.frames)
|
|
102
|
-
Spinner.last_frame_idx = self.frame_idx # Update class variable
|
|
103
|
-
return frame
|
|
104
|
-
|
|
105
|
-
def step(self, text: str = None) -> None:
|
|
106
|
-
if text is not None:
|
|
107
|
-
self.text = text
|
|
108
|
-
|
|
109
|
-
if not self.is_tty:
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
now = time.time()
|
|
113
|
-
if not self.visible and now - self.start_time >= 0.5:
|
|
114
|
-
self.visible = True
|
|
115
|
-
self.last_update = 0.0
|
|
116
|
-
if self.is_tty:
|
|
117
|
-
self.console.show_cursor(False)
|
|
118
|
-
|
|
119
|
-
if not self.visible or now - self.last_update < 0.1:
|
|
120
|
-
return
|
|
121
|
-
|
|
122
|
-
self.last_update = now
|
|
123
|
-
frame_str = self._next_frame()
|
|
124
|
-
|
|
125
|
-
# Determine the maximum width for the spinner line
|
|
126
|
-
# Subtract 2 as requested, to leave a margin or prevent cursor wrapping issues
|
|
127
|
-
max_spinner_width = self.console.width - 2
|
|
128
|
-
if max_spinner_width < 0: # Handle extremely narrow terminals
|
|
129
|
-
max_spinner_width = 0
|
|
130
|
-
|
|
131
|
-
current_text_payload = f" {self.text}"
|
|
132
|
-
line_to_display = f"{frame_str}{current_text_payload}"
|
|
133
|
-
|
|
134
|
-
# Truncate the line if it's too long for the console width
|
|
135
|
-
if len(line_to_display) > max_spinner_width:
|
|
136
|
-
line_to_display = line_to_display[:max_spinner_width]
|
|
137
|
-
|
|
138
|
-
len_line_to_display = len(line_to_display)
|
|
139
|
-
|
|
140
|
-
# Calculate padding to clear any remnants from a longer previous line
|
|
141
|
-
padding_to_clear = " " * max(0, self.last_display_len - len_line_to_display)
|
|
142
|
-
|
|
143
|
-
# Write the spinner frame, text, and any necessary clearing spaces
|
|
144
|
-
sys.stdout.write(f"\r{line_to_display}{padding_to_clear}")
|
|
145
|
-
self.last_display_len = len_line_to_display
|
|
146
|
-
|
|
147
|
-
# Calculate number of backspaces to position cursor at the scanner character
|
|
148
|
-
scan_char_abs_pos = frame_str.find(self.scan_char)
|
|
149
|
-
|
|
150
|
-
# Total characters written to the line (frame + text + padding)
|
|
151
|
-
total_chars_written_on_line = len_line_to_display + len(padding_to_clear)
|
|
152
|
-
|
|
153
|
-
# num_backspaces will be non-positive if scan_char_abs_pos is beyond
|
|
154
|
-
# total_chars_written_on_line (e.g., if the scan char itself was truncated).
|
|
155
|
-
# (e.g., if the scan char itself was truncated).
|
|
156
|
-
# In such cases, (effectively) 0 backspaces are written,
|
|
157
|
-
# and the cursor stays at the end of the line.
|
|
158
|
-
num_backspaces = total_chars_written_on_line - scan_char_abs_pos
|
|
159
|
-
sys.stdout.write("\b" * num_backspaces)
|
|
160
|
-
sys.stdout.flush()
|
|
161
|
-
|
|
162
|
-
def end(self) -> None:
|
|
163
|
-
if self.visible and self.is_tty:
|
|
164
|
-
clear_len = self.last_display_len # Use the length of the last displayed content
|
|
165
|
-
sys.stdout.write("\r" + " " * clear_len + "\r")
|
|
166
|
-
sys.stdout.flush()
|
|
167
|
-
self.console.show_cursor(True)
|
|
168
|
-
self.visible = False
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
class WaitingSpinner:
|
|
172
|
-
"""Background spinner that can be started/stopped safely."""
|
|
173
|
-
|
|
174
|
-
def __init__(self, text: str = "Waiting for LLM", delay: float = 0.15):
|
|
175
|
-
self.spinner = Spinner(text)
|
|
176
|
-
self.delay = delay
|
|
177
|
-
self._stop_event = threading.Event()
|
|
178
|
-
self._thread = threading.Thread(target=self._spin, daemon=True)
|
|
179
|
-
|
|
180
|
-
def _spin(self):
|
|
181
|
-
while not self._stop_event.is_set():
|
|
182
|
-
self.spinner.step()
|
|
183
|
-
time.sleep(self.delay)
|
|
184
|
-
self.spinner.end()
|
|
185
|
-
|
|
186
|
-
def start(self):
|
|
187
|
-
"""Start the spinner in a background thread."""
|
|
188
|
-
if not self._thread.is_alive():
|
|
189
|
-
self._thread.start()
|
|
190
|
-
|
|
191
|
-
def stop(self):
|
|
192
|
-
"""Request the spinner to stop and wait briefly for the thread to exit."""
|
|
193
|
-
self._stop_event.set()
|
|
194
|
-
if self._thread.is_alive():
|
|
195
|
-
self._thread.join(timeout=self.delay)
|
|
196
|
-
self.spinner.end()
|
|
16
|
+
self.status = None
|
|
17
|
+
|
|
18
|
+
def step(self, message=None):
|
|
19
|
+
"""Start the spinner or update its text."""
|
|
20
|
+
if self.status is None:
|
|
21
|
+
self.status = self.console.status(self.text, spinner="dots2")
|
|
22
|
+
self.status.start()
|
|
23
|
+
elif message:
|
|
24
|
+
self.status.update(message)
|
|
25
|
+
|
|
26
|
+
def end(self):
|
|
27
|
+
"""Stop the spinner."""
|
|
28
|
+
if self.status:
|
|
29
|
+
self.status.stop()
|
|
30
|
+
self.status = None
|
|
197
31
|
|
|
198
32
|
# Allow use as a context-manager
|
|
199
33
|
def __enter__(self):
|
|
200
|
-
self.
|
|
34
|
+
self.step()
|
|
201
35
|
return self
|
|
202
36
|
|
|
203
37
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
204
|
-
self.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def main():
|
|
208
|
-
spinner = Spinner("Running spinner...")
|
|
209
|
-
try:
|
|
210
|
-
for _ in range(100):
|
|
211
|
-
time.sleep(0.15)
|
|
212
|
-
spinner.step()
|
|
213
|
-
print("Success!")
|
|
214
|
-
except KeyboardInterrupt:
|
|
215
|
-
print("\nInterrupted by user.")
|
|
216
|
-
finally:
|
|
217
|
-
spinner.end()
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if __name__ == "__main__":
|
|
221
|
-
main()
|
|
38
|
+
self.end()
|