Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion aider/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from packaging import version

__version__ = "0.88.5.dev"
__version__ = "0.88.6.dev"
safe_version = __version__

try:
Expand Down
3 changes: 2 additions & 1 deletion aider/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1928,6 +1928,7 @@ async def send_message(self, inp):
self.usage_report = None
exhausted = False
interrupted = False

try:
while True:
try:
Expand Down Expand Up @@ -2461,7 +2462,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():
Expand Down
207 changes: 197 additions & 10 deletions aider/commands.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -1490,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"
Expand Down Expand Up @@ -2035,6 +2031,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"""

Expand Down
3 changes: 3 additions & 0 deletions aider/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
28 changes: 17 additions & 11 deletions aider/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -1245,23 +1245,23 @@ 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))
self.stream_print(message, style=style)

if self.prompt_session and self.prompt_session.app:
self.prompt_session.app.invalidate()
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
Expand Down Expand Up @@ -1309,7 +1309,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()
Expand Down Expand Up @@ -1352,12 +1352,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
Expand Down
2 changes: 1 addition & 1 deletion aider/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading