aider-ce 0.87.13.dev3__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.

Files changed (60) hide show
  1. aider/__init__.py +1 -1
  2. aider/_version.py +2 -2
  3. aider/args.py +6 -0
  4. aider/coders/architect_coder.py +3 -3
  5. aider/coders/base_coder.py +505 -184
  6. aider/coders/context_coder.py +1 -1
  7. aider/coders/editblock_func_coder.py +2 -2
  8. aider/coders/navigator_coder.py +451 -649
  9. aider/coders/navigator_legacy_prompts.py +49 -284
  10. aider/coders/navigator_prompts.py +46 -473
  11. aider/coders/search_replace.py +0 -0
  12. aider/coders/wholefile_func_coder.py +2 -2
  13. aider/commands.py +56 -44
  14. aider/history.py +14 -12
  15. aider/io.py +354 -117
  16. aider/llm.py +12 -4
  17. aider/main.py +22 -19
  18. aider/mcp/__init__.py +65 -2
  19. aider/mcp/server.py +37 -11
  20. aider/models.py +45 -20
  21. aider/onboarding.py +4 -4
  22. aider/repo.py +7 -7
  23. aider/resources/model-metadata.json +8 -8
  24. aider/scrape.py +2 -2
  25. aider/sendchat.py +185 -15
  26. aider/tools/__init__.py +44 -23
  27. aider/tools/command.py +18 -0
  28. aider/tools/command_interactive.py +18 -0
  29. aider/tools/delete_block.py +23 -0
  30. aider/tools/delete_line.py +19 -1
  31. aider/tools/delete_lines.py +20 -1
  32. aider/tools/extract_lines.py +25 -2
  33. aider/tools/git.py +142 -0
  34. aider/tools/grep.py +47 -2
  35. aider/tools/indent_lines.py +25 -0
  36. aider/tools/insert_block.py +26 -0
  37. aider/tools/list_changes.py +15 -0
  38. aider/tools/ls.py +24 -1
  39. aider/tools/make_editable.py +18 -0
  40. aider/tools/make_readonly.py +19 -0
  41. aider/tools/remove.py +22 -0
  42. aider/tools/replace_all.py +21 -0
  43. aider/tools/replace_line.py +20 -1
  44. aider/tools/replace_lines.py +21 -1
  45. aider/tools/replace_text.py +22 -0
  46. aider/tools/show_numbered_context.py +18 -0
  47. aider/tools/undo_change.py +15 -0
  48. aider/tools/update_todo_list.py +131 -0
  49. aider/tools/view.py +23 -0
  50. aider/tools/view_files_at_glob.py +32 -27
  51. aider/tools/view_files_matching.py +51 -37
  52. aider/tools/view_files_with_symbol.py +41 -54
  53. aider/tools/view_todo_list.py +57 -0
  54. aider/waiting.py +20 -203
  55. {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.0.dist-info}/METADATA +21 -5
  56. {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.0.dist-info}/RECORD +59 -56
  57. {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.0.dist-info}/WHEEL +0 -0
  58. {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.0.dist-info}/entry_points.txt +0 -0
  59. {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.0.dist-info}/licenses/LICENSE.txt +0 -0
  60. {aider_ce-0.87.13.dev3.dist-info → aider_ce-0.88.0.dist-info}/top_level.txt +0 -0
@@ -10,7 +10,7 @@ import time
10
10
  import traceback
11
11
 
12
12
  # Add necessary imports if not already present
13
- from collections import defaultdict
13
+ from collections import Counter, defaultdict
14
14
  from datetime import datetime
15
15
  from pathlib import Path
16
16
 
@@ -24,12 +24,48 @@ from aider.mcp.server import LocalServer
24
24
  from aider.repo import ANY_GIT_ERROR
25
25
 
26
26
  # Import run_cmd for potentially interactive execution and run_cmd_subprocess for guaranteed non-interactive
27
+ from aider.tools import (
28
+ command_interactive_schema,
29
+ command_schema,
30
+ delete_block_schema,
31
+ delete_line_schema,
32
+ delete_lines_schema,
33
+ extract_lines_schema,
34
+ grep_schema,
35
+ indent_lines_schema,
36
+ insert_block_schema,
37
+ list_changes_schema,
38
+ ls_schema,
39
+ make_editable_schema,
40
+ make_readonly_schema,
41
+ remove_schema,
42
+ replace_all_schema,
43
+ replace_line_schema,
44
+ replace_lines_schema,
45
+ replace_text_schema,
46
+ show_numbered_context_schema,
47
+ undo_change_schema,
48
+ update_todo_list_schema,
49
+ view_files_matching_schema,
50
+ view_files_with_symbol_schema,
51
+ view_schema,
52
+ )
27
53
  from aider.tools.command import _execute_command
28
54
  from aider.tools.command_interactive import _execute_command_interactive
29
55
  from aider.tools.delete_block import _execute_delete_block
30
56
  from aider.tools.delete_line import _execute_delete_line
31
57
  from aider.tools.delete_lines import _execute_delete_lines
32
58
  from aider.tools.extract_lines import _execute_extract_lines
59
+ from aider.tools.git import (
60
+ _execute_git_diff,
61
+ _execute_git_log,
62
+ _execute_git_show,
63
+ _execute_git_status,
64
+ git_diff_schema,
65
+ git_log_schema,
66
+ git_show_schema,
67
+ git_status_schema,
68
+ )
33
69
  from aider.tools.grep import _execute_grep
34
70
  from aider.tools.indent_lines import _execute_indent_lines
35
71
  from aider.tools.insert_block import _execute_insert_block
@@ -44,10 +80,10 @@ from aider.tools.replace_lines import _execute_replace_lines
44
80
  from aider.tools.replace_text import _execute_replace_text
45
81
  from aider.tools.show_numbered_context import execute_show_numbered_context
46
82
  from aider.tools.undo_change import _execute_undo_change
83
+ from aider.tools.update_todo_list import _execute_update_todo_list
47
84
  from aider.tools.view import execute_view
48
85
 
49
86
  # Import tool functions
50
- from aider.tools.view_files_at_glob import execute_view_files_at_glob
51
87
  from aider.tools.view_files_matching import execute_view_files_matching
52
88
  from aider.tools.view_files_with_symbol import _execute_view_files_with_symbol
53
89
 
@@ -56,6 +92,22 @@ from .editblock_coder import do_replace, find_original_update_blocks, find_simil
56
92
  from .navigator_legacy_prompts import NavigatorLegacyPrompts
57
93
  from .navigator_prompts import NavigatorPrompts
58
94
 
95
+ # UNUSED TOOL SCHEMAS
96
+ # view_files_matching_schema,
97
+ # grep_schema,
98
+ # replace_all_schema,
99
+ # insert_block_schema,
100
+ # delete_block_schema,
101
+ # replace_line_schema,
102
+ # replace_lines_schema,
103
+ # indent_lines_schema,
104
+ # delete_line_schema,
105
+ # delete_lines_schema,
106
+ # undo_change_schema,
107
+ # list_changes_schema,
108
+ # extract_lines_schema,
109
+ # show_numbered_context_schema,
110
+
59
111
 
60
112
  class NavigatorCoder(Coder):
61
113
  """Mode where the LLM autonomously manages which files are in context."""
@@ -75,6 +127,29 @@ class NavigatorCoder(Coder):
75
127
  # Dictionary to track recently removed files
76
128
  self.recently_removed = {}
77
129
 
