Skip to content
Open
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ aider --model sonnet --api-key anthropic=<key>
aider --model o3-mini --api-key openai=<key>
```

### Configuration

Create a `.aider.conf.yml` file to configure model aliases and other settings:

```yaml
# Use a list to define multiple aliases
alias:
- "fast:gpt-4o-mini"
- "smart:o3-mini"
- "hacker:claude-3-sonnet-20240229"

# Default model
model: "gpt-4o-mini"
```

> **Note:** YAML does not support repeated keys. Always use a list format for multiple aliases.

See the [installation instructions](https://aider.chat/docs/install.html) and [usage documentation](https://aider.chat/docs/usage.html) for more details.

## More Information
Expand Down
6 changes: 6 additions & 0 deletions aider/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,12 @@ def get_parser(default_config_files, git_root):
" see https://pygments.org/styles for available themes)"
),
)
group.add_argument(
"--code-theme-no-background",
action="store_true",
help="Disable background colors in code theme (default: False)",
default=False,
)
group.add_argument(
"--show-diffs",
action="store_true",
Expand Down
3 changes: 3 additions & 0 deletions aider/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ def __init__(
completion_menu_current_color=None,
completion_menu_current_bg_color=None,
code_theme="default",
code_theme_no_background=False,
encoding="utf-8",
line_endings="platform",
dry_run=False,
Expand Down Expand Up @@ -297,6 +298,7 @@ def __init__(
)

self.code_theme = code_theme
self.code_theme_no_background = code_theme_no_background

self.input = input
self.output = output
Expand Down Expand Up @@ -1015,6 +1017,7 @@ def get_assistant_mdstream(self):
mdargs = dict(
style=self.assistant_output_color,
code_theme=self.code_theme,
code_theme_no_background=self.code_theme_no_background,
inline_code_lexer="text",
)
mdStream = MarkdownStream(mdargs=mdargs)
Expand Down
33 changes: 25 additions & 8 deletions aider/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,25 @@ def setup_git(git_root, io):

if user_name and user_email:
return repo.working_tree_dir

with repo.config_writer() as git_config:
if not user_name:
git_config.set_value("user", "name", "Your Name")
io.tool_warning('Update git name with: git config user.name "Your Name"')
if not user_email:
git_config.set_value("user", "email", "[email protected]")
io.tool_warning('Update git email with: git config user.email "[email protected]"')

try:
with repo.config_writer() as git_config:
if not user_name:
git_config.set_value("user", "name", "Your Name")
io.tool_warning('Update git name with: git config user.name "Your Name"')
if not user_email:
git_config.set_value("user", "email", "[email protected]")
io.tool_warning('Update git email with: git config user.email "[email protected]"')
except (OSError, PermissionError) as e:
io.tool_warning(
f"Warning: Could not write to git config: {e}\n"
f"This may be due to network drive permissions or file locking issues.\n"
f"You may need to manually set git config values using:\n"
f"git config user.name \"Your Name\"\n"
f"git config user.email \"[email protected]\""
)
# Continue without modifying config, in read-only mode
pass

return repo.working_tree_dir

Expand Down Expand Up @@ -542,6 +553,11 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F
args.tool_warning_color = "#FFA500"
args.assistant_output_color = "blue"
args.code_theme = "default"
# Clear background colors for light mode
args.completion_menu_bg_color = None
args.completion_menu_current_bg_color = None
# Override Pygments theme to disable backgrounds
args.code_theme_no_background = True

if return_coder and args.yes_always is None:
args.yes_always = True
Expand All @@ -566,6 +582,7 @@ def get_io(pretty):
completion_menu_current_bg_color=args.completion_menu_current_bg_color,
assistant_output_color=args.assistant_output_color,
code_theme=args.code_theme,
code_theme_no_background=args.code_theme_no_background,
dry_run=args.dry_run,
encoding=args.encoding,
line_endings=args.line_endings,
Expand Down
28 changes: 28 additions & 0 deletions aider/md_renderer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from rich.markdown import Markdown, Padding
from rich.syntax import Syntax

from .theme_utils import get_code_theme

class CustomMarkdown(Markdown):
"""Custom Markdown renderer that handles code block backgrounds"""

def __init__(self, text, code_theme="default", code_theme_no_background=False, **kwargs):
self.code_theme_name = code_theme
self.code_theme_no_background = code_theme_no_background
super().__init__(text, **kwargs)

def render_code_block(self, block, width):
"""Render a code block with optional background removal."""
code = block.text.rstrip()
lexer = block.lexer_name if hasattr(block, "lexer_name") else "default"

theme = get_code_theme(self.code_theme_name, self.code_theme_no_background)
syntax = Syntax(
code,
lexer,
theme=theme,
word_wrap=False,
padding=0,
background_color=None if self.code_theme_no_background else "default",
)
return Padding(syntax, pad=(0, 0))
6 changes: 4 additions & 2 deletions aider/mdstream.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
from rich import box
from rich.console import Console
from rich.live import Live
from rich.markdown import CodeBlock, Heading, Markdown
from rich.markdown import CodeBlock, Heading
from rich.panel import Panel
from rich.syntax import Syntax
from rich.text import Text

from .md_renderer import CustomMarkdown

from aider.dump import dump # noqa: F401

_text_prefix = """
Expand Down Expand Up @@ -131,7 +133,7 @@ def _render_markdown_to_lines(self, text):
# Render the markdown to a string buffer
string_io = io.StringIO()
console = Console(file=string_io, force_terminal=True)
markdown = NoInsetMarkdown(text, **self.mdargs)
markdown = CustomMarkdown(text, **self.mdargs)
console.print(markdown)
output = string_io.getvalue()

Expand Down
34 changes: 34 additions & 0 deletions aider/theme_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from pygments.style import Style as PygmentsStyle
from pygments.util import ClassNotFound
from pygments.styles import get_style_by_name


class NoBackgroundStyle(PygmentsStyle):
"""A style wrapper that removes background colors from another style."""

def __init__(self, base_style):
# Get the base style's colors and settings
self.styles = base_style.styles.copy()
# Remove background colors from all token styles
for token, style_string in self.styles.items():
if style_string:
# Split style into parts
parts = style_string.split()
# Filter out any bg:color settings
parts = [p for p in parts if not p.startswith('bg:')]
self.styles[token] = ' '.join(parts)


def get_code_theme(theme_name, no_background=False):
"""Get a Pygments style, optionally without backgrounds."""
try:
base_style = get_style_by_name(theme_name)
if no_background:
return NoBackgroundStyle(base_style)
return base_style
except ClassNotFound:
# Fallback to default style
base_style = get_style_by_name('default')
if no_background:
return NoBackgroundStyle(base_style)
return base_style
10 changes: 8 additions & 2 deletions aider/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,14 @@ def is_image_file(file_name):

def safe_abs_path(res):
"Gives an abs path, which safely returns a full (not 8.3) windows path"
res = Path(res).resolve()
return str(res)
try:
res = Path(res).resolve(strict=False)
return str(res)
except RuntimeError as e:
if "Symlink loop" in str(e):
# If we encounter a symlink loop, return the absolute path without resolving symlinks
return str(Path(res).absolute())
raise


def format_content(role, content):
Expand Down
46 changes: 46 additions & 0 deletions tests/basic/test_git_config_network.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import os
from pathlib import Path
from unittest import TestCase
from unittest.mock import patch, MagicMock

import git

from aider.io import InputOutput
from aider.main import setup_git
from aider.utils import GitTemporaryDirectory


class TestGitConfigNetworkDrive(TestCase):
def setUp(self):
self.tempdir = GitTemporaryDirectory()
self.old_cwd = os.getcwd()
os.chdir(self.tempdir.name)

def tearDown(self):
os.chdir(self.old_cwd)
self.tempdir.cleanup()

def test_setup_git_with_permission_error(self):
"""Test that setup_git handles permission errors gracefully"""
io = InputOutput(pretty=False, yes=True)

# Create a mock repo that raises PermissionError on config_writer
mock_repo = MagicMock(spec=git.Repo)
mock_config_writer = MagicMock()
mock_config_writer.__enter__ = MagicMock(side_effect=PermissionError("Permission denied"))
mock_repo.config_writer.return_value = mock_config_writer

# Create a test working directory to return
test_dir = str(Path(self.tempdir.name).resolve())
mock_repo.working_tree_dir = test_dir

# Mock git.Repo to return our mock
with patch('git.Repo', return_value=mock_repo):
result = setup_git(test_dir, io)

# Verify setup_git completes and returns working directory despite error
self.assertEqual(result, test_dir)

# Verify warning was shown
warnings = [call[0][0] for call in io.tool_warning.call_args_list]
self.assertTrue(any("Could not write to git config" in warning for warning in warnings))
60 changes: 60 additions & 0 deletions tests/test_utils_symlinks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import os
import unittest
from pathlib import Path
import tempfile

from aider.utils import safe_abs_path


class TestUtilsSymlinks(unittest.TestCase):
def setUp(self):
# Create a temporary directory for our tests
self.test_dir = tempfile.mkdtemp()
self.orig_cwd = os.getcwd()
os.chdir(self.test_dir)

def tearDown(self):
# Clean up
os.chdir(self.orig_cwd)
try:
import shutil
shutil.rmtree(self.test_dir)
except:
pass

def test_safe_abs_path_normal(self):
"""Test safe_abs_path with a normal path"""
test_file = Path(self.test_dir) / "test.txt"
test_file.touch()
result = safe_abs_path(test_file)
self.assertEqual(str(test_file.resolve()), result)

def test_safe_abs_path_symlink_loop(self):
"""Test safe_abs_path with a circular symlink"""
# Create a circular symlink
link1 = Path(self.test_dir) / "link1"
link2 = Path(self.test_dir) / "link2"

# Create the first link pointing to the second
os.symlink("link2", str(link1))
# Create the second link pointing back to the first
os.symlink("link1", str(link2))

# Test that safe_abs_path handles the symlink loop gracefully
result = safe_abs_path(link1)
self.assertTrue(result.endswith("link1"))
self.assertTrue(os.path.isabs(result))

def test_safe_abs_path_nonexistent(self):
"""Test safe_abs_path with a non-existent path"""
nonexistent = Path(self.test_dir) / "nonexistent"
result = safe_abs_path(nonexistent)
self.assertTrue(os.path.isabs(result))
self.assertTrue(result.endswith("nonexistent"))

def test_safe_abs_path_relative(self):
"""Test safe_abs_path with a relative path"""
rel_path = "relative/path"
result = safe_abs_path(rel_path)
self.assertTrue(os.path.isabs(result))
self.assertTrue(result.endswith(rel_path.replace("/", os.path.sep)))