From 73130d1480b907ebcd72b27b5c446dba8c77758d Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Mon, 3 Nov 2025 08:15:56 -0500 Subject: [PATCH 1/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 0321d2a09ea..84d65a203b4 100644 --- a/aider/__init__.py +++ b/aider/__init__.py @@ -1,6 +1,6 @@ from packaging import version -__version__ = "0.88.5.dev" +__version__ = "0.88.6.dev" safe_version = __version__ try: From 9d87f137720970f6734f7be017f75ce52196fae4 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Mon, 3 Nov 2025 08:57:43 -0500 Subject: [PATCH 2/6] Add /save-session, /load-session, and /list-sessions commands for some state persistance between program runs --- aider/commands.py | 194 +++++++++++++++++++++++++++++ tests/basic/test_sessions.py | 234 +++++++++++++++++++++++++++++++++++ 2 files changed, 428 insertions(+) create mode 100644 tests/basic/test_sessions.py diff --git a/aider/commands.py b/aider/commands.py index 9104ff6e68c..d7345814f0e 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -1,11 +1,14 @@ import asyncio import glob +import json import os import re import subprocess import sys import tempfile +import time from collections import OrderedDict +from datetime import datetime from os.path import expanduser from pathlib import Path @@ -2035,6 +2038,197 @@ def cmd_reasoning_effort(self, args): announcements = "\n".join(self.coder.get_announcements()) self.io.tool_output(announcements) + def _get_session_directory(self): + """Get the session storage directory, creating it if needed""" + session_dir = Path(self.coder.root) / ".aider" / "sessions" + session_dir.mkdir(parents=True, exist_ok=True) + return session_dir + + def _get_session_file_path(self, session_name): + """Get the full path for a session file""" + session_dir = self._get_session_directory() + # Sanitize the session name to be filesystem-safe + safe_name = re.sub(r"[^a-zA-Z0-9_.-]", "_", session_name) + return session_dir / f"{safe_name}.json" + + def _find_session_file(self, session_name): + """Find a session file by name, checking both name-based and full path""" + # First check if it's a full path + if Path(session_name).exists(): + return Path(session_name) + + # Then check in the sessions directory + session_file = self._get_session_file_path(session_name) + if session_file.exists(): + return session_file + + return None + + def cmd_save_session(self, args): + """Save the current chat session to a named file in .aider/sessions/""" + if not args.strip(): + self.io.tool_error("Please provide a session name.") + return + + session_name = args.strip() + session_file = self._get_session_file_path(session_name) + + # Collect session data + session_data = { + "version": "1.0", + "timestamp": time.time(), + "session_name": session_name, + "model": self.coder.main_model.name, + "edit_format": self.coder.edit_format, + "chat_history": { + "done_messages": self.coder.done_messages, + "cur_messages": self.coder.cur_messages, + }, + "files": { + "editable": [self.coder.get_rel_fname(f) for f in self.coder.abs_fnames], + "read_only": [self.coder.get_rel_fname(f) for f in self.coder.abs_read_only_fnames], + "read_only_stubs": [ + self.coder.get_rel_fname(f) for f in self.coder.abs_read_only_stubs_fnames + ], + }, + "settings": { + "root": self.coder.root, + "auto_commits": self.coder.auto_commits, + "auto_lint": self.coder.auto_lint, + "auto_test": self.coder.auto_test, + }, + } + + try: + with open(session_file, "w", encoding="utf-8") as f: + json.dump(session_data, f, indent=2, ensure_ascii=False) + self.io.tool_output(f"Session saved to: {session_file}") + except Exception as e: + self.io.tool_error(f"Error saving session: {e}") + + def cmd_list_sessions(self, args): + """List all saved sessions in .aider/sessions/""" + session_dir = self._get_session_directory() + session_files = list(session_dir.glob("*.json")) + + if not session_files: + self.io.tool_output("No saved sessions found.") + return + + self.io.tool_output("Saved sessions:") + for session_file in sorted(session_files): + try: + with open(session_file, "r", encoding="utf-8") as f: + session_data = json.load(f) + session_name = session_data.get("session_name", session_file.stem) + timestamp = session_data.get("timestamp", 0) + model = session_data.get("model", "unknown") + edit_format = session_data.get("edit_format", "unknown") + + # Format timestamp + if timestamp: + date_str = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M") + else: + date_str = "unknown date" + + self.io.tool_output( + f" {session_name} (model: {model}, format: {edit_format}, {date_str})" + ) + except Exception as e: + self.io.tool_output(f" {session_file.stem} [error reading: {e}]") + + def cmd_load_session(self, args): + """Load a saved session by name or file path""" + if not args.strip(): + self.io.tool_error("Please provide a session name or file path.") + return + + session_name = args.strip() + session_file = self._find_session_file(session_name) + + if not session_file: + self.io.tool_error(f"Session not found: {session_name}") + self.io.tool_output("Use /list-sessions to see available sessions.") + return + + try: + with open(session_file, "r", encoding="utf-8") as f: + session_data = json.load(f) + except Exception as e: + self.io.tool_error(f"Error loading session: {e}") + return + + # Verify session format + if not isinstance(session_data, dict) or "version" not in session_data: + self.io.tool_error("Invalid session format.") + return + + # Load session data + try: + # Clear current state + self.coder.abs_fnames = set() + self.coder.abs_read_only_fnames = set() + self.coder.abs_read_only_stubs_fnames = set() + self.coder.done_messages = [] + self.coder.cur_messages = [] + + # Load chat history + chat_history = session_data.get("chat_history", {}) + self.coder.done_messages = chat_history.get("done_messages", []) + self.coder.cur_messages = chat_history.get("cur_messages", []) + + # Load files + files = session_data.get("files", {}) + for rel_fname in files.get("editable", []): + abs_fname = self.coder.abs_root_path(rel_fname) + if os.path.exists(abs_fname): + self.coder.abs_fnames.add(abs_fname) + else: + self.io.tool_warning(f"File not found, skipping: {rel_fname}") + + for rel_fname in files.get("read_only", []): + abs_fname = self.coder.abs_root_path(rel_fname) + if os.path.exists(abs_fname): + self.coder.abs_read_only_fnames.add(abs_fname) + else: + self.io.tool_warning(f"File not found, skipping: {rel_fname}") + + for rel_fname in files.get("read_only_stubs", []): + abs_fname = self.coder.abs_root_path(rel_fname) + if os.path.exists(abs_fname): + self.coder.abs_read_only_stubs_fnames.add(abs_fname) + else: + self.io.tool_warning(f"File not found, skipping: {rel_fname}") + + # Load settings + settings = session_data.get("settings", {}) + if "auto_commits" in settings: + self.coder.auto_commits = settings["auto_commits"] + if "auto_lint" in settings: + self.coder.auto_lint = settings["auto_lint"] + if "auto_test" in settings: + self.coder.auto_test = settings["auto_test"] + + self.io.tool_output( + f"Session loaded: {session_data.get('session_name', session_file.stem)}" + ) + self.io.tool_output( + f"Model: {session_data.get('model', 'unknown')}, Edit format:" + f" {session_data.get('edit_format', 'unknown')}" + ) + + # Show summary + num_messages = len(self.coder.done_messages) + len(self.coder.cur_messages) + num_files = ( + len(self.coder.abs_fnames) + + len(self.coder.abs_read_only_fnames) + + len(self.coder.abs_read_only_stubs_fnames) + ) + self.io.tool_output(f"Loaded {num_messages} messages and {num_files} files") + + except Exception as e: + self.io.tool_error(f"Error applying session data: {e}") + def cmd_copy_context(self, args=None): """Copy the current chat context as markdown, suitable to paste into a web UI""" diff --git a/tests/basic/test_sessions.py b/tests/basic/test_sessions.py new file mode 100644 index 00000000000..88367e49b9a --- /dev/null +++ b/tests/basic/test_sessions.py @@ -0,0 +1,234 @@ +import json +import os +import shutil +import tempfile +import time +from pathlib import Path +from unittest import TestCase, mock + +from aider.coders import Coder +from aider.commands import Commands +from aider.io import InputOutput +from aider.models import Model +from aider.utils import GitTemporaryDirectory + + +class TestSessionCommands(TestCase): + def setUp(self): + self.original_cwd = os.getcwd() + self.tempdir = tempfile.mkdtemp() + os.chdir(self.tempdir) + + self.GPT35 = Model("gpt-3.5-turbo") + + def tearDown(self): + os.chdir(self.original_cwd) + shutil.rmtree(self.tempdir, ignore_errors=True) + + async def test_cmd_save_session_basic(self): + """Test basic session save functionality""" + with GitTemporaryDirectory() as repo_dir: + io = InputOutput(pretty=False, fancy_input=False, yes=True) + coder = await Coder.create(self.GPT35, None, io) + commands = Commands(io, coder) + + # Create test files + test_files = { + "file1.txt": "Content of file 1", + "file2.py": "print('Content of file 2')", + "subdir/file3.md": "# Content of file 3", + } + + for file_path, content in test_files.items(): + full_path = Path(repo_dir) / file_path + full_path.parent.mkdir(parents=True, exist_ok=True) + full_path.write_text(content) + + # Add files to chat + commands.cmd_add("file1.txt file2.py") + commands.cmd_read_only("subdir/file3.md") + + # Add chat history + coder.done_messages = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"}, + ] + coder.cur_messages = [ + {"role": "user", "content": "Can you help me?"}, + ] + + # Save session + session_name = "test_session" + commands.cmd_save_session(session_name) + + # Verify session file was created + session_file = Path(".aider") / "sessions" / f"{session_name}.json" + self.assertTrue(session_file.exists()) + + # Verify session content + with open(session_file, "r", encoding="utf-8") as f: + session_data = json.load(f) + + self.assertEqual(session_data["version"], "1.0") + self.assertEqual(session_data["session_name"], session_name) + self.assertEqual(session_data["model"], self.GPT35.name) + self.assertEqual(session_data["edit_format"], coder.edit_format) + + # Verify chat history + chat_history = session_data["chat_history"] + self.assertEqual(chat_history["done_messages"], coder.done_messages) + self.assertEqual(chat_history["cur_messages"], coder.cur_messages) + + # Verify files + files = session_data["files"] + self.assertEqual(set(files["editable"]), {"file1.txt", "file2.py"}) + self.assertEqual(set(files["read_only"]), {"subdir/file3.md"}) + self.assertEqual(files["read_only_stubs"], []) + + # Verify settings + settings = session_data["settings"] + self.assertEqual(settings["root"], coder.root) + self.assertEqual(settings["auto_commits"], coder.auto_commits) + self.assertEqual(settings["auto_lint"], coder.auto_lint) + self.assertEqual(settings["auto_test"], coder.auto_test) + + async def test_cmd_load_session_basic(self): + """Test basic session load functionality""" + with GitTemporaryDirectory() as repo_dir: + io = InputOutput(pretty=False, fancy_input=False, yes=True) + coder = await Coder.create(self.GPT35, None, io) + commands = Commands(io, coder) + + # Create test files + test_files = { + "file1.txt": "Content of file 1", + "file2.py": "print('Content of file 2')", + "subdir/file3.md": "# Content of file 3", + } + + for file_path, content in test_files.items(): + full_path = Path(repo_dir) / file_path + full_path.parent.mkdir(parents=True, exist_ok=True) + full_path.write_text(content) + + # Create a session file manually + session_data = { + "version": "1.0", + "timestamp": time.time(), + "session_name": "test_session", + "model": self.GPT35.name, + "edit_format": "diff", + "chat_history": { + "done_messages": [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"}, + ], + "cur_messages": [ + {"role": "user", "content": "Can you help me?"}, + ], + }, + "files": { + "editable": ["file1.txt", "file2.py"], + "read_only": ["subdir/file3.md"], + "read_only_stubs": [], + }, + "settings": { + "root": str(repo_dir), + "auto_commits": True, + "auto_lint": False, + "auto_test": False, + }, + } + + # Save session file + session_file = Path(".aider") / "sessions" / "test_session.json" + session_file.parent.mkdir(parents=True, exist_ok=True) + with open(session_file, "w", encoding="utf-8") as f: + json.dump(session_data, f, indent=2, ensure_ascii=False) + + # Load the session + commands.cmd_load_session("test_session") + + # Verify chat history was loaded + self.assertEqual(coder.done_messages, session_data["chat_history"]["done_messages"]) + self.assertEqual(coder.cur_messages, session_data["chat_history"]["cur_messages"]) + + # Verify files were loaded + editable_files = {coder.get_rel_fname(f) for f in coder.abs_fnames} + read_only_files = {coder.get_rel_fname(f) for f in coder.abs_read_only_fnames} + + self.assertEqual(editable_files, {"file1.txt", "file2.py"}) + self.assertEqual(read_only_files, {"subdir/file3.md"}) + self.assertEqual(len(coder.abs_read_only_stubs_fnames), 0) + + # Verify settings were loaded + self.assertEqual(coder.auto_commits, True) + self.assertEqual(coder.auto_lint, False) + self.assertEqual(coder.auto_test, False) + + async def test_cmd_list_sessions_basic(self): + """Test basic session list functionality""" + with GitTemporaryDirectory(): + io = InputOutput(pretty=False, fancy_input=False, yes=True) + coder = await Coder.create(self.GPT35, None, io) + commands = Commands(io, coder) + + # Create multiple session files + sessions_data = [ + { + "version": "1.0", + "timestamp": time.time() - 3600, # 1 hour ago + "session_name": "session1", + "model": "gpt-3.5-turbo", + "edit_format": "diff", + "chat_history": {"done_messages": [], "cur_messages": []}, + "files": {"editable": [], "read_only": [], "read_only_stubs": []}, + "settings": { + "root": ".", + "auto_commits": True, + "auto_lint": False, + "auto_test": False, + }, + }, + { + "version": "1.0", + "timestamp": time.time(), # current time + "session_name": "session2", + "model": "gpt-4", + "edit_format": "whole", + "chat_history": {"done_messages": [], "cur_messages": []}, + "files": {"editable": [], "read_only": [], "read_only_stubs": []}, + "settings": { + "root": ".", + "auto_commits": True, + "auto_lint": False, + "auto_test": False, + }, + }, + ] + + # Save session files + session_dir = Path(".aider") / "sessions" + session_dir.mkdir(parents=True, exist_ok=True) + + for session_data in sessions_data: + session_file = session_dir / f"{session_data['session_name']}.json" + with open(session_file, "w", encoding="utf-8") as f: + json.dump(session_data, f, indent=2, ensure_ascii=False) + + # Capture output of list_sessions + with mock.patch.object(io, "tool_output") as mock_tool_output: + commands.cmd_list_sessions("") + + # Verify that tool_output was called with session information + calls = mock_tool_output.call_args_list + + # Check that we got at least the header and session entries + self.assertGreater(len(calls), 2) + + # Check that both sessions are listed + output_text = "\n".join([call[0][0] if call[0] else "" for call in calls]) + self.assertIn("session1", output_text) + self.assertIn("session2", output_text) + self.assertIn("gpt-3.5-turbo", output_text) + self.assertIn("gpt-4", output_text) From b4884dcdd15228f39a1a055f65cc10ec3a68c222 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Mon, 3 Nov 2025 09:18:17 -0500 Subject: [PATCH 3/6] Add documentation for session commands --- README.md | 2 + aider/website/docs/sessions.md | 182 +++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 aider/website/docs/sessions.md diff --git a/README.md b/README.md index f0e98f46b4a..879fe204283 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ This project aims to be compatible with upstream Aider, but with priority commit ### Other Notes * [MCP Configuration](https://github.com/dwash96/aider-ce/blob/main/aider/website/docs/config/mcp.md) +* [Session Management](https://github.com/dwash96/aider-ce/blob/main/aider/website/docs/sessions.md) + ### Installation Instructions This project can be installed using several methods: diff --git a/aider/website/docs/sessions.md b/aider/website/docs/sessions.md new file mode 100644 index 00000000000..d38f449d583 --- /dev/null +++ b/aider/website/docs/sessions.md @@ -0,0 +1,182 @@ +# Session Management + +Aider provides session management commands that allow you to save, load, and manage your chat sessions. This is particularly useful for: + +- Continuing work on complex projects across multiple sessions +- Recreating specific development environments +- Archiving important conversations and file configurations + +## Session Commands + +### `/save-session ` +Save the current chat session to a named file in `.aider/sessions/`. + +**Usage:** +``` +/save-session my-project-session +``` + +**What gets saved:** +- Chat history (both done and current messages) +- All files in the chat (editable, read-only, and read-only stubs) +- Current model and edit format settings +- Auto-commit, auto-lint, and auto-test settings +- Session metadata (timestamp, version) + +### `/load-session ` +Load a previously saved session by name or file path. + +**Usage:** +``` +/load-session my-project-session +``` + +**What gets loaded:** +- Restores chat history and file configurations +- Recreates the exact session state +- Preserves all settings and model configurations + +### `/list-sessions` +List all available saved sessions in `.aider/sessions/`. + +**Usage:** +``` +/list-sessions +``` + +**Shows:** +- Session names +- Model used +- Edit format +- Creation timestamp + +## How Sessions Work + +### Session Storage +Sessions are stored as JSON files in the `.aider/sessions/` directory within your project. Each session file contains: + +```json +{ + "version": "1.0", + "timestamp": 1700000000, + "session_name": "my-session", + "model": "gpt-4", + "edit_format": "diff", + "chat_history": { + "done_messages": [...], + "cur_messages": [...] + }, + "files": { + "editable": ["file1.py", "file2.js"], + "read_only": ["docs/README.md"], + "read_only_stubs": [] + }, + "settings": { + "root": "/path/to/project", + "auto_commits": true, + "auto_lint": false, + "auto_test": false + } +} +``` + +### Session File Location +- **Relative paths**: Files within your project are stored with relative paths +- **Absolute paths**: External files are stored with absolute paths + +## Use Cases + +### Project Continuation +``` +# Start working on a project +/add src/main.py src/utils.py +# ... have a conversation ... +/save-session my-project + +# Later, continue where you left off +/load-session my-project +``` + +### Multiple Contexts +``` +# Work on frontend +/add src/components/*.jsx src/styles/*.css +/save-session frontend-work + +# Switch to backend +/reset +/add server/*.py database/*.sql +/save-session backend-work + +# Easily switch between contexts +/load-session frontend-work +``` + +## Best Practices + +### Naming Conventions +- Use descriptive names: `feature-auth-session`, `bugfix-issue-123` +- Include dates if needed: `2024-01-project-setup` + +### File Management +- Session files include all file paths, so they work best when project structure is stable +- External files (outside the project root) are stored with absolute paths +- Missing files are skipped with warnings during loading + +### Version Control +- Consider adding `.aider/sessions/` to your `.gitignore` if sessions contain sensitive information + +## Troubleshooting + +### Session Not Found +If `/load-session` reports "Session not found": +- Check that the session file exists in `.aider/sessions/` +- Verify the session name matches exactly +- Use `/list-sessions` to see available sessions + +### Missing Files +If files are reported as missing during loading: +- The files may have been moved or deleted +- Session files store relative paths, so directory structure changes can affect this +- External files must exist at their original locations + +### Corrupted Sessions +If a session fails to load: +- Check the session file is valid JSON +- Verify the session version is compatible +- Try creating a new session and compare file structures + +## Related Commands +- `/reset` - Clear chat history and drop files (useful before loading a session) + +## Examples + +### Complete Workflow +``` +# Start a new project session +/add package.json src/main.js src/components/ +# ... work on the project ... +/save-session react-project + +# Later, continue working +/load-session react-project +# All files and chat history are restored +``` + +### Session with External Files +``` +# Include documentation from outside the project +/read-only ~/docs/api-reference.md +/save-session project-with-docs +``` + +### Multiple Model Sessions +``` +# Save session with specific model +/model gpt-4 +/save-session gpt4-session + +# Try different model +/model claude-3 +/save-session claude-session +``` From 12db7aa949231303fe8b45fa45eb108c6014bf07 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Mon, 3 Nov 2025 19:19:37 -0500 Subject: [PATCH 4/6] #84: Detect and properly re-encode ansi escape sequence text --- aider/coders/base_coder.py | 2 +- aider/io.py | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 1678934574d..412a454afea 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -2461,7 +2461,7 @@ async def get_server_tools(server): return (server.name, server_tools) except Exception as e: if server.name != "unnamed-server": - self.io.tool_warning(f"Error initializing MCP server {server.name}:\n{e}") + self.io.tool_warning(f"Error initializing MCP server {server.name}: {e}") return None async def get_all_server_tools(): diff --git a/aider/io.py b/aider/io.py index 1707eeb90d6..f6a2c3c4a64 100644 --- a/aider/io.py +++ b/aider/io.py @@ -991,7 +991,7 @@ def display_user_input(self, inp): else: style = dict() - self.console.print(Text(inp), **style) + self.stream_print(Text(inp), **style) def user_input(self, inp, log_only=True): if not log_only: @@ -1245,23 +1245,20 @@ def _tool_message(self, message="", strip=True, color=None): message = Text(message) style = dict() - if self.pretty: - color = ensure_hash_prefix(color) if color else None if color: - style["color"] = color + style["color"] = ensure_hash_prefix(color) + + style = RichStyle(**style) try: - self.stream_print(message, style=RichStyle(**style)) + self.stream_print(message, style=style) except UnicodeEncodeError: # Fallback to ASCII-safe output if isinstance(message, Text): message = message.plain message = str(message).encode("ascii", errors="replace").decode("ascii") - self.stream_print(message, style=RichStyle(**style)) - - if self.prompt_session and self.prompt_session.app: - self.prompt_session.app.invalidate() + self.stream_print(message, style=style) def tool_error(self, message="", strip=True): self.num_error_outputs += 1 @@ -1309,7 +1306,7 @@ def assistant_output(self, message, pretty=None): else: show_resp = Text(message or "(empty response)") - self.console.print(show_resp) + self.stream_print(show_resp) def render_markdown(self, text): output = StringIO() @@ -1352,12 +1349,18 @@ def stream_output(self, text, final=False): if not final: if len(lines) > 1: - self.console.print(output) + self.console.print( + Text.from_ansi(output) if self.has_ansi_codes(output) else output + ) else: # Ensure any remaining buffered content is printed using the full response - self.console.print(output) + self.console.print(Text.from_ansi(output) if self.has_ansi_codes(output) else output) self.reset_streaming_response() + def has_ansi_codes(self, s: str) -> bool: + """Check if a string contains the ANSI escape character.""" + return "\x1b" in s + def reset_streaming_response(self): self._stream_buffer = "" self._stream_line_count = 0 From 9f952a8f20a4b895c5e570b2467b97603462cc53 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Mon, 3 Nov 2025 20:15:57 -0500 Subject: [PATCH 5/6] #83: Exempt invalid exception classes (probably dependent on litellm version and has knock on effects) --- aider/coders/base_coder.py | 1 + aider/exceptions.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 412a454afea..b2a783f0cf5 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1928,6 +1928,7 @@ async def send_message(self, inp): self.usage_report = None exhausted = False interrupted = False + try: while True: try: diff --git a/aider/exceptions.py b/aider/exceptions.py index 2aaa7d93206..0348df5b4b0 100644 --- a/aider/exceptions.py +++ b/aider/exceptions.py @@ -71,6 +71,9 @@ def _load(self, strict=False): ex = getattr(litellm, var, "default") if ex != "default": + if not issubclass(ex, BaseException): + continue + self.exceptions[ex] = self.exception_info[var] def exceptions_tuple(self): From 5c3e7228ef1d9df2db0cb1d1c1ddd49b8b7b59a1 Mon Sep 17 00:00:00 2001 From: Dustin Washington Date: Mon, 3 Nov 2025 21:07:33 -0500 Subject: [PATCH 6/6] #85: Fix propagation of commit hashes in the case of running a command with /code from another top level Coder class variant --- aider/commands.py | 13 +++---------- aider/io.py | 3 +++ aider/repo.py | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/aider/commands.py b/aider/commands.py index d7345814f0e..8a00b5f64c7 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -1493,19 +1493,12 @@ async def _generic_chat_command(self, args, edit_format, placeholder=None): edit_format=edit_format, summarize_from_coder=False, num_cache_warming_pings=0, + aider_commit_hashes=self.coder.aider_commit_hashes, ) user_msg = args - await coder.run(user_msg) - - # Use the provided placeholder if any - raise SwitchCoder( - edit_format=self.coder.edit_format, - summarize_from_coder=False, - from_coder=coder, - show_announcements=False, - placeholder=placeholder, - ) + await coder.run(user_msg, False) + self.coder.aider_commit_hashes = coder.aider_commit_hashes def get_help_md(self): "Show help about all commands in markdown" diff --git a/aider/io.py b/aider/io.py index f6a2c3c4a64..90e353ee802 100644 --- a/aider/io.py +++ b/aider/io.py @@ -1260,6 +1260,9 @@ def _tool_message(self, message="", strip=True, color=None): message = str(message).encode("ascii", errors="replace").decode("ascii") self.stream_print(message, style=style) + def tool_success(self, message="", strip=True): + self._tool_message(message, strip, self.user_input_color) + def tool_error(self, message="", strip=True): self.num_error_outputs += 1 self._tool_message(message, strip, self.tool_error_color) diff --git a/aider/repo.py b/aider/repo.py index 5b7fbb57d8a..e26fa7ea5b9 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -310,7 +310,7 @@ async def commit(self, fnames=None, context=None, message=None, aider_edits=Fals # Perform the commit self.repo.git.commit(cmd) commit_hash = self.get_head_commit_sha(short=True) - self.io.tool_output(f"Commit {commit_hash} {commit_message}", bold=True) + self.io.tool_success(f"Commit {commit_hash} {commit_message}") return commit_hash, commit_message except ANY_GIT_ERROR as err: