From afefb6b2d5019915e65f513c848fa32551720e18 Mon Sep 17 00:00:00 2001 From: Lutz Leonhardt Date: Sat, 8 Mar 2025 23:59:41 +0100 Subject: [PATCH 1/6] feat: Add export command and create timestamp directory --- .gitignore | 3 ++- aider/commands.py | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 8ad33fd3c..3e709e8ad 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ aider/_version.py .venv/ .#* .gitattributes -tmp.benchmarks/ \ No newline at end of file +tmp.benchmarks/ +.qodo diff --git a/aider/commands.py b/aider/commands.py index b6ece0373..9fe2318f9 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -1413,7 +1413,6 @@ class Commands: def cmd_copy_context(self, args=None): """Copy the current chat context as markdown, suitable to paste into a web UI""" - chunks = self.coder.format_chat_chunks() markdown = "" @@ -1455,6 +1454,24 @@ 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 _create_export_directory(self): + """Create and return a timestamped directory for exports""" + from datetime import datetime + import os + import tempfile + + timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S") + export_dir = os.path.join(tempfile.gettempdir(), f"export_{timestamp}") + os.makedirs(export_dir, exist_ok=True) + return export_dir + + def cmd_export_context(self, args): + "Export files in chat context to a timestamped directory" + + export_dir = self._create_export_directory() + self.io.tool_output(f"Created directory: {export_dir}") + return + def expand_subdir(file_path): if file_path.is_file(): From 7aca65dd20a652283ab09688bc8de8ef520eec06 Mon Sep 17 00:00:00 2001 From: Lutz Leonhardt Date: Sun, 9 Mar 2025 00:04:08 +0100 Subject: [PATCH 2/6] feat: Add command to export files in chat context --- aider/commands.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/aider/commands.py b/aider/commands.py index 9fe2318f9..428996463 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -1464,12 +1464,26 @@ Just show me the edits I need to make. export_dir = os.path.join(tempfile.gettempdir(), f"export_{timestamp}") os.makedirs(export_dir, exist_ok=True) return export_dir + + def _get_context_files(self): + """Get all files in the chat context (both editable and read-only)""" + inchat_files = self.coder.get_inchat_relative_files() + read_only_files = [self.coder.get_rel_fname(fname) for fname in self.coder.abs_read_only_fnames] + all_files = sorted(set(inchat_files + read_only_files)) + return all_files def cmd_export_context(self, args): "Export files in chat context to a timestamped directory" + all_files = self._get_context_files() + if not all_files: + self.io.tool_output("No files in context to export.") + return + export_dir = self._create_export_directory() - self.io.tool_output(f"Created directory: {export_dir}") + self.io.tool_output(f"Will export {len(all_files)} files to {export_dir}") + for fname in all_files: + self.io.tool_output(f" {fname}") return From fa25e96c214f5b2f6b6baf8eca7686be504eda24 Mon Sep 17 00:00:00 2001 From: Lutz Leonhardt Date: Sun, 9 Mar 2025 00:22:04 +0100 Subject: [PATCH 3/6] feat: Add file export functionality to export chat context --- aider/commands.py | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/aider/commands.py b/aider/commands.py index 428996463..15e53dbf8 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -1464,7 +1464,7 @@ Just show me the edits I need to make. export_dir = os.path.join(tempfile.gettempdir(), f"export_{timestamp}") os.makedirs(export_dir, exist_ok=True) return export_dir - + def _get_context_files(self): """Get all files in the chat context (both editable and read-only)""" inchat_files = self.coder.get_inchat_relative_files() @@ -1472,19 +1472,53 @@ Just show me the edits I need to make. all_files = sorted(set(inchat_files + read_only_files)) return all_files + def _export_flat(self, files, export_dir): + """Export each file to its own file with flattened path""" + import os + + count = 0 + for rel_fname in files: + # Get content + abs_fname = self.coder.abs_root_path(rel_fname) + content = self.io.read_text(abs_fname) + + if content is None: + self.io.tool_warning(f"Could not read {rel_fname}, skipping") + continue + + # Flatten filename + flat_fname = rel_fname.replace('/', '|').replace('\\', '|') + out_path = os.path.join(export_dir, flat_fname) + + # Write content + try: + with open(out_path, 'w', encoding=self.io.encoding) as f: + f.write(content) + count += 1 + except Exception as e: + self.io.tool_error(f"Error writing {flat_fname}: {str(e)}") + + return count + def cmd_export_context(self, args): "Export files in chat context to a timestamped directory" + # Parse arguments (we'll implement chunking later) + chunk_size = None + + # Get files in context all_files = self._get_context_files() if not all_files: self.io.tool_output("No files in context to export.") return + # Create timestamped directory export_dir = self._create_export_directory() - self.io.tool_output(f"Will export {len(all_files)} files to {export_dir}") - for fname in all_files: - self.io.tool_output(f" {fname}") - return + + # Export files in flat mode + count = self._export_flat(all_files, export_dir) + + self.io.tool_output(f"Exported {count} files to {export_dir}/") def expand_subdir(file_path): From c9f3c1722a863d131e490f39f962089120bc150b Mon Sep 17 00:00:00 2001 From: Lutz Leonhardt Date: Sun, 9 Mar 2025 00:36:54 +0100 Subject: [PATCH 4/6] feat: Implement chunked export of context files --- aider/commands.py | 103 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 6 deletions(-) diff --git a/aider/commands.py b/aider/commands.py index 15e53dbf8..4866247e0 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -817,6 +817,10 @@ class Commands: all_files = files + read_only_files all_files = [self.quote_fname(fn) for fn in all_files] return all_files + + def completions_export_context(self): + """Return possible completions for export-context command""" + return ["100", "1000", "5000"] def cmd_drop(self, args=""): "Remove files from the chat session to free up context space" @@ -1472,6 +1476,22 @@ Just show me the edits I need to make. all_files = sorted(set(inchat_files + read_only_files)) return all_files + def _parse_chunk_size(self, args): + """Parse the chunk size argument, return None for flat mode or a valid chunk size""" + if not args.strip(): + return None + + try: + chunk_size = int(args.strip()) + if chunk_size < 50: + chunk_size = 50 + self.io.tool_warning(f"Chunk size must be at least 50. Using default: 50") + except ValueError: + chunk_size = 50 + self.io.tool_warning(f"Invalid chunk size. Using default: 50") + + return chunk_size + def _export_flat(self, files, export_dir): """Export each file to its own file with flattened path""" import os @@ -1500,11 +1520,79 @@ Just show me the edits I need to make. return count + def _export_chunked(self, files, export_dir, chunk_size): + """Export files as chunks, treating each file as atomic""" + import os + + # Collect file contents with line counts (excluding empty lines) + file_data = [] + for rel_fname in files: + abs_fname = self.coder.abs_root_path(rel_fname) + content = self.io.read_text(abs_fname) + + if content is None: + self.io.tool_warning(f"Could not read {rel_fname}, skipping") + continue + + # Count non-empty lines + non_empty_lines = len([line for line in content.splitlines() if line.strip()]) + file_data.append((rel_fname, content, non_empty_lines)) + + # Create chunks (treating each file as atomic) + chunks = [] + current_chunk = [] + current_chunk_size = 0 + + for rel_fname, content, lines in file_data: + # If adding this file would exceed chunk size and we already have files, + # start a new chunk (unless this is the first file in a new chunk) + if current_chunk and current_chunk_size + lines > chunk_size: + chunks.append(current_chunk) + current_chunk = [] + current_chunk_size = 0 + + # Add file to current chunk + current_chunk.append((rel_fname, content)) + current_chunk_size += lines + + # Add the last chunk if not empty + if current_chunk: + chunks.append(current_chunk) + + # Write chunk files + count = 0 + for i, chunk in enumerate(chunks, 1): + # Create chunk filename - different format if only one file in the chunk + if len(chunk) == 1: + # Just use the single filename for single-file chunks + chunk_fname = f"{chunk[0][0]}_chunk_{i}.txt" + else: + # Get first and last file in chunk for multi-file chunks + first_file = chunk[0][0] + last_file = chunk[-1][0] + chunk_fname = f"{first_file}...{last_file}_chunk_{i}.txt" + # Replace path separators with pipe characters for better readability + chunk_fname = chunk_fname.replace('/', '|').replace('\\', '|') + out_path = os.path.join(export_dir, chunk_fname) + + # Write content with file markers + try: + with open(out_path, 'w', encoding=self.io.encoding) as f: + for rel_fname, content in chunk: + f.write(f"========== START FILE: {rel_fname} ==========\n") + f.write(content) + f.write("\n\n") + count += 1 + except Exception as e: + self.io.tool_error(f"Error writing chunk {i}: {str(e)}") + + return count + def cmd_export_context(self, args): "Export files in chat context to a timestamped directory" - # Parse arguments (we'll implement chunking later) - chunk_size = None + # Parse arguments + chunk_size = self._parse_chunk_size(args) # Get files in context all_files = self._get_context_files() @@ -1515,10 +1603,13 @@ Just show me the edits I need to make. # Create timestamped directory export_dir = self._create_export_directory() - # Export files in flat mode - count = self._export_flat(all_files, export_dir) - - self.io.tool_output(f"Exported {count} files to {export_dir}/") + # Export files based on mode + if chunk_size is None: + count = self._export_flat(all_files, export_dir) + self.io.tool_output(f"Exported {count} files to {export_dir}/") + else: + count = self._export_chunked(all_files, export_dir, chunk_size) + self.io.tool_output(f"Exported {count} chunk files to {export_dir}/") def expand_subdir(file_path): From b9d0a24810f2b528c712af8ea28c613c36bc9274 Mon Sep 17 00:00:00 2001 From: Lutz Leonhardt Date: Sun, 9 Mar 2025 01:16:30 +0100 Subject: [PATCH 5/6] fix: Ensure .txt extension for flattened filenames and chunk filenames during export --- .gitignore | 1 - aider/commands.py | 11 ++++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 3e709e8ad..0a401f9ab 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,3 @@ aider/_version.py .#* .gitattributes tmp.benchmarks/ -.qodo diff --git a/aider/commands.py b/aider/commands.py index 4866247e0..677fff596 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -1506,8 +1506,10 @@ Just show me the edits I need to make. self.io.tool_warning(f"Could not read {rel_fname}, skipping") continue - # Flatten filename + # Flatten filename and ensure .txt extension flat_fname = rel_fname.replace('/', '|').replace('\\', '|') + if not flat_fname.endswith('.txt'): + flat_fname += '.txt' out_path = os.path.join(export_dir, flat_fname) # Write content @@ -1565,14 +1567,17 @@ Just show me the edits I need to make. # Create chunk filename - different format if only one file in the chunk if len(chunk) == 1: # Just use the single filename for single-file chunks - chunk_fname = f"{chunk[0][0]}_chunk_{i}.txt" + chunk_fname = f"{chunk[0][0]}_chunk_{i}" else: # Get first and last file in chunk for multi-file chunks first_file = chunk[0][0] last_file = chunk[-1][0] - chunk_fname = f"{first_file}...{last_file}_chunk_{i}.txt" + chunk_fname = f"{first_file}...{last_file}_chunk_{i}" # Replace path separators with pipe characters for better readability chunk_fname = chunk_fname.replace('/', '|').replace('\\', '|') + # Ensure .txt extension + if not chunk_fname.endswith('.txt'): + chunk_fname += '.txt' out_path = os.path.join(export_dir, chunk_fname) # Write content with file markers From 0352ddf7e1aca0cf57e392d8c3a114ce0bd3fa6a Mon Sep 17 00:00:00 2001 From: Lutz Leonhardt Date: Mon, 10 Mar 2025 09:36:13 +0100 Subject: [PATCH 6/6] feat: Improve file handling and export in _get_context_files and _export_flat/_export_chunked --- aider/commands.py | 127 +++++++++++++++++++++++++++++++--------------- 1 file changed, 86 insertions(+), 41 deletions(-) diff --git a/aider/commands.py b/aider/commands.py index 677fff596..f4ce6ce4e 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -817,7 +817,7 @@ class Commands: all_files = files + read_only_files all_files = [self.quote_fname(fn) for fn in all_files] return all_files - + def completions_export_context(self): """Return possible completions for export-context command""" return ["100", "1000", "5000"] @@ -1469,12 +1469,33 @@ Just show me the edits I need to make. os.makedirs(export_dir, exist_ok=True) return export_dir + def _get_file_content(self, abs_fname, relative_fname): + """Get content for a file with appropriate handling for image files""" + if is_image_file(relative_fname): + # For image files we can't get content directly + return None + + content = self.io.read_text(abs_fname) + return content + def _get_context_files(self): - """Get all files in the chat context (both editable and read-only)""" - inchat_files = self.coder.get_inchat_relative_files() - read_only_files = [self.coder.get_rel_fname(fname) for fname in self.coder.abs_read_only_fnames] - all_files = sorted(set(inchat_files + read_only_files)) - return all_files + """Get file information categorized by type (editable, read-only)""" + file_info = { + 'editable': [], + 'read_only': [] + } + + # Regular editable files + for fname in self.coder.abs_fnames: + relative_fname = self.coder.get_rel_fname(fname) + file_info['editable'].append((fname, relative_fname)) + + # Read-only files + for fname in self.coder.abs_read_only_fnames: + relative_fname = self.coder.get_rel_fname(fname) + file_info['read_only'].append((fname, relative_fname)) + + return file_info def _parse_chunk_size(self, args): """Parse the chunk size argument, return None for flat mode or a valid chunk size""" @@ -1492,53 +1513,74 @@ Just show me the edits I need to make. return chunk_size - def _export_flat(self, files, export_dir): + def _export_flat(self, file_info, export_dir): """Export each file to its own file with flattened path""" import os count = 0 - for rel_fname in files: - # Get content - abs_fname = self.coder.abs_root_path(rel_fname) - content = self.io.read_text(abs_fname) - if content is None: - self.io.tool_warning(f"Could not read {rel_fname}, skipping") - continue + # Process all file types + for file_type, files in file_info.items(): + for abs_fname, rel_fname in files: + # Get content + content = self._get_file_content(abs_fname, rel_fname) - # Flatten filename and ensure .txt extension - flat_fname = rel_fname.replace('/', '|').replace('\\', '|') - if not flat_fname.endswith('.txt'): - flat_fname += '.txt' - out_path = os.path.join(export_dir, flat_fname) + if content is None: + if is_image_file(rel_fname): + self.io.tool_warning(f"Skipping image file: {rel_fname}") + else: + self.io.tool_warning(f"Could not read {rel_fname}, skipping") + continue - # Write content - try: - with open(out_path, 'w', encoding=self.io.encoding) as f: - f.write(content) - count += 1 - except Exception as e: - self.io.tool_error(f"Error writing {flat_fname}: {str(e)}") + # Add a marker for read-only files + marker = "" + if file_type == 'read_only': + marker = " (read-only)" + + # Flatten filename and ensure .txt extension + flat_fname = f"{rel_fname}{marker}".replace('/', '|').replace('\\', '|') + if not flat_fname.endswith('.txt'): + flat_fname += '.txt' + out_path = os.path.join(export_dir, flat_fname) + + # Write content + try: + with open(out_path, 'w', encoding=self.io.encoding) as f: + f.write(content) + count += 1 + except Exception as e: + self.io.tool_error(f"Error writing {flat_fname}: {str(e)}") return count - def _export_chunked(self, files, export_dir, chunk_size): + def _export_chunked(self, file_info, export_dir, chunk_size): """Export files as chunks, treating each file as atomic""" import os # Collect file contents with line counts (excluding empty lines) file_data = [] - for rel_fname in files: - abs_fname = self.coder.abs_root_path(rel_fname) - content = self.io.read_text(abs_fname) - if content is None: - self.io.tool_warning(f"Could not read {rel_fname}, skipping") - continue + # Process all file types + for file_type, files in file_info.items(): + for abs_fname, rel_fname in files: + # Get content + content = self._get_file_content(abs_fname, rel_fname) - # Count non-empty lines - non_empty_lines = len([line for line in content.splitlines() if line.strip()]) - file_data.append((rel_fname, content, non_empty_lines)) + if content is None: + if is_image_file(rel_fname): + self.io.tool_warning(f"Skipping image file: {rel_fname}") + else: + self.io.tool_warning(f"Could not read {rel_fname}, skipping") + continue + + # Add a marker for read-only files + marker = "" + if file_type == 'read_only': + marker = " (read-only)" + + # Count non-empty lines + non_empty_lines = len([line for line in content.splitlines() if line.strip()]) + file_data.append((f"{rel_fname}{marker}", content, non_empty_lines)) # Create chunks (treating each file as atomic) chunks = [] @@ -1599,9 +1641,12 @@ Just show me the edits I need to make. # Parse arguments chunk_size = self._parse_chunk_size(args) - # Get files in context - all_files = self._get_context_files() - if not all_files: + # Get files in context by category + file_info = self._get_context_files() + + # Check if there are any files to export + total_files = len(file_info['editable']) + len(file_info['read_only']) + if total_files == 0: self.io.tool_output("No files in context to export.") return @@ -1610,10 +1655,10 @@ Just show me the edits I need to make. # Export files based on mode if chunk_size is None: - count = self._export_flat(all_files, export_dir) + count = self._export_flat(file_info, export_dir) self.io.tool_output(f"Exported {count} files to {export_dir}/") else: - count = self._export_chunked(all_files, export_dir, chunk_size) + count = self._export_chunked(file_info, export_dir, chunk_size) self.io.tool_output(f"Exported {count} chunk files to {export_dir}/")