130
+ # Tool usage history
131
+ self.tool_usage_history = []
132
+ self.tool_usage_retries = 10
133
+ self.read_tools = {
134
+ "viewfilesatglob",
135
+ "viewfilesmatching",
136
+ "ls",
137
+ "viewfileswithsymbol",
138
+ "grep",
139
+ "listchanges",
140
+ "extractlines",
141
+ "shownumberedcontext",
142
+ }
143
+ self.write_tools = {
144
+ "command",
145
+ "commandinteractive",
146
+ "insertblock",
147
+ "replaceblock",
148
+ "replaceall",
149
+ "replacetext",
150
+ "undochange",
151
+ }
152
+
78
153
  # Configuration parameters
79
154
  self.max_tool_calls = 100 # Maximum number of tool calls per response
80
155
 
@@ -110,11 +185,42 @@ class NavigatorCoder(Coder):
110
185
  self.tokens_calculated = False
111
186
 
112
187
  super().__init__(*args, **kwargs)
113
- self.initialize_local_tools()
114
188
 
115
- def initialize_local_tools(self):
116
- if not self.use_granular_editing:
117
- return
189
+ def get_local_tool_schemas(self):
190
+ """Returns the JSON schemas for all local tools."""
191
+ return [
192
+ view_files_matching_schema,
193
+ ls_schema,
194
+ view_schema,
195
+ remove_schema,
196
+ make_editable_schema,
197
+ make_readonly_schema,
198
+ view_files_with_symbol_schema,
199
+ command_schema,
200
+ command_interactive_schema,
201
+ grep_schema,
202
+ replace_text_schema,
203
+ replace_all_schema,
204
+ insert_block_schema,
205
+ delete_block_schema,
206
+ replace_line_schema,
207
+ replace_lines_schema,
208
+ indent_lines_schema,
209
+ delete_line_schema,
210
+ delete_lines_schema,
211
+ undo_change_schema,
212
+ list_changes_schema,
213
+ extract_lines_schema,
214
+ show_numbered_context_schema,
215
+ update_todo_list_schema,
216
+ git_diff_schema,
217
+ git_log_schema,
218
+ git_show_schema,
219
+ git_status_schema,
220
+ ]
221
+
222
+ async def initialize_mcp_tools(self):
223
+ await super().initialize_mcp_tools()
118
224
 
119
225
  local_tools = self.get_local_tool_schemas()
120
226
  if not local_tools:
@@ -133,491 +239,6 @@ class NavigatorCoder(Coder):
133
239
 
134
240
  if "local_tools" not in [name for name, _ in self.mcp_tools]:
135
241
  self.mcp_tools.append((local_server.name, local_tools))
