mirror of
https://github.com/Aider-AI/aider.git
synced 2025-12-23 08:48:18 +00:00
Merge c5caf3b13b into 1354e0bfa4
This commit is contained in:
commit
b9b77bfb03
5 changed files with 346 additions and 54 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -16,4 +16,5 @@ aider/_version.py
|
|||
.#*
|
||||
.gitattributes
|
||||
tmp.benchmarks/
|
||||
.docker_bash_history
|
||||
.docker_bash_history
|
||||
.idea/
|
||||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
91
tests/test_session.py
Normal 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]
|
||||
Loading…
Add table
Add a link
Reference in a new issue