diff --git a/.github/workflows/docker-build-test.yml b/.github/workflows/docker-build-test.yml index edae497ee..5254f5be0 100644 --- a/.github/workflows/docker-build-test.yml +++ b/.github/workflows/docker-build-test.yml @@ -24,6 +24,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index b9268de91..9e7efc2f5 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -12,6 +12,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 1eb78382e..cfd3ee257 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -36,7 +36,9 @@ jobs: working-directory: aider/website steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 274be72c5..ade95897a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 diff --git a/.github/workflows/ubuntu-tests.yml b/.github/workflows/ubuntu-tests.yml index 3654df7bb..41545cfb0 100644 --- a/.github/workflows/ubuntu-tests.yml +++ b/.github/workflows/ubuntu-tests.yml @@ -25,6 +25,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 @@ -38,5 +40,7 @@ jobs: pip install . - name: Run tests + env: + AIDER_ANALYTICS: false run: | pytest diff --git a/.github/workflows/windows-tests.yml b/.github/workflows/windows-tests.yml index c79b0dd5f..bbc6a9b37 100644 --- a/.github/workflows/windows-tests.yml +++ b/.github/workflows/windows-tests.yml @@ -25,6 +25,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 @@ -38,6 +40,8 @@ jobs: pip install . - name: Run tests + env: + AIDER_ANALYTICS: false run: | pytest diff --git a/.gitignore b/.gitignore index 35d79f127..a01bb2f26 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ _site .jekyll-cache/ .jekyll-metadata aider/__version__.py +aider/_version.py .venv/ .#* .gitattributes diff --git a/HISTORY.md b/HISTORY.md index 8a74254ec..af6e5963a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -3,12 +3,17 @@ ### main branch - PDF support for Sonnet and Gemini models. +- Added `--voice-input-device` to select audio input device for voice recording, by @preynal. +- Added `--timeout` option to configure API call timeouts. - Set cwd to repo root when running shell commands. - Improved error handling for failed .gitignore file operations. - Improved error handling for input history file permissions. - Improved error handling for analytics file access. - Removed broken support for Dart. -- Aider wrote 85% of the code in this release. +- Bugfix when scraping URLs found in chat messages. +- Better handling of __version__ import errors. +- Improved `/drop` command to support substring matching for non-glob patterns. +- Aider wrote 79% of the code in this release. ### Aider v0.65.1 diff --git a/aider/__init__.py b/aider/__init__.py index db056f0ff..c25992a6c 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,20 @@ +from packaging import version + +__version__ = "0.65.2.dev" +safe_version = __version__ + try: - from aider.__version__ import __version__ + from aider._version import __version__ except Exception: - __version__ = "0.65.2.dev" + __version__ = safe_version + "+import" + +if type(__version__) is not str: + __version__ = safe_version + "+type" +else: + try: + if version.parse(__version__) < version.parse(safe_version): + __version__ = safe_version + "+less" + except Exception: + __version__ = safe_version + "+parse" __all__ = [__version__] diff --git a/aider/analytics.py b/aider/analytics.py index 82c888247..f994a804f 100644 --- a/aider/analytics.py +++ b/aider/analytics.py @@ -214,7 +214,3 @@ class Analytics: with open(self.logfile, "a") as f: json.dump(log_entry, f) f.write("\n") - - def __del__(self): - if self.ph: - self.ph.shutdown() diff --git a/aider/args.py b/aider/args.py index 58a08f5a0..151724565 100644 --- a/aider/args.py +++ b/aider/args.py @@ -205,6 +205,12 @@ def get_parser(default_config_files, git_root): default=True, help="Verify the SSL cert when connecting to models (default: True)", ) + group.add_argument( + "--timeout", + type=int, + default=None, + help="Timeout in seconds for API calls (default: None)", + ) group.add_argument( "--edit-format", "--chat-mode", @@ -559,7 +565,7 @@ def get_parser(default_config_files, git_root): group.add_argument( "--test", action="store_true", - help="Run tests and fix problems found", + help="Run tests, fix problems found and then exit", default=False, ) @@ -770,6 +776,12 @@ def get_parser(default_config_files, git_root): default="en", help="Specify the language for voice using ISO 639-1 code (default: auto)", ) + group.add_argument( + "--voice-input-device", + metavar="VOICE_INPUT_DEVICE", + default=None, + help="Specify the input device name for voice recording", + ) return parser diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 3926cd987..646f0e64b 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -771,6 +771,7 @@ class Coder: self.lint_outcome = None self.test_outcome = None self.shell_commands = [] + self.message_cost = 0 if self.repo: self.commit_before_message.append(self.repo.get_head_commit_sha()) @@ -884,6 +885,7 @@ class Coder: thresh = 2 # seconds if self.last_keyboard_interrupt and now - self.last_keyboard_interrupt < thresh: self.io.tool_warning("\n\n^C KeyboardInterrupt") + self.event("exit", reason="Control-C") sys.exit() self.io.tool_warning("\n\n^C again to exit") @@ -1185,6 +1187,8 @@ class Coder: return chunks def send_message(self, inp): + self.event("message_send_starting") + self.cur_messages += [ dict(role="user", content=inp), ] @@ -1264,6 +1268,7 @@ class Coder: lines = traceback.format_exception(type(err), err, err.__traceback__) self.io.tool_warning("".join(lines)) self.io.tool_error(str(err)) + self.event("message_send_exception", exception=str(err)) return finally: if self.mdstream: diff --git a/aider/commands.py b/aider/commands.py index af74e281c..f56152bc3 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -808,15 +808,33 @@ class Commands: # Expand tilde in the path expanded_word = os.path.expanduser(word) - # Handle read-only files separately, without glob_filtered_to_repo - read_only_matched = [f for f in self.coder.abs_read_only_fnames if expanded_word in f] + # Handle read-only files with substring matching and samefile check + read_only_matched = [] + for f in self.coder.abs_read_only_fnames: + if expanded_word in f: + read_only_matched.append(f) + continue - 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") + # Try samefile comparison for relative paths + try: + abs_word = os.path.abspath(expanded_word) + if os.path.samefile(abs_word, f): + read_only_matched.append(f) + except (FileNotFoundError, OSError): + continue - matched_files = self.glob_filtered_to_repo(expanded_word) + 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") + + # For editable files, use glob if word contains glob chars, otherwise use substring + if any(c in expanded_word for c in "*?[]"): + matched_files = self.glob_filtered_to_repo(expanded_word) + else: + # Use substring matching like we do for read-only files + matched_files = [ + self.coder.get_rel_fname(f) for f in self.coder.abs_fnames if expanded_word in f + ] if not matched_files: matched_files.append(expanded_word) @@ -904,11 +922,12 @@ class Commands: def cmd_exit(self, args): "Exit the application" + self.coder.event("exit", reason="/exit") sys.exit() def cmd_quit(self, args): "Exit the application" - sys.exit() + self.cmd_exit(args) def cmd_ls(self, args): "List all known files and indicate which are included in the chat session" @@ -1080,7 +1099,9 @@ class Commands: self.io.tool_error("To use /voice you must provide an OpenAI API key.") return try: - self.voice = voice.Voice(audio_format=self.args.voice_format) + self.voice = voice.Voice( + audio_format=self.args.voice_format, device_name=self.args.voice_input_device + ) except voice.SoundDeviceError: self.io.tool_error( "Unable to import `sounddevice` and/or `soundfile`, is portaudio installed?" diff --git a/aider/main.py b/aider/main.py index 7637c7715..20c6a59fd 100644 --- a/aider/main.py +++ b/aider/main.py @@ -468,6 +468,10 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F litellm._lazy_module.client_session = httpx.Client(verify=False) litellm._lazy_module.aclient_session = httpx.AsyncClient(verify=False) + if args.timeout: + litellm._load_litellm() + litellm._lazy_module.request_timeout = args.timeout + if args.dark_mode: args.user_input_color = "#32FF32" args.tool_error_color = "#FF3333" @@ -548,9 +552,11 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F if args.gui and not return_coder: if not check_streamlit_install(io): + analytics.event("exit", reason="Streamlit not installed") return analytics.event("gui session") launch_gui(argv) + analytics.event("exit", reason="GUI session ended") return if args.verbose: @@ -577,6 +583,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F io.tool_output( "Provide either a single directory of a git repo, or a list of one or more files." ) + analytics.event("exit", reason="Invalid directory input") return 1 git_dname = None @@ -587,6 +594,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F fnames = [] else: io.tool_error(f"{all_files[0]} is a directory, but --no-git selected.") + analytics.event("exit", reason="Directory with --no-git") return 1 # We can't know the git repo for sure until after parsing the args. @@ -595,18 +603,22 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F if args.git and not force_git_root: right_repo_root = guessed_wrong_repo(io, git_root, fnames, git_dname) if right_repo_root: + analytics.event("exit", reason="Recursing with correct repo") 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, verbose=args.verbose) + analytics.event("exit", reason="Just checking update") return 0 if not update_available else 1 if args.install_main_branch: success = install_from_main_branch(io) + analytics.event("exit", reason="Installed main branch") return 0 if success else 1 if args.upgrade: success = install_upgrade(io) + analytics.event("exit", reason="Upgrade completed") return 0 if success else 1 if args.check_update: @@ -614,6 +626,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F if args.list_models: models.print_matching_models(io, args.list_models) + analytics.event("exit", reason="Listed models") return 0 if args.git: @@ -657,6 +670,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F if len(parts) != 2: io.tool_error(f"Invalid alias format: {alias_def}") io.tool_output("Format should be: alias:model-name") + analytics.event("exit", reason="Invalid alias format error") return 1 alias, model = parts models.MODEL_ALIASES[alias.strip()] = model.strip() @@ -685,6 +699,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F lint_cmds = parse_lint_cmds(args.lint_cmd, io) if lint_cmds is None: + analytics.event("exit", reason="Invalid lint command format") return 1 if args.show_model_warnings: @@ -697,6 +712,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F io.offer_url(urls.model_warnings, "Open documentation url for more info?") io.tool_output() except KeyboardInterrupt: + analytics.event("exit", reason="Keyboard interrupt during model warnings") return 1 repo = None @@ -720,6 +736,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F if not args.skip_sanity_check_repo: if not sanity_check_repo(repo, io): + analytics.event("exit", reason="Repository sanity check failed") return 1 if repo: @@ -787,12 +804,15 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F except UnknownEditFormat as err: io.tool_error(str(err)) io.offer_url(urls.edit_formats, "Open documentation about edit formats?") + analytics.event("exit", reason="Unknown edit format") return 1 except ValueError as err: io.tool_error(str(err)) + analytics.event("exit", reason="ValueError during coder creation") return 1 if return_coder: + analytics.event("exit", reason="Returning coder object") return coder ignores = [] @@ -810,6 +830,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F ] messages = coder.format_messages().all_messages() utils.show_messages(messages) + analytics.event("exit", reason="Showed prompts") return if args.lint: @@ -818,6 +839,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F if args.test: if not args.test_cmd: io.tool_error("No --test-cmd provided.") + analytics.event("exit", reason="No test command provided") return 1 test_errors = coder.commands.cmd_test(args.test_cmd) if test_errors: @@ -830,32 +852,30 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F coder.commands.cmd_commit() if args.lint or args.test or args.commit: + analytics.event("exit", reason="Completed lint/test/commit") return if args.show_repo_map: repo_map = coder.get_repo_map() if repo_map: io.tool_output(repo_map) + analytics.event("exit", reason="Showed repo map") return if args.apply: content = io.read_text(args.apply) if content is None: + analytics.event("exit", reason="Failed to read apply content") return coder.partial_response_content = content coder.apply_updates() + analytics.event("exit", reason="Applied updates") return if args.apply_clipboard_edits: args.edit_format = main_model.editor_edit_format args.message = "/paste" - if "VSCODE_GIT_IPC_HANDLE" in os.environ: - args.pretty = False - io.tool_output("VSCode terminal detected, pretty output has been disabled.") - - io.tool_output('Use /help for help, run "aider --help" to see cmd line args') - if args.show_release_notes is True: io.tool_output(f"Opening release notes: {urls.release_notes}") io.tool_output() @@ -887,6 +907,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F coder.run(with_message=args.message) except SwitchCoder: pass + analytics.event("exit", reason="Completed --message") return if args.message_file: @@ -896,13 +917,18 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F coder.run(with_message=message_from_file) except FileNotFoundError: io.tool_error(f"Message file not found: {args.message_file}") + analytics.event("exit", reason="Message file not found") return 1 except IOError as e: io.tool_error(f"Error reading message file: {e}") + analytics.event("exit", reason="Message file IO error") return 1 + + analytics.event("exit", reason="Completed --message-file") return if args.exit: + analytics.event("exit", reason="Exit flag set") return analytics.event("cli session", main_model=main_model, edit_format=main_model.edit_format) @@ -910,6 +936,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F while True: try: coder.run() + analytics.event("exit", reason="Completed main CLI coder.run") return except SwitchCoder as switch: kwargs = dict(io=io, from_coder=coder) @@ -928,6 +955,10 @@ def is_first_run_of_new_version(io, verbose=False): installs_file = Path.home() / ".aider" / "installs.json" key = (__version__, sys.executable) + # Never show notes for .dev versions + if ".dev" in __version__: + return False + if verbose: io.tool_output( f"Checking imports for version {__version__} and executable {sys.executable}" diff --git a/aider/voice.py b/aider/voice.py index 47fb49c6e..1e9f700b6 100644 --- a/aider/voice.py +++ b/aider/voice.py @@ -34,7 +34,7 @@ class Voice: threshold = 0.15 - def __init__(self, audio_format="wav"): + def __init__(self, audio_format="wav", device_name=None): if sf is None: raise SoundDeviceError try: @@ -42,6 +42,27 @@ class Voice: import sounddevice as sd self.sd = sd + + + devices = sd.query_devices() + + if device_name: + # Find the device with matching name + device_id = None + for i, device in enumerate(devices): + if device_name in device["name"]: + device_id = i + break + if device_id is None: + available_inputs = [d["name"] for d in devices if d["max_input_channels"] > 0] + raise ValueError(f"Device '{device_name}' not found. Available input devices: {available_inputs}") + + print(f"Using input device: {device_name} (ID: {device_id})") + + self.device_id = device_id + else: + self.device_id = None + except (OSError, ModuleNotFoundError): raise SoundDeviceError if audio_format not in ["wav", "mp3", "webm"]: @@ -93,7 +114,7 @@ class Voice: temp_wav = tempfile.mktemp(suffix=".wav") try: - sample_rate = int(self.sd.query_devices(None, "input")["default_samplerate"]) + sample_rate = int(self.sd.query_devices(self.device_id, "input")["default_samplerate"]) except (TypeError, ValueError): sample_rate = 16000 # fallback to 16kHz if unable to query device except self.sd.PortAudioError: @@ -104,7 +125,7 @@ class Voice: self.start_time = time.time() try: - with self.sd.InputStream(samplerate=sample_rate, channels=1, callback=self.callback): + with self.sd.InputStream(samplerate=sample_rate, channels=1, callback=self.callback, device=self.device_id): prompt(self.get_prompt, refresh_interval=0.1) except self.sd.PortAudioError as err: raise SoundDeviceError(f"Error accessing audio input device: {err}") diff --git a/aider/website/HISTORY.md b/aider/website/HISTORY.md index 2aae866f9..40556eab5 100644 --- a/aider/website/HISTORY.md +++ b/aider/website/HISTORY.md @@ -27,12 +27,17 @@ cog.out(text) ### main branch - PDF support for Sonnet and Gemini models. +- Added `--voice-input-device` to select audio input device for voice recording, by @preynal. +- Added `--timeout` option to configure API call timeouts. - Set cwd to repo root when running shell commands. - Improved error handling for failed .gitignore file operations. - Improved error handling for input history file permissions. - Improved error handling for analytics file access. - Removed broken support for Dart. -- Aider wrote 85% of the code in this release. +- Bugfix when scraping URLs found in chat messages. +- Better handling of __version__ import errors. +- Improved `/drop` command to support substring matching for non-glob patterns. +- Aider wrote 79% of the code in this release. ### Aider v0.65.1 diff --git a/aider/website/_data/edit_leaderboard.yml b/aider/website/_data/edit_leaderboard.yml index 2ab3cee16..bcf166b8f 100644 --- a/aider/website/_data/edit_leaderboard.yml +++ b/aider/website/_data/edit_leaderboard.yml @@ -1945,4 +1945,78 @@ command: aider --model gemini/gemini-exp-1114 date: 2024-11-15 versions: 0.63.2.dev - seconds_per_case: 38.6 \ No newline at end of file + seconds_per_case: 38.6 +- dirname: 2024-11-27-07-41-51--qwen2.5-coder-14b-whole-1 + test_cases: 133 + model: ollama/qwen2.5-coder:14b + edit_format: whole + commit_hash: 200295e + pass_rate_1: 53.4 + pass_rate_2: 61.7 + percent_cases_well_formed: 98.5 + error_outputs: 4 + num_malformed_responses: 4 + num_with_malformed_responses: 2 + user_asks: 48 + lazy_comments: 0 + syntax_errors: 2 + indentation_errors: 2 + exhausted_context_windows: 0 + test_timeouts: 2 + command: aider --model ollama/qwen2.5-coder:14b + date: 2024-11-27 + versions: 0.65.2.dev + seconds_per_case: 58.0 + total_cost: 0.0000 + +- dirname: 2024-11-28-07-42-56--qwen2.5-coder-32b-whole-4 + test_cases: 133 + model: ollama/qwen2.5-coder:32b + edit_format: whole + commit_hash: 200295e + pass_rate_1: 58.6 + pass_rate_2: 72.9 + percent_cases_well_formed: 100.0 + num_malformed_responses: 0 + num_with_malformed_responses: 0 + lazy_comments: 0 + syntax_errors: 0 + indentation_errors: 0 + exhausted_context_windows: 0 + command: aider --model ollama/qwen2.5-coder:32b + date: 2024-11-28 + versions: 0.65.2.dev + seconds_per_case: 147.5 + total_cost: 0.0000 +- dirname: 2024-11-28-13-14-00--tulu3-whole-2 + test_cases: 133 + model: ollama/tulu3 + edit_format: whole + commit_hash: 200295e + pass_rate_1: 21.8 + pass_rate_2: 26.3 + percent_cases_well_formed: 100.0 + error_outputs: 0 + num_malformed_responses: 0 + num_with_malformed_responses: 0 + exhausted_context_windows: 0 + command: aider --model ollama/tulu3 + date: 2024-11-28 + versions: 0.65.2.dev + seconds_per_case: 35.8 + total_cost: 0.0000 + +- dirname: 2024-11-28-14-41-46--granite3-dense-8b-whole-1 + test_cases: 133 + model: ollama/granite3-dense:8b + edit_format: whole + commit_hash: 200295e + pass_rate_1: 17.3 + pass_rate_2: 20.3 + percent_cases_well_formed: 78.9 + exhausted_context_windows: 0 + command: aider --model ollama/granite3-dense:8b + date: 2024-11-28 + versions: 0.65.2.dev + seconds_per_case: 38.1 + total_cost: 0.0000 diff --git a/aider/website/_posts/2024-11-21-quantization.md b/aider/website/_posts/2024-11-21-quantization.md index f2426b9c2..33677b5d8 100644 --- a/aider/website/_posts/2024-11-21-quantization.md +++ b/aider/website/_posts/2024-11-21-quantization.md @@ -81,6 +81,9 @@ if run with Ollama's default 2k context window. ## Benchmark results +{: .note :} +These are results from single benchmark runs, so expect normal variance of +/- 1-2%. +