136
- self.functions = self.get_tool_list()
137
-
138
- def get_local_tool_schemas(self):
139
- """Returns the JSON schemas for all local tools."""
140
- return [
141
- {
142
- "type": "function",
143
- "function": {
144
- "name": "ViewFilesAtGlob",
145
- "description": "View files matching a glob pattern.",
146
- "parameters": {
147
- "type": "object",
148
- "properties": {
149
- "pattern": {
150
- "type": "string",
151
- "description": "The glob pattern to match files.",
152
- },
153
- },
154
- "required": ["pattern"],
155
- },
156
- },
157
- },
158
- {
159
- "type": "function",
160
- "function": {
161
- "name": "ViewFilesMatching",
162
- "description": "View files containing a specific pattern.",
163
- "parameters": {
164
- "type": "object",
165
- "properties": {
166
- "pattern": {
167
- "type": "string",
168
- "description": "The pattern to search for in file contents.",
169
- },
170
- "file_pattern": {
171
- "type": "string",
172
- "description": (
173
- "An optional glob pattern to filter which files are searched."
174
- ),
175
- },
176
- "regex": {
177
- "type": "boolean",
178
- "description": (
179
- "Whether the pattern is a regular expression. Defaults to"
180
- " False."
181
- ),
182
- },
183
- },
184
- "required": ["pattern"],
185
- },
186
- },
187
- },
188
- {
189
- "type": "function",
190
- "function": {
191
- "name": "Ls",
192
- "description": "List files in a directory.",
193
- "parameters": {
194
- "type": "object",
195
- "properties": {
196
- "directory": {
197
- "type": "string",
198
- "description": "The directory to list.",
199
- },
200
- },
201
- "required": ["directory"],
202
- },
203
- },
204
- },
205
- {
206
- "type": "function",
207
- "function": {
208
- "name": "View",
209
- "description": "View a specific file.",
210
- "parameters": {
211
- "type": "object",
212
- "properties": {
213
- "file_path": {
214
- "type": "string",
215
- "description": "The path to the file to view.",
216
- },
217
- },
218
- "required": ["file_path"],
219
- },
220
- },
221
- },
222
- {
223
- "type": "function",
224
- "function": {
225
- "name": "Remove",
226
- "description": "Remove a file from the chat context.",
227
- "parameters": {
228
- "type": "object",
229
- "properties": {
230
- "file_path": {
231
- "type": "string",
232
- "description": "The path to the file to remove.",
233
- },
234
- },
235
- "required": ["file_path"],
236
- },
237
- },
238
- },
239
- {
240
- "type": "function",
241
- "function": {
242
- "name": "MakeEditable",
243
- "description": "Make a read-only file editable.",
244
- "parameters": {
245
- "type": "object",
246
- "properties": {
247
- "file_path": {
248
- "type": "string",
249
- "description": "The path to the file to make editable.",
250
- },
251
- },
252
- "required": ["file_path"],
253
- },
254
- },
255
- },
256
- {
257
- "type": "function",
258
- "function": {
259
- "name": "MakeReadonly",
260
- "description": "Make an editable file read-only.",
261
- "parameters": {
262
- "type": "object",
263
- "properties": {
264
- "file_path": {
265
- "type": "string",
266
- "description": "The path to the file to make read-only.",
267
- },
268
- },
269
- "required": ["file_path"],
270
- },
271
- },
272
- },
273
- {
274
- "type": "function",
275
- "function": {
276
- "name": "ViewFilesWithSymbol",
277
- "description": (
278
- "View files that contain a specific symbol (e.g., class, function)."
279
- ),
280
- "parameters": {
281
- "type": "object",
282
- "properties": {
283
- "symbol": {
284
- "type": "string",
285
- "description": "The symbol to search for.",
286
- },
287
- },
288
- "required": ["symbol"],
289
- },
290
- },
291
- },
292
- {
293
- "type": "function",
294
- "function": {
295
- "name": "Command",
296
- "description": "Execute a shell command.",
297
- "parameters": {
298
- "type": "object",
299
- "properties": {
300
- "command_string": {
301
- "type": "string",
302
- "description": "The shell command to execute.",
303
- },
304
- },
305
- "required": ["command_string"],
306
- },
307
- },
308
- },
309
- {
310
- "type": "function",
311
- "function": {
312
- "name": "CommandInteractive",
313
- "description": "Execute a shell command interactively.",
314
- "parameters": {
315
- "type": "object",
316
- "properties": {
317
- "command_string": {
318
- "type": "string",
319
- "description": "The interactive shell command to execute.",
320
- },
321
- },
322
- "required": ["command_string"],
323
- },
324
- },
325
- },
326
- {
327
- "type": "function",
328
- "function": {
329
- "name": "Grep",
330
- "description": "Search for a pattern in files.",
331
- "parameters": {
332
- "type": "object",
333
- "properties": {
334
- "pattern": {
335
- "type": "string",
336
- "description": "The pattern to search for.",
337
- },
338
- "file_pattern": {
339
- "type": "string",
340
- "description": "Glob pattern for files to search. Defaults to '*'.",
341
- },
342
- "directory": {
343
- "type": "string",
344
- "description": "Directory to search in. Defaults to '.'.",
345
- },
346
- "use_regex": {
347
- "type": "boolean",
348
- "description": "Whether to use regex. Defaults to False.",
349
- },
350
- "case_insensitive": {
351
- "type": "boolean",
352
- "description": (
353
- "Whether to perform a case-insensitive search. Defaults to"
354
- " False."
355
- ),
356
- },
357
- "context_before": {
358
- "type": "integer",
359
- "description": (
360
- "Number of lines to show before a match. Defaults to 5."
361
- ),
362
- },
363
- "context_after": {
364
- "type": "integer",
365
- "description": (
366
- "Number of lines to show after a match. Defaults to 5."
367
- ),
368
- },
369
- },
370
- "required": ["pattern"],
371
- },
372
- },
373
- },
374
- {
375
- "type": "function",
376
- "function": {
377
- "name": "ReplaceText",
378
- "description": "Replace text in a file.",
379
- "parameters": {
380
- "type": "object",
381
- "properties": {
382
- "file_path": {"type": "string"},
383
- "find_text": {"type": "string"},
384
- "replace_text": {"type": "string"},
385
- "near_context": {"type": "string"},
386
- "occurrence": {"type": "integer", "default": 1},
387
- "change_id": {"type": "string"},
388
- "dry_run": {"type": "boolean", "default": False},
389
- },
390
- "required": ["file_path", "find_text", "replace_text"],
391
- },
392
- },
393
- },
394
- {
395
- "type": "function",
396
- "function": {
397
- "name": "ReplaceAll",
398
- "description": "Replace all occurrences of text in a file.",
399
- "parameters": {
400
- "type": "object",
401
- "properties": {
402
- "file_path": {"type": "string"},
403
- "find_text": {"type": "string"},
404
- "replace_text": {"type": "string"},
405
- "change_id": {"type": "string"},
406
- "dry_run": {"type": "boolean", "default": False},
407
- },
408
- "required": ["file_path", "find_text", "replace_text"],
409
- },
410
- },
411
- },
412
- {
413
- "type": "function",
414
- "function": {
415
- "name": "InsertBlock",
416
- "description": "Insert a block of content into a file.",
417
- "parameters": {
418
- "type": "object",
419
- "properties": {
420
- "file_path": {"type": "string"},
421
- "content": {"type": "string"},
422
- "after_pattern": {"type": "string"},
423
- "before_pattern": {"type": "string"},
424
- "occurrence": {"type": "integer", "default": 1},
425
- "change_id": {"type": "string"},
426
- "dry_run": {"type": "boolean", "default": False},
427
- "position": {"type": "string", "enum": ["top", "bottom"]},
428
- "auto_indent": {"type": "boolean", "default": True},
429
- "use_regex": {"type": "boolean", "default": False},
430
- },
431
- "required": ["file_path", "content"],
432
- },
433
- },
434
- },
435
- {
436
- "type": "function",
437
- "function": {
438
- "name": "DeleteBlock",
439
- "description": "Delete a block of lines from a file.",
440
- "parameters": {
441
- "type": "object",
442
- "properties": {
443
- "file_path": {"type": "string"},
444
- "start_pattern": {"type": "string"},
445
- "end_pattern": {"type": "string"},
446
- "line_count": {"type": "integer"},
447
- "near_context": {"type": "string"},
448
- "occurrence": {"type": "integer", "default": 1},
449
- "change_id": {"type": "string"},
450
- "dry_run": {"type": "boolean", "default": False},
451
- },
452
- "required": ["file_path", "start_pattern"],
453
- },
454
- },
455
- },
456
- {
457
- "type": "function",
458
- "function": {
459
- "name": "ReplaceLine",
460
- "description": "Replace a single line in a file.",
461
- "parameters": {
462
- "type": "object",
463
- "properties": {
464
- "file_path": {"type": "string"},
465
- "line_number": {"type": "integer"},
466
- "new_content": {"type": "string"},
467
- "change_id": {"type": "string"},
468
- "dry_run": {"type": "boolean", "default": False},
469
- },
470
- "required": ["file_path", "line_number", "new_content"],
471
- },
472
- },
473
- },
474
- {
475
- "type": "function",
476
- "function": {
477
- "name": "ReplaceLines",
478
- "description": "Replace a range of lines in a file.",
479
- "parameters": {
480
- "type": "object",
481
- "properties": {
482
- "file_path": {"type": "string"},
483
- "start_line": {"type": "integer"},
484
- "end_line": {"type": "integer"},
485
- "new_content": {"type": "string"},
486
- "change_id": {"type": "string"},
487
- "dry_run": {"type": "boolean", "default": False},
488
- },
489
- "required": ["file_path", "start_line", "end_line", "new_content"],
490
- },
491
- },
492
- },
493
- {
494
- "type": "function",
495
- "function": {
496
- "name": "IndentLines",
497
- "description": "Indent a block of lines in a file.",
498
- "parameters": {
499
- "type": "object",
500
- "properties": {
501
- "file_path": {"type": "string"},
502
- "start_pattern": {"type": "string"},
503
- "end_pattern": {"type": "string"},
504
- "line_count": {"type": "integer"},
505
- "indent_levels": {"type": "integer", "default": 1},
506
- "near_context": {"type": "string"},
507
- "occurrence": {"type": "integer", "default": 1},
508
- "change_id": {"type": "string"},
509
- "dry_run": {"type": "boolean", "default": False},
510
- },
511
- "required": ["file_path", "start_pattern"],
512
- },
513
- },
514
- },
515
- {
516
- "type": "function",
517
- "function": {
518
- "name": "DeleteLine",
519
- "description": "Delete a single line from a file.",
520
- "parameters": {
521
- "type": "object",
522
- "properties": {
523
- "file_path": {"type": "string"},
524
- "line_number": {"type": "integer"},
525
- "change_id": {"type": "string"},
526
- "dry_run": {"type": "boolean", "default": False},
527
- },
528
- "required": ["file_path", "line_number"],
529
- },
530
- },
531
- },
532
- {
533
- "type": "function",
534
- "function": {
535
- "name": "DeleteLines",
536
- "description": "Delete a range of lines from a file.",
537
- "parameters": {
538
- "type": "object",
539
- "properties": {
540
- "file_path": {"type": "string"},
541
- "start_line": {"type": "integer"},
542
- "end_line": {"type": "integer"},
543
- "change_id": {"type": "string"},
544
- "dry_run": {"type": "boolean", "default": False},
545
- },
546
- "required": ["file_path", "start_line", "end_line"],
547
- },
548
- },
549
- },
550
- {
551
- "type": "function",
552
- "function": {
553
- "name": "UndoChange",
554
- "description": "Undo a previously applied change.",
555
- "parameters": {
556
- "type": "object",
557
- "properties": {
558
- "change_id": {"type": "string"},
559
- "file_path": {"type": "string"},
560
- },
561
- },
562
- },
563
- },
564
- {
565
- "type": "function",
566
- "function": {
567
- "name": "ListChanges",
568
- "description": "List recent changes made.",
569
- "parameters": {
570
- "type": "object",
571
- "properties": {
572
- "file_path": {"type": "string"},
573
- "limit": {"type": "integer", "default": 10},
574
- },
575
- },
576
- },
577
- },
578
- {
579
- "type": "function",
580
- "function": {
581
- "name": "ExtractLines",
582
- "description": (
583
- "Extract lines from a source file and append them to a target file."
584
- ),
585
- "parameters": {
586
- "type": "object",
587
- "properties": {
588
- "source_file_path": {"type": "string"},
589
- "target_file_path": {"type": "string"},
590
- "start_pattern": {"type": "string"},
591
- "end_pattern": {"type": "string"},
592
- "line_count": {"type": "integer"},
593
- "near_context": {"type": "string"},
594
- "occurrence": {"type": "integer", "default": 1},
595
- "dry_run": {"type": "boolean", "default": False},
596
- },
597
- "required": ["source_file_path", "target_file_path", "start_pattern"],
598
- },
599
- },
600
- },
601
- {
602
- "type": "function",
603
- "function": {
604
- "name": "ShowNumberedContext",
605
- "description": (
606
- "Show numbered lines of context around a pattern or line number."
607
- ),
608
- "parameters": {
609
- "type": "object",
610
- "properties": {
611
- "file_path": {"type": "string"},
612
- "pattern": {"type": "string"},
613
- "line_number": {"type": "integer"},
614
- "context_lines": {"type": "integer", "default": 3},
615
- },
616
- "required": ["file_path"],
617
- },
618
- },
619
- },
620
- ]
621
242
 
622
243
  async def _execute_local_tool_calls(self, tool_calls_list):
623
244
  tool_responses = []
@@ -646,61 +267,52 @@ class NavigatorCoder(Coder):
646
267
  all_results_content = []
647
268
  norm_tool_name = tool_name.lower()
648
269
 
649
- for params in parsed_args_list:
650
- single_result = ""
651
- # Dispatch to the correct tool execution function
652
- if norm_tool_name == "viewfilesatglob":
653
- single_result = execute_view_files_at_glob(self, **params)
654
- elif norm_tool_name == "viewfilesmatching":
655
- single_result = execute_view_files_matching(self, **params)
656
- elif norm_tool_name == "ls":
657
- single_result = execute_ls(self, **params)
658
- elif norm_tool_name == "view":
659
- single_result = execute_view(self, **params)
660
- elif norm_tool_name == "remove":
661
- single_result = _execute_remove(self, **params)
662
- elif norm_tool_name == "makeeditable":
663
- single_result = _execute_make_editable(self, **params)
664
- elif norm_tool_name == "makereadonly":
665
- single_result = _execute_make_readonly(self, **params)
666
- elif norm_tool_name == "viewfileswithsymbol":
667
- single_result = _execute_view_files_with_symbol(self, **params)
668
- elif norm_tool_name == "command":
669
- single_result = _execute_command(self, **params)
670
- elif norm_tool_name == "commandinteractive":
671
- single_result = _execute_command_interactive(self, **params)
672
- elif norm_tool_name == "grep":
673
- single_result = _execute_grep(self, **params)
674
- elif norm_tool_name == "replacetext":
675
- single_result = _execute_replace_text(self, **params)
676
- elif norm_tool_name == "replaceall":
677
- single_result = _execute_replace_all(self, **params)
678
- elif norm_tool_name == "insertblock":
679
- single_result = _execute_insert_block(self, **params)
680
- elif norm_tool_name == "deleteblock":
681
- single_result = _execute_delete_block(self, **params)
682
- elif norm_tool_name == "replaceline":
683
- single_result = _execute_replace_line(self, **params)
684
- elif norm_tool_name == "replacelines":
685
- single_result = _execute_replace_lines(self, **params)
686
- elif norm_tool_name == "indentlines":
687
- single_result = _execute_indent_lines(self, **params)
688
- elif norm_tool_name == "deleteline":
689
- single_result = _execute_delete_line(self, **params)
690
- elif norm_tool_name == "deletelines":
691
- single_result = _execute_delete_lines(self, **params)
692
- elif norm_tool_name == "undochange":
693
- single_result = _execute_undo_change(self, **params)
694
- elif norm_tool_name == "listchanges":
695
- single_result = _execute_list_changes(self, **params)
696
- elif norm_tool_name == "extractlines":
697
- single_result = _execute_extract_lines(self, **params)
698
- elif norm_tool_name == "shownumberedcontext":
699
- single_result = execute_show_numbered_context(self, **params)
700
- else:
701
- single_result = f"Error: Unknown local tool name '{tool_name}'"
270
+ tasks = []
271
+ tool_functions = {
272
+ "viewfilesmatching": execute_view_files_matching,
273
+ "ls": execute_ls,
274
+ "view": execute_view,
275
+ "remove": _execute_remove,
276
+ "makeeditable": _execute_make_editable,
277
+ "makereadonly": _execute_make_readonly,
278
+ "viewfileswithsymbol": _execute_view_files_with_symbol,
279
+ "command": _execute_command,
280
+ "commandinteractive": _execute_command_interactive,
281
+ "grep": _execute_grep,
282
+ "replacetext": _execute_replace_text,
283
+ "replaceall": _execute_replace_all,
284
+ "insertblock": _execute_insert_block,
285
+ "deleteblock": _execute_delete_block,
286
+ "replaceline": _execute_replace_line,
287
+ "replacelines": _execute_replace_lines,
288
+ "indentlines": _execute_indent_lines,
289
+ "deleteline": _execute_delete_line,
290
+ "deletelines": _execute_delete_lines,
291
+ "undochange": _execute_undo_change,
292
+ "listchanges": _execute_list_changes,
293
+ "extractlines": _execute_extract_lines,
294
+ "shownumberedcontext": execute_show_numbered_context,
295
+ "updatetodolist": _execute_update_todo_list,
296
+ "git_diff": _execute_git_diff,
297
+ "git_log": _execute_git_log,
298
+ "git_show": _execute_git_show,
299
+ "git_status": _execute_git_status,
300
+ }
301
+
302
+ func = tool_functions.get(norm_tool_name)
303
+
304
+ if func:
305
+ for params in parsed_args_list:
306
+ if asyncio.iscoroutinefunction(func):
307
+ tasks.append(func(self, **params))
308
+ else:
309
+ tasks.append(asyncio.to_thread(func, self, **params))
310
+ else:
311
+ all_results_content.append(f"Error: Unknown local tool name '{tool_name}'")
702
312
 
703
- all_results_content.append(str(single_result))
313
+ if tasks:
314
+ task_results = await asyncio.gather(*tasks)
315
+ all_results_content.extend(str(res) for res in task_results)
704
316
 
705
317
  result_message = "\n\n".join(all_results_content)
706
318
 
@@ -714,13 +326,12 @@ class NavigatorCoder(Coder):
714
326
  {
715
327
  "role": "tool",
716
328
  "tool_call_id": tool_call.id,
717
- "name": tool_name,
718
329
  "content": result_message,
719
330
  }
720
331
  )
721
332
  return tool_responses
722
333
 
723
- def _execute_mcp_tool(self, server, tool_name, params):
334
+ async def _execute_mcp_tool(self, server, tool_name, params):
724
335
  """Helper to execute a single MCP tool call, created from legacy format."""
725
336
 
726
337
  # This is a simplified, synchronous wrapper around async logic
@@ -767,10 +378,8 @@ class NavigatorCoder(Coder):
767
378
  f"Executing {tool_name} on {server.name} failed: \n Error: {e}\n"
768
379
  )
769
380
  return f"Error executing tool call {tool_name}: {e}"
770
- finally:
771
- await server.disconnect()
772
381
 
773
- return asyncio.run(_exec_async())
382
+ return await _exec_async()
774
383
 
775
384
  def _calculate_context_block_tokens(self, force=False):
776
385
  """
@@ -840,6 +449,8 @@ class NavigatorCoder(Coder):
840
449
  content = self.get_context_symbol_outline()
841
450
  elif block_name == "context_summary":
842
451
  content = self.get_context_summary()
452
+ elif block_name == "todo_list":
453
+ content = self.get_todo_list()
843
454
 
844
455
  # Cache the result if it's not None
845
456
  if content is not None:
@@ -965,63 +576,11 @@ class NavigatorCoder(Coder):
965
576
 
966
577
  This approach preserves prefix caching while providing fresh context information.
967
578
  """
968
- # First get the normal chat chunks from the parent method without calling super
969
- # We'll manually build the chunks to control placement of context blocks
970
- chunks = self.format_chat_chunks_base()
971
-
972
- # If enhanced context blocks are not enabled, just return the base chunks
579
+ # If enhanced context blocks are not enabled, use the base implementation
973
580
  if not self.use_enhanced_context:
974
- return chunks
975
-
976
- # Make sure token counts are updated - using centralized method
977
- # This also populates the context block cache
978
- self._calculate_context_block_tokens()
581
+ return super().format_chat_chunks()
979
582
 
980
- # Get blocks from cache to avoid regenerating them
981
- env_context = self.get_cached_context_block("environment_info")
982
- dir_structure = self.get_cached_context_block("directory_structure")
983
- git_status = self.get_cached_context_block("git_status")
984
- symbol_outline = self.get_cached_context_block("symbol_outline")
985
-
986
- # Context summary needs special handling because it depends on other blocks
987
- context_summary = self.get_context_summary()
988
-
989
- # 1. Add relatively static blocks BEFORE done_messages
990
- # These blocks change less frequently and can be part of the cacheable prefix
991
- static_blocks = []
992
- if dir_structure:
993
- static_blocks.append(dir_structure)
994
- if env_context:
995
- static_blocks.append(env_context)
996
-
997
- if static_blocks:
998
- static_message = "\n\n".join(static_blocks)
999
- # Insert as a system message right before done_messages
1000
- chunks.done.insert(0, dict(role="system", content=static_message))
1001
-
1002
- # 2. Add dynamic blocks AFTER chat_files
1003
- # These blocks change with the current files in context
1004
- dynamic_blocks = []
1005
- if context_summary:
1006
- dynamic_blocks.append(context_summary)
1007
- if symbol_outline:
1008
- dynamic_blocks.append(symbol_outline)
1009
- if git_status:
1010
- dynamic_blocks.append(git_status)
1011
-
1012
- if dynamic_blocks:
1013
- dynamic_message = "\n\n".join(dynamic_blocks)
1014
- # Append as a system message after chat_files
1015
- chunks.chat_files.append(dict(role="system", content=dynamic_message))
1016
-
1017
- return chunks
1018
-
1019
- def format_chat_chunks_base(self):
1020
- """
1021
- Create base chat chunks without enhanced context blocks.
1022
- This is a copy of the parent's format_chat_chunks method to avoid
1023
- calling super() which would create a recursive loop.
1024
- """
583
+ # Build chunks from scratch to avoid duplication with enhanced context blocks
1025
584
  self.choose_fence()
1026
585
  main_sys = self.fmt_system_prompt(self.gpt_prompts.main_system)
1027
586
 
@@ -1072,12 +631,65 @@ class NavigatorCoder(Coder):
1072
631
  chunks.examples = example_messages
1073
632
 
1074
633
  self.summarize_end()
1075
- chunks.done = self.done_messages
634
+ chunks.done = list(self.done_messages)
1076
635
 
1077
636
  chunks.repo = self.get_repo_messages()
1078
637
  chunks.readonly_files = self.get_readonly_files_messages()
1079
638
  chunks.chat_files = self.get_chat_files_messages()
1080
639
 
640
+ # Make sure token counts are updated - using centralized method
641
+ # This also populates the context block cache
642
+ self._calculate_context_block_tokens()
643
+
644
+ # Get blocks from cache to avoid regenerating them
645
+ env_context = self.get_cached_context_block("environment_info")
646
+ dir_structure = self.get_cached_context_block("directory_structure")
647
+ git_status = self.get_cached_context_block("git_status")
648
+ symbol_outline = self.get_cached_context_block("symbol_outline")
649
+ todo_list = self.get_cached_context_block("todo_list")
650
+
651
+ # Context summary needs special handling because it depends on other blocks
652
+ context_summary = self.get_context_summary()
653
+
654
+ # 1. Add relatively static blocks BEFORE done_messages
655
+ # These blocks change less frequently and can be part of the cacheable prefix
656
+ static_blocks = []
657
+ if dir_structure:
658
+ static_blocks.append(dir_structure)
659
+ if env_context:
660
+ static_blocks.append(env_context)
661
+
662
+ if static_blocks:
663
+ static_message = "\n\n".join(static_blocks)
664
+ # Insert as a system message right before done_messages
665
+ chunks.done.insert(0, dict(role="system", content=static_message))
666
+
667
+ # 2. Add dynamic blocks AFTER chat_files
668
+ # These blocks change with the current files in context
669
+ dynamic_blocks = []
670
+ if todo_list:
671
+ dynamic_blocks.append(todo_list)
672
+ if context_summary:
673
+ dynamic_blocks.append(context_summary)
674
+ if symbol_outline:
675
+ dynamic_blocks.append(symbol_outline)
676
+ if git_status:
677
+ dynamic_blocks.append(git_status)
678
+
679
+ # Add tool usage context if there are repetitive tools
680
+ if hasattr(self, "tool_usage_history") and self.tool_usage_history:
681
+ repetitive_tools = self._get_repetitive_tools()
682
+ if repetitive_tools:
683
+ tool_context = self._generate_tool_context(repetitive_tools)
684
+ if tool_context:
685
+ dynamic_blocks.append(tool_context)
686
+
687
+ if dynamic_blocks:
688
+ dynamic_message = "\n\n".join(dynamic_blocks)
689
+ # Append as a system message after chat_files
690
+ chunks.chat_files.append(dict(role="system", content=dynamic_message))
691
+
692
+ # Add reminder if needed
1081
693
  if self.gpt_prompts.system_reminder:
