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.

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.dist-info → aider_ce-0.88.0.dist-info}/METADATA +21 -5
  56. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/RECORD +59 -56
  57. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/WHEEL +0 -0
  58. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/entry_points.txt +0 -0
  59. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/licenses/LICENSE.txt +0 -0
  60. {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/top_level.txt +0 -0
@@ -14,6 +14,7 @@ import sys
14
14
  import threading
15
15
  import time
16
16
  import traceback
17
+ import weakref
17
18
  from collections import defaultdict
18
19
  from datetime import datetime
19
20
 
@@ -27,7 +28,16 @@ from json.decoder import JSONDecodeError
27
28
  from pathlib import Path
28
29
  from typing import List
29
30
 
31
+ import httpx
30
32
  from litellm import experimental_mcp_client
33
+ from litellm.types.utils import (
34
+ ChatCompletionMessageToolCall,
35
+ Choices,
36
+ Function,
37
+ Message,
38
+ ModelResponse,
39
+ )
40
+ from prompt_toolkit.patch_stdout import patch_stdout
31
41
  from rich.console import Console
32
42
 
33
43
  from aider import __version__, models, prompts, urls, utils
@@ -50,7 +60,6 @@ from aider.repo import ANY_GIT_ERROR, GitRepo
50
60
  from aider.repomap import RepoMap
51
61
  from aider.run_cmd import run_cmd
52
62
  from aider.utils import format_content, format_messages, format_tokens, is_image_file
53
- from aider.waiting import WaitingSpinner
54
63
 
55
64
  from ..dump import dump # noqa: F401
56
65
  from .chat_chunks import ChatChunks
@@ -115,7 +124,7 @@ class Coder:
115
124
  test_outcome = None
116
125
  multi_response_content = ""
117
126
  partial_response_content = ""
118
- partial_response_tool_call = []
127
+ partial_response_tool_calls = []
119
128
  commit_before_message = []
120
129
  message_cost = 0.0
121
130
  add_cache_headers = False
@@ -129,6 +138,10 @@ class Coder:
129
138
  file_watcher = None
130
139
  mcp_servers = None
131
140
  mcp_tools = None
141
+ run_one_completed = True
142
+ compact_context_completed = True
143
+ suppress_announcements_for_next_prompt = False
144
+ tool_reflection = False
132
145
 
133
146
  # Context management settings (for all modes)
134
147
  context_management_enabled = False # Disabled by default except for navigator mode
@@ -137,7 +150,7 @@ class Coder:
137
150
  )
138
151
 
139
152
  @classmethod
