From 93f20a6d23fa9969906fe4f05277d44c27a79b8e Mon Sep 17 00:00:00 2001 From: timput Date: Sun, 2 Nov 2025 07:53:50 -0700 Subject: [PATCH 1/6] add initial haskell-tags.scm for repomap --- aider/queries/tree-sitter-languages/haskell-tags.scm | 3 +++ tests/basic/test_repomap.py | 3 +++ tests/fixtures/languages/haskell/test.hs | 7 +++++++ 3 files changed, 13 insertions(+) create mode 100644 aider/queries/tree-sitter-languages/haskell-tags.scm create mode 100644 tests/fixtures/languages/haskell/test.hs diff --git a/aider/queries/tree-sitter-languages/haskell-tags.scm b/aider/queries/tree-sitter-languages/haskell-tags.scm new file mode 100644 index 00000000000..f5c073750a6 --- /dev/null +++ b/aider/queries/tree-sitter-languages/haskell-tags.scm @@ -0,0 +1,3 @@ +(function (variable) @name.definition.function) +(bind (variable) @name.definition.function) +(signature (variable) @name.definition.type) diff --git a/tests/basic/test_repomap.py b/tests/basic/test_repomap.py index 185e6e62d5f..035c7c31cea 100644 --- a/tests/basic/test_repomap.py +++ b/tests/basic/test_repomap.py @@ -302,6 +302,9 @@ def test_language_elixir(self): def test_language_gleam(self): self._test_language_repo_map("gleam", "gleam", "greet") + def test_language_haskell(self): + self._test_language_repo_map("haskell", "hs", "add") + def test_language_java(self): self._test_language_repo_map("java", "java", "Greeting") diff --git a/tests/fixtures/languages/haskell/test.hs b/tests/fixtures/languages/haskell/test.hs new file mode 100644 index 00000000000..890ff94b742 --- /dev/null +++ b/tests/fixtures/languages/haskell/test.hs @@ -0,0 +1,7 @@ +module Main where + +add :: Int -> Int -> Int +add a b = a + b + +main :: IO () +main = print (add 2 3) From be8da40b1f050fa325265a75e92b1d7d308679dd Mon Sep 17 00:00:00 2001 From: timput Date: Sun, 2 Nov 2025 07:54:12 -0700 Subject: [PATCH 2/6] add initial zig-tags.scm for repomap --- aider/queries/tree-sitter-languages/zig-tags.scm | 3 +++ tests/basic/test_repomap.py | 3 +++ tests/fixtures/languages/zig/test.zig | 10 ++++++++++ 3 files changed, 16 insertions(+) create mode 100644 aider/queries/tree-sitter-languages/zig-tags.scm create mode 100644 tests/fixtures/languages/zig/test.zig diff --git a/aider/queries/tree-sitter-languages/zig-tags.scm b/aider/queries/tree-sitter-languages/zig-tags.scm new file mode 100644 index 00000000000..c02028ea8a1 --- /dev/null +++ b/aider/queries/tree-sitter-languages/zig-tags.scm @@ -0,0 +1,3 @@ +(FnProto) @name.definition.function +(VarDecl "const" @name.definition.constant) +(VarDecl "var" @name.definition.variable) diff --git a/tests/basic/test_repomap.py b/tests/basic/test_repomap.py index 185e6e62d5f..2c965bdffa6 100644 --- a/tests/basic/test_repomap.py +++ b/tests/basic/test_repomap.py @@ -334,6 +334,9 @@ def test_language_typescript(self): def test_language_tsx(self): self._test_language_repo_map("tsx", "tsx", "UserProps") + def test_language_zig(self): + self._test_language_repo_map("zig", "zig", "add") + def test_language_csharp(self): self._test_language_repo_map("csharp", "cs", "IGreeter") diff --git a/tests/fixtures/languages/zig/test.zig b/tests/fixtures/languages/zig/test.zig new file mode 100644 index 00000000000..3cebad3a088 --- /dev/null +++ b/tests/fixtures/languages/zig/test.zig @@ -0,0 +1,10 @@ +const std = @import("std"); + +pub fn add(a: i32, b: i32) i32 { + return a + b; +} + +pub fn main() !void { + const stdout = std.io.getStdOut().writer(); + try stdout.print("{}", .{add(2, 3)}); +} From 21ce93c4afdba9d5dd7244b3a57707ace428a4e3 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sun, 2 Nov 2025 18:09:11 -0500 Subject: [PATCH 3/6] Bump Version --- aider/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/__init__.py b/aider/__init__.py index 67ec05c2f26..0321d2a09ea 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.88.4.dev" +__version__ = "0.88.5.dev" safe_version = __version__ try: From b889db5a94a0149020d2b8b595fb9dca55aa5c95 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sun, 2 Nov 2025 19:46:40 -0500 Subject: [PATCH 4/6] #48: Fix /test command and make both /test and /run command outputs play better with async output --- aider/coders/base_coder.py | 18 +++++- aider/commands.py | 88 ++++++++++++++++------------ tests/basic/test_deprecated.py | 4 +- tests/basic/test_ssl_verification.py | 10 ++-- 4 files changed, 73 insertions(+), 47 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index e46099d22c1..e7dea813374 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1077,6 +1077,10 @@ async def _run_patched(self, with_message=None, preproc=True): while True: try: + if self.commands.cmd_running: + await asyncio.sleep(0.1) + continue + if ( not self.io.confirmation_in_progress and not user_message @@ -1134,6 +1138,10 @@ async def _run_patched(self, with_message=None, preproc=True): try: user_message = self.io.input_task.result() await self.io.cancel_input_task() + + if self.commands.is_run_command(user_message): + self.commands.cmd_running = True + except (asyncio.CancelledError, KeyboardInterrupt): user_message = None @@ -1170,7 +1178,6 @@ async def _run_patched(self, with_message=None, preproc=True): user_message = None except (asyncio.CancelledError, KeyboardInterrupt): - print("error of some sort") pass # Stop spinner when processing task completes @@ -1180,6 +1187,7 @@ async def _run_patched(self, with_message=None, preproc=True): self.io.processing_task = asyncio.create_task( self._processing_logic(user_message, preproc) ) + # Start spinner for processing task self.io.start_spinner("Processing...") @@ -1243,6 +1251,12 @@ async def preproc_user_input(self, inp): return if self.commands.is_command(inp): + if inp[0] in "!": + inp = f"/run {inp[1:]}" + + if self.commands.is_run_command(inp): + self.commands.cmd_running = True + return await self.commands.run(inp) await self.check_for_file_mentions(inp) @@ -1254,8 +1268,6 @@ async def run_one(self, user_message, preproc): self.init_before_message() if preproc: - if user_message[0] in "!": - user_message = f"/run {user_message[1:]}" message = await self.preproc_user_input(user_message) else: message = user_message diff --git a/aider/commands.py b/aider/commands.py index f6b23174cae..d4563377179 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -84,6 +84,7 @@ def __init__( # Store the original read-only filenames provided via args.read self.original_read_only_fnames = set(original_read_only_fnames or []) + self.cmd_running = False def cmd_model(self, args): "Switch the Main Model to a new LLM" @@ -256,6 +257,9 @@ async def cmd_web(self, args, return_content=False): def is_command(self, inp): return inp[0] in "/!" + def is_run_command(self, inp): + return inp[0] in "!" or inp[:5] == "/test" or inp[:4] == "/run" + def get_raw_completions(self, cmd): assert cmd.startswith("/") cmd = cmd[1:] @@ -1151,51 +1155,61 @@ async def cmd_test(self, args): async def cmd_run(self, args, add_on_nonzero_exit=False): "Run a shell command and optionally add the output to the chat (alias: !)" - exit_status, combined_output = await asyncio.to_thread( - run_cmd, - args, - verbose=self.verbose, - error_print=self.io.tool_error, - cwd=self.coder.root, - ) + try: + self.cmd_running = True + exit_status, combined_output = await asyncio.to_thread( + run_cmd, + args, + verbose=self.verbose, + error_print=self.io.tool_error, + cwd=self.coder.root, + ) + self.cmd_running = False - if combined_output is None: - return + # This print statement, for whatever reason, + # allows the thread to properly yield control of the terminal + # to the main program + print("") - # Calculate token count of output - token_count = self.coder.main_model.token_count(combined_output) - k_tokens = token_count / 1000 + if combined_output is None: + return - if add_on_nonzero_exit: - add = exit_status != 0 - else: - add = await self.io.confirm_ask( - f"Add {k_tokens:.1f}k tokens of command output to the chat?" - ) + # Calculate token count of output + token_count = self.coder.main_model.token_count(combined_output) + k_tokens = token_count / 1000 - if add: - num_lines = len(combined_output.strip().splitlines()) - line_plural = "line" if num_lines == 1 else "lines" - self.io.tool_output(f"Added {num_lines} {line_plural} of output to the chat.") + if add_on_nonzero_exit: + add = exit_status != 0 + else: + add = await self.io.confirm_ask( + f"Add {k_tokens:.1f}k tokens of command output to the chat?" + ) - msg = prompts.run_output.format( - command=args, - output=combined_output, - ) + if add: + num_lines = len(combined_output.strip().splitlines()) + line_plural = "line" if num_lines == 1 else "lines" + self.io.tool_output(f"Added {num_lines} {line_plural} of output to the chat.") - self.coder.cur_messages += [ - dict(role="user", content=msg), - dict(role="assistant", content="Ok."), - ] + msg = prompts.run_output.format( + command=args, + output=combined_output, + ) + + self.coder.cur_messages += [ + dict(role="user", content=msg), + dict(role="assistant", content="Ok."), + ] - if add_on_nonzero_exit and exit_status != 0: - # Return the formatted output message for test failures - return msg - elif add and exit_status != 0: - self.io.placeholder = "What's wrong? Fix" + if add_on_nonzero_exit and exit_status != 0: + # Return the formatted output message for test failures + return msg + elif add and exit_status != 0: + self.io.placeholder = "What's wrong? Fix" - # Return None if output wasn't added or command succeeded - return None + # Return None if output wasn't added or command succeeded + return None + finally: + self.cmd_running = False def cmd_exit(self, args): "Exit the application" diff --git a/tests/basic/test_deprecated.py b/tests/basic/test_deprecated.py index 62f9b2ada56..5596ed2e7bc 100644 --- a/tests/basic/test_deprecated.py +++ b/tests/basic/test_deprecated.py @@ -23,7 +23,7 @@ def tearDown(self): @patch("aider.io.InputOutput.tool_warning") @patch("aider.io.InputOutput.offer_url") - def test_deprecated_args_show_warnings(self, mock_offer_url, mock_tool_warning): + async def test_deprecated_args_show_warnings(self, mock_offer_url, mock_tool_warning): # Prevent URL launches during tests mock_offer_url.return_value = False # Test all deprecated flags to ensure they show warnings @@ -73,7 +73,7 @@ def test_deprecated_args_show_warnings(self, mock_offer_url, mock_tool_warning): @patch("aider.io.InputOutput.tool_warning") @patch("aider.io.InputOutput.offer_url") - def test_model_alias_in_warning(self, mock_offer_url, mock_tool_warning): + async def test_model_alias_in_warning(self, mock_offer_url, mock_tool_warning): # Prevent URL launches during tests mock_offer_url.return_value = False # Test that the warning uses the model alias if available diff --git a/tests/basic/test_ssl_verification.py b/tests/basic/test_ssl_verification.py index 3a12f85811e..7ddb9d90e0a 100644 --- a/tests/basic/test_ssl_verification.py +++ b/tests/basic/test_ssl_verification.py @@ -1,6 +1,6 @@ import os from unittest import TestCase -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from prompt_toolkit.input import DummyInput from prompt_toolkit.output import DummyOutput @@ -25,7 +25,7 @@ def tearDown(self): @patch("httpx.Client") @patch("httpx.AsyncClient") @patch("aider.models.fuzzy_match_models", return_value=[]) - def test_no_verify_ssl_flag_sets_model_info_manager( + async def test_no_verify_ssl_flag_sets_model_info_manager( self, mock_fuzzy_match, mock_async_client, @@ -52,7 +52,7 @@ def test_no_verify_ssl_flag_sets_model_info_manager( with patch("aider.llm.litellm._lazy_module", mock_module): # Run main with --no-verify-ssl flag main( - ["--no-verify-ssl", "--exit", "--yes"], + ["--no-verify-ssl", "--exit", "--yes", "--map-tokens", "1024"], input=DummyInput(), output=DummyOutput(), ) @@ -67,9 +67,9 @@ def test_no_verify_ssl_flag_sets_model_info_manager( # Verify SSL_VERIFY environment variable was set to empty string self.assertEqual(os.environ.get("SSL_VERIFY"), "") - @patch("aider.io.InputOutput.offer_url") + @patch("aider.io.InputOutput.offer_url", new=AsyncMock) @patch("aider.models.model_info_manager.set_verify_ssl") - def test_default_ssl_verification(self, mock_set_verify_ssl, mock_offer_url): + async def test_default_ssl_verification(self, mock_set_verify_ssl, mock_offer_url): # Prevent actual URL opening mock_offer_url.return_value = False # Run main without --no-verify-ssl flag From 860804a7aac0148c76303f2b9d26cbbc0d4a76d0 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sun, 2 Nov 2025 20:07:15 -0500 Subject: [PATCH 5/6] Simplify _confirm_ask to return booleans instead of the overly complicated future logic --- aider/io.py | 38 ++++++++------------------------------ 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/aider/io.py b/aider/io.py index bf1e2281beb..e832d928100 100644 --- a/aider/io.py +++ b/aider/io.py @@ -462,7 +462,6 @@ def __init__( self.file_watcher = file_watcher self.root = root - self.outstanding_confirmations = [] # Variables used to interface with base_coder self.coder = None @@ -474,7 +473,6 @@ def __init__( # State tracking for confirmation input self.confirmation_input_active = False self.saved_input_text = "" - self.confirmation_future = None # Validate color settings after console is initialized self._validate_color_settings() @@ -686,10 +684,8 @@ def interrupt_input(self): def reject_outstanding_confirmations(self): """Reject all outstanding confirmation dialogs.""" - for future in self.outstanding_confirmations: - if not future.done(): - future.set_result(False) - self.outstanding_confirmations = [] + # This method is now a no-op since we removed the confirmation_future logic + pass async def get_input( self, @@ -701,7 +697,6 @@ async def get_input( abs_read_only_stubs_fnames=None, edit_format=None, ): - self.reject_outstanding_confirmations() self.rule() rel_fnames = list(rel_fnames) @@ -1070,14 +1065,9 @@ async def _confirm_ask( question_id = (question, subject) - confirmation_future = asyncio.get_running_loop().create_future() - self.outstanding_confirmations.append(confirmation_future) - try: if question_id in self.never_prompts: - if not confirmation_future.done(): - confirmation_future.set_result(False) - return await confirmation_future + return False if group and not group.show_group: group = None @@ -1160,9 +1150,7 @@ async def _confirm_ask( res = default break except asyncio.CancelledError: - if not confirmation_future.done(): - confirmation_future.set_result(False) - raise + return False if not res: res = default @@ -1181,9 +1169,7 @@ async def _confirm_ask( self.never_prompts.add(question_id) hist = f"{question.strip()} {res}" self.append_chat_history(hist, linebreak=True, blockquote=True) - if not confirmation_future.done(): - confirmation_future.set_result(False) - return await confirmation_future + return False if explicit_yes_required: is_yes = res == "y" @@ -1201,19 +1187,11 @@ async def _confirm_ask( hist = f"{question.strip()} {res}" self.append_chat_history(hist, linebreak=True, blockquote=True) - - if not confirmation_future.done(): - confirmation_future.set_result(is_yes) - except asyncio.CancelledError: - if not confirmation_future.done(): - confirmation_future.set_result(False) - raise + return False finally: - if confirmation_future in self.outstanding_confirmations: - self.outstanding_confirmations.remove(confirmation_future) - - return await confirmation_future + pass + return is_yes @restore_multiline def prompt_ask(self, question, default="", subject=None): From 00992d4e72e27d69fda8022a3bde262884aa159f Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Sun, 2 Nov 2025 21:38:32 -0500 Subject: [PATCH 6/6] #48: Add linear_output mode because it is theorhetically more accessible --- aider/args.py | 16 +++++++++-- aider/coders/base_coder.py | 57 ++++++++++++++++++++++++++++++++++++++ aider/commands.py | 2 +- aider/io.py | 4 +-- aider/main.py | 3 ++ 5 files changed, 77 insertions(+), 5 deletions(-) diff --git a/aider/args.py b/aider/args.py index 5b19006e040..98b42145e5b 100644 --- a/aider/args.py +++ b/aider/args.py @@ -315,7 +315,12 @@ def get_parser(default_config_files, git_root): " (default: current directory)" ), ) - + group.add_argument( + "--map-memory-cache", + action="store_true", + help="Store repo map in memory (default: False)", + default=False, + ) ########## group = parser.add_argument_group("History Files") default_input_history_file = ( @@ -745,7 +750,14 @@ def get_parser(default_config_files, git_root): help="Print the system prompts and exit (debug)", default=False, ) - + group.add_argument( + "--linear-output", + action="store_true", + help=( + "Run input and output sequentially instead of us simultaneous streams (default: False)" + ), + default=False, + ) ########## group = parser.add_argument_group("Voice settings") group.add_argument( diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index e7dea813374..1678934574d 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -381,6 +381,7 @@ def __init__( map_cache_dir=".", repomap_in_memory=False, preserve_todo_list=False, + linear_output=False, ): # initialize from args.map_cache_dir self.map_cache_dir = map_cache_dir @@ -469,6 +470,7 @@ def __init__( self.dry_run = dry_run self.pretty = self.io.pretty + self.linear_output = linear_output self.main_model = main_model @@ -1058,12 +1060,67 @@ async def run(self, with_message=None, preproc=True): while self.io.confirmation_in_progress: await asyncio.sleep(0.1) # Yield control and wait briefly + if self.linear_output: + return await self._run_linear(with_message, preproc) + if self.io.prompt_session: with patch_stdout(raw=True): return await self._run_patched(with_message, preproc) else: return await self._run_patched(with_message, preproc) + async def _run_linear(self, with_message=None, preproc=True): + try: + if with_message: + self.io.user_input(with_message) + await self.run_one(with_message, preproc) + return self.partial_response_content + + user_message = None + await self.io.cancel_input_task() + await self.io.cancel_processing_task() + + while True: + try: + if self.commands.cmd_running: + await asyncio.sleep(0.1) + continue + + if not self.suppress_announcements_for_next_prompt: + self.show_announcements() + self.suppress_announcements_for_next_prompt = True + + self.io.input_task = asyncio.create_task(self.get_input()) + await asyncio.sleep(0) + await self.io.input_task + user_message = self.io.input_task.result() + + self.io.processing_task = asyncio.create_task( + self._processing_logic(user_message, preproc) + ) + + await self.io.processing_task + + self.io.ring_bell() + user_message = None + except KeyboardInterrupt: + if self.io.input_task: + self.io.set_placeholder("") + await self.io.cancel_input_task() + + if self.io.processing_task: + await self.io.cancel_processing_task() + self.io.stop_spinner() + + self.keyboard_interrupt() + except (asyncio.CancelledError, IndexError): + pass + except EOFError: + return + finally: + await self.io.cancel_input_task() + await self.io.cancel_processing_task() + async def _run_patched(self, with_message=None, preproc=True): try: if with_message: diff --git a/aider/commands.py b/aider/commands.py index d4563377179..9104ff6e68c 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -258,7 +258,7 @@ def is_command(self, inp): return inp[0] in "/!" def is_run_command(self, inp): - return inp[0] in "!" or inp[:5] == "/test" or inp[:4] == "/run" + return inp and (inp[0] in "!" or inp[:5] == "/test" or inp[:4] == "/run") def get_raw_completions(self, cmd): assert cmd.startswith("/") diff --git a/aider/io.py b/aider/io.py index e832d928100..1707eeb90d6 100644 --- a/aider/io.py +++ b/aider/io.py @@ -941,7 +941,7 @@ async def cancel_input_task(self): try: input_task.cancel() await input_task - except asyncio.CancelledError: + except (asyncio.CancelledError, IndexError): pass async def cancel_processing_task(self): @@ -951,7 +951,7 @@ async def cancel_processing_task(self): try: processing_task.cancel() await processing_task - except asyncio.CancelledError: + except (asyncio.CancelledError, IndexError): pass def add_to_input_history(self, inp): diff --git a/aider/main.py b/aider/main.py index 4b58887f2be..dfbc36f6278 100644 --- a/aider/main.py +++ b/aider/main.py @@ -1057,6 +1057,9 @@ def get_io(pretty): context_compaction_max_tokens=args.context_compaction_max_tokens, context_compaction_summary_tokens=args.context_compaction_summary_tokens, map_cache_dir=args.map_cache_dir, + repomap_in_memory=args.map_memory_cache, + preserve_todo_list=args.preserve_todo_list, + linear_output=args.linear_output, ) except UnknownEditFormat as err: io.tool_error(str(err))