This commit is contained in:
Vikash 2025-12-19 16:20:45 +01:00 committed by GitHub
commit b9b77bfb03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 346 additions and 54 deletions

3
.gitignore vendored
View file

@ -16,4 +16,5 @@ aider/_version.py
.#*
.gitattributes
tmp.benchmarks/
.docker_bash_history
.docker_bash_history
.idea/

View file

@ -1,10 +1,12 @@
import glob
import json
import os
import re
import subprocess
import sys
import tempfile
from collections import OrderedDict
from datetime import datetime
from os.path import expanduser
from pathlib import Path
@ -13,7 +15,7 @@ from PIL import Image, ImageGrab
from prompt_toolkit.completion import Completion, PathCompleter
from prompt_toolkit.document import Document
from aider import models, prompts, voice
from aider import __version__, models, prompts, voice
from aider.editor import pipe_editor
from aider.format_settings import format_settings
from aider.help import Help, install_help_extra
@ -145,7 +147,7 @@ class Commands:
sorted(
(
coder.edit_format,
coder.__doc__.strip().split("\n")[0] if coder.__doc__ else "No description",
(coder.__doc__.strip().split("\n")[0] if coder.__doc__ else "No description"),
)
for coder in coders.__all__
if getattr(coder, "edit_format", None)
@ -1003,7 +1005,10 @@ class Commands:
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 = run_cmd(
args, verbose=self.verbose, error_print=self.io.tool_error, cwd=self.coder.root
args,
verbose=self.verbose,
error_print=self.io.tool_error,
cwd=self.coder.root,
)
if combined_output is None:
@ -1240,7 +1245,8 @@ class Commands:
return
try:
self.voice = voice.Voice(
audio_format=self.voice_format or "wav", device_name=self.voice_input_device
audio_format=self.voice_format or "wav",
device_name=self.voice_input_device,
)
except voice.SoundDeviceError:
self.io.tool_error(
@ -1283,7 +1289,8 @@ class Commands:
# 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
(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)
@ -1661,6 +1668,182 @@ Just show me the edits I need to make.
except Exception as e:
self.io.tool_error(f"An unexpected error occurred while copying to clipboard: {str(e)}")
def cmd_session(self, args):
"""Manage chat sessions. Subcommands: list, save <name>, load <name>, delete <name>, view <name>."""
parts = args.strip().split()
if not parts:
self.io.tool_error(
"Please provide a subcommand for /session (list, save, load, delete, view)."
)
return
subcommand = parts[0]
subcommand_args = " ".join(parts[1:])
if subcommand == "list":
self._session_list()
elif subcommand == "save":
self._session_save(subcommand_args)
elif subcommand == "load":
self._session_load(subcommand_args)
elif subcommand == "delete":
self._session_delete(subcommand_args)
elif subcommand == "view":
self._session_view(subcommand_args)
else:
self.io.tool_error(
f"Invalid subcommand '{subcommand}'. Use list, save, load, view, or delete."
)
def _get_sessions_dir(self):
"""Helper to get the sessions directory, creating it if needed."""
if not self.coder.root:
self.io.tool_error("Cannot manage sessions without a git repository root.")
return None
sessions_dir = Path(self.coder.root) / ".aider" / "sessions"
sessions_dir.mkdir(parents=True, exist_ok=True)
return sessions_dir
def _session_list(self):
"""List all saved sessions"""
sessions_dir = self._get_sessions_dir()
if not sessions_dir:
return
sessions = sorted(sessions_dir.glob("*.json"))
if not sessions:
self.io.tool_output("No saved sessions found.")
return
self.io.tool_output("Saved sessions:")
for session_file in sessions:
self.io.tool_output(f"- {session_file.stem}")
def _session_save(self, session_name):
"Save the current chat session to a named file"
if not session_name:
self.io.tool_error("Please provide a name for the session.")
return
sessions_dir = self._get_sessions_dir()
if not sessions_dir:
return
session_file = self._get_session_file(session_name, sessions_dir)
chat_history = self.coder.done_messages + self.coder.cur_messages
session_data = {
"version": __version__,
"timestamp": datetime.now().isoformat(),
"chat_history": chat_history,
"editable_files": sorted(list(self.coder.abs_fnames)),
"read_only_files": sorted(list(self.coder.abs_read_only_fnames)),
}
try:
with open(session_file, "w") as f:
json.dump(session_data, f, indent=4)
self.io.tool_output(f"Session '{session_name}' saved.")
except (IOError, OSError) as e:
self.io.tool_error(f"Error saving session: {e}")
def _session_load(self, session_name):
"""Load a chat session from a named file"""
if not session_name:
self.io.tool_error("Please provide the name of the session to load.")
return
session_data = self._get_session_data(session_name)
if not session_data:
return
self._clear_chat_history()
self._drop_all_files()
self.coder.done_messages = session_data.get("chat_history", [])
self.coder.abs_fnames = set(session_data.get("editable_files", []))
self.coder.abs_read_only_fnames = set(session_data.get("read_only_files", []))
self.io.tool_output(f"Session '{session_name}' loaded.")
self.io.tool_output("Use /ls to see the files that are now in the chat.")
def _get_session_data(self, session_name):
sessions_dir = self._get_sessions_dir()
if not sessions_dir:
return None
session_file = self._get_session_file(session_name, sessions_dir)
if not session_file.exists():
self.io.tool_error(f"Session '{session_name}' not found.")
return None
try:
with open(session_file, "r") as f:
session_data = json.load(f)
return session_data
except Exception as e:
self.io.tool_error(f"Error loading session file: {e}")
return None
@staticmethod
def _get_session_file(session_name, sessions_dir):
session_file = sessions_dir / f"{session_name}.json"
return session_file
def _session_delete(self, session_name):
"""Delete a saved session file"""
if not session_name:
self.io.tool_error("Please provide the name of the session to delete.")
return
sessions_dir = self._get_sessions_dir()
if not sessions_dir:
return
session_file = self._get_session_file(session_name, sessions_dir)
if not session_file.exists():
self.io.tool_error(f"Session '{session_name}' not found.")
return
try:
session_file.unlink()
self.io.tool_output(f"Session '{session_name}' deleted.")
except OSError as e:
self.io.tool_error(f"Error deleting session file: {e}")
def _session_view(self, session_name):
"""View the details of a saved session"""
if not session_name:
self.io.tool_error("Please provide the name of the session to view.")
return
session_data = self._get_session_data(session_name)
if not session_data:
return
self.io.tool_output(f"Session Details: {session_name}")
self.io.tool_output("-" * (len(session_name) + 17))
self.io.tool_output(f" Version: {session_data.get('version', 'N/A')}")
self.io.tool_output(f" Timestamp: {session_data.get('timestamp', 'N/A')}")
self.io.tool_output("\n Editable Files:")
for fname in session_data.get("editable_files", []):
self.io.tool_output(f" - {self.coder.get_rel_fname(fname)}")
self.io.tool_output("\n Read-only Files:")
for fname in session_data.get("read_only_files", []):
self.io.tool_output(f" - {self.coder.get_rel_fname(fname)}")
self.io.tool_output("\n Chat History Preview:")
for msg in session_data.get("chat_history", []):
role = msg.get("role", "unknown")
content = msg.get("content", "")
preview = (content[:70] + "...") if len(content) > 70 else content
self.io.tool_output(f" - {role.capitalize()}: {preview.splitlines()[0]}")
def expand_subdir(file_path):
if file_path.is_file():

View file

@ -90,3 +90,17 @@ so they are easy to track and undo.
You can always use the `/undo` command to undo AI changes that you don't
like.
## Chat Sessions
Aider allows you to save and load your chat sessions. This can be useful if you want to take a break and resume your work later, or if you want to switch between different tasks.
The `/session` command provides subcommands to manage your chat sessions:
* `/session list`: Lists all saved sessions.
* `/session save <name>`: Saves the current chat session with the given name.
* `/session load <name>`: Loads a previously saved chat session. This will restore the chat history and the files that were in the chat.
* `/session delete <name>`: Deletes a saved session.
* `/session view <name>`: Shows the details of a saved session, including the chat history and the files in the chat.
All session data is stored locally in the `.aider/sessions` directory inside your project's git repository.

View file

@ -5,10 +5,11 @@ description: Control aider with in-chat commands like /add, /model, etc.
---
# In-chat commands
{: .no_toc }
- TOC
{:toc}
{:toc}
## Slash commands
@ -19,50 +20,51 @@ from aider.commands import get_help_md
cog.out(get_help_md())
]]]-->
|Command|Description|
|:------|:----------|
| **/add** | Add files to the chat so aider can edit them or review them in detail |
| **/architect** | Enter architect/editor mode using 2 different models. If no prompt provided, switches to architect/editor mode. |
| **/ask** | Ask questions about the code base without editing any files. If no prompt provided, switches to ask mode. |
| **/chat-mode** | Switch to a new chat mode |
| **/clear** | Clear the chat history |
| **/code** | Ask for changes to your code. If no prompt provided, switches to code mode. |
| **/commit** | Commit edits to the repo made outside the chat (commit message optional) |
| **/context** | Enter context mode to see surrounding code context. If no prompt provided, switches to context mode. |
| **/copy** | Copy the last assistant message to the clipboard |
| **/copy-context** | Copy the current chat context as markdown, suitable to paste into a web UI |
| **/diff** | Display the diff of changes since the last message |
| **/drop** | Remove files from the chat session to free up context space |
| **/edit** | Alias for /editor: Open an editor to write a prompt |
| **/editor** | Open an editor to write a prompt |
| **/editor-model** | Switch the Editor Model to a new LLM |
| **/exit** | Exit the application |
| **/git** | Run a git command (output excluded from chat) |
| **/help** | Ask questions about aider |
| **/lint** | Lint and fix in-chat files or all dirty files if none in chat |
| **/load** | Load and execute commands from a file |
| **/ls** | List all known files and indicate which are included in the chat session |
| **/map** | Print out the current repository map |
| **/map-refresh** | Force a refresh of the repository map |
| **/model** | Switch the Main Model to a new LLM |
| **/models** | Search the list of available models |
| **/multiline-mode** | Toggle multiline mode (swaps behavior of Enter and Meta+Enter) |
| **/paste** | Paste image/text from the clipboard into the chat. Optionally provide a name for the image. |
| **/quit** | Exit the application |
| **/read-only** | Add files to the chat that are for reference only, or turn added files to read-only |
| **/reasoning-effort** | Set the reasoning effort level (values: number or low/medium/high depending on model) |
| **/report** | Report a problem by opening a GitHub Issue |
| **/reset** | Drop all files and clear the chat history |
| **/run** | Run a shell command and optionally add the output to the chat (alias: !) |
| **/save** | Save commands to a file that can reconstruct the current chat session's files |
| **/settings** | Print out the current settings |
| **/test** | Run a shell command and add the output to the chat on non-zero exit code |
| **/think-tokens** | Set the thinking token budget, eg: 8096, 8k, 10.5k, 0.5M, or 0 to disable. |
| **/tokens** | Report on the number of tokens used by the current chat context |
| **/undo** | Undo the last git commit if it was done by aider |
| **/voice** | Record and transcribe voice input |
| **/weak-model** | Switch the Weak Model to a new LLM |
| **/web** | Scrape a webpage, convert to markdown and send in a message |
| Command | Description |
|:----------------------|:----------------------------------------------------------------------------------------------------------------|
| **/add** | Add files to the chat so aider can edit them or review them in detail |
| **/architect** | Enter architect/editor mode using 2 different models. If no prompt provided, switches to architect/editor mode. |
| **/ask** | Ask questions about the code base without editing any files. If no prompt provided, switches to ask mode. |
| **/chat-mode** | Switch to a new chat mode |
| **/clear** | Clear the chat history |
| **/code** | Ask for changes to your code. If no prompt provided, switches to code mode. |
| **/commit** | Commit edits to the repo made outside the chat (commit message optional) |
| **/context** | Enter context mode to see surrounding code context. If no prompt provided, switches to context mode. |
| **/copy** | Copy the last assistant message to the clipboard |
| **/copy-context** | Copy the current chat context as markdown, suitable to paste into a web UI |
| **/diff** | Display the diff of changes since the last message |
| **/drop** | Remove files from the chat session to free up context space |
| **/edit** | Alias for /editor: Open an editor to write a prompt |
| **/editor** | Open an editor to write a prompt |
| **/editor-model** | Switch the Editor Model to a new LLM |
| **/exit** | Exit the application |
| **/git** | Run a git command (output excluded from chat) |
| **/help** | Ask questions about aider |
| **/lint** | Lint and fix in-chat files or all dirty files if none in chat |
| **/load** | Load and execute commands from a file |
| **/ls** | List all known files and indicate which are included in the chat session |
| **/map** | Print out the current repository map |
| **/map-refresh** | Force a refresh of the repository map |
| **/model** | Switch the Main Model to a new LLM |
| **/models** | Search the list of available models |
| **/multiline-mode** | Toggle multiline mode (swaps behavior of Enter and Meta+Enter) |
| **/paste** | Paste image/text from the clipboard into the chat. Optionally provide a name for the image. |
| **/quit** | Exit the application |
| **/read-only** | Add files to the chat that are for reference only, or turn added files to read-only |
| **/reasoning-effort** | Set the reasoning effort level (values: number or low/medium/high depending on model) |
| **/report** | Report a problem by opening a GitHub Issue |
| **/reset** | Drop all files and clear the chat history |
| **/run** | Run a shell command and optionally add the output to the chat (alias: !) |
| **/save** | Save commands to a file that can reconstruct the current chat session's files |
| **/session** | Manage chat sessions. Subcommands: list, save \<name\>, load \<name\>, delete \<name\>, view \<name\>. |
| **/settings** | Print out the current settings |
| **/test** | Run a shell command and add the output to the chat on non-zero exit code |
| **/think-tokens** | Set the thinking token budget, eg: 8096, 8k, 10.5k, 0.5M, or 0 to disable. |
| **/tokens** | Report on the number of tokens used by the current chat context |
| **/undo** | Undo the last git commit if it was done by aider |
| **/voice** | Record and transcribe voice input |
| **/weak-model** | Switch the Weak Model to a new LLM |
| **/web** | Scrape a webpage, convert to markdown and send in a message |
<!--[[[end]]]-->
@ -77,11 +79,13 @@ or CONTROL-R to search your message history.
## Interrupting with CONTROL-C
It's always safe to use Control-C to interrupt aider if it isn't providing a useful response. The partial response remains in the conversation, so you can refer to it when you reply to the LLM with more information or direction.
It's always safe to use Control-C to interrupt aider if it isn't providing a useful response. The partial response
remains in the conversation, so you can refer to it when you reply to the LLM with more information or direction.
## Keybindings
The interactive prompt is built with [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) which provides emacs and vi keybindings.
The interactive prompt is built with [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) which
provides emacs and vi keybindings.
### Emacs
@ -102,7 +106,6 @@ The interactive prompt is built with [prompt-toolkit](https://github.com/prompt-
- `Ctrl-X Ctrl-E` : Open the current input in an external editor
- `Ctrl-Y` : Paste (yank) text that was previously cut.
### Vi
To use vi/vim keybindings, run aider with the `--vim` switch.

91
tests/test_session.py Normal file
View file

@ -0,0 +1,91 @@
import json
import os
from pathlib import Path
from unittest.mock import MagicMock
import git
from aider.commands import Commands
from aider.io import InputOutput
from aider.repo import GitRepo
def test_session_management(monkeypatch, tmp_path):
# Setup: Create a git repo and a file
os.chdir(tmp_path)
io = InputOutput(pretty=False, yes=True)
repo_path = str(tmp_path)
repo = git.Repo.init(repo_path)
# Use a real GitRepo
git_repo = GitRepo(io, fnames=[], git_dname=repo_path)
# Create some files
file1_path = tmp_path / "file1.py"
file1_path.write_text("print('hello')\n")
file2_path = tmp_path / "file2.py"
file2_path.write_text("print('world')\n")
repo.git.add(".")
repo.git.commit("-m", "initial commit")
# Mock the coder
coder = MagicMock()
coder.io = io
coder.repo = git_repo
coder.root = git_repo.root
def get_rel_fname(path):
return os.path.relpath(path, git_repo.root)
coder.get_rel_fname.side_effect = get_rel_fname
# Set up initial state on the coder
coder.abs_fnames = {str(file1_path.resolve())}
coder.abs_read_only_fnames = {str(file2_path.resolve())}
coder.done_messages = [{"role": "user", "content": "hello"}]
coder.cur_messages = []
commands = Commands(io, coder, original_read_only_fnames=coder.abs_read_only_fnames)
# --- Test /session save ---
session_name = "my_test_session"
commands.cmd_session(f"save {session_name}")
sessions_dir = Path(git_repo.root) / ".aider" / "sessions"
session_file = sessions_dir / f"{session_name}.json"
assert session_file.exists()
with open(session_file, "r") as f:
data = json.load(f)
assert data["chat_history"] == coder.done_messages
# --- Test /session list ---
captured_output = []
io.tool_output = lambda msg: captured_output.append(msg)
commands.cmd_session("list")
assert f"- {session_name}" in captured_output
# --- Test /session view ---
captured_output.clear()
commands.cmd_session(f"view {session_name}")
output = "".join(captured_output)
assert session_name in output
assert "file1.py" in output
assert "file2.py" in output
assert "hello" in output # from chat history
# --- Test /session load ---
commands.cmd_reset("") # Reset state
commands.cmd_session(f"load {session_name}")
assert coder.done_messages == data["chat_history"]
assert coder.abs_fnames == set(data["editable_files"])
# --- Test /session delete ---
commands.cmd_session(f"delete {session_name}")
assert not session_file.exists()
# --- Test invalid command ---
captured_errors = []
io.tool_error = lambda msg: captured_errors.append(msg)
commands.cmd_session("invalid_subcommand")
assert "Invalid subcommand" in captured_errors[0]