diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 70ea3402e..274be72c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,12 +21,12 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine importlib-metadata==7.2.1 + pip install build setuptools wheel twine importlib-metadata==7.2.1 - name: Build and publish env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: | - python setup.py sdist bdist_wheel + python -m build twine upload dist/* diff --git a/.gitignore b/.gitignore index a6a3dbfed..ae668b707 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ aider.code-workspace .aider* aider_chat.egg-info/ build +dist/ Gemfile.lock _site .jekyll-cache/ diff --git a/HISTORY.md b/HISTORY.md index 239e3b238..2807e037d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,51 @@ # Release history +### Aider v0.50.0 + +- Infinite output for DeepSeek Coder, Mistral models in addition to Anthropic's models. +- New `--deepseek` switch to use DeepSeek Coder. +- DeepSeek Coder uses 8k token output. +- New `--chat-mode ` switch to launch in ask/help/code modes. +- New `/code ` command request a code edit while in `ask` mode. +- Web scraper is more robust if page never idles. +- Improved token and cost reporting for infinite output. +- Improvements and bug fixes for `/read` only files. +- Switched from `setup.py` to `pyproject.toml`, by @branchvincent. +- Bug fix to persist files added during `/ask`. +- Bug fix for chat history size in `/tokens`. +- Aider wrote 66% of the code in this release. + +### Aider v0.49.1 + +- Bugfix to `/help`. + +### Aider v0.49.0 + +- Add read-only files to the chat context with `/read` and `--read`, including from outside the git repo. +- `/diff` now shows diffs of all changes resulting from your request, including lint and test fixes. +- New `/clipboard` command to paste images or text from the clipboard, replaces `/add-clipboard-image`. +- Now shows the markdown scraped when you add a url with `/web`. +- When [scripting aider](https://aider.chat/docs/scripting.html) messages can now contain in-chat `/` commands. +- Aider in docker image now suggests the correct command to update to latest version. +- Improved retries on API errors (was easy to test during Sonnet outage). +- Added `--mini` for `gpt-4o-mini`. +- Bugfix to keep session cost accurate when using `/ask` and `/help`. +- Performance improvements for repo map calculation. +- `/tokens` now shows the active model. +- Enhanced commit message attribution options: + - New `--attribute-commit-message-author` to prefix commit messages with 'aider: ' if aider authored the changes, replaces `--attribute-commit-message`. + - New `--attribute-commit-message-committer` to prefix all commit messages with 'aider: '. +- Aider wrote 61% of the code in this release. + +### Aider v0.48.1 + +- Added `openai/gpt-4o-2024-08-06`. +- Worked around litellm bug that removes OpenRouter app headers when using `extra_headers`. +- Improved progress indication during repo map processing. +- Corrected instructions for upgrading the docker container to latest aider version. +- Removed obsolete 16k token limit on commit diffs, use per-model limits. + ### Aider v0.48.0 - Performance improvements for large/mono repos. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index f9bd1455b..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include requirements.txt diff --git a/README.md b/README.md index 9b0b12ab7..25a9399f4 100644 --- a/README.md +++ b/README.md @@ -35,18 +35,18 @@ cog.out(open("aider/website/_includes/get-started.md").read()) You can get started quickly like this: ``` -$ pip install aider-chat +python -m pip install aider-chat # Change directory into a git repo -$ cd /to/your/git/repo +cd /to/your/git/repo # Work with Claude 3.5 Sonnet on your repo -$ export ANTHROPIC_API_KEY=your-key-goes-here -$ aider +export ANTHROPIC_API_KEY=your-key-goes-here +aider # Work with GPT-4o on your repo -$ export OPENAI_API_KEY=your-key-goes-here -$ aider +export OPENAI_API_KEY=your-key-goes-here +aider ``` diff --git a/aider/__init__.py b/aider/__init__.py index 1ec4c9f61..41d36926a 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1 +1 @@ -__version__ = "0.48.1-dev" +__version__ = "0.50.1-dev" diff --git a/aider/args.py b/aider/args.py index ff061c4b1..2b18fb6a0 100644 --- a/aider/args.py +++ b/aider/args.py @@ -82,6 +82,14 @@ def get_parser(default_config_files, git_root): const=gpt_4o_model, help=f"Use {gpt_4o_model} model for the main chat", ) + gpt_4o_mini_model = "gpt-4o-mini" + group.add_argument( + "--mini", + action="store_const", + dest="model", + const=gpt_4o_mini_model, + help=f"Use {gpt_4o_mini_model} model for the main chat", + ) gpt_4_turbo_model = "gpt-4-1106-preview" group.add_argument( "--4-turbo", @@ -101,6 +109,14 @@ def get_parser(default_config_files, git_root): const=gpt_3_model_name, help=f"Use {gpt_3_model_name} model for the main chat", ) + deepseek_model = "deepseek/deepseek-coder" + group.add_argument( + "--deepseek", + action="store_const", + dest="model", + const=deepseek_model, + help=f"Use {deepseek_model} model for the main chat", + ) ########## group = parser.add_argument_group("Model Settings") @@ -159,6 +175,7 @@ def get_parser(default_config_files, git_root): ) group.add_argument( "--edit-format", + "--chat-mode", metavar="EDIT_FORMAT", default=None, help="Specify what edit format the LLM should use (default depends on model)", @@ -350,10 +367,16 @@ def get_parser(default_config_files, git_root): help="Attribute aider commits in the git committer name (default: True)", ) group.add_argument( - "--attribute-commit-message", + "--attribute-commit-message-author", action=argparse.BooleanOptionalAction, default=False, - help="Prefix commit messages with 'aider: ' (default: False)", + help="Prefix commit messages with 'aider: ' if aider authored the changes (default: False)", + ) + group.add_argument( + "--attribute-commit-message-committer", + action=argparse.BooleanOptionalAction, + default=False, + help="Prefix all commit messages with 'aider: ' (default: False)", ) group.add_argument( "--commit", @@ -420,6 +443,12 @@ def get_parser(default_config_files, git_root): metavar="FILE", help="specify a file to edit (can be used multiple times)", ) + group.add_argument( + "--read", + action="append", + metavar="FILE", + help="specify a read-only file (can be used multiple times)", + ) group.add_argument( "--vim", action="store_true", diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 733d314a7..f4f1319bf 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -30,7 +30,7 @@ from aider.llm import litellm from aider.mdstream import MarkdownStream from aider.repo import GitRepo from aider.repomap import RepoMap -from aider.sendchat import send_with_retries +from aider.sendchat import retry_exceptions, send_completion from aider.utils import format_content, format_messages, is_image_file from ..dump import dump # noqa: F401 @@ -50,6 +50,7 @@ def wrap_fence(name): class Coder: abs_fnames = None + abs_read_only_fnames = None repo = None last_aider_commit_hash = None aider_edited_files = None @@ -70,6 +71,11 @@ class Coder: lint_outcome = None test_outcome = None multi_response_content = "" + partial_response_content = "" + commit_before_message = [] + message_cost = 0.0 + message_tokens_sent = 0 + message_tokens_received = 0 @classmethod def create( @@ -89,6 +95,8 @@ class Coder: else: main_model = models.Model(models.DEFAULT_MODEL_NAME) + if edit_format == "code": + edit_format = None if edit_format is None: if from_coder: edit_format = from_coder.edit_format @@ -112,6 +120,7 @@ class Coder: # Bring along context from the old Coder update = dict( fnames=list(from_coder.abs_fnames), + read_only_fnames=list(from_coder.abs_read_only_fnames), # Copy read-only files done_messages=done_messages, cur_messages=from_coder.cur_messages, aider_commit_hashes=from_coder.aider_commit_hashes, @@ -143,7 +152,10 @@ class Coder: main_model = self.main_model weak_model = main_model.weak_model prefix = "Model:" - output = f" {main_model.name} with {self.edit_format} edit format" + output = f" {main_model.name} with" + if main_model.info.get("supports_assistant_prefill"): + output += " ♾️" + output += f" {self.edit_format} edit format" if weak_model is not main_model: prefix = "Models:" output += f", weak model {weak_model.name}" @@ -193,7 +205,7 @@ class Coder: io, repo=None, fnames=None, - pretty=True, + read_only_fnames=None, show_diffs=False, auto_commits=True, dirty_commits=True, @@ -217,6 +229,7 @@ class Coder: summarizer=None, total_cost=0.0, ): + self.commit_before_message = [] self.aider_commit_hashes = set() self.rejected_urls = set() self.abs_root_path_cache = {} @@ -240,6 +253,7 @@ class Coder: self.verbose = verbose self.abs_fnames = set() + self.abs_read_only_fnames = set() if cur_messages: self.cur_messages = cur_messages @@ -263,9 +277,9 @@ class Coder: self.code_theme = code_theme self.dry_run = dry_run - self.pretty = pretty + self.pretty = self.io.pretty - if pretty: + if self.pretty: self.console = Console() else: self.console = Console(force_terminal=False, no_color=True) @@ -314,6 +328,15 @@ class Coder: if not self.repo: self.find_common_root() + if read_only_fnames: + self.abs_read_only_fnames = set() + for fname in read_only_fnames: + abs_fname = self.abs_root_path(fname) + if os.path.exists(abs_fname): + self.abs_read_only_fnames.add(abs_fname) + else: + self.io.tool_error(f"Error: Read-only file {fname} does not exist. Skipping.") + if map_tokens is None: use_repo_map = main_model.use_repo_map map_tokens = 1024 @@ -376,8 +399,10 @@ class Coder: self.linter.set_linter(lang, cmd) def show_announcements(self): + bold = True for line in self.get_announcements(): - self.io.tool_output(line) + self.io.tool_output(line, bold=bold) + bold = False def find_common_root(self): if len(self.abs_fnames) == 1: @@ -444,6 +469,10 @@ class Coder: all_content = "" for _fname, content in self.get_abs_fnames_content(): all_content += content + "\n" + for _fname in self.abs_read_only_fnames: + content = self.io.read_text(_fname) + if content is not None: + all_content += content + "\n" good = False for fence_open, fence_close in self.fences: @@ -485,6 +514,19 @@ class Coder: return prompt + def get_read_only_files_content(self): + prompt = "" + for fname in self.abs_read_only_fnames: + content = self.io.read_text(fname) + if content is not None and not is_image_file(fname): + relative_fname = self.get_rel_fname(fname) + prompt += "\n" + prompt += relative_fname + prompt += f"\n{self.fence[0]}\n" + prompt += content + prompt += f"{self.fence[1]}\n" + return prompt + def get_cur_message_text(self): text = "" for msg in self.cur_messages: @@ -522,9 +564,13 @@ class Coder: mentioned_fnames.update(self.get_ident_filename_matches(mentioned_idents)) - other_files = set(self.get_all_abs_files()) - set(self.abs_fnames) + all_abs_files = set(self.get_all_abs_files()) + repo_abs_read_only_fnames = set(self.abs_read_only_fnames) & all_abs_files + chat_files = set(self.abs_fnames) | repo_abs_read_only_fnames + other_files = all_abs_files - chat_files + repo_content = self.repo_map.get_repo_map( - self.abs_fnames, + chat_files, other_files, mentioned_fnames=mentioned_fnames, mentioned_idents=mentioned_idents, @@ -534,7 +580,7 @@ class Coder: if not repo_content: repo_content = self.repo_map.get_repo_map( set(), - set(self.get_all_abs_files()), + all_abs_files, mentioned_fnames=mentioned_fnames, mentioned_idents=mentioned_idents, ) @@ -543,7 +589,7 @@ class Coder: if not repo_content: repo_content = self.repo_map.get_repo_map( set(), - set(self.get_all_abs_files()), + all_abs_files, ) return repo_content @@ -572,12 +618,6 @@ class Coder: files_content = self.gpt_prompts.files_no_full_files files_reply = "Ok." - if files_content: - files_messages += [ - dict(role="user", content=files_content), - dict(role="assistant", content=files_reply), - ] - images_message = self.get_images_message() if images_message is not None: files_messages += [ @@ -585,6 +625,24 @@ class Coder: dict(role="assistant", content="Ok."), ] + read_only_content = self.get_read_only_files_content() + if read_only_content: + files_messages += [ + dict( + role="user", content=self.gpt_prompts.read_only_files_prefix + read_only_content + ), + dict( + role="assistant", + content="Ok, I will use these files as references.", + ), + ] + + if files_content: + files_messages += [ + dict(role="user", content=files_content), + dict(role="assistant", content=files_reply), + ] + return files_messages def get_images_message(self): @@ -597,9 +655,11 @@ class Coder: mime_type, _ = mimetypes.guess_type(fname) if mime_type and mime_type.startswith("image/"): image_url = f"data:{mime_type};base64,{content}" - image_messages.append( - {"type": "image_url", "image_url": {"url": image_url, "detail": "high"}} - ) + rel_fname = self.get_rel_fname(fname) + image_messages += [ + {"type": "text", "text": f"Image file: {rel_fname}"}, + {"type": "image_url", "image_url": {"url": image_url, "detail": "high"}}, + ] if not image_messages: return None @@ -609,7 +669,7 @@ class Coder: def run_stream(self, user_message): self.io.user_input(user_message) self.init_before_message() - yield from self.send_new_user_message(user_message) + yield from self.send_message(user_message) def init_before_message(self): self.reflected_message = None @@ -617,48 +677,39 @@ class Coder: self.lint_outcome = None self.test_outcome = None self.edit_outcome = None + if self.repo: + self.commit_before_message.append(self.repo.get_head()) - def run(self, with_message=None): - while True: - self.init_before_message() + def run(self, with_message=None, preproc=True): + try: + if with_message: + self.io.user_input(with_message) + self.run_one(with_message, preproc) + return self.partial_response_content - try: - if with_message: - new_user_message = with_message - self.io.user_input(with_message) - else: - new_user_message = self.run_loop() + while True: + try: + user_message = self.get_input() + self.run_one(user_message, preproc) + self.show_undo_hint() + except KeyboardInterrupt: + self.keyboard_interrupt() + except EOFError: + return - while new_user_message: - self.reflected_message = None - list(self.send_new_user_message(new_user_message)) - - new_user_message = None - if self.reflected_message: - if self.num_reflections < self.max_reflections: - self.num_reflections += 1 - new_user_message = self.reflected_message - else: - self.io.tool_error( - f"Only {self.max_reflections} reflections allowed, stopping." - ) - - if with_message: - return self.partial_response_content - - except KeyboardInterrupt: - self.keyboard_interrupt() - except EOFError: - return - - def run_loop(self): - inp = self.io.get_input( + def get_input(self): + inchat_files = self.get_inchat_relative_files() + read_only_files = [self.get_rel_fname(fname) for fname in self.abs_read_only_fnames] + all_files = sorted(set(inchat_files + read_only_files)) + return self.io.get_input( self.root, - self.get_inchat_relative_files(), + all_files, self.get_addable_relative_files(), self.commands, + self.abs_read_only_fnames, ) + def preproc_user_input(self, inp): if not inp: return @@ -670,6 +721,28 @@ class Coder: return inp + def run_one(self, user_message, preproc): + self.init_before_message() + + if preproc: + message = self.preproc_user_input(user_message) + else: + message = user_message + + while message: + self.reflected_message = None + list(self.send_message(message)) + + if not self.reflected_message: + break + + if self.num_reflections >= self.max_reflections: + self.io.tool_error(f"Only {self.max_reflections} reflections allowed, stopping.") + return + + self.num_reflections += 1 + message = self.reflected_message + def check_for_urls(self, inp): url_pattern = re.compile(r"(https?://[^\s/$.?#].[^\s]*[^\s,.])") urls = list(set(url_pattern.findall(inp))) # Use set to remove duplicates @@ -678,7 +751,7 @@ class Coder: if url not in self.rejected_urls: if self.io.confirm_ask(f"Add {url} to the chat?"): inp += "\n\n" - inp += self.commands.cmd_web(url) + inp += self.commands.cmd_web(url, paginate=False) added_urls.append(url) else: self.rejected_urls.add(url) @@ -826,6 +899,7 @@ class Coder: self.summarize_end() messages += self.done_messages + messages += self.get_files_messages() if self.gpt_prompts.system_reminder: @@ -852,7 +926,7 @@ class Coder: final = messages[-1] - max_input_tokens = self.main_model.info.get("max_input_tokens") + max_input_tokens = self.main_model.info.get("max_input_tokens") or 0 # Add the reminder prompt if we still have room to include it. if ( max_input_tokens is None @@ -872,7 +946,7 @@ class Coder: return messages - def send_new_user_message(self, inp): + def send_message(self, inp): self.aider_edited_files = None self.cur_messages += [ @@ -891,6 +965,8 @@ class Coder: else: self.mdstream = None + retry_delay = 0.125 + self.usage_report = None exhausted = False interrupted = False @@ -899,6 +975,14 @@ class Coder: try: yield from self.send(messages, functions=self.functions) break + except retry_exceptions() as err: + self.io.tool_error(str(err)) + retry_delay *= 2 + if retry_delay > 60: + break + self.io.tool_output(f"Retrying in {retry_delay:.1f} seconds...") + time.sleep(retry_delay) + continue except KeyboardInterrupt: interrupted = True break @@ -911,7 +995,7 @@ class Coder: return except FinishReasonLength: # We hit the output limit! - if not self.main_model.can_prefill: + if not self.main_model.info.get("supports_assistant_prefill"): exhausted = True break @@ -920,7 +1004,9 @@ class Coder: if messages[-1]["role"] == "assistant": messages[-1]["content"] = self.multi_response_content else: - messages.append(dict(role="assistant", content=self.multi_response_content)) + messages.append( + dict(role="assistant", content=self.multi_response_content, prefix=True) + ) except Exception as err: self.io.tool_error(f"Unexpected error: {err}") traceback.print_exc() @@ -935,8 +1021,7 @@ class Coder: self.io.tool_output() - if self.usage_report: - self.io.tool_output(self.usage_report) + self.show_usage_report() if exhausted: self.show_exhausted_error() @@ -1011,10 +1096,10 @@ class Coder: output_tokens = 0 if self.partial_response_content: output_tokens = self.main_model.token_count(self.partial_response_content) - max_output_tokens = self.main_model.info.get("max_output_tokens", 0) + max_output_tokens = self.main_model.info.get("max_output_tokens") or 0 input_tokens = self.main_model.token_count(self.format_messages()) - max_input_tokens = self.main_model.info.get("max_input_tokens", 0) + max_input_tokens = self.main_model.info.get("max_input_tokens") or 0 total_tokens = input_tokens + output_tokens @@ -1159,9 +1244,8 @@ class Coder: self.io.log_llm_history("TO LLM", format_messages(messages)) - interrupted = False try: - hash_object, completion = send_with_retries( + hash_object, completion = send_completion( model.name, messages, functions, @@ -1176,9 +1260,9 @@ class Coder: yield from self.show_send_output_stream(completion) else: self.show_send_output(completion) - except KeyboardInterrupt: + except KeyboardInterrupt as kbi: self.keyboard_interrupt() - interrupted = True + raise kbi finally: self.io.log_llm_history( "LLM RESPONSE", @@ -1193,10 +1277,7 @@ class Coder: if args: self.io.ai_output(json.dumps(args, indent=4)) - if interrupted: - raise KeyboardInterrupt - - self.calculate_and_show_tokens_and_cost(messages, completion) + self.calculate_and_show_tokens_and_cost(messages, completion) def show_send_output(self, completion): if self.verbose: @@ -1218,7 +1299,7 @@ class Coder: show_func_err = func_err try: - self.partial_response_content = completion.choices[0].message.content + self.partial_response_content = completion.choices[0].message.content or "" except AttributeError as content_err: show_content_err = content_err @@ -1312,13 +1393,19 @@ class Coder: prompt_tokens = self.main_model.token_count(messages) completion_tokens = self.main_model.token_count(self.partial_response_content) - self.usage_report = f"Tokens: {prompt_tokens:,} sent, {completion_tokens:,} received." + self.message_tokens_sent += prompt_tokens + self.message_tokens_received += completion_tokens + + tokens_report = ( + f"Tokens: {self.message_tokens_sent:,} sent, {self.message_tokens_received:,} received." + ) if self.main_model.info.get("input_cost_per_token"): cost += prompt_tokens * self.main_model.info.get("input_cost_per_token") if self.main_model.info.get("output_cost_per_token"): cost += completion_tokens * self.main_model.info.get("output_cost_per_token") self.total_cost += cost + self.message_cost += cost def format_cost(value): if value == 0: @@ -1329,13 +1416,24 @@ class Coder: else: return f"{value:.{max(2, 2 - int(math.log10(magnitude)))}f}" - self.usage_report += ( - f" Cost: ${format_cost(cost)} request, ${format_cost(self.total_cost)} session." + cost_report = ( + f" Cost: ${format_cost(self.message_cost)} message," + f" ${format_cost(self.total_cost)} session." ) + self.usage_report = tokens_report + cost_report + else: + self.usage_report = tokens_report + + def show_usage_report(self): + if self.usage_report: + self.io.tool_output(self.usage_report) + self.message_cost = 0.0 + self.message_tokens_sent = 0 + self.message_tokens_received = 0 def get_multi_response_content(self, final=False): - cur = self.multi_response_content - new = self.partial_response_content + cur = self.multi_response_content or "" + new = self.partial_response_content or "" if new.rstrip() != new and not final: new = new.rstrip() @@ -1377,7 +1475,10 @@ class Coder: return max(path.stat().st_mtime for path in files) def get_addable_relative_files(self): - return set(self.get_all_relative_files()) - set(self.get_inchat_relative_files()) + all_files = set(self.get_all_relative_files()) + inchat_files = set(self.get_inchat_relative_files()) + read_only_files = set(self.get_rel_fname(fname) for fname in self.abs_read_only_fnames) + return all_files - inchat_files - read_only_files def check_for_dirty_commit(self, path): if not self.repo: @@ -1590,7 +1691,11 @@ class Coder: if self.show_diffs: self.commands.cmd_diff() - self.io.tool_output(f"You can use /undo to revert and discard commit {commit_hash}.") + def show_undo_hint(self): + if not self.commit_before_message: + return + if self.commit_before_message[-1] != self.repo.get_head(): + self.io.tool_output("You can use /undo to undo and discard each aider commit.") def dirty_commit(self): if not self.need_commit_before_edits: diff --git a/aider/coders/base_prompts.py b/aider/coders/base_prompts.py index f0c9979e3..d4e91b5e0 100644 --- a/aider/coders/base_prompts.py +++ b/aider/coders/base_prompts.py @@ -18,7 +18,7 @@ You always COMPLETELY IMPLEMENT the needed code! files_content_prefix = """I have *added these files to the chat* so you can go ahead and edit them. -*Trust this message as the true contents of the files!* +*Trust this message as the true contents of these files!* Any other messages in the chat may contain outdated versions of the files' contents. """ # noqa: E501 @@ -38,4 +38,8 @@ Don't include files that might contain relevant context, just files that will ne repo_content_prefix = """Here are summaries of some files present in my git repository. Do not propose changes to these files, treat them as *read-only*. If you need to edit any of these files, ask me to *add them to the chat* first. +""" + + read_only_files_prefix = """Here are some READ ONLY files, provided for your reference. +Do not edit these files! """ diff --git a/aider/commands.py b/aider/commands.py index a1a225261..599d5013d 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -7,7 +7,9 @@ from collections import OrderedDict from pathlib import Path import git -from PIL import ImageGrab +import pyperclip +from PIL import Image, ImageGrab +from rich.text import Text from aider import models, prompts, voice from aider.help import Help, install_help_extra @@ -117,13 +119,15 @@ class Commands: else: self.io.tool_output("Please provide a partial model name to search for.") - def cmd_web(self, args): - "Use headless selenium to scrape a webpage and add the content to the chat" + def cmd_web(self, args, paginate=True): + "Scrape a webpage, convert to markdown and add to the chat" + url = args.strip() if not url: self.io.tool_error("Please provide a URL to scrape.") return + self.io.tool_output(f"Scraping {url}...") if not self.scraper: res = install_playwright(self.io) if not res: @@ -134,11 +138,14 @@ class Commands: ) content = self.scraper.scrape(url) or "" - # if content: - # self.io.tool_output(content) - content = f"{url}:\n\n" + content + self.io.tool_output("... done.") + + if paginate: + with self.io.console.pager(): + self.io.console.print(Text(content)) + return content def is_command(self, inp): @@ -304,7 +311,6 @@ class Commands: # chat history msgs = self.coder.done_messages + self.coder.cur_messages if msgs: - msgs = [dict(role="dummy", content=msg) for msg in msgs] tokens = self.coder.main_model.token_count(msgs) res.append((tokens, "chat history", "use /clear to clear")) @@ -316,6 +322,8 @@ class Commands: tokens = self.coder.main_model.token_count(repo_content) res.append((tokens, "repository map", "use --map-tokens to resize")) + fence = "`" * 3 + # files for fname in self.coder.abs_fnames: relative_fname = self.coder.get_rel_fname(fname) @@ -324,11 +332,23 @@ class Commands: tokens = self.coder.main_model.token_count_for_image(fname) else: # approximate - content = f"{relative_fname}\n```\n" + content + "```\n" + content = f"{relative_fname}\n{fence}\n" + content + "{fence}\n" tokens = self.coder.main_model.token_count(content) - res.append((tokens, f"{relative_fname}", "use /drop to drop from chat")) + res.append((tokens, f"{relative_fname}", "/drop to remove")) - self.io.tool_output("Approximate context window usage, in tokens:") + # read-only files + for fname in self.coder.abs_read_only_fnames: + relative_fname = self.coder.get_rel_fname(fname) + content = self.io.read_text(fname) + if content is not None and not is_image_file(relative_fname): + # approximate + content = f"{relative_fname}\n{fence}\n" + content + "{fence}\n" + tokens = self.coder.main_model.token_count(content) + res.append((tokens, f"{relative_fname} (read-only)", "/drop to remove")) + + self.io.tool_output( + f"Approximate context window usage for {self.coder.main_model.name}, in tokens:" + ) self.io.tool_output() width = 8 @@ -344,7 +364,7 @@ class Commands: total_cost = 0.0 for tk, msg, tip in res: total += tk - cost = tk * self.coder.main_model.info.get("input_cost_per_token", 0) + cost = tk * (self.coder.main_model.info.get("input_cost_per_token") or 0) total_cost += cost msg = msg.ljust(col_width) self.io.tool_output(f"${cost:7.4f} {fmt(tk)} {msg} {tip}") # noqa: E231 @@ -352,7 +372,7 @@ class Commands: self.io.tool_output("=" * (width + cost_width + 1)) self.io.tool_output(f"${total_cost:7.4f} {fmt(total)} tokens total") # noqa: E231 - limit = self.coder.main_model.info.get("max_input_tokens", 0) + limit = self.coder.main_model.info.get("max_input_tokens") or 0 if not limit: return @@ -440,27 +460,36 @@ class Commands: # Get the current HEAD after undo current_head_hash = self.coder.repo.repo.head.commit.hexsha[:7] current_head_message = self.coder.repo.repo.head.commit.message.strip() - self.io.tool_output(f"HEAD is: {current_head_hash} {current_head_message}") + self.io.tool_output(f"Now at: {current_head_hash} {current_head_message}") if self.coder.main_model.send_undo_reply: return prompts.undo_command_reply def cmd_diff(self, args=""): - "Display the diff of the last aider commit" + "Display the diff of changes since the last message" if not self.coder.repo: self.io.tool_error("No git repository found.") return - last_commit_hash = self.coder.repo.repo.head.commit.hexsha[:7] - - if last_commit_hash not in self.coder.aider_commit_hashes: - self.io.tool_error(f"Last commit {last_commit_hash} was not an aider commit.") - self.io.tool_error("You could try `/git diff` or `/git diff HEAD^`.") + current_head = self.coder.repo.get_head() + if current_head is None: + self.io.tool_error("Unable to get current commit. The repository might be empty.") return + if len(self.coder.commit_before_message) < 2: + commit_before_message = current_head + "^" + else: + commit_before_message = self.coder.commit_before_message[-2] + + if not commit_before_message or commit_before_message == current_head: + self.io.tool_error("No changes to display since the last message.") + return + + self.io.tool_output(f"Diff since {commit_before_message[:7]}...") + diff = self.coder.repo.diff_commits( self.coder.pretty, - "HEAD^", + commit_before_message, "HEAD", ) @@ -472,6 +501,9 @@ class Commands: fname = f'"{fname}"' return fname + def completions_read(self): + return self.completions_add() + def completions_add(self): files = set(self.coder.get_all_relative_files()) files = files - set(self.coder.get_inchat_relative_files()) @@ -558,6 +590,18 @@ class Commands: if abs_file_path in self.coder.abs_fnames: self.io.tool_error(f"{matched_file} is already in the chat") + elif abs_file_path in self.coder.abs_read_only_fnames: + if self.coder.repo and self.coder.repo.path_in_repo(matched_file): + self.coder.abs_read_only_fnames.remove(abs_file_path) + self.coder.abs_fnames.add(abs_file_path) + self.io.tool_output( + f"Moved {matched_file} from read-only to editable files in the chat" + ) + added_fnames.append(matched_file) + else: + self.io.tool_error( + f"Cannot add {matched_file} as it's not part of the repository" + ) else: if is_image_file(matched_file) and not self.coder.main_model.accepts_images: self.io.tool_error( @@ -575,20 +619,12 @@ class Commands: self.coder.check_added_files() added_fnames.append(matched_file) - if not added_fnames: - return - - # only reply if there's been some chatting since the last edit - if not self.coder.cur_messages: - return - - reply = prompts.added_files.format(fnames=", ".join(added_fnames)) - return reply - def completions_drop(self): files = self.coder.get_inchat_relative_files() - files = [self.quote_fname(fn) for fn in files] - return files + read_only_files = [self.coder.get_rel_fname(fn) for fn in self.coder.abs_read_only_fnames] + all_files = files + read_only_files + all_files = [self.quote_fname(fn) for fn in all_files] + return all_files def cmd_drop(self, args=""): "Remove files from the chat session to free up context space" @@ -596,9 +632,19 @@ class Commands: if not args.strip(): self.io.tool_output("Dropping all files from the chat session.") self.coder.abs_fnames = set() + self.coder.abs_read_only_fnames = set() + return filenames = parse_quoted_filenames(args) for word in filenames: + # Handle read-only files separately, without glob_filtered_to_repo + read_only_matched = [f for f in self.coder.abs_read_only_fnames if word in f] + + if read_only_matched: + for matched_file in read_only_matched: + self.coder.abs_read_only_fnames.remove(matched_file) + self.io.tool_output(f"Removed read-only file {matched_file} from the chat") + matched_files = self.glob_filtered_to_repo(word) if not matched_files: @@ -678,7 +724,7 @@ class Commands: add = result.returncode != 0 else: response = self.io.prompt_ask( - "Add the output to the chat? (y/n/instructions): ", default="y" + "Add the output to the chat?\n(y/n/instructions)", default="" ).strip() if response.lower() in ["yes", "y"]: @@ -718,6 +764,7 @@ class Commands: other_files = [] chat_files = [] + read_only_files = [] for file in files: abs_file_path = self.coder.abs_root_path(file) if abs_file_path in self.coder.abs_fnames: @@ -725,8 +772,13 @@ class Commands: else: other_files.append(file) - if not chat_files and not other_files: - self.io.tool_output("\nNo files in chat or git repo.") + # Add read-only files + for abs_file_path in self.coder.abs_read_only_fnames: + rel_file_path = self.coder.get_rel_fname(abs_file_path) + read_only_files.append(rel_file_path) + + if not chat_files and not other_files and not read_only_files: + self.io.tool_output("\nNo files in chat, git repo, or read-only list.") return if other_files: @@ -734,6 +786,11 @@ class Commands: for file in other_files: self.io.tool_output(f" {file}") + if read_only_files: + self.io.tool_output("\nRead-only files:\n") + for file in read_only_files: + self.io.tool_output(f" {file}") + if chat_files: self.io.tool_output("\nFiles in chat:\n") for file in chat_files: @@ -787,13 +844,23 @@ class Commands: """ user_msg += "\n".join(self.coder.get_announcements()) + "\n" - assistant_msg = coder.run(user_msg) + coder.run(user_msg, preproc=False) - self.coder.cur_messages += [ - dict(role="user", content=user_msg), - dict(role="assistant", content=assistant_msg), - ] - self.coder.total_cost += coder.total_cost + if self.coder.repo_map: + map_tokens = self.coder.repo_map.max_map_tokens + map_mul_no_files = self.coder.repo_map.map_mul_no_files + else: + map_tokens = 0 + map_mul_no_files = 1 + + raise SwitchCoder( + edit_format=self.coder.edit_format, + summarize_from_coder=False, + from_coder=coder, + map_tokens=map_tokens, + map_mul_no_files=map_mul_no_files, + show_announcements=False, + ) def clone(self): return Commands( @@ -805,28 +872,35 @@ class Commands: def cmd_ask(self, args): "Ask questions about the code base without editing any files" + return self._generic_chat_command(args, "ask") + def cmd_code(self, args): + "Ask for changes to your code" + return self._generic_chat_command(args, self.coder.main_model.edit_format) + + def _generic_chat_command(self, args, edit_format): if not args.strip(): - self.io.tool_error("Please provide a question or topic for the chat.") + self.io.tool_error(f"Please provide a question or topic for the {edit_format} chat.") return from aider.coders import Coder - chat_coder = Coder.create( + coder = Coder.create( io=self.io, from_coder=self.coder, - edit_format="ask", + edit_format=edit_format, summarize_from_coder=False, ) user_msg = args - assistant_msg = chat_coder.run(user_msg) + coder.run(user_msg) - self.coder.cur_messages += [ - dict(role="user", content=user_msg), - dict(role="assistant", content=assistant_msg), - ] - self.coder.total_cost += chat_coder.total_cost + raise SwitchCoder( + edit_format=self.coder.edit_format, + summarize_from_coder=False, + from_coder=coder, + show_announcements=False, + ) def get_help_md(self): "Show help about all commands in markdown" @@ -894,27 +968,82 @@ class Commands: return text - def cmd_add_clipboard_image(self, args): - "Add an image from the clipboard to the chat" + def cmd_clipboard(self, args): + "Add image/text from the clipboard to the chat (optionally provide a name for the image)" try: + # Check for image first image = ImageGrab.grabclipboard() - if image is None: - self.io.tool_error("No image found in clipboard.") + if isinstance(image, Image.Image): + if args.strip(): + filename = args.strip() + ext = os.path.splitext(filename)[1].lower() + if ext in (".jpg", ".jpeg", ".png"): + basename = filename + else: + basename = f"{filename}.png" + else: + basename = "clipboard_image.png" + + temp_dir = tempfile.mkdtemp() + temp_file_path = os.path.join(temp_dir, basename) + image_format = "PNG" if basename.lower().endswith(".png") else "JPEG" + image.save(temp_file_path, image_format) + + abs_file_path = Path(temp_file_path).resolve() + + # Check if a file with the same name already exists in the chat + existing_file = next( + (f for f in self.coder.abs_fnames if Path(f).name == abs_file_path.name), None + ) + if existing_file: + self.coder.abs_fnames.remove(existing_file) + self.io.tool_output(f"Replaced existing image in the chat: {existing_file}") + + self.coder.abs_fnames.add(str(abs_file_path)) + self.io.tool_output(f"Added clipboard image to the chat: {abs_file_path}") + self.coder.check_added_files() + return - with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file: - image.save(temp_file.name, "PNG") - temp_file_path = temp_file.name + # If not an image, try to get text + text = pyperclip.paste() + if text: + self.io.tool_output(text) + return text - abs_file_path = Path(temp_file_path).resolve() - self.coder.abs_fnames.add(str(abs_file_path)) - self.io.tool_output(f"Added clipboard image to the chat: {abs_file_path}") - self.coder.check_added_files() - - return prompts.added_files.format(fnames=str(abs_file_path)) + self.io.tool_error("No image or text content found in clipboard.") + return except Exception as e: - self.io.tool_error(f"Error adding clipboard image: {e}") + self.io.tool_error(f"Error processing clipboard content: {e}") + + def cmd_read(self, args): + "Add a file to the chat that is for reference, not to be edited" + if not args.strip(): + self.io.tool_error("Please provide a filename to read.") + return + + filename = args.strip() + abs_path = os.path.abspath(filename) + + if not os.path.exists(abs_path): + self.io.tool_error(f"File not found: {abs_path}") + return + + if not os.path.isfile(abs_path): + self.io.tool_error(f"Not a file: {abs_path}") + return + + self.coder.abs_read_only_fnames.add(abs_path) + self.io.tool_output(f"Added {abs_path} to read-only files.") + + def cmd_map(self, args): + "Print out the current repository map" + repo_map = self.coder.get_repo_map() + if repo_map: + self.io.tool_output(repo_map) + else: + self.io.tool_output("No repository map available.") def expand_subdir(file_path): diff --git a/aider/io.py b/aider/io.py index 7a836c10c..f4e3c2eb2 100644 --- a/aider/io.py +++ b/aider/io.py @@ -15,6 +15,7 @@ from pygments.lexers import MarkdownLexer, guess_lexer_for_filename from pygments.token import Token from pygments.util import ClassNotFound from rich.console import Console +from rich.style import Style as RichStyle from rich.text import Text from .dump import dump # noqa: F401 @@ -22,10 +23,13 @@ from .utils import is_image_file class AutoCompleter(Completer): - def __init__(self, root, rel_fnames, addable_rel_fnames, commands, encoding): + def __init__( + self, root, rel_fnames, addable_rel_fnames, commands, encoding, abs_read_only_fnames=None + ): self.addable_rel_fnames = addable_rel_fnames self.rel_fnames = rel_fnames self.encoding = encoding + self.abs_read_only_fnames = abs_read_only_fnames or [] fname_to_rel_fnames = defaultdict(list) for rel_fname in addable_rel_fnames: @@ -47,7 +51,11 @@ class AutoCompleter(Completer): for rel_fname in rel_fnames: self.words.add(rel_fname) - fname = Path(root) / rel_fname + all_fnames = [Path(root) / rel_fname for rel_fname in rel_fnames] + if abs_read_only_fnames: + all_fnames.extend(abs_read_only_fnames) + + for fname in all_fnames: try: with open(fname, "r", encoding=self.encoding) as f: content = f.read() @@ -217,7 +225,7 @@ class InputOutput: with open(str(filename), "w", encoding=self.encoding) as f: f.write(content) - def get_input(self, root, rel_fnames, addable_rel_fnames, commands): + def get_input(self, root, rel_fnames, addable_rel_fnames, commands, abs_read_only_fnames=None): if self.pretty: style = dict(style=self.user_input_color) if self.user_input_color else dict() self.console.rule(**style) @@ -244,7 +252,12 @@ class InputOutput: style = None completer_instance = AutoCompleter( - root, rel_fnames, addable_rel_fnames, commands, self.encoding + root, + rel_fnames, + addable_rel_fnames, + commands, + self.encoding, + abs_read_only_fnames=abs_read_only_fnames, ) while True: @@ -317,7 +330,7 @@ class InputOutput: def user_input(self, inp, log_only=True): if not log_only: style = dict(style=self.user_input_color) if self.user_input_color else dict() - self.console.print(inp, **style) + self.console.print(Text(inp), **style) prefix = "####" if inp: @@ -341,18 +354,19 @@ class InputOutput: self.num_user_asks += 1 if self.yes is True: - res = "yes" + res = "y" elif self.yes is False: - res = "no" + res = "n" else: res = prompt(question + " ", default=default) - hist = f"{question.strip()} {res.strip()}" + res = res.lower().strip() + is_yes = res in ("y", "yes") + + hist = f"{question.strip()} {'y' if is_yes else 'n'}" self.append_chat_history(hist, linebreak=True, blockquote=True) - if not res or not res.strip(): - return - return res.strip().lower().startswith("y") + return is_yes def prompt_ask(self, question, default=None): self.num_user_asks += 1 @@ -389,7 +403,7 @@ class InputOutput: style = dict(style=self.tool_error_color) if self.tool_error_color else dict() self.console.print(message, **style) - def tool_output(self, *messages, log_only=False): + def tool_output(self, *messages, log_only=False, bold=False): if messages: hist = " ".join(messages) hist = f"{hist.strip()}" @@ -397,8 +411,10 @@ class InputOutput: if not log_only: messages = list(map(Text, messages)) - style = dict(style=self.tool_output_color) if self.tool_output_color else dict() - self.console.print(*messages, **style) + style = dict(color=self.tool_output_color) if self.tool_output_color else dict() + style["reverse"] = bold + style = RichStyle(**style) + self.console.print(*messages, style=style) def append_chat_history(self, text, linebreak=False, blockquote=False, strip=True): if blockquote: diff --git a/aider/llm.py b/aider/llm.py index f65398729..950f1a29a 100644 --- a/aider/llm.py +++ b/aider/llm.py @@ -4,8 +4,11 @@ import warnings warnings.filterwarnings("ignore", category=UserWarning, module="pydantic") -os.environ["OR_SITE_URL"] = "http://aider.chat" -os.environ["OR_APP_NAME"] = "Aider" +AIDER_SITE_URL = "https://aider.chat" +AIDER_APP_NAME = "Aider" + +os.environ["OR_SITE_URL"] = AIDER_SITE_URL +os.environ["OR_APP_NAME"] = AIDER_APP_NAME # `import litellm` takes 1.5 seconds, defer it! diff --git a/aider/main.py b/aider/main.py index 8bb04ef2b..bb647d55d 100644 --- a/aider/main.py +++ b/aider/main.py @@ -384,6 +384,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F all_files = args.files + (args.file or []) fnames = [str(Path(fn).resolve()) for fn in all_files] + read_only_fnames = [str(Path(fn).resolve()) for fn in (args.read or [])] if len(all_files) > 1: good = True for fname in all_files: @@ -415,11 +416,11 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F return main(argv, input, output, right_repo_root, return_coder=return_coder) if args.just_check_update: - update_available = check_version(io, just_check=True) + update_available = check_version(io, just_check=True, verbose=args.verbose) return 0 if not update_available else 1 if args.check_update: - check_version(io) + check_version(io, verbose=args.verbose) if args.models: models.print_matching_models(io, args.models) @@ -475,12 +476,13 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F repo = GitRepo( io, fnames, - git_dname or ".", + git_dname, args.aiderignore, models=main_model.commit_message_models(), attribute_author=args.attribute_author, attribute_committer=args.attribute_committer, - attribute_commit_message=args.attribute_commit_message, + attribute_commit_message_author=args.attribute_commit_message_author, + attribute_commit_message_committer=args.attribute_commit_message_committer, commit_prompt=args.commit_prompt, subtree_only=args.subtree_only, ) @@ -501,7 +503,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F io=io, repo=repo, fnames=fnames, - pretty=args.pretty, + read_only_fnames=read_only_fnames, show_diffs=args.show_diffs, auto_commits=args.auto_commits, dirty_commits=args.dirty_commits, @@ -618,8 +620,15 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F coder.run() return except SwitchCoder as switch: - coder = Coder.create(io=io, from_coder=coder, **switch.kwargs) - coder.show_announcements() + kwargs = dict(io=io, from_coder=coder) + kwargs.update(switch.kwargs) + if "show_announcements" in kwargs: + del kwargs["show_announcements"] + + coder = Coder.create(**kwargs) + + if switch.kwargs.get("show_announcements") is not False: + coder.show_announcements() def load_slow_imports(): diff --git a/aider/models.py b/aider/models.py index 344fc9d78..cad99d1df 100644 --- a/aider/models.py +++ b/aider/models.py @@ -3,6 +3,7 @@ import importlib import json import math import os +import platform import sys from dataclasses import dataclass, fields from pathlib import Path @@ -13,7 +14,7 @@ from PIL import Image from aider import urls from aider.dump import dump # noqa: F401 -from aider.llm import litellm +from aider.llm import AIDER_APP_NAME, AIDER_SITE_URL, litellm DEFAULT_MODEL_NAME = "gpt-4o" @@ -70,7 +71,6 @@ class ModelSettings: lazy: bool = False reminder_as_sys_msg: bool = False examples_as_sys_msg: bool = False - can_prefill: bool = False extra_headers: Optional[dict] = None max_tokens: Optional[int] = None @@ -152,6 +152,16 @@ MODEL_SETTINGS = [ lazy=True, reminder_as_sys_msg=True, ), + ModelSettings( + "gpt-4o-2024-08-06", + "diff", + weak_model_name="gpt-4o-mini", + use_repo_map=True, + send_undo_reply=True, + accepts_images=True, + lazy=True, + reminder_as_sys_msg=True, + ), ModelSettings( "gpt-4o", "diff", @@ -238,7 +248,6 @@ MODEL_SETTINGS = [ weak_model_name="claude-3-haiku-20240307", use_repo_map=True, send_undo_reply=True, - can_prefill=True, ), ModelSettings( "openrouter/anthropic/claude-3-opus", @@ -246,13 +255,11 @@ MODEL_SETTINGS = [ weak_model_name="openrouter/anthropic/claude-3-haiku", use_repo_map=True, send_undo_reply=True, - can_prefill=True, ), ModelSettings( "claude-3-sonnet-20240229", "whole", weak_model_name="claude-3-haiku-20240307", - can_prefill=True, ), ModelSettings( "claude-3-5-sonnet-20240620", @@ -260,7 +267,6 @@ MODEL_SETTINGS = [ weak_model_name="claude-3-haiku-20240307", use_repo_map=True, examples_as_sys_msg=True, - can_prefill=True, accepts_images=True, max_tokens=8192, extra_headers={"anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15"}, @@ -271,9 +277,12 @@ MODEL_SETTINGS = [ weak_model_name="claude-3-haiku-20240307", use_repo_map=True, examples_as_sys_msg=True, - can_prefill=True, max_tokens=8192, - extra_headers={"anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15"}, + extra_headers={ + "anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15", + "HTTP-Referer": AIDER_SITE_URL, + "X-Title": AIDER_APP_NAME, + }, ), ModelSettings( "openrouter/anthropic/claude-3.5-sonnet", @@ -281,10 +290,13 @@ MODEL_SETTINGS = [ weak_model_name="openrouter/anthropic/claude-3-haiku-20240307", use_repo_map=True, examples_as_sys_msg=True, - can_prefill=True, accepts_images=True, max_tokens=8192, - extra_headers={"anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15"}, + extra_headers={ + "anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15", + "HTTP-Referer": "https://aider.chat", + "X-Title": "Aider", + }, ), # Vertex AI Claude models # Does not yet support 8k token @@ -294,7 +306,6 @@ MODEL_SETTINGS = [ weak_model_name="vertex_ai/claude-3-haiku@20240307", use_repo_map=True, examples_as_sys_msg=True, - can_prefill=True, accepts_images=True, ), ModelSettings( @@ -303,13 +314,11 @@ MODEL_SETTINGS = [ weak_model_name="vertex_ai/claude-3-haiku@20240307", use_repo_map=True, send_undo_reply=True, - can_prefill=True, ), ModelSettings( "vertex_ai/claude-3-sonnet@20240229", "whole", weak_model_name="vertex_ai/claude-3-haiku@20240307", - can_prefill=True, ), # Cohere ModelSettings( @@ -405,9 +414,7 @@ class Model: self.missing_keys = res.get("missing_keys") self.keys_in_environment = res.get("keys_in_environment") - max_input_tokens = self.info.get("max_input_tokens") - if not max_input_tokens: - max_input_tokens = 0 + max_input_tokens = self.info.get("max_input_tokens") or 0 if max_input_tokens < 32 * 1024: self.max_chat_history_tokens = 1024 else: @@ -470,14 +477,10 @@ class Model: if "gpt-3.5" in model or "gpt-4" in model: self.reminder_as_sys_msg = True - if "anthropic" in model: - self.can_prefill = True - if "3.5-sonnet" in model or "3-5-sonnet" in model: self.edit_format = "diff" self.use_repo_map = True self.examples_as_sys_msg = True - self.can_prefill = True # use the defaults if self.edit_format == "diff": @@ -512,6 +515,9 @@ class Model: return litellm.encode(model=self.name, text=text) def token_count(self, messages): + if type(messages) is list: + return litellm.token_counter(model=self.name, messages=messages) + if not self.tokenizer: return @@ -669,6 +675,13 @@ def sanity_check_model(io, model): io.tool_error(f"Model {model}: Missing these environment variables:") for key in model.missing_keys: io.tool_error(f"- {key}") + + if platform.system() == "Windows" or True: + io.tool_output( + "If you just set these environment variables using `setx` you may need to restart" + " your terminal or command prompt for the changes to take effect." + ) + elif not model.keys_in_environment: show = True io.tool_output(f"Model {model}: Unknown which environment variables are required.") diff --git a/aider/repo.py b/aider/repo.py index 5403c5845..8122df748 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -29,7 +29,8 @@ class GitRepo: models=None, attribute_author=True, attribute_committer=True, - attribute_commit_message=False, + attribute_commit_message_author=False, + attribute_commit_message_committer=False, commit_prompt=None, subtree_only=False, ): @@ -41,7 +42,8 @@ class GitRepo: self.attribute_author = attribute_author self.attribute_committer = attribute_committer - self.attribute_commit_message = attribute_commit_message + self.attribute_commit_message_author = attribute_commit_message_author + self.attribute_commit_message_committer = attribute_commit_message_committer self.commit_prompt = commit_prompt self.subtree_only = subtree_only self.ignore_file_cache = {} @@ -98,7 +100,9 @@ class GitRepo: else: commit_message = self.get_commit_message(diffs, context) - if aider_edits and self.attribute_commit_message: + if aider_edits and self.attribute_commit_message_author: + commit_message = "aider: " + commit_message + elif self.attribute_commit_message_committer: commit_message = "aider: " + commit_message if not commit_message: @@ -130,7 +134,7 @@ class GitRepo: self.repo.git.commit(cmd) commit_hash = self.repo.head.commit.hexsha[:7] - self.io.tool_output(f"Commit {commit_hash} {commit_message}") + self.io.tool_output(f"Commit {commit_hash} {commit_message}", bold=True) # Restore the env @@ -155,10 +159,6 @@ class GitRepo: return self.repo.git_dir def get_commit_message(self, diffs, context): - if len(diffs) >= 4 * 1024 * 4: - self.io.tool_error("Diff is too large to generate a commit message.") - return - diffs = "# Diffs:\n" + diffs content = "" @@ -172,7 +172,12 @@ class GitRepo: dict(role="user", content=content), ] + commit_message = None for model in self.models: + num_tokens = model.token_count(messages) + max_tokens = model.info.get("max_input_tokens") or 0 + if max_tokens and num_tokens > max_tokens: + continue commit_message = simple_send_with_retries(model.name, messages) if commit_message: break @@ -226,6 +231,8 @@ class GitRepo: args = [] if pretty: args += ["--color"] + else: + args += ["--color=never"] args += [from_commit, to_commit] diffs = self.repo.git.diff(*args) @@ -355,3 +362,9 @@ class GitRepo: return True return self.repo.is_dirty(path=path) + + def get_head(self): + try: + return self.repo.head.commit.hexsha + except ValueError: + return None diff --git a/aider/repomap.py b/aider/repomap.py index c7b2629fb..27ca3c0c2 100644 --- a/aider/repomap.py +++ b/aider/repomap.py @@ -60,6 +60,9 @@ class RepoMap: self.main_model = main_model + self.tree_cache = {} + self.tree_context_cache = {} + def token_count(self, text): len_text = len(text) if len_text < 200: @@ -471,24 +474,28 @@ class RepoMap: if key in self.tree_cache: return self.tree_cache[key] - code = self.io.read_text(abs_fname) or "" - if not code.endswith("\n"): - code += "\n" + if rel_fname not in self.tree_context_cache: + code = self.io.read_text(abs_fname) or "" + if not code.endswith("\n"): + code += "\n" - context = TreeContext( - rel_fname, - code, - color=False, - line_number=False, - child_context=False, - last_line=False, - margin=0, - mark_lois=False, - loi_pad=0, - # header_max=30, - show_top_of_file_parent_scope=False, - ) + context = TreeContext( + rel_fname, + code, + color=False, + line_number=False, + child_context=False, + last_line=False, + margin=0, + mark_lois=False, + loi_pad=0, + # header_max=30, + show_top_of_file_parent_scope=False, + ) + self.tree_context_cache[rel_fname] = context + context = self.tree_context_cache[rel_fname] + context.lines_of_interest = set() context.add_lines_of_interest(lois) context.add_context() res = context.format() diff --git a/aider/scrape.py b/aider/scrape.py index 7d3bed945..282bf7cdc 100755 --- a/aider/scrape.py +++ b/aider/scrape.py @@ -87,26 +87,48 @@ class Scraper: def scrape(self, url): """ - Scrape a url and turn it into readable markdown. + Scrape a url and turn it into readable markdown if it's HTML. + If it's plain text or non-HTML, return it as-is. - `url` - the URLto scrape. + `url` - the URL to scrape. """ if self.playwright_available: - content = self.scrape_with_playwright(url) + content, mime_type = self.scrape_with_playwright(url) else: - content = self.scrape_with_httpx(url) + content, mime_type = self.scrape_with_httpx(url) if not content: self.print_error(f"Failed to retrieve content from {url}") return None - self.try_pandoc() - - content = self.html_to_markdown(content) + # Check if the content is HTML based on MIME type or content + if (mime_type and mime_type.startswith("text/html")) or ( + mime_type is None and self.looks_like_html(content) + ): + self.try_pandoc() + content = self.html_to_markdown(content) return content + def looks_like_html(self, content): + """ + Check if the content looks like HTML. + """ + if isinstance(content, str): + # Check for common HTML tags + html_patterns = [ + r"", + r"` switch to launch in ask/help/code modes. +- New `/code ` command request a code edit while in `ask` mode. +- Web scraper is more robust if page never idles. +- Improved token and cost reporting for infinite output. +- Improvements and bug fixes for `/read` only files. +- Switched from `setup.py` to `pyproject.toml`, by @branchvincent. +- Bug fix to persist files added during `/ask`. +- Bug fix for chat history size in `/tokens`. +- Aider wrote 66% of the code in this release. + +### Aider v0.49.1 + +- Bugfix to `/help`. + +### Aider v0.49.0 + +- Add read-only files to the chat context with `/read` and `--read`, including from outside the git repo. +- `/diff` now shows diffs of all changes resulting from your request, including lint and test fixes. +- New `/clipboard` command to paste images or text from the clipboard, replaces `/add-clipboard-image`. +- Now shows the markdown scraped when you add a url with `/web`. +- When [scripting aider](https://aider.chat/docs/scripting.html) messages can now contain in-chat `/` commands. +- Aider in docker image now suggests the correct command to update to latest version. +- Improved retries on API errors (was easy to test during Sonnet outage). +- Added `--mini` for `gpt-4o-mini`. +- Bugfix to keep session cost accurate when using `/ask` and `/help`. +- Performance improvements for repo map calculation. +- `/tokens` now shows the active model. +- Enhanced commit message attribution options: + - New `--attribute-commit-message-author` to prefix commit messages with 'aider: ' if aider authored the changes, replaces `--attribute-commit-message`. + - New `--attribute-commit-message-committer` to prefix all commit messages with 'aider: '. +- Aider wrote 61% of the code in this release. + +### Aider v0.48.1 + +- Added `openai/gpt-4o-2024-08-06`. +- Worked around litellm bug that removes OpenRouter app headers when using `extra_headers`. +- Improved progress indication during repo map processing. +- Corrected instructions for upgrading the docker container to latest aider version. +- Removed obsolete 16k token limit on commit diffs, use per-model limits. + ### Aider v0.48.0 - Performance improvements for large/mono repos. diff --git a/aider/website/_data/blame.yml b/aider/website/_data/blame.yml index 5d80b75bc..2fc051448 100644 --- a/aider/website/_data/blame.yml +++ b/aider/website/_data/blame.yml @@ -1,8 +1,11 @@ -- aider_percentage: 32.38 - aider_total: 34 +- aider_percentage: 31.33 + aider_total: 47 end_date: '2023-06-15' end_tag: v0.6.0 file_counts: + aider/coder.py: + Paul Gauthier: 32 + Paul Gauthier (aider): 4 aider/commands.py: Paul Gauthier: 2 aider/main.py: @@ -19,13 +22,15 @@ setup.py: Paul Gauthier: 7 Paul Gauthier (aider): 7 + tests/test_utils.py: + Paul Gauthier (aider): 9 grand_total: - Paul Gauthier: 71 - Paul Gauthier (aider): 34 + Paul Gauthier: 103 + Paul Gauthier (aider): 47 start_tag: v0.5.0 - total_lines: 105 -- aider_percentage: 7.48 - aider_total: 97 + total_lines: 150 +- aider_percentage: 9.67 + aider_total: 182 end_date: '2023-06-25' end_tag: v0.7.0 file_counts: @@ -65,19 +70,37 @@ Paul Gauthier (aider): 8 aider/models.py: Paul Gauthier: 52 + scripts/benchmark.py: + Paul Gauthier: 312 + Paul Gauthier (aider): 22 scripts/versionbump.py: Paul Gauthier: 13 Paul Gauthier (aider): 44 setup.py: Paul Gauthier: 2 Paul Gauthier (aider): 2 + tests/test_coder.py: + Paul Gauthier: 15 + Paul Gauthier (aider): 18 + tests/test_commands.py: + Paul Gauthier: 3 + tests/test_editblock.py: + Paul Gauthier: 28 + tests/test_main.py: + Paul Gauthier: 8 + tests/test_models.py: + Paul Gauthier: 21 + Paul Gauthier (aider): 7 + tests/test_wholefile.py: + Paul Gauthier: 113 + Paul Gauthier (aider): 38 grand_total: - Paul Gauthier: 1200 - Paul Gauthier (aider): 97 + Paul Gauthier: 1700 + Paul Gauthier (aider): 182 start_tag: v0.6.0 - total_lines: 1297 -- aider_percentage: 5.69 - aider_total: 82 + total_lines: 1882 +- aider_percentage: 7.61 + aider_total: 150 end_date: '2023-07-06' end_tag: v0.8.0 file_counts: @@ -147,14 +170,31 @@ Paul Gauthier: 60 benchmark/test_benchmark.py: Paul Gauthier: 47 + tests/test_coder.py: + Paul Gauthier: 95 + Paul Gauthier (aider): 38 + tests/test_commands.py: + Paul Gauthier: 21 + Paul Gauthier (aider): 24 + tests/test_editblock.py: + Paul Gauthier: 94 + tests/test_io.py: + Paul Gauthier: 3 + Paul Gauthier (aider): 6 + tests/test_main.py: + Paul Gauthier: 29 + tests/test_repomap.py: + Paul Gauthier: 26 + tests/test_wholefile.py: + Paul Gauthier: 193 grand_total: - Paul Gauthier: 1354 - Paul Gauthier (aider): 82 + Paul Gauthier: 1815 + Paul Gauthier (aider): 150 kwmiebach: 5 start_tag: v0.7.0 - total_lines: 1441 -- aider_percentage: 12.82 - aider_total: 71 + total_lines: 1970 +- aider_percentage: 16.7 + aider_total: 161 end_date: '2023-07-16' end_tag: v0.9.0 file_counts: @@ -191,13 +231,32 @@ Paul Gauthier (aider): 13 setup.py: Paul Gauthier (aider): 1 + tests/test_coder.py: + Paul Gauthier: 121 + Paul Gauthier (aider): 30 + tests/test_commands.py: + Paul Gauthier: 38 + Paul Gauthier (aider): 59 + tests/test_editblock.py: + Paul Gauthier: 1 + tests/test_io.py: + Paul Gauthier: 1 + tests/test_main.py: + Paul Gauthier: 23 + tests/test_repomap.py: + Paul Gauthier: 13 + Paul Gauthier (aider): 1 + tests/test_wholefile.py: + Paul Gauthier: 77 + tests/utils.py: + Paul Gauthier: 46 grand_total: - Paul Gauthier: 483 - Paul Gauthier (aider): 71 + Paul Gauthier: 803 + Paul Gauthier (aider): 161 start_tag: v0.8.0 - total_lines: 554 -- aider_percentage: 16.57 - aider_total: 29 + total_lines: 964 +- aider_percentage: 13.17 + aider_total: 44 end_date: '2023-07-22' end_tag: v0.10.0 file_counts: @@ -230,14 +289,26 @@ Paul Gauthier: 1 scripts/versionbump.py: Paul Gauthier (aider): 2 + tests/test_coder.py: + Paul Gauthier: 43 + tests/test_commands.py: + Paul Gauthier: 31 + Paul Gauthier (aider): 12 + tests/test_editblock.py: + Paul Gauthier: 20 + tests/test_main.py: + Paul Gauthier: 44 + Paul Gauthier (aider): 3 + tests/utils.py: + Paul Gauthier: 6 grand_total: Amer Amayreh: 4 - Paul Gauthier: 142 - Paul Gauthier (aider): 29 + Paul Gauthier: 286 + Paul Gauthier (aider): 44 start_tag: v0.9.0 - total_lines: 175 -- aider_percentage: 5.3 - aider_total: 36 + total_lines: 334 +- aider_percentage: 4.95 + aider_total: 48 end_date: '2023-08-02' end_tag: v0.11.0 file_counts: @@ -269,13 +340,28 @@ Paul Gauthier: 64 scripts/versionbump.py: Paul Gauthier: 4 + tests/test_coder.py: + Paul Gauthier: 35 + tests/test_commands.py: + Paul Gauthier: 57 + Paul Gauthier (aider): 6 + tests/test_main.py: + Paul Gauthier: 30 + Paul Gauthier (aider): 1 + tests/test_repo.py: + Paul Gauthier: 109 + Paul Gauthier (aider): 5 + tests/test_sendchat.py: + Paul Gauthier: 41 + tests/utils.py: + Paul Gauthier: 6 grand_total: - Paul Gauthier: 643 - Paul Gauthier (aider): 36 + Paul Gauthier: 921 + Paul Gauthier (aider): 48 start_tag: v0.10.0 - total_lines: 679 -- aider_percentage: 2.72 - aider_total: 11 + total_lines: 969 +- aider_percentage: 5.05 + aider_total: 28 end_date: '2023-08-11' end_tag: v0.12.0 file_counts: @@ -311,15 +397,24 @@ Paul Gauthier: 60 scripts/versionbump.py: Paul Gauthier: 1 + tests/test_coder.py: + Paul Gauthier: 1 + Paul Gauthier (aider): 17 + tests/test_commands.py: + Paul Gauthier: 22 + tests/test_editblock.py: + Paul Gauthier: 52 + tests/test_repo.py: + Paul Gauthier: 58 grand_total: Arseniy Pavlenko: 3 Joshua Vial: 2 - Paul Gauthier: 389 - Paul Gauthier (aider): 11 + Paul Gauthier: 522 + Paul Gauthier (aider): 28 start_tag: v0.11.0 - total_lines: 405 -- aider_percentage: 8.3 - aider_total: 23 + total_lines: 555 +- aider_percentage: 4.07 + aider_total: 24 end_date: '2023-08-22' end_tag: v0.13.0 file_counts: @@ -355,13 +450,22 @@ Paul Gauthier: 26 setup.py: Paul Gauthier (aider): 1 + tests/test_coder.py: + Paul Gauthier: 283 + Paul Gauthier (aider): 1 + tests/test_main.py: + Paul Gauthier: 1 + tests/test_repo.py: + Paul Gauthier: 27 + tests/test_wholefile.py: + Paul Gauthier: 1 grand_total: - Paul Gauthier: 254 - Paul Gauthier (aider): 23 + Paul Gauthier: 566 + Paul Gauthier (aider): 24 start_tag: v0.12.0 - total_lines: 277 -- aider_percentage: 0.0 - aider_total: 0 + total_lines: 590 +- aider_percentage: 0.53 + aider_total: 1 end_date: '2023-09-08' end_tag: v0.14.0 file_counts: @@ -378,6 +482,21 @@ aider/main.py: JV: 1 Joshua Vial: 1 + aider/models/__init__.py: + JV: 1 + Paul Gauthier: 14 + aider/models/model.py: + JV: 27 + Joshua Vial: 4 + Paul Gauthier: 8 + aider/models/openai.py: + JV: 3 + Paul Gauthier: 3 + aider/models/openrouter.py: + JV: 28 + Joshua Vial: 2 + Paul Gauthier: 15 + Paul Gauthier (aider): 1 aider/repo.py: JV: 2 aider/repomap.py: @@ -391,13 +510,17 @@ Paul Gauthier: 1 setup.py: Paul Gauthier: 1 + tests/test_models.py: + Joshua Vial: 22 + Paul Gauthier: 13 grand_total: - JV: 8 - Joshua Vial: 32 - Paul Gauthier: 8 + JV: 67 + Joshua Vial: 60 + Paul Gauthier: 61 + Paul Gauthier (aider): 1 start_tag: v0.13.0 - total_lines: 48 -- aider_percentage: 19.52 + total_lines: 189 +- aider_percentage: 9.69 aider_total: 41 end_date: '2023-10-20' end_tag: v0.15.0 @@ -432,15 +555,21 @@ Paul Gauthier: 9 scripts/versionbump.py: Paul Gauthier: 2 + tests/test_commands.py: + Paul Gauthier: 129 + tests/test_main.py: + Paul Gauthier: 17 + tests/test_repo.py: + Paul Gauthier: 67 grand_total: Alexander Kjeldaas (aider): 1 Joshua Vial: 2 - Paul Gauthier: 158 + Paul Gauthier: 371 Paul Gauthier (aider): 40 Thinh Nguyen: 9 start_tag: v0.14.0 - total_lines: 210 -- aider_percentage: 1.96 + total_lines: 423 +- aider_percentage: 1.68 aider_total: 16 end_date: '2023-10-29' end_tag: v0.16.0 @@ -455,6 +584,8 @@ Paul Gauthier: 19 aider/commands.py: Paul Gauthier: 5 + aider/queries/tree-sitter-c-sharp-tags.scm: + Paul Gauthier: 46 aider/queries/tree-sitter-c-tags.scm: Paul Gauthier: 5 Paul Gauthier (aider): 4 @@ -497,12 +628,22 @@ setup.py: Paul Gauthier: 2 Paul Gauthier (aider): 2 + tests/test_coder.py: + Paul Gauthier: 21 + tests/test_commands.py: + Paul Gauthier: 10 + paul-gauthier: 1 + tests/test_editblock.py: + Paul Gauthier: 55 + tests/test_repomap.py: + Paul Gauthier: 5 grand_total: - Paul Gauthier: 800 + Paul Gauthier: 937 Paul Gauthier (aider): 16 + paul-gauthier: 1 start_tag: v0.15.0 - total_lines: 816 -- aider_percentage: 13.41 + total_lines: 954 +- aider_percentage: 7.38 aider_total: 22 end_date: '2023-11-06' end_tag: v0.17.0 @@ -528,6 +669,8 @@ Paul Gauthier: 1 aider/main.py: Paul Gauthier: 3 + aider/models/openai.py: + Paul Gauthier: 9 aider/queries/tree-sitter-elisp-tags.scm: Paul Gauthier: 3 aider/repomap.py: @@ -541,15 +684,23 @@ setup.py: Jack Hallam: 3 Paul Gauthier: 10 + tests/test_commands.py: + Paul Gauthier: 72 + tests/test_editblock.py: + Paul Gauthier: 23 + tests/test_io.py: + Paul Gauthier: 24 + tests/utils.py: + Paul Gauthier: 6 grand_total: Jack Hallam: 3 Omri Bloch: 1 - Paul Gauthier: 138 + Paul Gauthier: 272 Paul Gauthier (aider): 22 start_tag: v0.16.0 - total_lines: 164 -- aider_percentage: 27.41 - aider_total: 94 + total_lines: 298 +- aider_percentage: 23.57 + aider_total: 107 end_date: '2023-11-17' end_tag: v0.18.0 file_counts: @@ -564,6 +715,8 @@ Paul Gauthier (aider): 3 aider/io.py: Paul Gauthier: 3 + aider/models/model.py: + Paul Gauthier: 13 aider/repomap.py: Paul Gauthier: 10 benchmark/benchmark.py: @@ -573,12 +726,21 @@ Paul Gauthier: 16 scripts/versionbump.py: Paul Gauthier (aider): 41 + tests/test_coder.py: + Paul Gauthier: 25 + tests/test_commands.py: + Paul Gauthier: 26 + tests/test_main.py: + Paul Gauthier: 4 + Paul Gauthier (aider): 13 + tests/test_repomap.py: + Paul Gauthier: 30 grand_total: - Paul Gauthier: 249 - Paul Gauthier (aider): 94 + Paul Gauthier: 347 + Paul Gauthier (aider): 107 start_tag: v0.17.0 - total_lines: 343 -- aider_percentage: 0.71 + total_lines: 454 +- aider_percentage: 0.67 aider_total: 14 end_date: '2023-12-19' end_tag: v0.19.0 @@ -609,6 +771,14 @@ Paul Gauthier: 44 Your Name: 3 Your Name (aider): 14 + aider/models/__init__.py: + Paul Gauthier: 3 + aider/models/model.py: + Paul Gauthier: 7 + aider/models/openai.py: + Paul Gauthier: 13 + aider/models/openrouter.py: + Paul Gauthier: 4 aider/repo.py: Paul Gauthier: 4 aider/sendchat.py: @@ -623,14 +793,33 @@ Paul Gauthier: 2 benchmark/refactor_tools.py: Paul Gauthier: 209 + tests/test_coder.py: + Paul Gauthier: 11 + tests/test_commands.py: + Paul Gauthier: 1 + tests/test_io.py: + Paul Gauthier: 1 + tests/test_main.py: + Paul Gauthier: 10 + Your Name: 18 + tests/test_models.py: + Paul Gauthier: 10 + tests/test_repo.py: + Paul Gauthier: 1 + tests/test_repomap.py: + Paul Gauthier: 1 + tests/test_sendchat.py: + Paul Gauthier: 23 + tests/test_wholefile.py: + Paul Gauthier: 10 grand_total: - Paul Gauthier: 1946 - Your Name: 3 + Paul Gauthier: 2041 + Your Name: 21 Your Name (aider): 14 start_tag: v0.18.0 - total_lines: 1963 -- aider_percentage: 5.11 - aider_total: 9 + total_lines: 2076 +- aider_percentage: 11.3 + aider_total: 40 end_date: '2024-01-04' end_tag: v0.20.0 file_counts: @@ -653,6 +842,10 @@ Paul Gauthier (aider): 7 aider/io.py: Joshua Vial: 21 + aider/models/model.py: + Joshua Vial: 43 + aider/models/openrouter.py: + Joshua Vial: 4 aider/repo.py: Christopher Toth: 5 aider/repomap.py: @@ -663,14 +856,22 @@ Joshua Vial: 29 benchmark/benchmark.py: Joshua Vial: 16 + tests/test_commands.py: + Paul Gauthier: 21 + Paul Gauthier (aider): 24 + tests/test_models.py: + Joshua Vial: 13 + tests/test_udiff.py: + Paul Gauthier: 66 + Paul Gauthier (aider): 7 grand_total: Christopher Toth: 7 - Joshua Vial: 119 - Paul Gauthier: 41 - Paul Gauthier (aider): 9 + Joshua Vial: 179 + Paul Gauthier: 128 + Paul Gauthier (aider): 40 start_tag: v0.19.0 - total_lines: 176 -- aider_percentage: 40.0 + total_lines: 354 +- aider_percentage: 19.78 aider_total: 18 end_date: '2024-01-08' end_tag: v0.21.0 @@ -689,11 +890,13 @@ Paul Gauthier (aider): 8 setup.py: Paul Gauthier: 2 + tests/test_udiff.py: + Paul Gauthier: 46 grand_total: - Paul Gauthier: 27 + Paul Gauthier: 73 Paul Gauthier (aider): 18 start_tag: v0.20.0 - total_lines: 45 + total_lines: 91 - aider_percentage: 0.0 aider_total: 0 end_date: '2024-01-22' @@ -715,7 +918,7 @@ Paul Gauthier: 63 start_tag: v0.21.0 total_lines: 63 -- aider_percentage: 1.13 +- aider_percentage: 1.11 aider_total: 2 end_date: '2024-02-03' end_tag: v0.23.0 @@ -732,15 +935,17 @@ aider/mdstream.py: Paul Gauthier: 120 Paul Gauthier (aider): 2 + aider/models/openai.py: + Paul Gauthier: 3 benchmark/benchmark.py: Paul Gauthier: 17 grand_total: - Paul Gauthier: 168 + Paul Gauthier: 171 Paul Gauthier (aider): 2 Zachary Vorhies: 7 start_tag: v0.22.0 - total_lines: 177 -- aider_percentage: 8.37 + total_lines: 180 +- aider_percentage: 5.07 aider_total: 19 end_date: '2024-02-10' end_tag: v0.24.0 @@ -754,17 +959,25 @@ Paul Gauthier (aider): 8 aider/main.py: Paul Gauthier: 2 + aider/models/__init__.py: + Paul Gauthier: 2 + aider/models/model.py: + Paul Gauthier: 3 + aider/models/openai.py: + Paul Gauthier: 135 aider/scrape.py: Paul Gauthier: 176 Paul Gauthier (aider): 11 aider/utils.py: Paul Gauthier: 8 + tests/test_models.py: + Paul Gauthier: 8 grand_total: - Paul Gauthier: 208 + Paul Gauthier: 356 Paul Gauthier (aider): 19 start_tag: v0.23.0 - total_lines: 227 -- aider_percentage: 9.41 + total_lines: 375 +- aider_percentage: 5.48 aider_total: 8 end_date: '2024-03-04' end_tag: v0.25.0 @@ -778,15 +991,21 @@ aider/main.py: Paul Gauthier: 4 Paul Gauthier (aider): 8 + aider/models/openai.py: + Paul Gauthier: 1 aider/repo.py: Paul Gauthier: 11 aider/scrape.py: Paul Gauthier: 1 + tests/test_coder.py: + Paul Gauthier: 28 + tests/test_commands.py: + Paul Gauthier: 32 grand_total: - Paul Gauthier: 77 + Paul Gauthier: 138 Paul Gauthier (aider): 8 start_tag: v0.24.0 - total_lines: 85 + total_lines: 146 - aider_percentage: 0.0 aider_total: 0 end_date: '2024-03-08' @@ -819,11 +1038,15 @@ Paul Gauthier: 6 benchmark/benchmark.py: Paul Gauthier: 136 + tests/test_commands.py: + Paul Gauthier: 3 + tests/test_repomap.py: + Ryan Freckleton: 59 grand_total: - Paul Gauthier: 167 - Ryan Freckleton: 32 + Paul Gauthier: 170 + Ryan Freckleton: 91 start_tag: v0.26.0 - total_lines: 199 + total_lines: 261 - aider_percentage: 0.0 aider_total: 0 end_date: '2024-04-09' @@ -831,11 +1054,13 @@ file_counts: aider/__init__.py: Paul Gauthier: 1 + aider/models/openai.py: + Paul Gauthier: 10 grand_total: - Paul Gauthier: 1 + Paul Gauthier: 11 start_tag: v0.27.0 - total_lines: 1 -- aider_percentage: 6.8 + total_lines: 11 +- aider_percentage: 5.33 aider_total: 35 end_date: '2024-04-21' end_tag: v0.29.0 @@ -877,12 +1102,28 @@ Paul Gauthier: 3 benchmark/benchmark.py: Paul Gauthier: 60 + tests/test_coder.py: + Paul Gauthier: 28 + tests/test_commands.py: + Paul Gauthier: 25 + tests/test_editblock.py: + Paul Gauthier: 4 + tests/test_models.py: + Paul Gauthier: 13 + tests/test_repo.py: + Paul Gauthier: 37 + tests/test_repomap.py: + Paul Gauthier: 13 + tests/test_sendchat.py: + Paul Gauthier: 8 + tests/test_wholefile.py: + Paul Gauthier: 14 grand_total: Aloha: 1 - Paul Gauthier: 479 + Paul Gauthier: 621 Paul Gauthier (aider): 35 start_tag: v0.28.0 - total_lines: 515 + total_lines: 657 - aider_percentage: 0.0 aider_total: 0 end_date: '2024-04-23' @@ -906,10 +1147,24 @@ Paul Gauthier: 2 benchmark/benchmark.py: Paul Gauthier: 1 + tests/test_coder.py: + Paul Gauthier: 1 + tests/test_commands.py: + Paul Gauthier: 1 + tests/test_editblock.py: + Paul Gauthier: 1 + tests/test_models.py: + Paul Gauthier: 6 + tests/test_repo.py: + Paul Gauthier: 1 + tests/test_repomap.py: + Paul Gauthier: 2 + tests/test_wholefile.py: + Paul Gauthier: 1 grand_total: - Paul Gauthier: 224 + Paul Gauthier: 237 start_tag: v0.29.0 - total_lines: 224 + total_lines: 237 - aider_percentage: 0.15 aider_total: 2 end_date: '2024-05-02' @@ -940,12 +1195,20 @@ Paul Gauthier: 32 aider/sendchat.py: Paul Gauthier: 3 + tests/test_coder.py: + Paul Gauthier: 16 + tests/test_commands.py: + Paul Gauthier: 17 + tests/test_editblock.py: + Paul Gauthier: 4 + tests/test_wholefile.py: + Paul Gauthier: 1 grand_total: - Paul Gauthier: 1295 + Paul Gauthier: 1333 Paul Gauthier (aider): 2 start_tag: v0.30.0 - total_lines: 1297 -- aider_percentage: 0.36 + total_lines: 1335 +- aider_percentage: 0.35 aider_total: 3 end_date: '2024-05-07' end_tag: v0.32.0 @@ -987,11 +1250,15 @@ Paul Gauthier: 86 benchmark/plots.py: Paul Gauthier: 417 + tests/test_main.py: + Paul Gauthier: 18 + tests/test_sendchat.py: + Paul Gauthier: 4 grand_total: - Paul Gauthier: 835 + Paul Gauthier: 857 Paul Gauthier (aider): 3 start_tag: v0.31.0 - total_lines: 838 + total_lines: 860 - aider_percentage: 0.0 aider_total: 0 end_date: '2024-05-08' @@ -1003,6 +1270,8 @@ Paul Gauthier: 1 aider/commands.py: Paul Gauthier: 1 + aider/litellm.py: + Paul Gauthier: 11 aider/main.py: Paul Gauthier: 1 aider/models.py: @@ -1012,9 +1281,9 @@ aider/voice.py: Paul Gauthier: 2 grand_total: - Paul Gauthier: 16 + Paul Gauthier: 27 start_tag: v0.32.0 - total_lines: 16 + total_lines: 27 - aider_percentage: 0.0 aider_total: 0 end_date: '2024-05-10' @@ -1042,10 +1311,12 @@ Paul Gauthier: 3 aider/sendchat.py: Paul Gauthier: 7 + tests/test_sendchat.py: + Paul Gauthier: 4 grand_total: - Paul Gauthier: 87 + Paul Gauthier: 91 start_tag: v0.33.0 - total_lines: 87 + total_lines: 91 - aider_percentage: 6.39 aider_total: 17 end_date: '2024-05-13' @@ -1084,11 +1355,13 @@ Paul Gauthier (aider): 17 start_tag: v0.34.0 total_lines: 266 -- aider_percentage: 13.7 - aider_total: 84 +- aider_percentage: 14.35 + aider_total: 89 end_date: '2024-05-22' end_tag: v0.36.0 file_counts: + Gemfile: + Paul Gauthier (aider): 5 aider/__init__.py: Paul Gauthier: 1 aider/args.py: @@ -1108,6 +1381,8 @@ aider/linter.py: Paul Gauthier: 211 Paul Gauthier (aider): 29 + aider/litellm.py: + Paul Gauthier: 2 aider/main.py: Paul Gauthier: 48 Paul Gauthier (aider): 2 @@ -1128,12 +1403,12 @@ Paul Gauthier: 1 Paul Gauthier (aider): 3 grand_total: - Paul Gauthier: 529 - Paul Gauthier (aider): 84 + Paul Gauthier: 531 + Paul Gauthier (aider): 89 start_tag: v0.35.0 - total_lines: 613 -- aider_percentage: 16.42 - aider_total: 99 + total_lines: 620 +- aider_percentage: 18.17 + aider_total: 113 end_date: '2024-06-04' end_tag: v0.37.0 file_counts: @@ -1157,6 +1432,8 @@ Paul Gauthier (aider): 1 aider/linter.py: Paul Gauthier: 4 + aider/litellm.py: + Paul Gauthier: 1 aider/prompts.py: Paul Gauthier: 3 aider/repomap.py: @@ -1173,15 +1450,18 @@ scripts/blame.py: Paul Gauthier: 159 Paul Gauthier (aider): 53 + tests/test_io.py: + Paul Gauthier: 4 + Paul Gauthier (aider): 14 grand_total: Aleksandr Bobrov: 1 Aleksandr Bobrov (aider): 1 - Paul Gauthier: 503 - Paul Gauthier (aider): 98 + Paul Gauthier: 508 + Paul Gauthier (aider): 112 start_tag: v0.36.0 - total_lines: 603 -- aider_percentage: 5.31 - aider_total: 36 + total_lines: 622 +- aider_percentage: 6.31 + aider_total: 44 end_date: '2024-06-16' end_tag: v0.38.0 file_counts: @@ -1229,6 +1509,9 @@ Paul Gauthier: 13 aider/scrape.py: Paul Gauthier: 10 + aider/tests/test_urls.py: + Paul Gauthier: 7 + Paul Gauthier (aider): 8 aider/urls.py: Paul Gauthier: 8 scripts/jekyll_run.sh: @@ -1236,15 +1519,17 @@ scripts/update-docs.sh: Paul Gauthier: 14 Paul Gauthier (aider): 6 + website/Gemfile: + Paul Gauthier: 4 grand_total: Krazer: 28 - Paul Gauthier: 613 - Paul Gauthier (aider): 36 + Paul Gauthier: 624 + Paul Gauthier (aider): 44 develmusa: 1 start_tag: v0.37.0 - total_lines: 678 -- aider_percentage: 29.71 - aider_total: 71 + total_lines: 697 +- aider_percentage: 24.93 + aider_total: 95 end_date: '2024-06-20' end_tag: v0.39.0 file_counts: @@ -1276,6 +1561,13 @@ Paul Gauthier: 23 aider/scrape.py: Nicolas Perez: 1 + aider/tests/test_commands.py: + Paul Gauthier: 6 + aider/tests/test_main.py: + John-Mason P. Shackelford: 88 + aider/tests/test_repo.py: + Paul Gauthier: 24 + Paul Gauthier (aider): 24 aider/urls.py: Nicolas Perez: 1 Paul Gauthier: 1 @@ -1288,13 +1580,13 @@ grand_total: Daniel Vainsencher: 33 Daniel Vainsencher (aider): 16 - John-Mason P. Shackelford: 39 + John-Mason P. Shackelford: 127 Nicolas Perez: 2 - Paul Gauthier: 94 - Paul Gauthier (aider): 55 + Paul Gauthier: 124 + Paul Gauthier (aider): 79 start_tag: v0.38.0 - total_lines: 239 -- aider_percentage: 5.87 + total_lines: 381 +- aider_percentage: 5.47 aider_total: 21 end_date: '2024-06-24' end_tag: v0.40.0 @@ -1323,15 +1615,17 @@ Paul Gauthier: 28 aider/repo.py: Paul Gauthier: 26 + aider/tests/test_editblock.py: + Paul Gauthier: 26 grand_total: Dustin Miller: 14 Krazer: 73 - Paul Gauthier: 245 + Paul Gauthier: 271 Paul Gauthier (aider): 21 paul-gauthier: 5 start_tag: v0.39.0 - total_lines: 358 -- aider_percentage: 5.88 + total_lines: 384 +- aider_percentage: 5.54 aider_total: 15 end_date: '2024-07-01' end_tag: v0.41.0 @@ -1367,6 +1661,12 @@ Paul Gauthier: 12 aider/sendchat.py: Paul Gauthier: 2 + aider/tests/test_coder.py: + Paul Gauthier: 10 + aider/tests/test_editblock.py: + Paul Gauthier: 2 + aider/tests/test_wholefile.py: + Paul Gauthier: 4 scripts/update-docs.sh: Paul Gauthier: 3 setup.py: @@ -1374,11 +1674,11 @@ grand_total: Amir Elaguizy (aider): 6 Mitsuki Ogasahara: 3 - Paul Gauthier: 237 + Paul Gauthier: 253 Paul Gauthier (aider): 9 start_tag: v0.40.0 - total_lines: 255 -- aider_percentage: 2.3 + total_lines: 271 +- aider_percentage: 2.29 aider_total: 7 end_date: '2024-07-04' end_tag: v0.42.0 @@ -1410,6 +1710,8 @@ Paul Gauthier: 8 aider/sendchat.py: Paul Gauthier: 45 + aider/tests/test_sendchat.py: + Paul Gauthier: 1 aider/versioncheck.py: Paul Gauthier: 12 aider/voice.py: @@ -1417,12 +1719,12 @@ scripts/jekyll_run.sh: Paul Gauthier: 2 grand_total: - Paul Gauthier: 298 + Paul Gauthier: 299 Paul Gauthier (aider): 7 start_tag: v0.41.0 - total_lines: 305 -- aider_percentage: 3.41 - aider_total: 14 + total_lines: 306 +- aider_percentage: 8.6 + aider_total: 38 end_date: '2024-07-07' end_tag: v0.43.0 file_counts: @@ -1466,6 +1768,11 @@ Paul Gauthier: 36 aider/repomap.py: Paul Gauthier: 14 + aider/tests/test_commands.py: + Paul Gauthier: 1 + aider/tests/test_help.py: + Paul Gauthier: 7 + Paul Gauthier (aider): 24 aider/versioncheck.py: Paul Gauthier: 2 scripts/jekyll_run.sh: @@ -1476,10 +1783,10 @@ Paul Gauthier: 6 Paul Gauthier (aider): 3 grand_total: - Paul Gauthier: 396 - Paul Gauthier (aider): 14 + Paul Gauthier: 404 + Paul Gauthier (aider): 38 start_tag: v0.42.0 - total_lines: 410 + total_lines: 442 - aider_percentage: 26.86 aider_total: 159 end_date: '2024-07-16' @@ -1808,3 +2115,129 @@ paul-gauthier: 1 start_tag: v0.47.0 total_lines: 626 +- aider_percentage: 61.13 + aider_total: 478 + end_date: '2024-08-10' + end_tag: v0.49.0 + file_counts: + aider/__init__.py: + Paul Gauthier: 1 + aider/args.py: + Paul Gauthier: 9 + Paul Gauthier (aider): 13 + aider/coders/base_coder.py: + Paul Gauthier: 91 + Paul Gauthier (aider): 44 + aider/coders/base_prompts.py: + Paul Gauthier: 5 + aider/commands.py: + Paul Gauthier: 34 + Paul Gauthier (aider): 108 + aider/io.py: + Paul Gauthier: 7 + Paul Gauthier (aider): 24 + aider/llm.py: + Paul Gauthier (aider): 5 + aider/main.py: + Paul Gauthier: 1 + Paul Gauthier (aider): 4 + aider/models.py: + Paul Gauthier: 34 + Paul Gauthier (aider): 3 + aider/repo.py: + Paul Gauthier: 8 + Paul Gauthier (aider): 13 + aider/repomap.py: + Paul Gauthier: 11 + Paul Gauthier (aider): 23 + aider/scrape.py: + Paul Gauthier (aider): 17 + aider/sendchat.py: + Paul Gauthier: 21 + aider/urls.py: + Paul Gauthier: 1 + aider/utils.py: + Paul Gauthier (aider): 11 + aider/versioncheck.py: + Paul Gauthier: 3 + Paul Gauthier (aider): 11 + docker/Dockerfile: + Paul Gauthier: 5 + Paul Gauthier (aider): 2 + tests/basic/test_coder.py: + Paul Gauthier (aider): 7 + tests/basic/test_commands.py: + Paul Gauthier: 25 + Paul Gauthier (aider): 109 + tests/basic/test_editblock.py: + Paul Gauthier (aider): 1 + tests/basic/test_main.py: + Paul Gauthier (aider): 33 + tests/basic/test_sendchat.py: + Paul Gauthier: 47 + tests/basic/test_wholefile.py: + Paul Gauthier (aider): 1 + tests/scrape/test_scrape.py: + Paul Gauthier: 1 + Paul Gauthier (aider): 49 + grand_total: + Paul Gauthier: 304 + Paul Gauthier (aider): 478 + start_tag: v0.48.0 + total_lines: 782 +- aider_percentage: 66.05 + aider_total: 214 + end_date: '2024-08-13' + end_tag: v0.50.0 + file_counts: + .github/workflows/release.yml: + Branch Vincent: 2 + aider/__init__.py: + Paul Gauthier: 1 + aider/args.py: + Paul Gauthier (aider): 10 + aider/coders/base_coder.py: + Paul Gauthier: 24 + Paul Gauthier (aider): 32 + aider/commands.py: + Amir Elaguizy (aider): 13 + Paul Gauthier: 28 + Paul Gauthier (aider): 18 + aider/io.py: + Paul Gauthier: 1 + aider/main.py: + Paul Gauthier: 9 + Paul Gauthier (aider): 2 + aider/models.py: + Paul Gauthier: 4 + Paul Gauthier (aider): 4 + aider/scrape.py: + Paul Gauthier (aider): 26 + aider/sendchat.py: + Paul Gauthier (aider): 1 + aider/utils.py: + Paul Gauthier: 1 + aider/versioncheck.py: + Paul Gauthier: 7 + Paul Gauthier (aider): 1 + scripts/versionbump.py: + Paul Gauthier: 4 + Paul Gauthier (aider): 34 + tests/basic/test_coder.py: + Paul Gauthier: 3 + Paul Gauthier (aider): 24 + tests/basic/test_commands.py: + Paul Gauthier: 18 + Paul Gauthier (aider): 41 + tests/basic/test_main.py: + Paul Gauthier: 1 + Paul Gauthier (aider): 8 + tests/help/test_help.py: + Paul Gauthier: 7 + grand_total: + Amir Elaguizy (aider): 13 + Branch Vincent: 2 + Paul Gauthier: 108 + Paul Gauthier (aider): 201 + start_tag: v0.49.0 + total_lines: 324 diff --git a/aider/website/_includes/blame.md b/aider/website/_includes/blame.md index e5c66867e..3973ea812 100644 --- a/aider/website/_includes/blame.md +++ b/aider/website/_includes/blame.md @@ -1,90 +1,126 @@ - + +