140
- def create(
153
+ async def create(
141
154
  self,
142
155
  main_model=None,
143
156
  edit_format=None,
@@ -175,7 +188,7 @@ class Coder:
175
188
  done_messages = from_coder.done_messages
176
189
  if edit_format != from_coder.edit_format and done_messages and summarize_from_coder:
177
190
  try:
178
- done_messages = from_coder.summarizer.summarize_all(done_messages)
191
+ done_messages = await from_coder.summarizer.summarize_all(done_messages)
179
192
  except ValueError:
180
193
  # If summarization fails, keep the original messages and warn the user
181
194
  io.tool_warning(
@@ -208,6 +221,7 @@ class Coder:
208
221
  for coder in coders.__all__:
209
222
  if hasattr(coder, "edit_format") and coder.edit_format == edit_format:
210
223
  res = coder(main_model, io, **kwargs)
224
+ await res.initialize_mcp_tools()
211
225
  res.original_kwargs = dict(kwargs)
212
226
  return res
213
227
 
@@ -218,8 +232,8 @@ class Coder:
218
232
  ]
219
233
  raise UnknownEditFormat(edit_format, valid_formats)
220
234
 
221
- def clone(self, **kwargs):
222
- new_coder = Coder.create(from_coder=self, **kwargs)
235
+ async def clone(self, **kwargs):
236
+ new_coder = await Coder.create(from_coder=self, **kwargs)
223
237
  return new_coder
224
238
 
225
239
  def get_announcements(self):
@@ -296,13 +310,11 @@ class Coder:
296
310
  else:
297
311
  lines.append("Repo-map: disabled")
298
312
 
299
- # Files
300
- for fname in self.get_inchat_relative_files():
301
- lines.append(f"Added {fname} to the chat.")
302
-
303
- for fname in self.abs_read_only_fnames:
304
- rel_fname = self.get_rel_fname(fname)
305
- lines.append(f"Added {rel_fname} to the chat (read-only).")
313
+ if self.mcp_tools:
314
+ mcp_servers = []
315
+ for server_name, server_tools in self.mcp_tools:
316
+ mcp_servers.append(server_name)
317
+ lines.append(f"MCP servers configured: {', '.join(mcp_servers)}")
306
318
 
307
319
  for fname in self.abs_read_only_stubs_fnames:
308
320
  rel_fname = self.get_rel_fname(fname)
@@ -368,6 +380,7 @@ class Coder:
368
380
  context_compaction_summary_tokens=8192,
369
381
  map_cache_dir=".",
370
382
  repomap_in_memory=False,
383
+ preserve_todo_list=False,
371
384
  ):
372
385
  # initialize from args.map_cache_dir
373
386
  self.map_cache_dir = map_cache_dir
@@ -385,6 +398,7 @@ class Coder:
385
398
 
386
399
  self.auto_copy_context = auto_copy_context
387
400
  self.auto_accept_architect = auto_accept_architect
401
+ self.preserve_todo_list = preserve_todo_list
388
402
 
389
403
  self.ignore_mentions = ignore_mentions
390
404
  if not self.ignore_mentions:
@@ -442,8 +456,10 @@ class Coder:
442
456
  self.done_messages = []
443
457
 
444
458
  self.io = io
459
+ self.io.coder = weakref.ref(self)
445
460
 
446
461
  self.shell_commands = []
462
+ self.partial_response_tool_calls = []
447
463
 
448
464
  if not auto_commits:
449
465
  dirty_commits = False
@@ -455,6 +471,7 @@ class Coder:
455
471
  self.pretty = self.io.pretty
456
472
 
457
473
  self.main_model = main_model
474
+
458
475
  # Set the reasoning tag name based on model settings or default
459
476
  self.reasoning_tag_name = (
460
477
  self.main_model.reasoning_tag if self.main_model.reasoning_tag else REASONING_TAG
@@ -568,6 +585,10 @@ class Coder:
568
585
  self.summarizer_thread = None
569
586
  self.summarized_done_messages = []
570
587
  self.summarizing_messages = None
588
+ self.input_task = None
589
+ self.confirmation_in_progress = False
590
+
591
+ self.files_edited_by_tools = set()
571
592
 
572
593
  if not self.done_messages and restore_chat_history:
573
594
  history_md = self.io.read_text(self.io.chat_history_file)
@@ -583,9 +604,21 @@ class Coder:
583
604
  self.auto_test = auto_test
584
605
  self.test_cmd = test_cmd
585
606
 
607
+ # Clean up todo list file on startup unless preserve_todo_list is True
608
+ if not getattr(self, "preserve_todo_list", False):
609
+ todo_file_path = ".aider.todo.txt"
610
+ abs_path = self.abs_root_path(todo_file_path)
611
+ if os.path.isfile(abs_path):
612
+ try:
613
+ os.remove(abs_path)
614
+ if self.verbose:
615
+ self.io.tool_output(f"Removed existing todo list file: {todo_file_path}")
616
+ except Exception as e:
617
+ self.io.tool_warning(f"Could not remove todo list file {todo_file_path}: {e}")
618
+
586
619
  # Instantiate MCP tools
587
620
  if self.mcp_servers:
588
- self.initialize_mcp_tools()
621
+ pass
589
622
  # validate the functions jsonschema
590
623
  if self.functions:
591
624
  from jsonschema import Draft7Validator
@@ -644,12 +677,7 @@ class Coder:
644
677
 
645
678
  def _stop_waiting_spinner(self):
646
679
  """Stop and clear the waiting spinner if it is running."""
647
- spinner = getattr(self, "waiting_spinner", None)
648
- if spinner:
649
- try:
650
- spinner.stop()
651
- finally:
652
- self.waiting_spinner = None
680
+ self.io.stop_spinner()
653
681
 
654
682
  def get_abs_fnames_content(self):
655
683
  for fname in list(self.abs_fnames):
@@ -1013,10 +1041,11 @@ class Coder:
1013
1041
 
1014
1042
  return {"role": "user", "content": image_messages}
1015
1043
 
1016
- def run_stream(self, user_message):
1044
+ async def run_stream(self, user_message):
1017
1045
  self.io.user_input(user_message)
1018
1046
  self.init_before_message()
1019
- yield from self.send_message(user_message)
1047
+ async for chunk in self.send_message(user_message):
1048
+ yield chunk
1020
1049
 
1021
1050
  def init_before_message(self):
1022
1051
  self.aider_edited_files = set()
@@ -1030,36 +1059,139 @@ class Coder:
1030
1059
  if self.repo:
1031
1060
  self.commit_before_message.append(self.repo.get_head_commit_sha())
1032
1061
 
1033
- def run(self, with_message=None, preproc=True):
1062
+ async def run(self, with_message=None, preproc=True):
1063
+ while self.confirmation_in_progress:
1064
+ await asyncio.sleep(0.1) # Yield control and wait briefly
1065
+
1066
+ if self.io.prompt_session:
1067
+ with patch_stdout(raw=True):
1068
+ return await self._run_patched(with_message, preproc)
1069
+ else:
1070
+ return await self._run_patched(with_message, preproc)
1071
+
1072
+ async def _run_patched(self, with_message=None, preproc=True):
1073
+ input_task = None
1074
+ processing_task = None
1034
1075
  try:
1035
1076
  if with_message:
1036
1077
  self.io.user_input(with_message)
1037
- self.run_one(with_message, preproc)
1078
+ await self.run_one(with_message, preproc)
1038
1079
  return self.partial_response_content
1080
+
1081
+ user_message = None
1082
+
1039
1083
  while True:
1040
1084
  try:
1041
- if not self.io.placeholder:
1085
+ if (
1086
+ not self.confirmation_in_progress
1087
+ and not input_task
1088
+ and not user_message
1089
+ and (not processing_task or not self.io.placeholder)
1090
+ ):
1091
+ if not self.suppress_announcements_for_next_prompt:
1092
+ self.show_announcements()
1093
+ self.suppress_announcements_for_next_prompt = False
1094
+
1095
+ # Stop spinner before showing announcements or getting input
1096
+ self.io.stop_spinner()
1097
+
1042
1098
  self.copy_context()
1043
- user_message = self.get_input()
1044
- self.compact_context_if_needed()
1045
- self.run_one(user_message, preproc)
1046
- self.show_undo_hint()
1099
+ self.input_task = asyncio.create_task(self.get_input())
1100
+ input_task = self.input_task
1101
+
1102
+ tasks = set()
1103
+ if processing_task:
1104
+ tasks.add(processing_task)
1105
+ if input_task:
1106
+ tasks.add(input_task)
1107
+
1108
+ if tasks:
1109
+ done, pending = await asyncio.wait(
1110
+ tasks, return_when=asyncio.FIRST_COMPLETED
1111
+ )
1112
+
1113
+ if input_task and input_task in done:
1114
+ if processing_task:
1115
+ if not self.confirmation_in_progress:
1116
+ processing_task.cancel()
1117
+ try:
1118
+ await processing_task
1119
+ except asyncio.CancelledError:
1120
+ pass
1121
+ self.io.stop_spinner()
1122
+ processing_task = None
1123
+
1124
+ try:
1125
+ user_message = input_task.result()
1126
+ except (asyncio.CancelledError, KeyboardInterrupt):
1127
+ user_message = None
1128
+ input_task = None
1129
+ self.input_task = None
1130
+ if user_message is None:
1131
+ continue
1132
+
1133
+ if processing_task and processing_task in done:
1134
+ try:
1135
+ await processing_task
1136
+ except (asyncio.CancelledError, KeyboardInterrupt):
1137
+ pass
1138
+ processing_task = None
1139
+ # Stop spinner when processing task completes
1140
+ self.io.stop_spinner()
1141
+
1142
+ if user_message and self.run_one_completed and self.compact_context_completed:
1143
+ processing_task = asyncio.create_task(
1144
+ self._processing_logic(user_message, preproc)
1145
+ )
1146
+ # Start spinner for processing task
1147
+ self.io.start_spinner("Processing...")
1148
+ user_message = None # Clear message after starting task
1047
1149
  except KeyboardInterrupt:
1150
+ if processing_task:
1151
+ processing_task.cancel()
1152
+ processing_task = None
1153
+ # Stop spinner when processing task is cancelled
1154
+ self.io.stop_spinner()
1155
+ if input_task:
1156
+ self.io.set_placeholder("")
1157
+ input_task.cancel()
1158
+ input_task = None
1048
1159
  self.keyboard_interrupt()
1049
1160
  except EOFError:
1050
1161
  return
1162
+ finally:
1163
+ if input_task:
1164
+ input_task.cancel()
1165
+ if processing_task:
1166
+ processing_task.cancel()
1167
+
1168
+ async def _processing_logic(self, user_message, preproc):
1169
+ try:
1170
+ self.compact_context_completed = False
1171
+ await self.compact_context_if_needed()
1172
+ self.compact_context_completed = True
1173
+
1174
+ self.run_one_completed = False
1175
+ await self.run_one(user_message, preproc)
1176
+ self.show_undo_hint()
1177
+ except asyncio.CancelledError:
1178
+ # Don't show undo hint if cancelled
1179
+ raise
1180
+ finally:
1181
+ self.run_one_completed = True
1182
+ self.compact_context_completed = True
1051
1183
 
1052
1184
  def copy_context(self):
1053
1185
  if self.auto_copy_context:
1054
1186
  self.commands.cmd_copy_context()
1055
1187
 
1056
- def get_input(self):
1188
+ async def get_input(self):
1057
1189
  inchat_files = self.get_inchat_relative_files()
1058
1190
  all_read_only_fnames = self.abs_read_only_fnames | self.abs_read_only_stubs_fnames
1059
1191
  all_read_only_files = [self.get_rel_fname(fname) for fname in all_read_only_fnames]
1060
1192
  all_files = sorted(set(inchat_files + all_read_only_files))
1061
1193
  edit_format = "" if self.edit_format == self.main_model.edit_format else self.edit_format
1062
- return self.io.get_input(
1194
+ return await self.io.get_input(
1063
1195
  self.root,
1064
1196
  all_files,
1065
1197
  self.get_addable_relative_files(),
@@ -1069,29 +1201,35 @@ class Coder:
1069
1201
  edit_format=edit_format,
1070
1202
  )
1071
1203
 
1072
- def preproc_user_input(self, inp):
1204
+ async def preproc_user_input(self, inp):
1073
1205
  if not inp:
1074
1206
  return
1075
1207
 
1076
1208
  if self.commands.is_command(inp):
1077
- return self.commands.run(inp)
1209
+ return await self.commands.run(inp)
1078
1210
 
1079
- self.check_for_file_mentions(inp)
1080
- inp = self.check_for_urls(inp)
1211
+ await self.check_for_file_mentions(inp)
1212
+ inp = await self.check_for_urls(inp)
1081
1213
 
1082
1214
  return inp
1083
1215
 
1084
- def run_one(self, user_message, preproc):
1216
+ async def run_one(self, user_message, preproc):
1085
1217
  self.init_before_message()
1086
1218
 
1087
1219
  if preproc:
1088
- message = self.preproc_user_input(user_message)
1220
+ message = await self.preproc_user_input(user_message)
1089
1221
  else:
1090
1222
  message = user_message
1091
1223
 
1092
- while message:
1224
+ if self.commands.is_command(user_message):
1225
+ return
1226
+
1227
+ while True:
1093
1228
  self.reflected_message = None
1094
- list(self.send_message(message))
1229
+ self.tool_reflection = False
1230
+
1231
+ async for _ in self.send_message(message):
1232
+ pass
1095
1233
 
1096
1234
  if not self.reflected_message:
1097
1235
  break
@@ -1101,7 +1239,14 @@ class Coder:
1101
1239
  return
1102
1240
 
1103
1241
  self.num_reflections += 1
1104
- message = self.reflected_message
1242
+
1243
+ if self.tool_reflection:
1244
+ self.num_reflections -= 1
1245
+
1246
+ if self.reflected_message is True:
1247
+ message = None
1248
+ else:
1249
+ message = self.reflected_message
1105
1250
 
1106
1251
  def check_and_open_urls(self, exc, friendly_msg=None):
1107
1252
  """Check exception for URLs, offer to open in a browser, with user-friendly error msgs."""
@@ -1122,7 +1267,7 @@ class Coder:
1122
1267
  self.io.offer_url(url)
1123
1268
  return urls
1124
1269
 
1125
- def check_for_urls(self, inp: str) -> List[str]:
1270
+ async def check_for_urls(self, inp: str) -> List[str]:
1126
1271
  """Check input for URLs and offer to add them to the chat."""
1127
1272
  if not self.detect_urls:
1128
1273
  return inp
@@ -1135,11 +1280,11 @@ class Coder:
1135
1280
  for url in urls:
1136
1281
  if url not in self.rejected_urls:
1137
1282
  url = url.rstrip(".',\"")
1138
- if self.io.confirm_ask(
1283
+ if await self.io.confirm_ask(
1139
1284
  "Add URL to the chat?", subject=url, group=group, allow_never=True
1140
1285
  ):
1141
1286
  inp += "\n\n"
1142
- inp += self.commands.cmd_web(url, return_content=True)
1287
+ inp += await self.commands.cmd_web(url, return_content=True)
1143
1288
  else:
1144
1289
  self.rejected_urls.add(url)
1145
1290
 
@@ -1149,17 +1294,9 @@ class Coder:
1149
1294
  # Ensure cursor is visible on exit
1150
1295
  Console().show_cursor(True)
1151
1296
 
1152
- now = time.time()
1153
-
1154
- thresh = 2 # seconds
1155
- if self.last_keyboard_interrupt and now - self.last_keyboard_interrupt < thresh:
1156
- self.io.tool_warning("\n\n^C KeyboardInterrupt")
1157
- self.event("exit", reason="Control-C")
1158
- sys.exit()
1159
-
1160
- self.io.tool_warning("\n\n^C again to exit")
1297
+ self.io.tool_warning("\n\n^C KeyboardInterrupt")
1161
1298
 
1162
- self.last_keyboard_interrupt = now
1299
+ self.last_keyboard_interrupt = time.time()
1163
1300
 
1164
1301
  def summarize_start(self):
1165
1302
  if not self.summarizer.check_max_tokens(self.done_messages):
@@ -1176,7 +1313,9 @@ class Coder:
1176
1313
  def summarize_worker(self):
1177
1314
  self.summarizing_messages = list(self.done_messages)
1178
1315
  try:
1179
- self.summarized_done_messages = self.summarizer.summarize(self.summarizing_messages)
1316
+ self.summarized_done_messages = asyncio.run(
1317
+ self.summarizer.summarize(self.summarizing_messages)
1318
+ )
1180
1319
  except ValueError as err:
1181
1320
  self.io.tool_warning(err.args[0])
1182
1321
  self.summarized_done_messages = self.summarizing_messages
@@ -1196,7 +1335,7 @@ class Coder:
1196
1335
  self.summarizing_messages = None
1197
1336
  self.summarized_done_messages = []
1198
1337
 
1199
- def compact_context_if_needed(self):
1338
+ async def compact_context_if_needed(self):
1200
1339
  if not self.enable_context_compaction:
1201
1340
  self.summarize_start()
1202
1341
  return
@@ -1210,7 +1349,7 @@ class Coder:
1210
1349
 
1211
1350
  try:
1212
1351
  # Create a summary of the conversation
1213
- summary_text = self.summarizer.summarize_all_as_text(
1352
+ summary_text = await self.summarizer.summarize_all_as_text(
1214
1353
  self.done_messages,
1215
1354
  self.gpt_prompts.compaction_prompt,
1216
1355
  self.context_compaction_summary_tokens,
@@ -1517,7 +1656,13 @@ class Coder:
1517
1656
  cur_tokens = self.main_model.token_count(chunks.cur)
1518
1657
 
1519
1658
  if None not in (messages_tokens, reminder_tokens, cur_tokens):
1520
- total_tokens = messages_tokens + reminder_tokens + cur_tokens
1659
+ total_tokens = messages_tokens
1660
+ # Only add tokens for reminder and cur if they're not already included
1661
+ # in the messages_tokens calculation
1662
+ if not chunks.reminder:
1663
+ total_tokens += reminder_tokens
1664
+ if not chunks.cur:
1665
+ total_tokens += cur_tokens
1521
1666
  else:
1522
1667
  # add the reminder anyway
1523
1668
  total_tokens = 0
@@ -1633,15 +1778,16 @@ class Coder:
1633
1778
  return False
1634
1779
  return True
1635
1780
 
1636
- def send_message(self, inp):
1781
+ async def send_message(self, inp):
1637
1782
  self.event("message_send_starting")
1638
1783
 
1639
1784
  # Notify IO that LLM processing is starting
1640
1785
  self.io.llm_started()
1641
1786
 
1642
- self.cur_messages += [
1643
- dict(role="user", content=inp),
1644
- ]
1787
+ if inp:
1788
+ self.cur_messages += [
1789
+ dict(role="user", content=inp),
1790
+ ]
1645
1791
 
1646
1792
  chunks = self.format_messages()
1647
1793
  messages = chunks.all_messages()
@@ -1655,10 +1801,13 @@ class Coder:
1655
1801
 
1656
1802
  self.multi_response_content = ""
1657
1803
  if self.show_pretty():
1658
- self.waiting_spinner = WaitingSpinner("Waiting for " + self.main_model.name)
1659
- self.waiting_spinner.start()
1804
+ spinner_text = (
1805
+ f"Waiting for {self.main_model.name} • ${self.format_cost(self.total_cost)} session"
1806
+ )
1807
+ self.io.start_spinner(spinner_text)
1808
+
1660
1809
  if self.stream:
1661
- self.mdstream = self.io.get_assistant_mdstream()
1810
+ self.mdstream = True
1662
1811
  else:
1663
1812
  self.mdstream = None
1664
1813
  else:
@@ -1674,7 +1823,8 @@ class Coder:
1674
1823
  try:
1675
1824
  while True:
1676
1825
  try:
1677
- yield from self.send(messages, functions=self.functions)
1826
+ async for chunk in self.send(messages, tools=self.get_tool_list()):
1827
+ yield chunk
1678
1828
  break
1679
1829
  except litellm_ex.exceptions_tuple() as err:
1680
1830
  ex_info = litellm_ex.get_ex_info(err)
@@ -1702,7 +1852,7 @@ class Coder:
1702
1852
  self.io.tool_error(err_msg)
1703
1853
 
1704
1854
  self.io.tool_output(f"Retrying in {retry_delay:.1f} seconds...")
1705
- time.sleep(retry_delay)
1855
+ await asyncio.sleep(retry_delay)
1706
1856
  continue
1707
1857
  except KeyboardInterrupt:
1708
1858
  interrupted = True
@@ -1730,8 +1880,9 @@ class Coder:
1730
1880
  return
1731
1881
  finally:
1732
1882
  if self.mdstream:
1733
- self.live_incremental_response(True)
1734
- self.mdstream = None
1883
+ content_to_show = self.live_incremental_response(True)
1884
+ self.stream_wrapper(content_to_show, final=True)
1885
+ self.mdstream = None
1735
1886
 
1736
1887
  # Ensure any waiting spinner is stopped
1737
1888
  self._stop_waiting_spinner()
@@ -1740,11 +1891,6 @@ class Coder:
1740
1891
  self.remove_reasoning_content()
1741
1892
  self.multi_response_content = ""
1742
1893
 
1743
- ###
1744
- # print()
1745
- # print("=" * 20)
1746
- # dump(self.partial_response_content)
1747
-
1748
1894
  self.io.tool_output()
1749
1895
 
1750
1896
  self.show_usage_report()
@@ -1785,11 +1931,11 @@ class Coder:
1785
1931
  ]
1786
1932
  return
1787
1933
 
1788
- edited = self.apply_updates()
1934
+ edited = await self.apply_updates()
1789
1935
 
1790
1936
  if edited:
1791
1937
  self.aider_edited_files.update(edited)
1792
- saved_message = self.auto_commit(edited)
1938
+ saved_message = await self.auto_commit(edited)
1793
1939
 
1794
1940
  if not saved_message and hasattr(self.gpt_prompts, "files_content_gpt_edits_no_repo"):
1795
1941
  saved_message = self.gpt_prompts.files_content_gpt_edits_no_repo
@@ -1797,7 +1943,7 @@ class Coder:
1797
1943
  self.move_back_cur_messages(saved_message)
1798
1944
 
1799
1945
  if not interrupted:
1800
- add_rel_files_message = self.check_for_file_mentions(content)
1946
+ add_rel_files_message = await self.check_for_file_mentions(content)
1801
1947
  if add_rel_files_message:
1802
1948
  if self.reflected_message:
1803
1949
  self.reflected_message += "\n\n" + add_rel_files_message
@@ -1806,15 +1952,49 @@ class Coder:
1806
1952
  return
1807
1953
 
1808
1954
  # Process any tools using MCP servers
1809
- tool_call_response = litellm.stream_chunk_builder(self.partial_response_tool_call)
1810
- if self.process_tool_calls(tool_call_response):
1811
- self.num_tool_calls += 1
1812
- return self.run(with_message="Continue with tool call response", preproc=False)
1955
+ try:
1956
+ if self.partial_response_tool_calls:
1957
+ tool_calls = []
1958
+ for tool_call_dict in self.partial_response_tool_calls:
1959
+ tool_calls.append(
1960
+ ChatCompletionMessageToolCall(
1961
+ id=tool_call_dict.get("id"),
1962
+ function=Function(
1963
+ name=tool_call_dict.get("function", {}).get("name"),
1964
+ arguments=tool_call_dict.get("function", {}).get(
1965
+ "arguments", ""
1966
+ ),
1967
+ ),
1968
+ type=tool_call_dict.get("type"),
1969
+ )
1970
+ )
1971
+
1972
+ tool_call_response = ModelResponse(
1973
+ choices=[
1974
+ Choices(
1975
+ finish_reason="tool_calls",
1976
+ index=0,
1977
+ message=Message(
1978
+ content=None,
1979
+ role="assistant",
1980
+ tool_calls=tool_calls,
1981
+ ),
1982
+ )
1983
+ ]
1984
+ )
1985
+
1986
+ if await self.process_tool_calls(tool_call_response):
1987
+ self.num_tool_calls += 1
1988
+ self.reflected_message = True
1989
+ return
1990
+ except Exception as e:
1991
+ self.io.tool_error(f"Error processing tool calls: {str(e)}")
1992
+ # Continue without tool processing
1813
1993
 
1814
1994
  self.num_tool_calls = 0
1815
1995
 
1816
1996
  try:
1817
- if self.reply_completed():
1997
+ if await self.reply_completed():
1818
1998
  return
1819
1999
  except KeyboardInterrupt:
1820
2000
  interrupted = True
@@ -1824,15 +2004,15 @@ class Coder:
1824
2004
 
1825
2005
  if edited and self.auto_lint:
1826
2006
  lint_errors = self.lint_edited(edited)
1827
- self.auto_commit(edited, context="Ran the linter")
2007
+ await self.auto_commit(edited, context="Ran the linter")
1828
2008
  self.lint_outcome = not lint_errors
1829
2009
  if lint_errors:
1830
- ok = self.io.confirm_ask("Attempt to fix lint errors?")
2010
+ ok = await self.io.confirm_ask("Attempt to fix lint errors?")
1831
2011
  if ok:
1832
2012
  self.reflected_message = lint_errors
1833
2013
  return
1834
2014
 
1835
- shared_output = self.run_shell_commands()
2015
+ shared_output = await self.run_shell_commands()
1836
2016
  if shared_output:
1837
2017
  self.cur_messages += [
1838
2018
  dict(role="user", content=shared_output),
@@ -1840,19 +2020,33 @@ class Coder:
1840
2020
  ]
1841
2021
 
1842
2022
  if edited and self.auto_test:
1843
- test_errors = self.commands.cmd_test(self.test_cmd)
2023
+ test_errors = await self.commands.cmd_test(self.test_cmd)
1844
2024
  self.test_outcome = not test_errors
1845
2025
  if test_errors:
1846
- ok = self.io.confirm_ask("Attempt to fix test errors?")
2026
+ ok = await self.io.confirm_ask("Attempt to fix test errors?")
1847
2027
  if ok:
1848
2028
  self.reflected_message = test_errors
1849
2029
  return
1850
2030
 
1851
- def process_tool_calls(self, tool_call_response):
2031
+ async def process_tool_calls(self, tool_call_response):
1852
2032
  if tool_call_response is None:
1853
2033
  return False
1854
2034
 
1855
- original_tool_calls = tool_call_response.choices[0].message.tool_calls
2035
+ # Handle different response structures
2036
+ try:
2037
+ # Try to get tool calls from the standard OpenAI response format
2038
+ if hasattr(tool_call_response, "choices") and tool_call_response.choices:
2039
+ message = tool_call_response.choices[0].message
2040
+ if hasattr(message, "tool_calls") and message.tool_calls:
2041
+ original_tool_calls = message.tool_calls
2042
+ else:
2043
+ return False
2044
+ else:
2045
+ # Handle other response formats
2046
+ return False
2047
+ except (AttributeError, IndexError):
2048
+ return False
2049
+
1856
2050
  if not original_tool_calls:
1857
2051
  return False
1858
2052
 
@@ -1888,22 +2082,14 @@ class Coder:
1888
2082
  )
1889
2083
  expanded_tool_calls.append(new_tool_call)
1890
2084
 
1891
- # Replace the original tool_calls in the response object with the expanded list.
1892
- tool_call_response.choices[0].message.tool_calls = expanded_tool_calls
1893
- tool_calls = expanded_tool_calls
1894
-
1895
2085
  # Collect all tool calls grouped by server
1896
- server_tool_calls = self._gather_server_tool_calls(tool_calls)
2086
+ server_tool_calls = self._gather_server_tool_calls(expanded_tool_calls)
1897
2087
 
1898
2088
  if server_tool_calls and self.num_tool_calls < self.max_tool_calls:
1899
2089
  self._print_tool_call_info(server_tool_calls)
1900
2090
 
1901
- if self.io.confirm_ask("Run tools?"):
1902
- tool_responses = self._execute_tool_calls(server_tool_calls)
1903
-
1904
- # Add the assistant message with the modified (expanded) tool calls.
1905
- # This ensures that what's stored in history is valid.
1906
- self.cur_messages.append(tool_call_response.choices[0].message.to_dict())
2091
+ if await self.io.confirm_ask("Run tools?"):
2092
+ tool_responses = await self._execute_tool_calls(server_tool_calls)
1907
2093
 
1908
2094
  # Add all tool responses
1909
2095
  for tool_response in tool_responses:
@@ -1917,12 +2103,45 @@ class Coder:
1917
2103
 
1918
2104
  def _print_tool_call_info(self, server_tool_calls):
1919
2105
  """Print information about an MCP tool call."""
1920
- self.io.tool_output("Preparing to run MCP tools", bold=True)
2106
+ self.io.tool_output("Preparing to run MCP tools", bold=False)
1921
2107
 
1922
2108
  for server, tool_calls in server_tool_calls.items():
1923
2109
  for tool_call in tool_calls:
1924
2110
  self.io.tool_output(f"Tool Call: {tool_call.function.name}")
1925
- self.io.tool_output(f"Arguments: {tool_call.function.arguments}")
2111
+
2112
+ # Parse and format arguments as headers with values
2113
+ if tool_call.function.arguments:
2114
+ # Only do JSON unwrapping for tools containing "replace" in their name
2115
+ if "replace" in tool_call.function.name.lower():
2116
+ try:
2117
+ args_dict = json.loads(tool_call.function.arguments)
2118
+ first_key = True
2119
+ for key, value in args_dict.items():
2120
+ # Convert explicit \\n sequences to actual newlines using regex
2121
+ # Only match \\n that is not preceded by any other backslashes
2122
+ if isinstance(value, str):
2123
+ value = re.sub(r"(?<!\\)\\n", "\n", value)
2124
+ # Add extra newline before first key/header
2125
+ if first_key:
2126
+ self.io.tool_output("\n")
2127
+ first_key = False
2128
+ self.io.tool_output(f"{key}:")
2129
+ # Split the value by newlines and output each line separately
2130
+ if isinstance(value, str):
2131
+ for line in value.split("\n"):
2132
+ self.io.tool_output(f"{line}")
2133
+ else:
2134
+ self.io.tool_output(f"{str(value)}")
2135
+ self.io.tool_output("")
2136
+ except json.JSONDecodeError:
2137
+ # If JSON parsing fails, show raw arguments
2138
+ raw_args = tool_call.function.arguments
2139
+ self.io.tool_output(f"Arguments: {raw_args}")
2140
+ else:
2141
+ # For non-replace tools, show raw arguments
2142
+ raw_args = tool_call.function.arguments
2143
+ self.io.tool_output(f"Arguments: {raw_args}")
2144
+
1926
2145
  self.io.tool_output(f"MCP Server: {server.name}")
1927
2146
 
1928
2147
  if self.verbose:
@@ -1947,7 +2166,11 @@ class Coder:
1947
2166
  # Check if this tool_call matches any MCP tool
1948
2167
  for server_name, server_tools in self.mcp_tools:
1949
2168
  for tool in server_tools:
1950
- if tool.get("function", {}).get("name") == tool_call.function.name:
2169
+ tool_name_from_schema = tool.get("function", {}).get("name")
2170
+ if (
2171
+ tool_name_from_schema
2172
+ and tool_name_from_schema.lower() == tool_call.function.name.lower()
2173
+ ):
1951
2174
  # Find the McpServer instance that will be used for communication
1952
2175
  for server in self.mcp_servers:
1953
2176
  if server.name == server_name:
@@ -1958,7 +2181,7 @@ class Coder:
1958
2181
 
1959
2182
  return server_tool_calls
1960
2183
 
1961
- def _execute_tool_calls(self, tool_calls):
2184
+ async def _execute_tool_calls(self, tool_calls):
1962
2185
  """Process tool calls from the response and execute them if they match MCP tools.
1963
2186
  Returns a list of tool response messages."""
1964
2187
  tool_responses = []
@@ -2065,6 +2288,13 @@ class Coder:
2065
2288
  tool_responses.append(
2066
2289
  {"role": "tool", "tool_call_id": tool_call.id, "content": tool_error}
2067
2290
  )
2291
+ except httpx.RemoteProtocolError as e:
2292
+ connection_error = f"Server {server.name} disconnected unexpectedly: {e}"
2293
+ self.io.tool_warning(connection_error)
2294
+ for tool_call in tool_calls_list:
2295
+ tool_responses.append(
2296
+ {"role": "tool", "tool_call_id": tool_call.id, "content": connection_error}
2297
+ )
2068
2298
  except Exception as e:
2069
2299
  connection_error = f"Could not connect to server {server.name}\n{e}"
2070
2300
  self.io.tool_warning(connection_error)
@@ -2072,8 +2302,6 @@ class Coder:
2072
2302
  tool_responses.append(
2073
2303
  {"role": "tool", "tool_call_id": tool_call.id, "content": connection_error}
2074
2304
  )
2075
- finally:
2076
- await server.disconnect()
2077
2305
 
2078
2306
  return tool_responses
2079
2307
 
@@ -2092,11 +2320,11 @@ class Coder:
2092
2320
  max_retries = 3
2093
2321
  for i in range(max_retries):
2094
2322
  try:
2095
- all_results = asyncio.run(_execute_all_tool_calls())
2323
+ all_results = await _execute_all_tool_calls()
2096
2324
  break
2097
2325
  except asyncio.exceptions.CancelledError:
2098
2326
  if i < max_retries - 1:
2099
- time.sleep(0.1) # Brief pause before retrying
2327
+ await asyncio.sleep(0.1) # Brief pause before retrying
2100
2328
  else:
2101
2329
  self.io.tool_warning(
2102
2330
  "MCP tool execution failed after multiple retries due to cancellation."
@@ -2109,7 +2337,7 @@ class Coder:
2109
2337
 
2110
2338
  return tool_responses
2111
2339
 
2112
- def initialize_mcp_tools(self):
2340
+ async def initialize_mcp_tools(self):
2113
2341
  """
2114
2342
  Initialize tools from all configured MCP servers. MCP Servers that fail to be
2115
2343
  initialized will not be available to the Coder instance.
@@ -2126,8 +2354,6 @@ class Coder:
2126
2354
  except Exception as e:
2127
2355
  self.io.tool_warning(f"Error initializing MCP server {server.name}:\n{e}")
2128
2356
  return None
2129
- finally:
2130
- await server.disconnect()
2131
2357
 
2132
2358
  async def get_all_server_tools():
2133
2359
  tasks = [get_server_tools(server) for server in self.mcp_servers]
@@ -2139,11 +2365,11 @@ class Coder:
2139
2365
  max_retries = 3
2140
2366
  for i in range(max_retries):
2141
2367
  try:
2142
- tools = asyncio.run(get_all_server_tools())
2368
+ tools = await get_all_server_tools()
2143
2369
  break
2144
2370
  except asyncio.exceptions.CancelledError:
2145
2371
  if i < max_retries - 1:
2146
- time.sleep(0.1) # Brief pause before retrying
2372
+ await asyncio.sleep(0.1) # Brief pause before retrying
2147
2373
  else:
2148
2374
  self.io.tool_warning(
2149
2375
  "MCP tool initialization failed after multiple retries due to"
@@ -2152,11 +2378,12 @@ class Coder:
2152
2378
  tools = []
2153
2379
 
2154
2380
  if len(tools) > 0:
2155
- self.io.tool_output("MCP servers configured:")
2156
- for server_name, server_tools in tools:
2157
- self.io.tool_output(f" - {server_name}")
2381
+ if self.verbose:
2382
+ self.io.tool_output("MCP servers configured:")
2383
+
2384
+ for server_name, server_tools in tools:
2385
+ self.io.tool_output(f" - {server_name}")
2158
2386
 
2159
- if self.verbose:
2160
2387
  for tool in server_tools:
2161
2388
  tool_name = tool.get("function", {}).get("name", "unknown")
2162
2389
  tool_desc = tool.get("function", {}).get("description", "").split("\n")[0]
@@ -2172,7 +2399,7 @@ class Coder:
2172
2399
  tool_list.extend(server_tools)
2173
2400
  return tool_list
2174
2401
 
2175
- def reply_completed(self):
2402
+ async def reply_completed(self):
2176
2403
  pass
2177
2404
 
2178
2405
  def show_exhausted_error(self):
@@ -2250,16 +2477,33 @@ class Coder:
2250
2477
  self.ok_to_warm_cache = False
2251
2478
 
2252
2479
  def add_assistant_reply_to_cur_messages(self):
2253
- if self.partial_response_content:
2254
- self.cur_messages += [dict(role="assistant", content=self.partial_response_content)]
2255
- if self.partial_response_function_call:
2256
- self.cur_messages += [
2257
- dict(
2258
- role="assistant",
2259
- content=None,
2260
- function_call=self.partial_response_function_call,
2261
- )
2262
- ]
2480
+ """
2481
+ Add the assistant's reply to `cur_messages`.
2482
+ Handles model-specific quirks, like Deepseek which requires `content`
2483
+ to be `None` when `tool_calls` are present.
2484
+ """
2485
+ msg = dict(role="assistant")
2486
+ has_tool_calls = self.partial_response_tool_calls or self.partial_response_function_call
2487
+
2488
+ # If we have tool calls and we're using a Deepseek model, force content to be None.
2489
+ if has_tool_calls and self.main_model.is_deepseek():
2490
+ msg["content"] = None
2491
+ else:
2492
+ # Otherwise, use logic similar to the base implementation.
2493
+ content = self.partial_response_content
2494
+ if content:
2495
+ msg["content"] = content
2496
+ elif has_tool_calls:
2497
+ msg["content"] = None
2498
+
2499
+ if self.partial_response_tool_calls:
2500
+ msg["tool_calls"] = self.partial_response_tool_calls
2501
+ elif self.partial_response_function_call:
2502
+ msg["function_call"] = self.partial_response_function_call
2503
+
2504
+ # Only add a message if it's not empty.
2505
+ if msg.get("content") is not None or msg.get("tool_calls") or msg.get("function_call"):
2506
+ self.cur_messages.append(msg)
2263
2507
 
2264
2508
  def get_file_mentions(self, content, ignore_current=False):
2265
2509
  words = set(word for word in content.split())
@@ -2309,7 +2553,7 @@ class Coder:
2309
2553
 
2310
2554
  return mentioned_rel_fnames
2311
2555
 
2312
- def check_for_file_mentions(self, content):
2556
+ async def check_for_file_mentions(self, content):
2313
2557
  mentioned_rel_fnames = self.get_file_mentions(content)
2314
2558
 
2315
2559
  new_mentions = mentioned_rel_fnames - self.ignore_mentions
@@ -2320,7 +2564,7 @@ class Coder:
2320
2564
  added_fnames = []
2321
2565
  group = ConfirmGroup(new_mentions)
2322
2566
  for rel_fname in sorted(new_mentions):
2323
- if self.io.confirm_ask(
2567
+ if await self.io.confirm_ask(
2324
2568
  "Add file to the chat?", subject=rel_fname, group=group, allow_never=True
2325
2569
  ):
2326
2570
  self.add_rel_fname(rel_fname)
@@ -2331,35 +2575,38 @@ class Coder:
2331
2575
  if added_fnames:
2332
2576
  return prompts.added_files.format(fnames=", ".join(added_fnames))
2333
2577
 
2334
- def send(self, messages, model=None, functions=None):
2578
+ async def send(self, messages, model=None, functions=None, tools=None):
2335
2579
  self.got_reasoning_content = False
2336
2580
  self.ended_reasoning_content = False
2337
2581
 
2582
+ self._streaming_buffer_length = 0
2583
+ self.io.reset_streaming_response()
2584
+
2338
2585
  if not model:
2339
2586
  model = self.main_model
2340
2587
 
2341
2588
  self.partial_response_content = ""
2342
2589
  self.partial_response_function_call = dict()
2590
+ self.partial_response_tool_calls = []
2343
2591
 
2344
2592
  self.io.log_llm_history("TO LLM", format_messages(messages))
2345
2593
 
2346
2594
  completion = None
2347
2595
 
2348
2596
  try:
2349
- tool_list = self.get_tool_list()
2350
-
2351
- hash_object, completion = model.send_completion(
2597
+ hash_object, completion = await model.send_completion(
2352
2598
  messages,
2353
2599
  functions,
2354
2600
  self.stream,
2355
2601
  self.temperature,
2356
2602
  # This could include any tools, but for now it is just MCP tools
2357
- tools=tool_list,
2603
+ tools=tools,
2358
2604
  )
2359
2605
  self.chat_completion_call_hashes.append(hash_object.hexdigest())
2360
2606
 
2361
2607
  if self.stream:
2362
- yield from self.show_send_output_stream(completion)
2608
+ async for chunk in self.show_send_output_stream(completion):
2609
+ yield chunk
2363
2610
  else:
2364
2611
  self.show_send_output(completion)
2365
2612
 
@@ -2390,9 +2637,6 @@ class Coder:
2390
2637
  self.io.ai_output(json.dumps(args, indent=4))
2391
2638
 
2392
2639
  def show_send_output(self, completion):
2393
- # Stop spinner once we have a response
2394
- self._stop_waiting_spinner()
2395
-
2396
2640
  if self.verbose:
2397
2641
  print(completion)
2398
2642
 
@@ -2453,11 +2697,14 @@ class Coder:
2453
2697
  ):
2454
2698
  raise FinishReasonLength()
2455
2699
 
2456
- def show_send_output_stream(self, completion):
2700
+ async def show_send_output_stream(self, completion):
2457
2701
  received_content = False
2458
- self.partial_response_tool_call = []
2459
2702
 
2460
- for chunk in completion:
2703
+ async for chunk in completion:
2704
+ # Check if confirmation is in progress and wait if needed
2705
+ while self.confirmation_in_progress:
2706
+ await asyncio.sleep(0.1) # Yield control and wait briefly
2707
+
2461
2708
  if isinstance(chunk, str):
2462
2709
  text = chunk
2463
2710
  received_content = True
@@ -2471,13 +2718,59 @@ class Coder:
2471
2718
  ):
2472
2719
  raise FinishReasonLength()
2473
2720
 
2474
- if chunk.choices[0].delta.tool_calls:
2475
- self.partial_response_tool_call.append(chunk)
2721
+ try:
2722
+ if chunk.choices[0].delta.tool_calls:
2723
+ received_content = True
2724
+ for tool_call_chunk in chunk.choices[0].delta.tool_calls:
2725
+ self.tool_reflection = True
2726
+
2727
+ index = tool_call_chunk.index
2728
+ if len(self.partial_response_tool_calls) <= index:
2729
+ self.partial_response_tool_calls.extend(
2730
+ [{}] * (index - len(self.partial_response_tool_calls) + 1)
2731
+ )
2732
+
2733
+ if tool_call_chunk.id:
2734
+ self.partial_response_tool_calls[index]["id"] = tool_call_chunk.id
2735
+ if tool_call_chunk.type:
2736
+ self.partial_response_tool_calls[index][
2737
+ "type"
2738
+ ] = tool_call_chunk.type
2739
+ if tool_call_chunk.function:
2740
+ if "function" not in self.partial_response_tool_calls[index]:
2741
+ self.partial_response_tool_calls[index]["function"] = {}
2742
+ if tool_call_chunk.function.name:
2743
+ if (
2744
+ "name"
2745
+ not in self.partial_response_tool_calls[index]["function"]
2746
+ ):
2747
+ self.partial_response_tool_calls[index]["function"][
2748
+ "name"
2749
+ ] = ""
2750
+ self.partial_response_tool_calls[index]["function"][
2751
+ "name"
2752
+ ] += tool_call_chunk.function.name
2753
+ if tool_call_chunk.function.arguments:
2754
+ if (
2755
+ "arguments"
2756
+ not in self.partial_response_tool_calls[index]["function"]
2757
+ ):
2758
+ self.partial_response_tool_calls[index]["function"][
2759
+ "arguments"
2760
+ ] = ""
2761
+ self.partial_response_tool_calls[index]["function"][
2762
+ "arguments"
2763
+ ] += tool_call_chunk.function.arguments
2764
+ except (AttributeError, IndexError):
2765
+ # Handle cases where the response structure doesn't match expectations
2766
+ pass
2476
2767
 
2477
2768
  try:
2478
2769
  func = chunk.choices[0].delta.function_call
2479
2770
  # dump(func)
2480
2771
  for k, v in func.items():
2772
+ self.tool_reflection = True
2773
+
2481
2774
  if k in self.partial_response_function_call:
2482
2775
  self.partial_response_function_call[k] += v
2483
2776
  else:
@@ -2516,36 +2809,62 @@ class Coder:
2516
2809
  except AttributeError:
2517
2810
  pass
2518
2811
 
2519
- if received_content:
2520
- self._stop_waiting_spinner()
2521
2812
  self.partial_response_content += text
2522
-
2523
2813
  if self.show_pretty():
2524
- self.live_incremental_response(False)
2814
+ # Use simplified streaming - just call the method with full content
2815
+ content_to_show = self.live_incremental_response(False)
2816
+ self.stream_wrapper(content_to_show, final=False)
2525
2817
  elif text:
2526
- # Apply reasoning tag formatting
2818
+ # Apply reasoning tag formatting for non-pretty output
2527
2819
  text = replace_reasoning_tags(text, self.reasoning_tag_name)
2528
2820
  try:
2529
- sys.stdout.write(text)
2821
+ self.stream_wrapper(text, final=False)
2530
2822
  except UnicodeEncodeError:
2531
2823
  # Safely encode and decode the text
2532
2824
  safe_text = text.encode(sys.stdout.encoding, errors="backslashreplace").decode(
2533
2825
  sys.stdout.encoding
2534
2826
  )
2535
- sys.stdout.write(safe_text)
2536
- sys.stdout.flush()
2827
+ self.stream_wrapper(safe_text, final=False)
2537
2828
  yield text
2538
2829
 
2539
- if not received_content and len(self.partial_response_tool_call) == 0:
2830
+ if not received_content and len(self.partial_response_tool_calls) == 0:
2540
2831
  self.io.tool_warning("Empty response received from LLM. Check your provider account?")
2541
2832
 
2833
+ def stream_wrapper(self, content, final):
2834
+ if not hasattr(self, "_streaming_buffer_length"):
2835
+ self._streaming_buffer_length = 0
2836
+
2837
+ if final:
2838
+ content += "\n\n"
2839
+
2840
+ if isinstance(content, str):
2841
+ self._streaming_buffer_length += len(content)
2842
+
2843
+ self.io.stream_output(content, final=final)
2844
+
2845
+ if final:
2846
+ self._streaming_buffer_length = 0
2847
+
2542
2848
  def live_incremental_response(self, final):
2543
2849
  show_resp = self.render_incremental_response(final)
2544
2850
  # Apply any reasoning tag formatting
2545
2851
  show_resp = replace_reasoning_tags(show_resp, self.reasoning_tag_name)
2546
- self.mdstream.update(show_resp, final=final)
2852
+
2853
+ # Track streaming state to avoid repetitive output
2854
+ if not hasattr(self, "_streaming_buffer_length"):
2855
+ self._streaming_buffer_length = 0
2856
+
2857
+ # Only send new content that hasn't been streamed yet
2858
+ if len(show_resp) >= self._streaming_buffer_length:
2859
+ new_content = show_resp[self._streaming_buffer_length :]
2860
+ return new_content
2861
+ else:
2862
+ self._streaming_buffer_length = 0
2863
+ self.io.reset_streaming_response()
2864
+ return show_resp
2547
2865
 
2548
2866
  def render_incremental_response(self, final):
2867
+ # Just return the current content - the streaming logic will handle incremental updates
2549
2868
  return self.get_multi_response_content_in_progress()
2550
2869
 
2551
2870
  def remove_reasoning_content(self):
@@ -2611,18 +2930,9 @@ class Coder:
2611
2930
  self.total_cost += cost
2612
2931
  self.message_cost += cost
2613
2932
 
2614
- def format_cost(value):
2615
- if value == 0:
2616
- return "0.00"
2617
- magnitude = abs(value)
2618
- if magnitude >= 0.01:
2619
- return f"{value:.2f}"
2620
- else:
2621
- return f"{value:.{max(2, 2 - int(math.log10(magnitude)))}f}"
2622
-
2623
2933
  cost_report = (
2624
- f"Cost: ${format_cost(self.message_cost)} message,"
2625
- f" ${format_cost(self.total_cost)} session."
2934
+ f"Cost: ${self.format_cost(self.message_cost)} message,"
2935
+ f" ${self.format_cost(self.total_cost)} session."
2626
2936
  )
2627
2937
 
2628
2938
  if cache_hit_tokens and cache_write_tokens:
@@ -2632,6 +2942,15 @@ class Coder:
2632
2942
 
2633
2943
  self.usage_report = tokens_report + sep + cost_report
2634
2944
 
2945
+ def format_cost(self, value):
2946
+ if value == 0:
2947
+ return "0.00"
2948
+ magnitude = abs(value)
2949
+ if magnitude >= 0.01:
2950
+ return f"{value:.2f}"
2951
+ else:
2952
+ return f"{value:.{max(2, 2 - int(math.log10(magnitude)))}f}"
2953
+
2635
2954
  def compute_costs_from_tokens(
2636
2955
  self, prompt_tokens, completion_tokens, cache_write_tokens, cache_hit_tokens
2637
2956
  ):
@@ -2757,7 +3076,7 @@ class Coder:
2757
3076
  self.io.tool_output(f"Committing {path} before applying edits.")
2758
3077
  self.need_commit_before_edits.add(path)
2759
3078
 
2760
- def allowed_to_edit(self, path):
3079
+ async def allowed_to_edit(self, path):
2761
3080
  full_path = self.abs_root_path(path)
2762
3081
  if self.repo:
2763
3082
  need_to_add = not self.repo.path_in_repo(path)
@@ -2792,7 +3111,7 @@ class Coder:
2792
3111
  self.check_added_files()
2793
3112
  return True
2794
3113
 
2795
- if not self.io.confirm_ask(
3114
+ if not await self.io.confirm_ask(
2796
3115
  "Allow edits to file that has not been added to the chat?",
2797
3116
  subject=path,
2798
3117
  ):
@@ -2835,7 +3154,7 @@ class Coder:
2835
3154
  self.io.tool_warning(urls.edit_errors)
2836
3155
  self.warning_given = True
2837
3156
 
2838
- def prepare_to_edit(self, edits):
3157
+ async def prepare_to_edit(self, edits):
2839
3158
  res = []
2840
3159
  seen = dict()
2841
3160
 
@@ -2851,23 +3170,23 @@ class Coder:
2851
3170
  if path in seen:
2852
3171
  allowed = seen[path]
2853
3172
  else:
2854
- allowed = self.allowed_to_edit(path)
3173
+ allowed = await self.allowed_to_edit(path)
2855
3174
  seen[path] = allowed
2856
3175
 
2857
3176
  if allowed:
2858
3177
  res.append(edit)
2859
3178
 
2860
- self.dirty_commit()
3179
+ await self.dirty_commit()
2861
3180
  self.need_commit_before_edits = set()
2862
3181
 
2863
3182
  return res
2864
3183
 
2865
- def apply_updates(self):
3184
+ async def apply_updates(self):
2866
3185
  edited = set()
2867
3186
  try:
2868
3187
  edits = self.get_edits()
2869
3188
  edits = self.apply_edits_dry_run(edits)
2870
- edits = self.prepare_to_edit(edits)
3189
+ edits = await self.prepare_to_edit(edits)
2871
3190
  edited = set(edit[0] for edit in edits)
2872
3191
 
2873
3192
  self.apply_edits(edits)
@@ -2890,9 +3209,7 @@ class Coder:
2890
3209
  except Exception as err:
2891
3210
  self.io.tool_error("Exception while updating files:")
2892
3211
  self.io.tool_error(str(err), strip=False)
2893
-
2894
- traceback.print_exc()
2895
-
3212
+ self.io.tool_error(traceback.format_exc())
2896
3213
  self.reflected_message = str(err)
2897
3214
  return edited
2898
3215
 
@@ -2964,7 +3281,7 @@ class Coder:
2964
3281
 
2965
3282
  return context
2966
3283
 
2967
- def auto_commit(self, edited, context=None):
3284
+ async def auto_commit(self, edited, context=None):
2968
3285
  if not self.repo or not self.auto_commits or self.dry_run:
2969
3286
  return
2970
3287
 
@@ -2972,7 +3289,9 @@ class Coder:
2972
3289
  context = self.get_context_from_history(self.cur_messages)
2973
3290
 
2974
3291
  try:
2975
- res = self.repo.commit(fnames=edited, context=context, aider_edits=True, coder=self)
3292
+ res = await self.repo.commit(
3293
+ fnames=edited, context=context, aider_edits=True, coder=self
3294
+ )
2976
3295
  if res:
2977
3296
  self.show_auto_commit_outcome(res)
2978
3297
  commit_hash, commit_message = res
@@ -3000,7 +3319,7 @@ class Coder:
3000
3319
  if self.commit_before_message[-1] != self.repo.get_head_commit_sha():
3001
3320
  self.io.tool_output("You can use /undo to undo and discard each aider commit.")
3002
3321
 
3003
- def dirty_commit(self):
3322
+ async def dirty_commit(self):
3004
3323
  if not self.need_commit_before_edits:
3005
3324
  return
3006
3325
  if not self.dirty_commits:
@@ -3008,7 +3327,7 @@ class Coder:
3008
3327
  if not self.repo:
3009
3328
  return
3010
3329
 
3011
- self.repo.commit(fnames=self.need_commit_before_edits, coder=self)
3330
+ await self.repo.commit(fnames=self.need_commit_before_edits, coder=self)
3012
3331
 
3013
3332
  # files changed, move cur messages back behind the files messages
3014
3333
  # self.move_back_cur_messages(self.gpt_prompts.files_content_local_edits)
@@ -3023,7 +3342,7 @@ class Coder:
3023
3342
  def apply_edits_dry_run(self, edits):
3024
3343
  return edits
3025
3344
 
3026
- def run_shell_commands(self):
3345
+ async def run_shell_commands(self):
3027
3346
  if not self.suggest_shell_commands:
3028
3347
  return ""
3029
3348
 
@@ -3034,18 +3353,18 @@ class Coder:
3034
3353
  if command in done:
3035
3354
  continue
3036
3355
  done.add(command)
3037
- output = self.handle_shell_commands(command, group)
3356
+ output = await self.handle_shell_commands(command, group)
3038
3357
  if output:
3039
3358
  accumulated_output += output + "\n\n"
3040
3359
  return accumulated_output
3041
3360
 
3042
- def handle_shell_commands(self, commands_str, group):
3361
+ async def handle_shell_commands(self, commands_str, group):
3043
3362
  commands = commands_str.strip().splitlines()
3044
3363
  command_count = sum(
3045
3364
  1 for cmd in commands if cmd.strip() and not cmd.strip().startswith("#")
3046
3365
  )
3047
3366
  prompt = "Run shell command?" if command_count == 1 else "Run shell commands?"
3048
- if not self.io.confirm_ask(
3367
+ if not await self.io.confirm_ask(
3049
3368
  prompt,
3050
3369
  subject="\n".join(commands),
3051
3370
  explicit_yes_required=True,
@@ -3064,11 +3383,13 @@ class Coder:
3064
3383
  self.io.tool_output(f"Running {command}")
3065
3384
  # Add the command to input history
3066
3385
  self.io.add_to_input_history(f"/run {command.strip()}")
3067
- exit_status, output = run_cmd(command, error_print=self.io.tool_error, cwd=self.root)
3386
+ exit_status, output = await asyncio.to_thread(
3387
+ run_cmd, command, error_print=self.io.tool_error, cwd=self.root
3388
+ )
3068
3389
  if output:
3069
3390
  accumulated_output += f"Output from {command}\n{output}\n"
3070
3391
 
3071
- if accumulated_output.strip() and self.io.confirm_ask(
3392
+ if accumulated_output.strip() and await self.io.confirm_ask(
3072
3393
  "Add command output to the chat?", allow_never=True
3073
3394
  ):
3074
3395
  num_lines = len(accumulated_output.strip().splitlines())