diff --git a/Claudette.py b/Claudette.py index 4ef663c..4173be7 100644 --- a/Claudette.py +++ b/Claudette.py @@ -19,6 +19,10 @@ from .context.manage_files import ClaudetteContextManageFilesCommand from .context.refresh_files import ClaudetteContextRefreshFilesCommand +from .repomix.run_repomix import ClaudetteRunRepomixCommand +from .repomix.clear_repomix import ClaudetteClearRepomixCommand +from .repomix.show_repomix import ClaudetteShowRepomixCommand + from .settings.select_model_panel import ClaudetteSelectModelPanelCommand from .settings.select_api_key_panel import ClaudetteSelectApiKeyPanelCommand from .settings.select_system_message_panel import ClaudetteSelectSystemMessagePanelCommand diff --git a/Claudette.sublime-settings b/Claudette.sublime-settings index f608868..fea52e5 100644 --- a/Claudette.sublime-settings +++ b/Claudette.sublime-settings @@ -75,5 +75,8 @@ // If set_scratch is set to true, the chat view will be closed without // prompting to save. "set_scratch": true + }, + "repomix": { + "executable": "repomix" } } diff --git a/Default.sublime-commands b/Default.sublime-commands index 1fe54cd..6a262e1 100644 --- a/Default.sublime-commands +++ b/Default.sublime-commands @@ -62,5 +62,17 @@ "base_file": "${packages}/Claudette/Claudette.sublime-settings", "default": "{\n\t$0\n}\n" } + }, + { + "caption": "Claudette: Repomix Run", + "command": "claudette_run_repomix" + }, + { + "caption": "Claudette: Repomix Show Content", + "command": "claudette_show_repomix" + }, + { + "caption": "Claudette: Repomix Clear Content", + "command": "claudette_clear_repomix" } ] diff --git a/Main.sublime-menu b/Main.sublime-menu index c20ca4c..f28a40b 100644 --- a/Main.sublime-menu +++ b/Main.sublime-menu @@ -26,6 +26,23 @@ "caption": "Switch API Key", "command": "claudette_select_api_key_panel" }, + { + "caption": "Repomix", + "children": [ + { + "caption": "Run Repomix", + "command": "claudette_run_repomix" + }, + { + "caption": "Show Content", + "command": "claudette_show_repomix" + }, + { + "caption": "Clear Content", + "command": "claudette_clear_repomix" + } + ] + }, { "caption": "Chat History", "children": [ diff --git a/Side Bar.sublime-menu b/Side Bar.sublime-menu index d1c1cd5..3f43b0f 100644 --- a/Side Bar.sublime-menu +++ b/Side Bar.sublime-menu @@ -14,7 +14,16 @@ { "caption": "Clear Included Files", "command": "claudette_context_clear_files" - } + }, + { + "caption": "Repomix Run", + "command": "claudette_run_repomix", + "args": {"paths": []}, + }, + { + "caption": "Repomix Clear", + "command": "claudette_clear_repomix", + } ] } ] diff --git a/api/api.py b/api/api.py index ca73e3c..fbac1d6 100644 --- a/api/api.py +++ b/api/api.py @@ -1,3 +1,4 @@ +# api/api.py import sublime import json import urllib.request @@ -182,6 +183,18 @@ def handle_error(error_msg): system_messages.append(system_message) + repomix_content = chat_view.settings().get('claudette_repomix') + if repomix_content: + system_message = { + "type": "text", + "text": repomix_content.strip() + } + + if self.should_use_cache_control(self.model): + system_message["cache_control"] = {"type": "ephemeral"} + + system_messages.append(system_message) + data = { 'messages': filtered_messages, 'max_tokens': max_tokens, diff --git a/chat/chat_view.py b/chat/chat_view.py index ffc683b..5a4682c 100644 --- a/chat/chat_view.py +++ b/chat/chat_view.py @@ -380,3 +380,28 @@ def destroy(self): del self._instances[window_id] self.view = None + + def status_message(self, message: str, prefix: str = "", scroll_to_end: bool = True) -> None: + """ + Display a status message in the chat view without adding it to the conversation history. + + Args: + message (str): The status message to display + prefix (str, optional): Icon or text prefix for the message. + scroll_to_end (bool, optional): Whether to scroll to the end after displaying. Defaults to True + """ + if not self.view: + return + + if self.get_size() > 0: + message = f"\n\n{prefix} {message}\n" + else: + message = f"{prefix} {message}\n" + + self.view.set_read_only(False) + self.view.run_command('append', { + 'characters': message, + 'force': True, + 'scroll_to_end': scroll_to_end + }) + self.view.set_read_only(True) diff --git a/repomix.config.json b/repomix.config.json index 0ee797d..00991c2 100644 --- a/repomix.config.json +++ b/repomix.config.json @@ -5,8 +5,8 @@ "parsableStyle": false, "fileSummary": true, "directoryStructure": true, - "removeComments": false, - "removeEmptyLines": false, + "removeComments": true, + "removeEmptyLines": true, "topFilesLength": 5, "showLineNumbers": false, "copyToClipboard": false @@ -18,7 +18,7 @@ "customPatterns": [] }, "security": { - "enableSecurityCheck": true + "enableSecurityCheck": false }, "tokenCount": { "encoding": "o200k_base" diff --git a/repomix/clear_repomix.py b/repomix/clear_repomix.py new file mode 100644 index 0000000..17afc98 --- /dev/null +++ b/repomix/clear_repomix.py @@ -0,0 +1,38 @@ +import sublime +import sublime_plugin +from ..utils import claudette_chat_status_message + +class ClaudetteClearRepomixCommand(sublime_plugin.WindowCommand): + """Command to clear Repomix content from the active chat view.""" + + def get_active_chat_view(self): + """Get the current active chat view.""" + for view in self.window.views(): + if (view.settings().get('claudette_is_chat_view', False) and + view.settings().get('claudette_is_current_chat', False)): + return view + return None + + def is_visible(self): + """Always show the command in menus.""" + return True + + def is_enabled(self): + """Enable command only if there's Repomix content to clear.""" + chat_view = self.get_active_chat_view() + if not chat_view: + return False + return chat_view.settings().get('claudette_repomix') is not None + + def run(self): + """Clear Repomix content from the active chat view.""" + chat_view = self.get_active_chat_view() + if not chat_view: + sublime.status_message("No active chat view found") + return + + chat_view.settings().erase('claudette_repomix') + chat_view.settings().erase('claudette_repomix_tokens') + + claudette_chat_status_message(window, "Repomix content cleared", prefix="✅") + sublime.status_message("Repomix content cleared") diff --git a/repomix/run_repomix.py b/repomix/run_repomix.py new file mode 100644 index 0000000..8408309 --- /dev/null +++ b/repomix/run_repomix.py @@ -0,0 +1,214 @@ +import sublime +import sublime_plugin +import os +import subprocess +import re +import json +import tempfile +from ..constants import PLUGIN_NAME, SETTINGS_FILE +from ..utils import claudette_chat_status_message +from ..chat.ask_question import ClaudetteAskQuestionCommand + +class ClaudetteRunRepomixCommand(sublime_plugin.WindowCommand): + def is_visible(self): + # Show if there's either a folder open or files selected in sidebar + if hasattr(self, 'paths'): + return bool(self.paths) + return bool(self.window.folders()) + + def is_enabled(self): + return self.is_visible() + + def get_active_chat_view(self): + """Get the current chat view or create a new one""" + # First try to find an existing active chat view + existing_view = None + for view in self.window.views(): + if (view.settings().get('claudette_is_chat_view', False) and + view.settings().get('claudette_is_current_chat', False)): + existing_view = view + return existing_view, False + + # If no active chat view found, create a new one + ask_cmd = ClaudetteAskQuestionCommand(self.window.active_view()) + ask_cmd.load_settings() + new_view = ask_cmd.create_chat_panel(force_new=True) + return new_view, True + + def extract_token_count(self, output): + """Extract token count from repomix output using regex.""" + # Look for patterns like "Total Tokens: 12,868 tokens" + # First capture the number including any commas + match = re.search(r'(?:token count|tokens):\s*([\d,]+)', output, re.IGNORECASE) + if match: + # Remove commas and convert to int + token_count = match.group(1).replace(',', '') + return int(token_count) + return None + + def format_include_paths(self, paths, base_dir): + """Format file paths into repomix include pattern.""" + if not paths: + return None + + # Convert absolute paths to relative paths from base_dir + relative_paths = [] + for path in paths: + if os.path.isfile(path): + rel_path = os.path.relpath(path, base_dir) + # Replace backslashes with forward slashes for consistency + rel_path = rel_path.replace('\\', '/') + relative_paths.append(rel_path) + + if relative_paths: + # Join all paths with commas + return ','.join(relative_paths) + return None + + def run(self, paths=None): + # Get or create chat view first, and track if it's new + chat_view, is_new_view = self.get_active_chat_view() + if not chat_view: + sublime.error_message("Could not create chat view") + return + + self.paths = paths + settings = sublime.load_settings(SETTINGS_FILE) + repomix_settings = settings.get('repomix', {}) + executable = repomix_settings.get('executable', 'repomix') + + # Use selected paths if provided, otherwise use first folder + target_paths = paths if paths else [self.window.folders()[0]] + if not target_paths: + sublime.error_message("Please select a folder or files in the sidebar") + return + + # Get the working directory (use first folder if files selected) + if paths and all(os.path.isfile(p) for p in paths): + # All selected items are files, use their common parent directory + target_folder = os.path.dirname(paths[0]) + else: + # At least one folder selected, use the first folder + target_folder = next((p for p in target_paths if os.path.isdir(p)), None) + if not target_folder: + sublime.error_message("No valid folder found") + return + + # Look for config file in project root + config_file = None + project_root = self._find_project_root(target_folder) + + if project_root: + potential_config = os.path.join(project_root, 'repomix.config.json') + if os.path.exists(potential_config): + config_file = potential_config + + # Create temporary output file in Sublime's cache directory + cache_dir = os.path.join(sublime.cache_path(), PLUGIN_NAME) + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + + output_file = os.path.join(cache_dir, f'repomix-output-{os.urandom(4).hex()}.xml') + + try: + cmd = [executable] + + if config_file: + cmd.extend(['--config', config_file]) + + # Add output file flag + cmd.extend(['--output', output_file]) + + # Add include pattern for selected files + if paths and any(os.path.isfile(p) for p in paths): + include_pattern = self.format_include_paths(paths, target_folder) + if include_pattern: + cmd.extend(['--include', include_pattern]) + + # Print command being run + print(f"\n{PLUGIN_NAME}: Running command:", ' '.join(cmd)) + print(f"{PLUGIN_NAME}: Working directory:", target_folder) + + # Run repomix + process = subprocess.Popen( + cmd, + cwd=target_folder, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True if os.name == 'nt' else False, + text=True + ) + + stdout, stderr = process.communicate() + + if process.returncode != 0: + print(f"Repomix Error: {stderr}") + sublime.status_message('Repomix error.') + return + + # Read the output file + if not os.path.exists(output_file): + raise FileNotFoundError(f"Output file not found: {output_file}") + + with open(output_file, 'r', encoding='utf-8') as f: + file_content = f.read() + + # Store the file content in the chat view's settings + chat_view.settings().set('claudette_repomix', file_content) + + # Extract token count from stdout and create status message + token_count = self.extract_token_count(stdout) + status_msg = "Repomix completed successfully" + if token_count is not None: + status_msg += f" ({token_count:,} tokens)" + chat_view.settings().set('claudette_repomix_tokens', token_count) + + # Show success message in chat view + claudette_chat_status_message(self.window, status_msg, prefix="✅") + + # Print success message to console + print(f"\n{PLUGIN_NAME}: {status_msg}") + print("\nOutput stored in chat view settings") + + sublime.status_message(f"{PLUGIN_NAME}: {status_msg}") + + # Only open input panel if this is a new view + if is_new_view: + def show_input_panel(): + self.window.run_command('claudette_ask_question') + + # Slight delay to ensure status messages are visible + sublime.set_timeout(show_input_panel, 100) + + except FileNotFoundError as e: + if 'repomix' in str(e): + error_msg = ( + f"Could not find repomix executable. Please ensure repomix is installed and " + f"configured correctly in Package Settings > Claudette > Settings" + ) + else: + error_msg = str(e) + print(f"\n{PLUGIN_NAME} Error:", error_msg) + sublime.error_message(error_msg) + except Exception as e: + print(f"\n{PLUGIN_NAME} Error:", str(e)) + sublime.error_message(f"Error running repomix: {str(e)}") + finally: + # Clean up output file + if os.path.exists(output_file): + try: + os.remove(output_file) + except Exception as e: + print(f"{PLUGIN_NAME} Warning: Could not remove output file: {str(e)}") + + def _find_project_root(self, start_path): + """Walk up directories looking for repomix config files""" + current = start_path + while True: + if os.path.exists(os.path.join(current, 'repomix.config.json')): + return current + + parent = os.path.dirname(current) + if parent == current: + return None + current = parent diff --git a/repomix/show_repomix.py b/repomix/show_repomix.py new file mode 100644 index 0000000..cd9f235 --- /dev/null +++ b/repomix/show_repomix.py @@ -0,0 +1,38 @@ +import sublime +import sublime_plugin + +class ClaudetteShowRepomixCommand(sublime_plugin.WindowCommand): + def get_active_chat_view(self): + for view in self.window.views(): + if (view.settings().get('claudette_is_chat_view', False) and + view.settings().get('claudette_is_current_chat', False)): + return view + return None + + def is_visible(self): + return self.get_active_chat_view() is not None + + def run(self): + chat_view = self.get_active_chat_view() + if not chat_view: + sublime.message_dialog("No active chat view found") + return + + repomix_content = chat_view.settings().get('claudette_repomix') + if not repomix_content: + sublime.message_dialog("No Repomix content available for the active chat view") + return + + # Create a new scratch view for the repomix content + repomix_view = self.window.new_file() + repomix_view.set_name("Repomix Output") + repomix_view.set_scratch(True) + + # Insert the content + repomix_view.run_command('append', { + 'characters': repomix_content, + 'force': True + }) + + # Set syntax to markdown since repomix output uses markdown formatting + repomix_view.assign_syntax('Packages/Markdown/Markdown.sublime-syntax')