1082
694
  reminder_message = [
1083
695
  dict(
@@ -1293,7 +905,21 @@ class NavigatorCoder(Coder):
1293
905
  self.io.tool_error(f"Error generating environment info: {str(e)}")
1294
906
  return None
1295
907
 
1296
- def reply_completed(self):
908
+ async def process_tool_calls(self, tool_call_response):
909
+ """
910
+ Track tool usage before calling the base implementation.
911
+ """
912
+
913
+ if self.partial_response_tool_calls:
914
+ for tool_call in self.partial_response_tool_calls:
915
+ self.tool_usage_history.append(tool_call.get("function", {}).get("name"))
916
+
917
+ if len(self.tool_usage_history) > self.tool_usage_retries:
918
+ self.tool_usage_history.pop(0)
919
+
920
+ return await super().process_tool_calls(tool_call_response)
921
+
922
+ async def reply_completed(self):
1297
923
  """Process the completed response from the LLM.
1298
924
 
1299
925
  This is a key method that:
@@ -1305,8 +931,9 @@ class NavigatorCoder(Coder):
1305
931
  iteratively discover and analyze relevant files before providing
1306
932
  a final answer to the user's question.
1307
933
  """
1308
- # In granular editing mode, tool calls are handled by BaseCoder's process_tool_calls.
1309
- # This method is now only for legacy tool call format and search/replace blocks.
934
+ # In granular editing mode, tool calls are handled by BaseCoder's process_tool_calls,
935
+ # which is overridden in this class to track tool usage. This method is now only for
936
+ # legacy tool call format and search/replace blocks.
1310
937
  if self.use_granular_editing:
1311
938
  # Handle SEARCH/REPLACE blocks
1312
939
  content = self.partial_response_content
@@ -1319,7 +946,7 @@ class NavigatorCoder(Coder):
1319
946
  has_replace = ">>>>>>> REPLACE" in content
1320
947
  if has_search and has_divider and has_replace:
1321
948
  self.io.tool_output("Detected edit blocks, applying changes...")
1322
- edited_files = self._apply_edits_from_response()
949
+ edited_files = await self._apply_edits_from_response()
1323
950
  if self.reflected_message:
1324
951
  return False # Trigger reflection if edits failed
1325
952
 
@@ -1335,14 +962,19 @@ class NavigatorCoder(Coder):
1335
962
  # Legacy tool call processing for use_granular_editing=False
1336
963
  content = self.partial_response_content
1337
964
  if not content or not content.strip():
965
+ if len(self.tool_usage_history) > self.tool_usage_retries:
966
+ self.tool_usage_history = []
1338
967
  return True
1339
968
  original_content = content # Keep the original response
1340
969
 
1341
- # Process tool commands: returns content with tool calls removed, results, flag if any tool calls were found,
1342
- # and the content before the last '---' line
1343
- processed_content, result_messages, tool_calls_found, content_before_last_separator = (
1344
- self._process_tool_commands(content)
1345
- )
970
+ # Process tool commands: returns content with tool calls removed, results, flag if any tool calls were found
971
+ (
972
+ processed_content,
973
+ result_messages,
974
+ tool_calls_found,
975
+ content_before_last_separator,
976
+ tool_names_this_turn,
977
+ ) = await self._process_tool_commands(content)
1346
978
 
1347
979
  # Since we are no longer suppressing, the partial_response_content IS the final content.
1348
980
  # We might want to update it to the processed_content (without tool calls) if we don't
@@ -1370,7 +1002,7 @@ class NavigatorCoder(Coder):
1370
1002
 
1371
1003
  if edit_match:
1372
1004
  self.io.tool_output("Detected edit blocks, applying changes within Navigator...")
1373
- edited_files = self._apply_edits_from_response()
1005
+ edited_files = await self._apply_edits_from_response()
1374
1006
  # If _apply_edits_from_response set a reflected_message (due to errors),
1375
1007
  # return False to trigger a reflection loop.
1376
1008
  if self.reflected_message:
@@ -1408,6 +1040,7 @@ class NavigatorCoder(Coder):
1408
1040
  if tool_calls_found and self.num_reflections < self.max_reflections:
1409
1041
  # Reset tool counter for next iteration
1410
1042
  self.tool_call_count = 0
1043
+
1411
1044
  # Clear exploration files for the next round
1412
1045
  self.files_added_in_exploration = set()
1413
1046
 
@@ -1460,15 +1093,24 @@ class NavigatorCoder(Coder):
1460
1093
 
1461
1094
  # After applying edits OR determining no edits were needed (and no reflection needed),
1462
1095
  # the turn is complete. Reset counters and finalize history.
1096
+
1097
+ # Auto-commit any files edited by granular tools
1098
+ if self.files_edited_by_tools:
1099
+ saved_message = await self.auto_commit(self.files_edited_by_tools)
1100
+ if not saved_message and hasattr(self.gpt_prompts, "files_content_gpt_edits_no_repo"):
1101
+ saved_message = self.gpt_prompts.files_content_gpt_edits_no_repo
1102
+ self.move_back_cur_messages(saved_message)
1103
+
1463
1104
  self.tool_call_count = 0
1464
1105
  self.files_added_in_exploration = set()
1106
+ self.files_edited_by_tools = set()
1465
1107
  # Move cur_messages to done_messages
1466
1108
  self.move_back_cur_messages(
1467
1109
  None
1468
1110
  ) # Pass None as we handled commit message earlier if needed
1469
1111
  return True # Indicate exploration is finished for this round
1470
1112
 
1471
- def _process_tool_commands(self, content):
1113
+ async def _process_tool_commands(self, content):
1472
1114
  """
1473
1115
  Process tool commands in the `[tool_call(name, param=value)]` format within the content.
1474
1116
 
@@ -1485,6 +1127,7 @@ class NavigatorCoder(Coder):
1485
1127
  tool_calls_found = False
1486
1128
  call_count = 0
1487
1129
  max_calls = self.max_tool_calls
1130
+ tool_names = []
1488
1131
 
1489
1132
  # Check if there's a '---' separator and only process tool calls after the LAST one
1490
1133
  separator_marker = "---"
@@ -1493,7 +1136,7 @@ class NavigatorCoder(Coder):
1493
1136
  # If there's no separator, treat the entire content as before the separator
1494
1137
  if len(content_parts) == 1:
1495
1138
  # Return the original content with no tool calls processed, and the content itself as before_separator
1496
- return content, result_messages, False, content
1139
+ return content, result_messages, False, content, tool_names
1497
1140
 
1498
1141
  # Take everything before the last separator (including intermediate separators)
1499
1142
  content_before_separator = separator_marker.join(content_parts[:-1])
@@ -1683,6 +1326,8 @@ class NavigatorCoder(Coder):
1683
1326
  else:
1684
1327
  raise ValueError("Tool name must be an identifier or a string literal")
1685
1328
 
1329
+ tool_names.append(tool_name)
1330
+
1686
1331
  # Extract keyword arguments
1687
1332
  for keyword in call_node.keywords:
1688
1333
  key = keyword.arg
@@ -1764,20 +1409,13 @@ class NavigatorCoder(Coder):
1764
1409
  # Normalize tool name for case-insensitive matching
1765
1410
  norm_tool_name = tool_name.lower()
1766
1411
 
1767
- if norm_tool_name == "viewfilesatglob":
1768
- pattern = params.get("pattern")
1769
- if pattern is not None:
1770
- # Call the imported function
1771
- result_message = execute_view_files_at_glob(self, pattern)
1772
- else:
1773
- result_message = "Error: Missing 'pattern' parameter for ViewFilesAtGlob"
1774
- elif norm_tool_name == "viewfilesmatching":
1412
+ if norm_tool_name == "viewfilesmatching":
1775
1413
  pattern = params.get("pattern")
1776
1414
  file_pattern = params.get("file_pattern") # Optional
1777
1415
  regex = params.get("regex", False) # Default to False if not provided
1778
1416
  if pattern is not None:
1779
1417
  result_message = execute_view_files_matching(
1780
- self, pattern, file_pattern, regex
1418
+ self, pattern=pattern, file_pattern=file_pattern, regex=regex
1781
1419
  )
1782
1420
  else:
1783
1421
  result_message = "Error: Missing 'pattern' parameter for ViewFilesMatching"
@@ -1823,13 +1461,13 @@ class NavigatorCoder(Coder):
1823
1461
  elif norm_tool_name == "command":
1824
1462
  command_string = params.get("command_string")
1825
1463
  if command_string is not None:
1826
- result_message = _execute_command(self, command_string)
1464
+ result_message = await _execute_command(self, command_string)
1827
1465
  else:
1828
1466
  result_message = "Error: Missing 'command_string' parameter for Command"
1829
1467
  elif norm_tool_name == "commandinteractive":
1830
1468
  command_string = params.get("command_string")
1831
1469
  if command_string is not None:
1832
- result_message = _execute_command_interactive(self, command_string)
1470
+ result_message = await _execute_command_interactive(self, command_string)
1833
1471
  else:
1834
1472
  result_message = (
1835
1473
  "Error: Missing 'command_string' parameter for CommandInteractive"
@@ -1848,10 +1486,8 @@ class NavigatorCoder(Coder):
1848
1486
  context_after = params.get("context_after", 5)
1849
1487
 
1850
1488
  if pattern is not None:
1851
- # Import the function if not already imported (it should be)
1852
- from aider.tools.grep import _execute_grep
1853
-
1854
- result_message = _execute_grep(
1489
+ result_message = await asyncio.to_thread(
1490
+ _execute_grep,
1855
1491
  self,
1856
1492
  pattern,
1857
1493
  file_pattern,
@@ -1942,6 +1578,7 @@ class NavigatorCoder(Coder):
1942
1578
  auto_indent,
1943
1579
  use_regex,
1944
1580
  )
1581
+
1945
1582
  else:
1946
1583
  result_message = (
1947
1584
  "Error: Missing required parameters for InsertBlock (file_path,"
@@ -2139,6 +1776,21 @@ class NavigatorCoder(Coder):
2139
1776
  " and either pattern or line_number)"
2140
1777
  )
2141
1778
 
1779
+ elif norm_tool_name == "updatetodolist":
1780
+ content = params.get("content")
1781
+ append = params.get("append", False)
1782
+ change_id = params.get("change_id")
1783
+ dry_run = params.get("dry_run", False)
1784
+
1785
+ if content is not None:
1786
+ result_message = _execute_update_todo_list(
1787
+ self, content, append, change_id, dry_run
1788
+ )
1789
+ else:
1790
+ result_message = (
1791
+ "Error: Missing required 'content' parameter for UpdateTodoList"
1792
+ )
1793
+
2142
1794
  else:
2143
1795
  result_message = f"Error: Unknown tool name '{tool_name}'"
2144
1796
  if self.mcp_tools:
@@ -2150,7 +1802,7 @@ class NavigatorCoder(Coder):
2150
1802
  (s for s in self.mcp_servers if s.name == server_name), None
2151
1803
  )
2152
1804
  if server:
2153
- result_message = self._execute_mcp_tool(
1805
+ result_message = await self._execute_mcp_tool(
2154
1806
  server, tool_name, params
2155
1807
  )
2156
1808
  else:
@@ -2176,12 +1828,124 @@ class NavigatorCoder(Coder):
2176
1828
  # Return the content with tool calls removed
2177
1829
  modified_content = processed_content
2178
1830
 
2179
- # Update internal counter
2180
- self.tool_call_count += call_count
1831
+ return (
1832
+ modified_content,
1833
+ result_messages,
1834
+ tool_calls_found,
1835
+ content_before_separator,
1836
+ tool_names,
1837
+ )
1838
+
1839
+ def _get_repetitive_tools(self):
1840
+ """
1841
+ Identifies repetitive tool usage patterns from a flat list of tool calls.
1842
+
1843
+ This method checks for the following patterns in order:
1844
+ 1. If the last tool used was a write tool, it assumes progress and returns no repetitive tools.
1845
+ 2. It checks for any read tool that has been used 2 or more times in the history.
1846
+ 3. If no tools are repeated, but all tools in the history are read tools,
1847
+ it flags all of them as potentially repetitive.
1848
+
1849
+ It avoids flagging repetition if a "write" tool was used recently,
1850
+ as that suggests progress is being made.
1851
+ """
1852
+ history_len = len(self.tool_usage_history)
1853
+
1854
+ # Not enough history to detect a pattern
1855
+ if history_len < 2:
1856
+ return set()
1857
+
1858
+ # If the last tool was a write tool, we're likely making progress.
1859
+ if isinstance(self.tool_usage_history[-1], str):
1860
+ last_tool_lower = self.tool_usage_history[-1].lower()
1861
+
1862
+ if last_tool_lower in self.write_tools:
1863
+ self.tool_usage_history = []
1864
+ return set()
1865
+
1866
+ # If all tools in history are read tools, return all of them
1867
+ if all(tool.lower() in self.read_tools for tool in self.tool_usage_history):
1868
+ return set(tool for tool in self.tool_usage_history)
1869
+
1870
+ # Check for any read tool used more than once
1871
+ tool_counts = Counter(tool for tool in self.tool_usage_history)
1872
+ repetitive_tools = {
1873
+ tool
1874
+ for tool, count in tool_counts.items()
1875
+ if count >= 2 and tool.lower() in self.read_tools
1876
+ }
1877
+
1878
+ if repetitive_tools:
1879
+ return repetitive_tools
1880
+
1881
+ return set()
1882
+
1883
+ def _generate_tool_context(self, repetitive_tools):
1884
+ """
1885
+ Generate a context message for the LLM about recent tool usage.
1886
+ """
1887
+ if not self.tool_usage_history:
1888
+ return ""
1889
+
1890
+ context_parts = ['<context name="tool_usage_history">']
1891
+
1892
+ # Add turn and tool call statistics
1893
+ context_parts.append("## Turn and Tool Call Statistics")
1894
+ context_parts.append(f"- Current turn: {self.num_reflections + 1}")
1895
+ context_parts.append(f"- Tool calls this turn: {self.tool_call_count}")
1896
+ context_parts.append(f"- Total tool calls in session: {self.num_tool_calls}")
1897
+ context_parts.append("\n\n")
1898
+
1899
+ # Add recent tool usage history
1900
+ context_parts.append("## Recent Tool Usage History")
1901
+ if len(self.tool_usage_history) > 10:
1902
+ recent_history = self.tool_usage_history[-10:]
1903
+ context_parts.append("(Showing last 10 tools)")
1904
+ else:
1905
+ recent_history = self.tool_usage_history
1906
+
1907
+ for i, tool in enumerate(recent_history, 1):
1908
+ context_parts.append(f"{i}. {tool}")
1909
+ context_parts.append("\n\n")
1910
+
1911
+ if repetitive_tools:
1912
+ context_parts.append(
1913
+ "**Instruction:**\nYou have used the following tool(s) repeatedly:"
1914
+ )
2181
1915
 
2182
- return modified_content, result_messages, tool_calls_found, content_before_separator
1916
+ context_parts.append("### DO NOT USE THE FOLLOWING TOOLS/FUNCTIONS")
2183
1917
 
2184
- def _apply_edits_from_response(self):
1918
+ for tool in repetitive_tools:
1919
+ context_parts.append(f"- `{tool}`")
1920
+ context_parts.append(
1921
+ "Your exploration appears to be stuck in a loop. Please try a different approach:"
1922
+ )
1923
+ context_parts.append("\n")
1924
+ context_parts.append("**Suggestions for alternative approaches:**")
1925
+ context_parts.append(
1926
+ "- If you've been searching for files, try working with the files already in"
1927
+ " context"
1928
+ )
1929
+ context_parts.append(
1930
+ "- If you've been viewing files, try making actual edits to move forward"
1931
+ )
1932
+ context_parts.append("- Consider using different tools that you haven't used recently")
1933
+ context_parts.append(
1934
+ "- Focus on making concrete progress rather than gathering more information"
1935
+ )
1936
+ context_parts.append(
1937
+ "- Use the files you've already discovered to implement the requested changes"
1938
+ )
1939
+ context_parts.append("\n")
1940
+ context_parts.append(
1941
+ "You most likely have enough context for a subset of the necessary changes."
1942
+ )
1943
+ context_parts.append("Please prioritize file editing over further exploration.")
1944
+
1945
+ context_parts.append("</context>")
1946
+ return "\n".join(context_parts)
1947
+
1948
+ async def _apply_edits_from_response(self):
2185
1949
  """
2186
1950
  Parses and applies SEARCH/REPLACE edits found in self.partial_response_content.
2187
1951
  Returns a set of relative file paths that were successfully edited.
@@ -2213,13 +1977,13 @@ class NavigatorCoder(Coder):
2213
1977
  allowed = seen_paths[path]
2214
1978
  else:
2215
1979
  # Use the base Coder's permission check method
2216
- allowed = self.allowed_to_edit(path)
1980
+ allowed = await self.allowed_to_edit(path)
2217
1981
  seen_paths[path] = allowed
2218
1982
  if allowed:
2219
1983
  prepared_edits.append(edit)
2220
1984
 
2221
1985
  # Commit any dirty files identified by allowed_to_edit
2222
- self.dirty_commit()
1986
+ await self.dirty_commit()
2223
1987
  self.need_commit_before_edits = set() # Clear after commit
2224
1988
 
2225
1989
  # 3. Apply edits (logic adapted from EditBlockCoder.apply_edits)
@@ -2318,20 +2082,20 @@ Just reply with fixed versions of the {blocks} above that failed to match.
2318
2082
  lint_errors = self.lint_edited(edited_files)
2319
2083
  self.auto_commit(edited_files, context="Ran the linter")
2320
2084
  if lint_errors and not self.reflected_message: # Reflect only if no edit errors
2321
- ok = self.io.confirm_ask("Attempt to fix lint errors?")
2085
+ ok = await self.io.confirm_ask("Attempt to fix lint errors?")
2322
2086
  if ok:
2323
2087
  self.reflected_message = lint_errors
2324
2088
 
2325
- shared_output = self.run_shell_commands()
2089
+ shared_output = await self.run_shell_commands()
2326
2090
  if shared_output:
2327
2091
  # Add shell output as a new user message? Or just display?
2328
2092
  # Let's just display for now to avoid complex history manipulation
2329
2093
  self.io.tool_output("Shell command output:\n" + shared_output)
2330
2094
 
2331
2095
  if self.auto_test and not self.reflected_message: # Reflect only if no prior errors
2332
- test_errors = self.commands.cmd_test(self.test_cmd)
2096
+ test_errors = await self.commands.cmd_test(self.test_cmd)
2333
2097
  if test_errors:
2334
- ok = self.io.confirm_ask("Attempt to fix test errors?")
2098
+ ok = await self.io.confirm_ask("Attempt to fix test errors?")
2335
2099
  if ok:
2336
2100
  self.reflected_message = test_errors
2337
2101
 
@@ -2352,7 +2116,7 @@ Just reply with fixed versions of the {blocks} above that failed to match.
2352
2116
  except Exception as err:
2353
2117
  self.io.tool_error("Exception while applying edits:")
2354
2118
  self.io.tool_error(str(err), strip=False)
2355
- traceback.print_exc()
2119
+ self.io.tool_error(traceback.format_exc())
2356
2120
  self.reflected_message = f"Exception while applying edits: {str(err)}"
2357
2121
 
2358
2122
  return edited_files
@@ -2363,7 +2127,7 @@ Just reply with fixed versions of the {blocks} above that failed to match.
2363
2127
 
2364
2128
  Parameters:
2365
2129
  - file_path: Path to the file to add
2366
- - explicit: Whether this was an explicit view command (vs. implicit through ViewFilesAtGlob/ViewFilesMatching)
2130
+ - explicit: Whether this was an explicit view command (vs. implicit through ViewFilesMatching)
2367
2131
  """
2368
2132
  # Check if file exists
2369
2133
  abs_path = self.abs_root_path(file_path)
@@ -2437,7 +2201,7 @@ Just reply with fixed versions of the {blocks} above that failed to match.
2437
2201
  # Do nothing here for implicit mentions.
2438
2202
  pass
2439
2203
 
2440
- def check_for_file_mentions(self, content):
2204
+ async def check_for_file_mentions(self, content):
2441
2205
  """
2442
2206
  Override parent's method to use our own file processing logic.
2443
2207
 
@@ -2448,13 +2212,13 @@ Just reply with fixed versions of the {blocks} above that failed to match.
2448
2212
  # Do nothing - disable implicit file adds in navigator mode.
2449
2213
  pass
2450
2214
 
2451
- def preproc_user_input(self, inp):
2215
+ async def preproc_user_input(self, inp):
2452
2216
  """
2453
2217
  Override parent's method to wrap user input in a context block.
2454
2218
  This clearly delineates user input from other sections in the context window.
2455
2219
  """
2456
2220
  # First apply the parent's preprocessing
2457
- inp = super().preproc_user_input(inp)
2221
+ inp = await super().preproc_user_input(inp)
2458
2222
 
2459
2223
  # If we still have input after preprocessing, wrap it in a context block
2460
2224
  if inp and not inp.startswith('<context name="user_input">'):
@@ -2566,6 +2330,44 @@ Just reply with fixed versions of the {blocks} above that failed to match.
2566
2330
  self.io.tool_error(f"Error generating directory structure: {str(e)}")
2567
2331
  return None
2568
2332
 
2333
+ def get_todo_list(self):
2334
+ """
2335
+ Generate a todo list context block from the .aider.todo.txt file.
2336
+ Returns formatted string with the current todo list or None if empty/not present.
2337
+ """
2338
+
2339
+ try:
2340
+ # Define the todo file path
2341
+ todo_file_path = ".aider.todo.txt"
2342
+ abs_path = self.abs_root_path(todo_file_path)
2343
+
2344
+ # Check if file exists
2345
+ import os
2346
+
2347
+ if not os.path.isfile(abs_path):
2348
+ return (
2349
+ '<context name="todo_list">\n'
2350
+ "Todo list does not exist. Please update it."
2351
+ "</context>"
2352
+ )
2353
+
2354
+ # Read todo list content
2355
+ content = self.io.read_text(abs_path)
2356
+ if content is None or not content.strip():
2357
+ return None
2358
+
2359
+ # Format the todo list context block
2360
+ result = '<context name="todo_list">\n'
2361
+ result += "## Current Todo List\n\n"
2362
+ result += "Below is the current todo list managed via `UpdateTodoList` tool:\n\n"
2363
+ result += f"```\n{content}\n```\n"
2364
+ result += "</context>"
2365
+
2366
+ return result
2367
+ except Exception as e:
2368
+ self.io.tool_error(f"Error generating todo list context: {str(e)}")
2369
+ return None
2370
+
2569
2371
  def get_git_status(self):
2570
2372
  """
2571
2373
  Generate a git status context block for repository information.