aider-ce 0.87.13__py3-none-any.whl → 0.88.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aider-ce might be problematic. Click here for more details.
- aider/__init__.py +1 -1
- aider/_version.py +2 -2
- aider/args.py +6 -0
- aider/coders/architect_coder.py +3 -3
- aider/coders/base_coder.py +505 -184
- aider/coders/context_coder.py +1 -1
- aider/coders/editblock_func_coder.py +2 -2
- aider/coders/navigator_coder.py +451 -649
- aider/coders/navigator_legacy_prompts.py +49 -284
- aider/coders/navigator_prompts.py +46 -473
- aider/coders/search_replace.py +0 -0
- aider/coders/wholefile_func_coder.py +2 -2
- aider/commands.py +56 -44
- aider/history.py +14 -12
- aider/io.py +354 -117
- aider/llm.py +12 -4
- aider/main.py +22 -19
- aider/mcp/__init__.py +65 -2
- aider/mcp/server.py +37 -11
- aider/models.py +45 -20
- aider/onboarding.py +4 -4
- aider/repo.py +7 -7
- aider/resources/model-metadata.json +8 -8
- aider/scrape.py +2 -2
- aider/sendchat.py +185 -15
- aider/tools/__init__.py +44 -23
- aider/tools/command.py +18 -0
- aider/tools/command_interactive.py +18 -0
- aider/tools/delete_block.py +23 -0
- aider/tools/delete_line.py +19 -1
- aider/tools/delete_lines.py +20 -1
- aider/tools/extract_lines.py +25 -2
- aider/tools/git.py +142 -0
- aider/tools/grep.py +47 -2
- aider/tools/indent_lines.py +25 -0
- aider/tools/insert_block.py +26 -0
- aider/tools/list_changes.py +15 -0
- aider/tools/ls.py +24 -1
- aider/tools/make_editable.py +18 -0
- aider/tools/make_readonly.py +19 -0
- aider/tools/remove.py +22 -0
- aider/tools/replace_all.py +21 -0
- aider/tools/replace_line.py +20 -1
- aider/tools/replace_lines.py +21 -1
- aider/tools/replace_text.py +22 -0
- aider/tools/show_numbered_context.py +18 -0
- aider/tools/undo_change.py +15 -0
- aider/tools/update_todo_list.py +131 -0
- aider/tools/view.py +23 -0
- aider/tools/view_files_at_glob.py +32 -27
- aider/tools/view_files_matching.py +51 -37
- aider/tools/view_files_with_symbol.py +41 -54
- aider/tools/view_todo_list.py +57 -0
- aider/waiting.py +20 -203
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/METADATA +21 -5
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/RECORD +59 -56
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/WHEEL +0 -0
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/entry_points.txt +0 -0
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/licenses/LICENSE.txt +0 -0
- {aider_ce-0.87.13.dist-info → aider_ce-0.88.0.dist-info}/top_level.txt +0 -0
aider/coders/base_coder.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
-
|
|
1224
|
+
if self.commands.is_command(user_message):
|
|
1225
|
+
return
|
|
1226
|
+
|
|
1227
|
+
while True:
|
|
1093
1228
|
self.reflected_message = None
|
|
1094
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
1643
|
-
|
|
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
|
-
|
|
1659
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
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
|
-
|
|
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(
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
2368
|
+
tools = await get_all_server_tools()
|
|
2143
2369
|
break
|
|
2144
2370
|
except asyncio.exceptions.CancelledError:
|
|
2145
2371
|
if i < max_retries - 1:
|
|
2146
|
-
|
|
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.
|
|
2156
|
-
|
|
2157
|
-
|
|
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
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
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
|
-
|
|
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=
|
|
2603
|
+
tools=tools,
|
|
2358
2604
|
)
|
|
2359
2605
|
self.chat_completion_call_hashes.append(hash_object.hexdigest())
|
|
2360
2606
|
|
|
2361
2607
|
if self.stream:
|
|
2362
|
-
|
|
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
|
-
|
|
2475
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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())
|