From 11e0dfec1e104c4f0a8ec943f46549c715617dcc Mon Sep 17 00:00:00 2001 From: TySP-Dev <68524461+TySP-Dev@users.noreply.github.com> Date: Thu, 13 Nov 2025 22:04:41 -1000 Subject: [PATCH] Fixed .gitignore --- .gitignore | 2 +- src/app_components/__init__.py | 54 + src/app_components/ai_action_planner.py | 617 +++ src/app_components/ai_manager.py | 3430 +++++++++++++++++ src/app_components/assets/flow-diagram.png | Bin 0 -> 130934 bytes .../assets/github_pulse_img.png | Bin 0 -> 93327 bytes .../assets/pulse_logo_gray_no_bkg.png | Bin 0 -> 25736 bytes .../assets/pulse_logo_white_no_bkg.png | Bin 0 -> 25961 bytes .../assets/pulse_logo_white_no_bkg_github.png | Bin 0 -> 73510 bytes .../assets/pulse_logo_white_w_black_bkg.png | Bin 0 -> 23355 bytes src/app_components/cache_manager.py | 185 + src/app_components/config_manager.py | 246 ++ src/app_components/github_api.py | 985 +++++ src/app_components/main_gui.py | 3123 +++++++++++++++ src/app_components/processing_log_dialog.py | 125 + src/app_components/settings_dialog.py | 1150 ++++++ src/app_components/settings_manager.py | 307 ++ src/app_components/utils.py | 662 ++++ src/app_components/workflow.py | 653 ++++ src/assets/icon.png | Bin 0 -> 25961 bytes src/assets/splash_android.png | Bin 0 -> 25961 bytes src/main.py | 218 ++ src/requirements/requirements-ai.txt | 11 + src/requirements/requirements-base.txt | 14 + src/requirements/requirements-dev.txt | 12 + src/requirements/requirements.txt | 23 + 26 files changed, 11816 insertions(+), 1 deletion(-) create mode 100644 src/app_components/__init__.py create mode 100644 src/app_components/ai_action_planner.py create mode 100644 src/app_components/ai_manager.py create mode 100644 src/app_components/assets/flow-diagram.png create mode 100644 src/app_components/assets/github_pulse_img.png create mode 100644 src/app_components/assets/pulse_logo_gray_no_bkg.png create mode 100644 src/app_components/assets/pulse_logo_white_no_bkg.png create mode 100644 src/app_components/assets/pulse_logo_white_no_bkg_github.png create mode 100644 src/app_components/assets/pulse_logo_white_w_black_bkg.png create mode 100644 src/app_components/cache_manager.py create mode 100644 src/app_components/config_manager.py create mode 100644 src/app_components/github_api.py create mode 100644 src/app_components/main_gui.py create mode 100644 src/app_components/processing_log_dialog.py create mode 100644 src/app_components/settings_dialog.py create mode 100644 src/app_components/settings_manager.py create mode 100644 src/app_components/utils.py create mode 100644 src/app_components/workflow.py create mode 100644 src/assets/icon.png create mode 100644 src/assets/splash_android.png create mode 100644 src/main.py create mode 100644 src/requirements/requirements-ai.txt create mode 100644 src/requirements/requirements-base.txt create mode 100644 src/requirements/requirements-dev.txt create mode 100644 src/requirements/requirements.txt diff --git a/.gitignore b/.gitignore index 65b5e4c..a4cadc1 100644 --- a/.gitignore +++ b/.gitignore @@ -189,7 +189,7 @@ windows/ web/ .flet build/ -pyproject.toml +*.toml # Configuration (generated during build, not in git) # Note: pyproject.toml is now tracked for proper builds diff --git a/src/app_components/__init__.py b/src/app_components/__init__.py new file mode 100644 index 0000000..5311d86 --- /dev/null +++ b/src/app_components/__init__.py @@ -0,0 +1,54 @@ +""" +GitHub Pulse - Application Components +Modular components for the application +""" + +import sys +import os + +# Version info +__version__ = "0.0.1" +__author__ = "TySP-Dev" +__app_name__ = "GitHub Pulse" + +# Determine if running in production build +IS_PRODUCTION = getattr(sys, 'frozen', False) + +# Get the application directory +if IS_PRODUCTION: + # In production build, get the executable directory + APP_DIR = os.path.dirname(sys.executable) +else: + # In development, get the source directory + APP_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Export main classes for easier imports +from .config_manager import ConfigManager +from .ai_manager import AIManager +from .github_api import GitHubAPI +from .settings_dialog import SettingsDialog +from .main_gui import MainGUI +from .utils import Logger, PRNumberManager, ContentBuilders +from .workflow import WorkflowManager, WorkflowItem, GitHubRepoFetcher +from .ai_action_planner import AIActionPlanner, ActionPlan + +__all__ = [ + 'ConfigManager', + 'AIManager', + 'GitHubAPI', + 'SettingsDialog', + 'MainGUI', + 'Logger', + 'PRNumberManager', + 'ContentBuilders', + 'WorkflowManager', + 'WorkflowItem', + 'GitHubRepoFetcher', + 'AIActionPlanner', + 'ActionPlan', + '__version__', + '__author__', + '__app_name__', + 'IS_PRODUCTION', + 'APP_DIR' +] diff --git a/src/app_components/ai_action_planner.py b/src/app_components/ai_action_planner.py new file mode 100644 index 0000000..b42fdc1 --- /dev/null +++ b/src/app_components/ai_action_planner.py @@ -0,0 +1,617 @@ +""" +AI Action Planner +Generates and executes action plans for GitHub issues and PRs using AI +""" + +import json +import re +import requests +from typing import List, Dict, Any, Optional, Callable +from pathlib import Path + + +class ActionPlan: + """Represents an AI-generated action plan""" + + def __init__(self, title: str, steps: List[Dict[str, Any]], context: Dict[str, Any]): + self.title = title + self.steps = steps # List of {description, file_path, changes, completed} + self.context = context # PR/Issue context + self.completed_steps = [] + self.failed_steps = [] + + def to_dict(self) -> Dict[str, Any]: + """Convert plan to dictionary""" + return { + 'title': self.title, + 'steps': self.steps, + 'context': self.context, + 'completed_steps': self.completed_steps, + 'failed_steps': self.failed_steps + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'ActionPlan': + """Create plan from dictionary""" + plan = cls(data['title'], data['steps'], data['context']) + plan.completed_steps = data.get('completed_steps', []) + plan.failed_steps = data.get('failed_steps', []) + return plan + + +class OllamaProvider: + """Simple Ollama API provider for AI action planning""" + + def __init__(self, base_url: str, model: str, logger): + self.base_url = base_url.rstrip('/') + self.model = model + self.logger = logger + + def generate(self, prompt: str) -> Optional[str]: + """Generate a response from Ollama""" + try: + response = requests.post( + f"{self.base_url}/api/generate", + json={ + "model": self.model, + "prompt": prompt, + "stream": False + }, + timeout=120 + ) + response.raise_for_status() + result = response.json() + return result.get('response', '') + except Exception as e: + self.logger.log(f"āŒ Ollama API error: {str(e)}") + return None + + def make_change(self, file_content: str, old_text: str, new_text: str, file_path: str, custom_instructions: str = None) -> Optional[str]: + """Make changes to file content using Ollama""" + # Try direct replacement first + if old_text and old_text.strip() in file_content: + return file_content.replace(old_text.strip(), new_text.strip()) + + # Use Ollama to make intelligent changes + prompt = f"""You are a code modification assistant. Modify the following file according to the instructions. + +File: {file_path} + +Current Content: +``` +{file_content} +``` + +Instructions: {new_text} +{f'Additional context: {custom_instructions}' if custom_instructions else ''} + +Return ONLY the complete modified file content. Do not include explanations or markdown code blocks.""" + + return self.generate(prompt) + + +class AIActionPlanner: + """Generates and executes action plans using AI""" + + def __init__(self, ai_manager, logger, config_manager): + self.ai_manager = ai_manager + self.logger = logger + self.config_manager = config_manager + + def generate_plan(self, item, custom_instructions: str = "") -> Optional[ActionPlan]: + """ + Generate an action plan for a PR or Issue + + Args: + item: The PR or Issue (WorkflowItem object or dict) + custom_instructions: Optional user-provided instructions + + Returns: + ActionPlan object or None if generation failed + """ + # Handle both WorkflowItem objects and dictionaries + if hasattr(item, 'item_type'): + # It's a WorkflowItem object + item_type = item.item_type + item_number = item.number + title = item.title + body = item.body or '' + repo = getattr(item, 'repo', None) + else: + # It's a dictionary + item_type = item.get('type', 'unknown') + item_number = item.get('number') + title = item.get('title', 'Untitled') + body = item.get('body', '') + repo = item.get('repo') + + self.logger.log(f"šŸ¤– Generating action plan for {item_type} #{item_number}...") + + # Get AI provider + config = self.config_manager.get_config() + ai_provider_name = config.get('AI_PROVIDER', 'none').lower() + + if ai_provider_name == 'none' or not ai_provider_name: + self.logger.log("āŒ No AI provider configured. Please configure in Settings.") + return None + + # Get provider instance + provider = self._get_ai_provider(ai_provider_name, config) + if not provider: + return None + + # Generate the plan using AI + try: + self.logger.log(f"šŸ“¤ Calling AI provider: {type(provider).__name__}") + plan_text = self._call_ai_for_plan(provider, item_type, title, body, custom_instructions) + + if not plan_text: + self.logger.log("āŒ AI did not generate a plan (empty response)") + return None + + self.logger.log(f"šŸ“„ Received response from AI ({len(plan_text)} characters)") + self.logger.log(f"šŸ“„ Response preview: {plan_text[:200]}...") + + # Parse the plan + self.logger.log("šŸ” Parsing AI response into steps...") + steps = self._parse_plan(plan_text) + + if not steps: + self.logger.log("āŒ Could not parse action steps from AI response") + return None + + # Get repo from item or config + if repo is None: + repo = config.get('GITHUB_REPO', '') + + plan = ActionPlan( + title=f"Action Plan for {item_type.upper()} #{item_number}: {title}", + steps=steps, + context={ + 'item_type': item_type, + 'item_number': item_number, + 'item_title': title, + 'item_body': body, + 'repo': repo + } + ) + + self.logger.log(f"āœ… Generated plan with {len(steps)} steps") + return plan + + except Exception as e: + self.logger.log(f"āŒ Error generating plan: {str(e)}") + return None + + def _get_ai_provider(self, provider_name: str, config: Dict[str, Any]): + """Get the AI provider instance""" + try: + if provider_name in ['claude', 'anthropic']: + # Try both CLAUDE_API_KEY and ANTHROPIC_API_KEY for compatibility + api_key = config.get('CLAUDE_API_KEY') + if not api_key: + api_key = config.get('ANTHROPIC_API_KEY') + if not api_key: + self.logger.log("āŒ Claude API key not found in secure storage (tried both CLAUDE_API_KEY and ANTHROPIC_API_KEY)") + return None + self.logger.log("ā„¹ļø Initializing Claude provider...") + from . import ai_manager + provider = ai_manager.ClaudeProvider(api_key, self.logger) + self.logger.log("āœ… Claude provider initialized successfully") + return provider + + elif provider_name in ['chatgpt', 'openai']: + api_key = config.get('OPENAI_API_KEY') + if not api_key: + self.logger.log("āŒ OpenAI API key not found in secure storage") + return None + self.logger.log("ā„¹ļø Initializing ChatGPT provider...") + from . import ai_manager + provider = ai_manager.ChatGPTProvider(api_key, self.logger) + self.logger.log("āœ… ChatGPT provider initialized successfully") + return provider + + elif provider_name == 'ollama': + # Ollama doesn't need an API key, uses URL from config + ollama_url = config.get('OLLAMA_URL', 'http://localhost:11434') + ollama_model = config.get('OLLAMA_MODEL', 'llama2') + self.logger.log(f"ā„¹ļø Using Ollama at {ollama_url} with model {ollama_model}") + # Create a simple Ollama provider wrapper + return OllamaProvider(ollama_url, ollama_model, self.logger) + + else: + self.logger.log(f"āŒ Unsupported AI provider: {provider_name}") + return None + + except Exception as e: + self.logger.log(f"āŒ Error creating AI provider: {str(e)}") + return None + + def _call_ai_for_plan(self, provider, item_type: str, title: str, body: str, custom_instructions: str) -> Optional[str]: + """Call AI to generate an action plan""" + + prompt = f"""You are an expert software engineer tasked with creating an actionable plan to address a GitHub {item_type}. + +{item_type.upper()} Title: {title} + +{item_type.upper()} Description: +{body} + +{"Additional Instructions: " + custom_instructions if custom_instructions else ""} + +Please create a detailed action plan with specific, executable steps. For each step, specify: +1. What needs to be done (clear description) +2. Which file(s) need to be modified (if applicable) +3. What changes should be made (if applicable) + +Format your response as a JSON array of steps, where each step has: +- "description": A clear description of what to do +- "file_path": Path to the file to modify (or null if not file-specific) +- "changes": Description of changes to make (or null if not applicable) +- "action_type": One of ["modify_file", "create_file", "delete_file", "investigate", "test", "document"] + +Example format: +```json +[ + {{ + "description": "Fix the authentication bug in login handler", + "file_path": "src/auth/login.py", + "changes": "Update the password validation logic to handle special characters correctly", + "action_type": "modify_file" + }}, + {{ + "description": "Add unit tests for authentication", + "file_path": "tests/test_auth.py", + "changes": "Add test cases for special characters in passwords", + "action_type": "create_file" + }} +] +``` + +IMPORTANT: Return ONLY the JSON array, no other text before or after.""" + + try: + if isinstance(provider, OllamaProvider): + # Use Ollama + self.logger.log(f"šŸ¤– Calling Ollama AI to generate plan...") + return provider.generate(prompt) + + elif hasattr(provider, '_generate_updated_document'): + # Use Claude's document generation + self.logger.log(f"šŸ¤– Calling Claude AI to generate plan...") + import anthropic + client = anthropic.Anthropic(api_key=provider.api_key) + + message = client.messages.create( + model="claude-sonnet-4-5", + max_tokens=4096, + messages=[{"role": "user", "content": prompt}] + ) + + return message.content[0].text + + elif hasattr(provider, 'client'): + # Use OpenAI/ChatGPT + self.logger.log(f"šŸ¤– Calling ChatGPT AI to generate plan...") + response = provider.client.chat.completions.create( + model="gpt-4", + messages=[{"role": "user", "content": prompt}], + max_tokens=4096 + ) + + self.logger.log(f"āœ… ChatGPT response received") + return response.choices[0].message.content + + else: + self.logger.log(f"āŒ Unknown provider type: {type(provider).__name__}") + return None + + except Exception as e: + self.logger.log(f"āŒ AI API call failed: {str(e)}") + import traceback + self.logger.log(f"āŒ Traceback: {traceback.format_exc()}") + return None + + def _parse_plan(self, plan_text: str) -> List[Dict[str, Any]]: + """Parse the AI-generated plan text into structured steps""" + + try: + # Extract JSON from response (might be wrapped in markdown) + json_match = re.search(r'```json\s*(\[.*?\])\s*```', plan_text, re.DOTALL) + if json_match: + json_text = json_match.group(1) + else: + # Try to find JSON array directly + json_match = re.search(r'\[.*\]', plan_text, re.DOTALL) + if json_match: + json_text = json_match.group(0) + else: + self.logger.log("āš ļø Could not find JSON in AI response") + return [] + + # Parse JSON + steps = json.loads(json_text) + + # Validate and clean up steps + validated_steps = [] + for i, step in enumerate(steps): + if isinstance(step, dict): + validated_step = { + 'step_number': i + 1, + 'description': step.get('description', f'Step {i+1}'), + 'file_path': step.get('file_path'), + 'changes': step.get('changes'), + 'action_type': step.get('action_type', 'investigate'), + 'completed': False, + 'status': 'pending' + } + validated_steps.append(validated_step) + + self.logger.log(f"āœ… Successfully parsed {len(validated_steps)} steps from AI response") + return validated_steps + + except json.JSONDecodeError as e: + self.logger.log(f"āŒ Failed to parse JSON: {str(e)}") + self.logger.log(f"Response was: {plan_text[:500]}...") + return [] + except Exception as e: + self.logger.log(f"āŒ Error parsing plan: {str(e)}") + return [] + + def execute_plan( + self, + plan: ActionPlan, + local_repo_path: str, + progress_callback: Optional[Callable[[int, int, str], None]] = None, + log_callback: Optional[Callable[[str], None]] = None + ) -> Dict[str, Any]: + """ + Execute an action plan + + Args: + plan: The ActionPlan to execute + local_repo_path: Path to local git repository + progress_callback: Callback function(current_step, total_steps, message) + log_callback: Callback function for logging thought process + + Returns: + Dictionary with execution results + """ + def log(message): + """Helper to log to both logger and callback""" + self.logger.log(message) + if log_callback: + log_callback(message) + + log(f"ā–¶ļø Starting execution of plan: {plan.title}") + + if not local_repo_path or not Path(local_repo_path).exists(): + log(f"āŒ Local repository path not found: {local_repo_path}") + return {'success': False, 'error': 'Invalid local repository path'} + + total_steps = len(plan.steps) + completed = 0 + failed = 0 + + for i, step in enumerate(plan.steps): + step_num = step['step_number'] + + # Mark step as in-progress + step['status'] = 'in_progress' + if progress_callback: + progress_callback(i + 1, total_steps, f"Executing step {step_num}...") + + log(f"\nšŸ“ Step {step_num}/{total_steps}: {step['description']}") + + try: + result = self._execute_step(step, local_repo_path, plan.context, log) + + if result['success']: + step['completed'] = True + step['status'] = 'completed' + plan.completed_steps.append(step_num) + completed += 1 + log(f"āœ… Step {step_num} completed") + else: + step['status'] = 'failed' + step['error'] = result.get('error', 'Unknown error') + plan.failed_steps.append(step_num) + failed += 1 + log(f"āŒ Step {step_num} failed: {result.get('error')}") + + except Exception as e: + step['status'] = 'failed' + step['error'] = str(e) + plan.failed_steps.append(step_num) + failed += 1 + log(f"āŒ Step {step_num} failed with exception: {str(e)}") + + log(f"\nšŸ“Š Execution complete: {completed}/{total_steps} steps successful, {failed} failed") + + # If we made changes successfully, commit and push them + if completed > 0: + try: + log("\nšŸ”§ Committing and pushing changes...") + + # Get PR/Issue info from context + item_type = plan.context.get('item_type', 'item') + item_number = plan.context.get('item_number', 'unknown') + item_title = plan.context.get('item_title', 'changes') + + # Commit message + commit_msg = f"AI: Execute action plan for {item_type} #{item_number}\n\n{item_title}\n\nAutomated changes by GitHub Pulse AI" + + # Get current branch (should be the PR branch) + import subprocess + result = subprocess.run( + ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], + cwd=local_repo_path, + capture_output=True, + text=True, + timeout=10 + ) + current_branch = result.stdout.strip() if result.returncode == 0 else 'main' + log(f"šŸ“ Current branch: {current_branch}") + + # Stage all changes + log("šŸ“ Staging changes...") + subprocess.run(['git', 'add', '-A'], cwd=local_repo_path, check=True, timeout=10) + + # Check if there are changes to commit + result = subprocess.run( + ['git', 'diff', '--cached', '--quiet'], + cwd=local_repo_path, + timeout=10 + ) + + if result.returncode != 0: # There are changes + # Commit + log("šŸ’¾ Committing changes...") + subprocess.run( + ['git', 'commit', '-m', commit_msg], + cwd=local_repo_path, + check=True, + timeout=10 + ) + + # Push + log(f"šŸš€ Pushing to {current_branch}...") + subprocess.run( + ['git', 'push', 'origin', current_branch], + cwd=local_repo_path, + check=True, + timeout=30 + ) + log(f"āœ… Changes pushed to {current_branch}") + else: + log("ā„¹ļø No changes to commit") + + except subprocess.TimeoutExpired: + log("āš ļø Git operation timed out") + except subprocess.CalledProcessError as e: + log(f"āš ļø Git operation failed: {e}") + except Exception as e: + log(f"āš ļø Error during git commit/push: {str(e)}") + + return { + 'success': failed == 0, + 'completed': completed, + 'failed': failed, + 'total': total_steps, + 'plan': plan + } + + def _execute_step(self, step: Dict[str, Any], local_repo_path: str, context: Dict[str, Any], log=None) -> Dict[str, Any]: + """Execute a single step of the plan""" + + action_type = step.get('action_type', 'investigate') + file_path = step.get('file_path') + changes = step.get('changes') + + # Use log function if provided, otherwise fall back to logger + log_func = log if log else self.logger.log + + if action_type == 'modify_file' and file_path: + return self._modify_file(file_path, changes, local_repo_path, log_func) + + elif action_type == 'create_file' and file_path: + return self._create_file(file_path, changes, local_repo_path, log_func) + + elif action_type == 'delete_file' and file_path: + return self._delete_file(file_path, local_repo_path, log_func) + + else: + # For investigate, test, document actions, just mark as completed + # (requires manual intervention) + log_func(f"ā„¹ļø Manual action required: {step['description']}") + return {'success': True, 'message': 'Manual action logged'} + + def _modify_file(self, file_path: str, changes: str, local_repo_path: str, log=None) -> Dict[str, Any]: + """Modify a file using AI""" + + log_func = log if log else self.logger.log + full_path = Path(local_repo_path) / file_path + + if not full_path.exists(): + return {'success': False, 'error': f'File not found: {file_path}'} + + try: + log_func(f"šŸ“ Reading file: {file_path}") + # Read current content + with open(full_path, 'r', encoding='utf-8') as f: + current_content = f.read() + + # Get AI provider to make changes + config = self.config_manager.get_config() + provider_name = config.get('AI_PROVIDER', 'none').lower() + provider = self._get_ai_provider(provider_name, config) + + if not provider: + return {'success': False, 'error': 'AI provider not available'} + + # Use AI to make the changes + log_func(f"šŸ¤– Using AI to modify {file_path}...") + log_func(f"šŸ” Analyzing changes needed...") + updated_content = provider.make_change( + file_content=current_content, + old_text=current_content[:200] + "...", # Context + new_text=changes, # What to change + file_path=str(full_path), + custom_instructions=changes + ) + + if updated_content and updated_content != current_content: + log_func(f"šŸ’¾ Writing changes to {file_path}...") + # Write updated content + with open(full_path, 'w', encoding='utf-8') as f: + f.write(updated_content) + + log_func(f"āœ… Successfully modified {file_path}") + return {'success': True, 'file': file_path} + else: + return {'success': False, 'error': 'AI could not generate changes'} + + except Exception as e: + return {'success': False, 'error': f'Error modifying file: {str(e)}'} + + def _create_file(self, file_path: str, content: str, local_repo_path: str, log=None) -> Dict[str, Any]: + """Create a new file""" + + log_func = log if log else self.logger.log + full_path = Path(local_repo_path) / file_path + + if full_path.exists(): + return {'success': False, 'error': f'File already exists: {file_path}'} + + try: + log_func(f"šŸ“„ Creating new file: {file_path}") + # Create parent directories if needed + full_path.parent.mkdir(parents=True, exist_ok=True) + + # Create file with content + with open(full_path, 'w', encoding='utf-8') as f: + f.write(content or f"# TODO: Implement {file_path}\n") + + log_func(f"āœ… Created {file_path}") + return {'success': True, 'file': file_path} + + except Exception as e: + return {'success': False, 'error': f'Error creating file: {str(e)}'} + + def _delete_file(self, file_path: str, local_repo_path: str, log=None) -> Dict[str, Any]: + """Delete a file""" + + log_func = log if log else self.logger.log + full_path = Path(local_repo_path) / file_path + + if not full_path.exists(): + return {'success': False, 'error': f'File not found: {file_path}'} + + try: + log_func(f"šŸ—‘ļø Deleting file: {file_path}") + full_path.unlink() + log_func(f"āœ… Deleted {file_path}") + return {'success': True, 'file': file_path} + + except Exception as e: + return {'success': False, 'error': f'Error deleting file: {str(e)}'} diff --git a/src/app_components/ai_manager.py b/src/app_components/ai_manager.py new file mode 100644 index 0000000..0a24d30 --- /dev/null +++ b/src/app_components/ai_manager.py @@ -0,0 +1,3430 @@ +""" +AI Manager +Handles AI module availability checking, installation, and provider management +Includes AI provider implementations (Claude, ChatGPT) and git operations +""" + +import os +import shutil +import subprocess +import sys +import tempfile +import time +from abc import ABC, abstractmethod +from pathlib import Path +from typing import List, Tuple, Optional + + +class Logger: + """Simple logger interface""" + def __init__(self, log_func): + self.log = log_func + + +class AIProvider(ABC): + """Base class for AI providers""" + + def __init__(self, api_key: str, logger: Logger): + self.api_key = api_key + self.logger = logger + + @abstractmethod + def make_change(self, file_content: str, old_text: str, new_text: str, file_path: str, custom_instructions: str = None) -> Optional[str]: + """ + Use AI to make a change in the file content. + + Args: + file_content: Current content of the file + old_text: Text to find and replace + new_text: New text to replace with + file_path: Path to the file (for context) + custom_instructions: Optional custom instructions from user + + Returns: + Updated file content, or None if AI couldn't make the change + """ + pass + + +class ClaudeProvider(AIProvider): + """Claude AI provider using Anthropic API""" + + def make_change(self, file_content: str, old_text: str, new_text: str, file_path: str, custom_instructions: str = None) -> Optional[str]: + """Make smart, targeted changes based on reference text and suggestions + + Args: + file_content: Full file content + old_text: Reference text (what user is talking about - may not be exact) + new_text: Suggested changes (what user wants to see) + file_path: Path to the file being modified + custom_instructions: Optional custom instructions from user + """ + + # Step 1: Try direct string replacement if reference text is exact match + if old_text and old_text.strip() in file_content: + self.logger.log("āœ… Making direct string replacement (reference text found exactly)") + updated_content = file_content.replace(old_text.strip(), new_text.strip()) + if updated_content != file_content: + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + self.logger.log(f"āœ… Direct replacement successful ({changed_lines} lines changed)") + return updated_content + + # Step 2: Use AI to generate full document with targeted changes + self.logger.log("šŸ“ Using AI to modify the document...") + return self._generate_updated_document(file_content, old_text, new_text, file_path, custom_instructions) + + def _generate_updated_document(self, file_content: str, old_text: str, new_text: str, file_path: str, custom_instructions: str = None) -> Optional[str]: + """Generate updated document content using Claude""" + + try: + import anthropic + client = anthropic.Anthropic(api_key=self.api_key) + + # Build custom instructions text + if custom_instructions and custom_instructions.strip(): + custom_instructions_text = f""" +**Additional Custom Instructions:** +{custom_instructions.strip()} + +""" + else: + custom_instructions_text = "" + + # Handle case where new_text is empty or just guidance + if new_text and new_text.strip() and not new_text.strip().lower().startswith(' [!IMPORTANT] +> OUTPUT REQUIREMENTS: +> - Return ONLY the complete file content - no explanatory text, dialog, or commentary +> - Do NOT add any text before or after the file content +> - Do NOT wrap output in markdown code blocks (```), just return the raw content +> - Return the ENTIRE document - no truncation, no placeholders like [Rest of the document here...] +> - Every single line of the original document must be present in your response +> - Preserve all markdown formatting, links, and code blocks exactly +> - Please ensure the changes align with Microsoft documentation standards +> - Only make changes that fulfill the specified request + +{custom_instructions_text} + +**Current File Content:** +``` +{file_content} +``` + +{guidance_text} + +Return the complete updated file content now (NO explanatory text):""" + + message = client.messages.create( + model="claude-3-5-haiku-20241022", + max_tokens=4096, + temperature=0.1, + messages=[{"role": "user", "content": prompt}] + ) + + updated_content = message.content[0].text.strip() + + # Basic validation - ensure content was actually changed + if updated_content and updated_content != file_content: + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + + self.logger.log(f"āœ… Claude document update successful ({changed_lines} lines affected)") + return updated_content + else: + self.logger.log("āš ļø No changes detected in AI response") + return None + + except Exception as e: + self.logger.log(f"āŒ Error generating updated document with Claude: {str(e)}") + return None + + def _generate_with_context_window_claude(self, file_content: str, old_text: str, new_text: str, file_path: str) -> Optional[str]: + """Use context window approach with Claude - AI only sees/modifies a small section + + This physically prevents AI from rewriting entire file by only giving it + the relevant section to work with. + """ + try: + import difflib + import anthropic + + # Step 1: Find where the reference text is located + lines = file_content.split('\n') + ref_lines = old_text.split('\n') if old_text else [] + + # Find best matching location for reference text + start_line = 0 + if ref_lines: + matcher = difflib.SequenceMatcher(None, ref_lines, lines) + match = matcher.find_longest_match(0, len(ref_lines), 0, len(lines)) + if match.size > 0: + start_line = match.b + self.logger.log(f"šŸ“ Found reference area at line {start_line + 1}") + else: + self.logger.log("šŸ“ Reference text not found, using beginning of file") + + # Step 2: Extract context window (30 lines before, 30 lines after) + window_before = 30 + window_after = 30 + + window_start = max(0, start_line - window_before) + window_end = min(len(lines), start_line + len(ref_lines) + window_after) + + context_window = lines[window_start:window_end] + self.logger.log(f"šŸ“„ Context window: lines {window_start + 1} to {window_end} ({len(context_window)} lines)") + self.logger.log(f" (AI can only modify this section, rest of file is protected)") + + # Step 3: Have AI modify only the context window + context_text = '\n'.join(context_window) + + client = anthropic.Anthropic(api_key=self.api_key) + + prompt = f"""You are helping modify a small section of a documentation file. You can ONLY modify the section provided below. + +File: {file_path} +Section location: Lines {window_start + 1} to {window_end} + +REFERENCE TEXT (what user is referring to): +{old_text} + +SUGGESTED CHANGES (what user wants): +{new_text} + +SECTION TO MODIFY: +``` +{context_text} +``` + +INSTRUCTIONS: +1. Understand the user's INTENT from the reference and suggestions: + - "add/include/incorporate a section" = Add a COMPLETE NEW SECTION with heading and full content + - "update/modify/change X" = Modify existing text X intelligently + - "fix/correct" = Make specific correction only + - Be generous with new content when asked to add something + +2. For ADDING content (sections, paragraphs, examples): + - Create complete, well-written content (not just stubs or brief additions) + - Add proper markdown headers (## Best Practices, ### Example, etc.) + - Place it logically (end of section, before ## Related content, etc.) + - Match the document's writing style and tone + +3. For MODIFYING content: + - Change only what's requested + - Leave everything else exactly as-is + +4. Return the ENTIRE section (all {len(context_window)} lines) with your changes +5. No explanations - just the modified section + +OUTPUT THE COMPLETE MODIFIED SECTION:""" + + message = client.messages.create( + model="claude-3-5-haiku-20241022", + max_tokens=4096, + temperature=0.1, + messages=[{"role": "user", "content": prompt}] + ) + + modified_window = message.content[0].text.strip() + + # Clean up code blocks if AI wrapped it + if modified_window.startswith('```'): + modified_window = '\n'.join(modified_window.split('\n')[1:-1]) + + # Step 4: Replace the context window in the full file + modified_lines = modified_window.split('\n') + result_lines = lines[:window_start] + modified_lines + lines[window_end:] + updated_content = '\n'.join(result_lines) + + # Verify change is minimal + diff = list(difflib.unified_diff(lines, result_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + + self.logger.log(f"āœ… Context window approach successful ({changed_lines} lines changed)") + + # Ensure we actually made changes + if updated_content == file_content: + self.logger.log("āš ļø No changes detected, falling back to full-document approach") + return self._generate_updated_document(file_content, old_text, new_text, file_path) + + return updated_content + + except Exception as e: + self.logger.log(f"āŒ Error with context window approach: {str(e)}") + self.logger.log("āš ļø Falling back to full-document approach") + return self._generate_updated_document(file_content, old_text, new_text, file_path) + + def _validate_diff_patch(self, diff_patch: str, original_content: str, old_text: str, new_text: str) -> bool: + """Validate that the AI-generated diff is safe and appropriate""" + try: + # Check for common problems + lines = diff_patch.split('\n') + + # Problem 0: Check for proper diff structure + has_hunk_header = any(line.startswith('@@') for line in lines) + if not has_hunk_header: + self.logger.log("āŒ Invalid diff: Missing @@ hunk headers") + return False + + # Problem 1: Check for duplicate +++ lines + plus_count = sum(1 for line in lines if line.startswith('+++')) + if plus_count > 1: + self.logger.log("āŒ Invalid diff: Multiple +++ lines detected") + return False + + # Problem 2: Check for removal of metadata (title, author, etc.) + for line in lines: + if line.startswith('-') and not line.startswith('---'): + removed_content = line[1:].strip() + # Check if removing metadata + if any(keyword in removed_content.lower() for keyword in ['title:', 'author:', 'description:', 'ms.author:', 'ms.date:']): + self.logger.log(f"āŒ Invalid diff: Attempting to remove metadata: {removed_content}") + return False + + # Problem 3: Check if diff is too large (indicates rewrite) + removed_lines = len([line for line in lines if line.startswith('-') and not line.startswith('---')]) + added_lines = len([line for line in lines if line.startswith('+') and not line.startswith('+++')]) + + if removed_lines > 10: # Too many removals for an additive change + self.logger.log(f"āŒ Invalid diff: Too many removals ({removed_lines} lines)") + return False + + return True + + except Exception as e: + self.logger.log(f"āŒ Error validating diff: {str(e)}") + return False + + def _create_safe_diff(self, file_content: str, old_text: str, new_text: str, file_path: str) -> Optional[str]: + """Create a safer, simpler diff that just adds content without removing anything""" + try: + # Strategy: Find the best location to add the new content and insert it there + lines = file_content.split('\n') + + # Look for common insertion points for adding sections + insertion_point = self._find_safe_insertion_point(lines, old_text, new_text) + + if insertion_point is None: + self.logger.log("āš ļø Could not find safe insertion point") + return None + + # Insert the new content at the found location + new_lines = lines[:insertion_point] + [new_text.strip(), ''] + lines[insertion_point:] + updated_content = '\n'.join(new_lines) + + self.logger.log(f"āœ… Created safe diff - inserting content at line {insertion_point}") + return updated_content + + except Exception as e: + self.logger.log(f"āŒ Error creating safe diff: {str(e)}") + return None + + def _find_safe_insertion_point(self, lines: list, old_text: str, new_text: str) -> Optional[int]: + """Find the best place to insert new content safely""" + try: + # Look for section headers to insert after + for i, line in enumerate(lines): + # If the old_text contains context about where to insert + if old_text and old_text.lower().strip() in line.lower(): + # Insert after this line + return i + 1 + + # Look for pattern where we should insert a new section + # Insert before conclusion, examples, or other sections + if line.strip().startswith('##') and any(keyword in line.lower() for keyword in ['example', 'conclusion', 'summary', 'next steps']): + return i + + # If no specific location found, insert before the last section + for i in range(len(lines) - 1, -1, -1): + if lines[i].strip().startswith('##'): + return i + + # Last resort: insert at 80% through the document + return int(len(lines) * 0.8) + + except Exception: + return None + + def _apply_diff_patch(self, original_content: str, diff_patch: str, file_path: str) -> Optional[str]: + """Apply a unified diff patch to the original content""" + try: + import tempfile + import subprocess + import os + + # Create temporary files + with tempfile.TemporaryDirectory() as temp_dir: + # Write original content to temp file + original_file = os.path.join(temp_dir, "original.txt") + with open(original_file, 'w', encoding='utf-8') as f: + f.write(original_content) + + # Write diff patch to temp file + patch_file = os.path.join(temp_dir, "changes.patch") + with open(patch_file, 'w', encoding='utf-8') as f: + f.write(diff_patch) + + # Apply patch using git apply (more reliable than patch command) + try: + # First try git apply + subprocess.run(['git', 'apply', '--verbose', patch_file], + cwd=temp_dir, check=True, capture_output=True, text=True) + + # Read the result + with open(original_file, 'r', encoding='utf-8') as f: + return f.read() + + except subprocess.CalledProcessError: + # Fallback to manual patch application + self.logger.log("šŸ“ Git apply failed, trying manual diff application...") + return self._manual_diff_apply(original_content, diff_patch) + + except Exception as e: + self.logger.log(f"āš ļø Patch application failed: {str(e)}") + return self._manual_diff_apply(original_content, diff_patch) + + def _manual_diff_apply(self, original_content: str, diff_patch: str) -> Optional[str]: + """Manually apply a diff patch when git apply fails""" + try: + # Detect original line ending style + has_crlf = '\r\n' in original_content + + original_lines = original_content.split('\n') + result_lines = original_lines.copy() + + # Parse the diff patch + diff_lines = diff_patch.split('\n') + current_original_line = 0 + + i = 0 + while i < len(diff_lines): + line = diff_lines[i] + + # Look for @@ headers + if line.startswith('@@'): + # Extract line numbers: @@ -start,count +start,count @@ + parts = line.split() + if len(parts) >= 3: + old_info = parts[1][1:] # Remove the - + if ',' in old_info: + start_line = int(old_info.split(',')[0]) - 1 # Convert to 0-based + else: + start_line = int(old_info) - 1 + + current_original_line = start_line + i += 1 + continue + + # Skip diff headers (must check before processing -/+ lines) + if line.startswith('---') or line.startswith('+++'): + i += 1 + continue + + # Process diff lines + if line.startswith('-'): + # Remove line + if current_original_line < len(result_lines): + del result_lines[current_original_line] + elif line.startswith('+'): + # Add line + new_line = line[1:] # Remove the + + # If original had CRLF and this line doesn't have \r, add it + if has_crlf and not new_line.endswith('\r'): + new_line = new_line + '\r' + result_lines.insert(current_original_line, new_line) + current_original_line += 1 + elif line.startswith(' '): + # Context line - advance + current_original_line += 1 + + i += 1 + + return '\n'.join(result_lines) + + except Exception as e: + self.logger.log(f"āŒ Manual diff application failed: {str(e)}") + return None + + def _detect_change_type(self, old_text: str, new_text: str, file_path: str) -> str: + """Detect the type of change requested""" + old_lower = old_text.lower() + new_lower = new_text.lower() + + # Additive indicators + additive_keywords = ['add', 'include', 'incorporate', 'insert', 'create section', 'new section', 'best practices'] + if any(keyword in old_lower or keyword in new_lower for keyword in additive_keywords): + return "ADDITIVE" + + # Corrective indicators + corrective_keywords = ['correct', 'fix', 'grammar', 'spelling', 'typo', 'misspell', 'wrong', 'error'] + if any(keyword in old_lower or keyword in new_lower for keyword in corrective_keywords): + return "CORRECTIVE" + + # If new text is much longer than old text, likely additive + if len(new_text.strip()) > len(old_text.strip()) * 2: + return "ADDITIVE" + + # If similar length, likely corrective + if abs(len(new_text.strip()) - len(old_text.strip())) < 50: + return "CORRECTIVE" + + return "GENERAL" + + def _handle_additive_change(self, file_content: str, old_text: str, new_text: str, file_path: str) -> Optional[str]: + """Handle additive changes by generating content and inserting it""" + self.logger.log("šŸ”Ø Handling additive change - generating new content...") + + try: + import anthropic + client = anthropic.Anthropic(api_key=self.api_key) + + prompt = f"""**Instructions:** + +Task: Add new content to the documentation file as requested. + +Steps to complete: + +1. Generate ONLY the new content that should be added to the documentation file +2. Maintain proper formatting, indentation, and markdown structure +3. Make content standalone - don't reference existing content in the file +4. Use Microsoft documentation standards + +> [!IMPORTANT] +> Only create the new content - do not rewrite or modify existing content. +> Preserve markdown formatting, links, and code blocks as appropriate. +> Please ensure the changes align with Microsoft documentation standards. + +File: {file_path} +Request: {old_text} +Content to add: {new_text} + +Generate only the new content that should be added:""" + + message = client.messages.create( + model="claude-3-5-haiku-20241022", + max_tokens=2048, + temperature=0.1, + messages=[{"role": "user", "content": prompt}] + ) + + new_content = message.content[0].text.strip() + + # Find best insertion point in the file + insertion_point = self._find_insertion_point(file_content, old_text, file_path) + + # Insert the new content + lines = file_content.split('\n') + lines.insert(insertion_point, '\n' + new_content + '\n') + updated_content = '\n'.join(lines) + + # Count actual changes + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+')]) + + self.logger.log(f"āœ… Added new content ({changed_lines} lines added)") + return updated_content + + except Exception as e: + self.logger.log(f"āŒ Error in additive change: {str(e)}") + return None + + def _handle_corrective_change(self, file_content: str, old_text: str, new_text: str, file_path: str) -> Optional[str]: + """Handle corrective changes by finding and fixing specific issues""" + self.logger.log("šŸ” Handling corrective change - finding specific issues...") + + try: + import anthropic + client = anthropic.Anthropic(api_key=self.api_key) + + prompt = f"""**Instructions:** + +Task: Find and fix a specific issue in the documentation file. + +Steps to complete: + +1. Locate the exact text that needs to be corrected in the file +2. Provide the precise replacement text +3. Make minimal changes - fix only what needs to be fixed +4. Maintain existing formatting and structure + +> [!IMPORTANT] +> Only make the specified correction - do not make additional changes. +> Preserve all markdown formatting, links, and code blocks. +> Please ensure the changes align with Microsoft documentation standards. + +Issue: {old_text} +Fix: {new_text} +File: {file_path} + +Return your response in this format: +OLD: [exact text to find] +NEW: [exact replacement text] + +Be very specific - find the minimal text that needs changing. For example: +- If fixing "Microsft" → return OLD: Microsft, NEW: Microsoft +- If fixing grammar → return OLD: [the incorrect phrase], NEW: [corrected phrase] + +Find the exact text to correct:""" + + message = client.messages.create( + model="claude-3-5-haiku-20241022", + max_tokens=1024, + temperature=0.1, + messages=[{"role": "user", "content": f"{prompt}\n\nFile content to search:\n{file_content}"}] + ) + + response = message.content[0].text.strip() + + # Parse the response to extract OLD and NEW + old_match = None + new_match = None + + for line in response.split('\n'): + if line.startswith('OLD:'): + old_match = line[4:].strip() + elif line.startswith('NEW:'): + new_match = line[4:].strip() + + if old_match and new_match and old_match in file_content: + updated_content = file_content.replace(old_match, new_match) + # Count changes + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + self.logger.log(f"āœ… Corrective change successful ({changed_lines} lines affected)") + return updated_content + else: + self.logger.log(f"āš ļø Could not find exact text to correct") + return None + + except Exception as e: + self.logger.log(f"āŒ Error in corrective change: {str(e)}") + return None + + def _find_insertion_point(self, file_content: str, context: str, file_path: str) -> int: + """Find the best place to insert new content""" + lines = file_content.split('\n') + + # For markdown files, try to find a good section to add after + if file_path.endswith('.md'): + # Look for existing sections + for i, line in enumerate(lines): + if line.startswith('#') and i < len(lines) - 1: + # Insert after this section + continue + + # If no good sections found, add at the end + return len(lines) + + # For other files, add at the end + return len(lines) + + def _handle_general_change(self, file_content: str, old_text: str, new_text: str, file_path: str) -> Optional[str]: + """Handle general changes with enhanced targeting""" + self.logger.log("šŸŽÆ Handling general change with enhanced targeting...") + + max_retries = 3 + base_delay = 2 + + for attempt in range(max_retries): + try: + import anthropic + client = anthropic.Anthropic(api_key=self.api_key) + + prompt = f"""**Instructions:** + +Task: Update the documentation file with the specific change requested. + +Steps to complete: + +1. Locate the specific section that needs changing in the file +2. Make ONLY the requested change +3. Maintain the existing formatting, indentation, and markdown structure +4. Preserve everything else exactly as-is +5. Return the complete updated file + +> [!IMPORTANT] +> Only make the specified change - do not rewrite or reorganize content. +> Preserve all markdown formatting, links, and code blocks. +> Please ensure the changes align with Microsoft documentation standards. +> Make the SMALLEST possible change. + +File: {file_path} +Change needed: {old_text} +New content: {new_text} + +Current file content: +{file_content}""" + + message = client.messages.create( + model="claude-3-5-haiku-20241022", + max_tokens=4096, + temperature=0.1, + messages=[{"role": "user", "content": prompt}] + ) + + updated_content = message.content[0].text + + if new_text.strip() in updated_content: + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + + if changed_lines > 30: + self.logger.log(f"āš ļø Change affected {changed_lines} lines - may be too broad") + else: + self.logger.log(f"āœ… General change successful ({changed_lines} lines affected)") + + return updated_content + else: + self.logger.log("āš ļø New text not found in result") + return None + + except Exception as e: + if attempt < max_retries - 1: + delay = base_delay * (2 ** attempt) + self.logger.log(f"āš ļø Retry {attempt + 1}/{max_retries} after {delay}s...") + time.sleep(delay) + continue + else: + self.logger.log(f"āŒ Error in general change: {str(e)}") + return None + + return None + + +class ChatGPTProvider(AIProvider): + """ChatGPT/GPT-4 provider using OpenAI API""" + + def __init__(self, api_key: str, logger: Logger): + """Initialize ChatGPT provider with OpenAI client""" + super().__init__(api_key, logger) + import openai + self.client = openai.OpenAI(api_key=api_key) + + def make_change(self, file_content: str, old_text: str, new_text: str, file_path: str, custom_instructions: str = None) -> Optional[str]: + """Make smart, targeted changes based on reference text and suggestions + + Args: + file_content: Full file content + old_text: Reference text (what user is talking about - may not be exact) + new_text: Suggested changes (what user wants to see) + file_path: Path to file being modified + custom_instructions: Optional custom instructions from user + """ + + # Step 1: Try direct string replacement if reference text is exact match + if old_text and old_text.strip() in file_content: + self.logger.log("āœ… Making direct string replacement (reference text found exactly)") + updated_content = file_content.replace(old_text.strip(), new_text.strip()) + if updated_content != file_content: + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + self.logger.log(f"āœ… Direct replacement successful ({changed_lines} lines changed)") + return updated_content + + # Step 2: Use AI to generate full document with targeted changes + self.logger.log("šŸ“ Using AI to modify the document...") + return self._generate_updated_document_chatgpt(file_content, old_text, new_text, file_path, custom_instructions) + + def _generate_updated_document_chatgpt(self, file_content: str, old_text: str, new_text: str, file_path: str, custom_instructions: str = None) -> Optional[str]: + """Generate updated document content using ChatGPT""" + + try: + # Use the client initialized in __init__ + client = self.client + + # Build custom instructions text + if custom_instructions and custom_instructions.strip(): + custom_instructions_text = f""" + +**Additional Custom Instructions:** +{custom_instructions.strip()} + +""" + else: + custom_instructions_text = "" + + # Handle blank new_text field with dynamic prompt + if not new_text or not new_text.strip(): + # General improvement request when new text is blank + prompt = f"""**Instructions:** + +Task: Review and improve the documentation file based on the reference context provided. + +Steps to complete: + +1. Review the current file content below +2. Look at the reference context: "{old_text}" +3. Improve the relevant sections based on Microsoft documentation standards +4. Maintain the existing formatting, indentation, and markdown structure +5. Return the complete updated file content + +> [!IMPORTANT] +> OUTPUT REQUIREMENTS: +> - Return ONLY the complete file content - no explanatory text, dialog, or commentary +> - Do NOT add any text before or after the file content +> - Do NOT wrap output in markdown code blocks (```), just return the raw content +> - Return the ENTIRE document - no truncation, no placeholders like [Rest of the document here...] +> - Every single line of the original document must be present in your response +> - Focus on areas related to: {old_text} +> - Preserve all markdown formatting, links, and code blocks exactly +> - Please ensure improvements align with Microsoft documentation standards +> - Only make improvements - do not remove existing content unless it's redundant + +{custom_instructions_text} + +**Current File Content:** +``` +{file_content} +``` + +**Context for improvements:** +``` +{old_text} +``` + +Return the complete updated file content now (NO explanatory text):""" + + else: + # Specific replacement when new text is provided + prompt = f"""**Instructions:** + +Task: Update the documentation file with the changes requested. + +Steps to complete: + +1. Review the current file content below +2. Find the reference text that needs to be updated +3. Replace it with the suggested new content +4. Maintain the existing formatting, indentation, and markdown structure +5. Return the complete updated file content + +> [!IMPORTANT] +> OUTPUT REQUIREMENTS: +> - Return ONLY the complete file content - no explanatory text, dialog, or commentary +> - Do NOT add any text before or after the file content +> - Do NOT wrap output in markdown code blocks (```), just return the raw content +> - Return the ENTIRE document - no truncation, no placeholders like [Rest of the document here...] +> - Every single line of the original document must be present in your response +> - Only replace the specified text - do not make additional changes +> - Preserve all markdown formatting, links, and code blocks exactly +> - If the current text cannot be found exactly, search for similar text +> - Please ensure the changes align with Microsoft documentation standards +> - Do not remove any text unless the reference or suggested guidance indicates to do so + +{custom_instructions_text} + +**Current File Content:** +``` +{file_content} +``` + +**Reference text to find and replace:** +``` +{old_text} +``` + +**Suggested new content:** +``` +{new_text} +``` + +Return the complete updated file content now (NO explanatory text):""" + + response = client.chat.completions.create( + model="gpt-4-turbo-preview", + messages=[ + {"role": "system", "content": "You are a document editor. Return ONLY the complete updated file content - no explanatory text, no dialog, no code blocks, no truncation, no placeholders. Output must be the raw complete file content with requested changes."}, + {"role": "user", "content": prompt} + ], + temperature=0.1 + ) + + updated_content = response.choices[0].message.content.strip() + + # Clean up code blocks if AI wrapped it + if updated_content.startswith('```'): + updated_content = '\n'.join(updated_content.split('\n')[1:-1]) + + # Basic validation - ensure content was actually changed + if updated_content and updated_content != file_content: + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + + self.logger.log(f"āœ… ChatGPT document update successful ({changed_lines} lines affected)") + return updated_content + else: + self.logger.log("āš ļø No changes detected in AI response") + return None + + except Exception as e: + self.logger.log(f"āŒ Error generating updated document with ChatGPT: {str(e)}") + return None + + def _generate_with_context_window(self, file_content: str, old_text: str, new_text: str, file_path: str) -> Optional[str]: + """Use context window approach - AI only sees/modifies a small section + + This physically prevents AI from rewriting entire file by only giving it + the relevant section to work with. + + Args: + file_content: Full file content + old_text: Reference text (guides where to look) + new_text: Suggestions (what to change to) + """ + try: + import difflib + + # Step 1: Find where the reference text is located + lines = file_content.split('\n') + ref_lines = old_text.split('\n') if old_text else [] + + # Find best matching location for reference text + start_line = 0 + if ref_lines: + matcher = difflib.SequenceMatcher(None, ref_lines, lines) + match = matcher.find_longest_match(0, len(ref_lines), 0, len(lines)) + if match.size > 0: + start_line = match.b + self.logger.log(f"šŸ“ Found reference area at line {start_line + 1}") + else: + self.logger.log("šŸ“ Reference text not found, using beginning of file") + + # Step 2: Extract context window (30 lines before, 30 lines after) + window_before = 30 + window_after = 30 + + window_start = max(0, start_line - window_before) + window_end = min(len(lines), start_line + len(ref_lines) + window_after) + + context_window = lines[window_start:window_end] + self.logger.log(f"šŸ“„ Context window: lines {window_start + 1} to {window_end} ({len(context_window)} lines)") + self.logger.log(f" (AI can only modify this section, rest of file is protected)") + + # Step 3: Have AI modify only the context window + context_text = '\n'.join(context_window) + + import openai + client = openai.OpenAI(api_key=self.api_key) + + prompt = f"""You are helping modify a small section of a documentation file. You can ONLY modify the section provided below. + +File: {file_path} +Section location: Lines {window_start + 1} to {window_end} + +REFERENCE TEXT (what user is referring to): +{old_text} + +SUGGESTED CHANGES (what user wants): +{new_text} + +SECTION TO MODIFY: +``` +{context_text} +``` + +INSTRUCTIONS: +1. Understand the user's INTENT from the reference and suggestions: + - "add/include/incorporate a section" = Add a COMPLETE NEW SECTION with heading and full content + - "update/modify/change X" = Modify existing text X intelligently + - "fix/correct" = Make specific correction only + - Be generous with new content when asked to add something + +2. For ADDING content (sections, paragraphs, examples): + - Create complete, well-written content (not just stubs or brief additions) + - Add proper markdown headers (## Best Practices, ### Example, etc.) + - Place it logically (end of section, before ## Related content, etc.) + - Match the document's writing style and tone + +3. For MODIFYING content: + - Change only what's requested + - Leave everything else exactly as-is + +4. Return the ENTIRE section (all {len(context_window)} lines) with your changes +5. No explanations - just the modified section + +OUTPUT THE COMPLETE MODIFIED SECTION:""" + + response = client.chat.completions.create( + model="gpt-4-turbo-preview", + messages=[ + {"role": "system", "content": "You make precise, targeted edits to documentation sections. Return only the modified text, nothing else."}, + {"role": "user", "content": prompt} + ], + temperature=0.1 + ) + + modified_window = response.choices[0].message.content.strip() + + # Clean up code blocks if AI wrapped it + if modified_window.startswith('```'): + modified_window = '\n'.join(modified_window.split('\n')[1:-1]) + + # Step 4: Replace the context window in the full file + modified_lines = modified_window.split('\n') + result_lines = lines[:window_start] + modified_lines + lines[window_end:] + updated_content = '\n'.join(result_lines) + + # Verify change is minimal + diff = list(difflib.unified_diff(lines, result_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + + self.logger.log(f"āœ… Context window approach successful ({changed_lines} lines changed)") + + # Ensure we actually made changes + if updated_content == file_content: + self.logger.log("āš ļø No changes detected, falling back to full-document approach") + return self._generate_updated_document_chatgpt(file_content, old_text, new_text, file_path) + + return updated_content + + except Exception as e: + self.logger.log(f"āŒ Error with context window approach: {str(e)}") + self.logger.log("āš ļø Falling back to full-document approach") + return self._generate_updated_document_chatgpt(file_content, old_text, new_text, file_path) + + def _validate_diff_patch(self, diff_patch: str, original_content: str, old_text: str, new_text: str) -> bool: + """Validate that the AI-generated diff is safe and appropriate""" + try: + # Check for common problems + lines = diff_patch.split('\n') + + # Problem 0: Check for proper diff structure + has_hunk_header = any(line.startswith('@@') for line in lines) + if not has_hunk_header: + self.logger.log("āŒ Invalid diff: Missing @@ hunk headers") + return False + + # Problem 1: Check for duplicate +++ lines + plus_count = sum(1 for line in lines if line.startswith('+++')) + if plus_count > 1: + self.logger.log("āŒ Invalid diff: Multiple +++ lines detected") + return False + + # Problem 2: Check for removal of metadata (title, author, etc.) + for line in lines: + if line.startswith('-') and not line.startswith('---'): + removed_content = line[1:].strip() + # Check if removing metadata + if any(keyword in removed_content.lower() for keyword in ['title:', 'author:', 'description:', 'ms.author:', 'ms.date:']): + self.logger.log(f"āŒ Invalid diff: Attempting to remove metadata: {removed_content}") + return False + + # Problem 3: Check if diff is too large (indicates rewrite) + removed_lines = len([line for line in lines if line.startswith('-') and not line.startswith('---')]) + added_lines = len([line for line in lines if line.startswith('+') and not line.startswith('+++')]) + + if removed_lines > 10: # Too many removals for an additive change + self.logger.log(f"āŒ Invalid diff: Too many removals ({removed_lines} lines)") + return False + + return True + + except Exception as e: + self.logger.log(f"āŒ Error validating diff: {str(e)}") + return False + + def _create_safe_diff(self, file_content: str, old_text: str, new_text: str, file_path: str) -> Optional[str]: + """Create a safer, simpler diff that just adds content without removing anything""" + try: + # Strategy: Find the best location to add the new content and insert it there + lines = file_content.split('\n') + + # Look for common insertion points for adding sections + insertion_point = self._find_safe_insertion_point(lines, old_text, new_text) + + if insertion_point is None: + self.logger.log("āš ļø Could not find safe insertion point") + return None + + # Insert the new content at the found location + new_lines = lines[:insertion_point] + [new_text.strip(), ''] + lines[insertion_point:] + updated_content = '\n'.join(new_lines) + + self.logger.log(f"āœ… Created safe diff - inserting content at line {insertion_point}") + return updated_content + + except Exception as e: + self.logger.log(f"āŒ Error creating safe diff: {str(e)}") + return None + + def _find_safe_insertion_point(self, lines: list, old_text: str, new_text: str) -> Optional[int]: + """Find the best place to insert new content safely""" + try: + # Look for section headers to insert after + for i, line in enumerate(lines): + # If the old_text contains context about where to insert + if old_text and old_text.lower().strip() in line.lower(): + # Insert after this line + return i + 1 + + # Look for pattern where we should insert a new section + # Insert before conclusion, examples, or other sections + if line.strip().startswith('##') and any(keyword in line.lower() for keyword in ['example', 'conclusion', 'summary', 'next steps']): + return i + + # If no specific location found, insert before the last section + for i in range(len(lines) - 1, -1, -1): + if lines[i].strip().startswith('##'): + return i + + # Last resort: insert at 80% through the document + return int(len(lines) * 0.8) + + except Exception: + return None + + def _apply_diff_patch_chatgpt(self, original_content: str, diff_patch: str, file_path: str) -> Optional[str]: + """Apply a unified diff patch to the original content""" + try: + import tempfile + import subprocess + import os + + # Create temporary files + with tempfile.TemporaryDirectory() as temp_dir: + # Write original content to temp file + original_file = os.path.join(temp_dir, "original.txt") + with open(original_file, 'w', encoding='utf-8') as f: + f.write(original_content) + + # Write diff patch to temp file + patch_file = os.path.join(temp_dir, "changes.patch") + with open(patch_file, 'w', encoding='utf-8') as f: + f.write(diff_patch) + + # Apply patch using git apply + try: + subprocess.run(['git', 'apply', '--verbose', patch_file], + cwd=temp_dir, check=True, capture_output=True, text=True) + + # Read the result + with open(original_file, 'r', encoding='utf-8') as f: + return f.read() + + except subprocess.CalledProcessError: + # Fallback to manual patch application + self.logger.log("šŸ“ Git apply failed, trying manual diff application...") + return self._manual_diff_apply_chatgpt(original_content, diff_patch) + + except Exception as e: + self.logger.log(f"āš ļø ChatGPT patch application failed: {str(e)}") + return self._manual_diff_apply_chatgpt(original_content, diff_patch) + + def _manual_diff_apply_chatgpt(self, original_content: str, diff_patch: str) -> Optional[str]: + """Manually apply a diff patch when git apply fails""" + try: + # Detect original line ending style + has_crlf = '\r\n' in original_content + + original_lines = original_content.split('\n') + result_lines = original_lines.copy() + + # Parse the diff patch + diff_lines = diff_patch.split('\n') + current_original_line = 0 + + i = 0 + while i < len(diff_lines): + line = diff_lines[i] + + # Look for @@ headers + if line.startswith('@@'): + # Extract line numbers: @@ -start,count +start,count @@ + parts = line.split() + if len(parts) >= 3: + old_info = parts[1][1:] # Remove the - + if ',' in old_info: + start_line = int(old_info.split(',')[0]) - 1 # Convert to 0-based + else: + start_line = int(old_info) - 1 + + current_original_line = start_line + i += 1 + continue + + # Skip diff headers (must check before processing -/+ lines) + if line.startswith('---') or line.startswith('+++'): + i += 1 + continue + + # Process diff lines + if line.startswith('-'): + # Remove line + if current_original_line < len(result_lines): + del result_lines[current_original_line] + elif line.startswith('+'): + # Add line + new_line = line[1:] # Remove the + + # If original had CRLF and this line doesn't have \r, add it + if has_crlf and not new_line.endswith('\r'): + new_line = new_line + '\r' + result_lines.insert(current_original_line, new_line) + current_original_line += 1 + elif line.startswith(' '): + # Context line - advance + current_original_line += 1 + + i += 1 + + return '\n'.join(result_lines) + + except Exception as e: + self.logger.log(f"āŒ ChatGPT manual diff application failed: {str(e)}") + return None + + def _detect_change_type(self, old_text: str, new_text: str, file_path: str) -> str: + """Detect the type of change requested""" + old_lower = old_text.lower() + new_lower = new_text.lower() + + # Additive indicators + additive_keywords = ['add', 'include', 'incorporate', 'insert', 'create section', 'new section', 'best practices'] + if any(keyword in old_lower or keyword in new_lower for keyword in additive_keywords): + return "ADDITIVE" + + # Corrective indicators + corrective_keywords = ['correct', 'fix', 'grammar', 'spelling', 'typo', 'misspell', 'wrong', 'error'] + if any(keyword in old_lower or keyword in new_lower for keyword in corrective_keywords): + return "CORRECTIVE" + + # If new text is much longer than old text, likely additive + if len(new_text.strip()) > len(old_text.strip()) * 2: + return "ADDITIVE" + + # If similar length, likely corrective + if abs(len(new_text.strip()) - len(old_text.strip())) < 50: + return "CORRECTIVE" + + return "GENERAL" + + def _handle_additive_change_chatgpt(self, file_content: str, old_text: str, new_text: str, file_path: str) -> Optional[str]: + """Handle additive changes using ChatGPT""" + self.logger.log("šŸ”Ø ChatGPT handling additive change - generating new content...") + + try: + import openai + client = openai.OpenAI(api_key=self.api_key) + + prompt = f"""**Instructions:** + +Task: Add new content to the documentation file as requested. + +Steps to complete: + +1. Generate ONLY the new content that should be added to the documentation file +2. Maintain proper formatting, indentation, and markdown structure +3. Make content standalone - don't reference existing content in the file +4. Use Microsoft documentation standards + +> [!IMPORTANT] +> Only create the new content - do not rewrite or modify existing content. +> Preserve markdown formatting, links, and code blocks as appropriate. +> Please ensure the changes align with Microsoft documentation standards. + +File: {file_path} +Request: {old_text} +Content to add: {new_text} + +Generate only the new content that should be added:""" + + response = client.chat.completions.create( + model="gpt-4-turbo-preview", + messages=[ + {"role": "system", "content": "You are a content generator. Generate only new content, never rewrite existing content."}, + {"role": "user", "content": prompt} + ], + temperature=0.1 + ) + + new_content = response.choices[0].message.content.strip() + + # Find best insertion point and insert + insertion_point = self._find_insertion_point(file_content, old_text, file_path) + lines = file_content.split('\n') + lines.insert(insertion_point, '\n' + new_content + '\n') + updated_content = '\n'.join(lines) + + # Count actual changes + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+')]) + + self.logger.log(f"āœ… ChatGPT added new content ({changed_lines} lines added)") + return updated_content + + except Exception as e: + self.logger.log(f"āŒ Error in ChatGPT additive change: {str(e)}") + return None + + def _handle_corrective_change_chatgpt(self, file_content: str, old_text: str, new_text: str, file_path: str) -> Optional[str]: + """Handle corrective changes using ChatGPT""" + self.logger.log("šŸ” ChatGPT handling corrective change - finding specific issues...") + + try: + import openai + client = openai.OpenAI(api_key=self.api_key) + + prompt = f"""**Instructions:** + +Task: Find and fix a specific issue in the documentation file. + +Steps to complete: + +1. Locate the exact text that needs to be corrected in the file +2. Provide the precise replacement text +3. Make minimal changes - fix only what needs to be fixed +4. Maintain existing formatting and structure + +> [!IMPORTANT] +> Only make the specified correction - do not make additional changes. +> Preserve all markdown formatting, links, and code blocks. +> Please ensure the changes align with Microsoft documentation standards. + +Issue: {old_text} +Fix: {new_text} +File: {file_path} + +Return your response in this format: +OLD: [exact text to find] +NEW: [exact replacement text] + +Be very specific - find the minimal text that needs changing. For example: +- If fixing "Microsft" → return OLD: Microsft, NEW: Microsoft +- If fixing grammar → return OLD: [the incorrect phrase], NEW: [corrected phrase] + +File content to search: +{file_content} + +Find the exact text to correct:""" + + response = client.chat.completions.create( + model="gpt-4-turbo-preview", + messages=[ + {"role": "system", "content": "You are a precise error detector. Find exact text that needs correction."}, + {"role": "user", "content": prompt} + ], + temperature=0.1 + ) + + response_text = response.choices[0].message.content.strip() + + # Parse OLD and NEW + old_match = None + new_match = None + + for line in response_text.split('\n'): + if line.startswith('OLD:'): + old_match = line[4:].strip() + elif line.startswith('NEW:'): + new_match = line[4:].strip() + + if old_match and new_match and old_match in file_content: + updated_content = file_content.replace(old_match, new_match) + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + self.logger.log(f"āœ… ChatGPT corrective change successful ({changed_lines} lines affected)") + return updated_content + else: + self.logger.log(f"āš ļø ChatGPT could not find exact text to correct") + return None + + except Exception as e: + self.logger.log(f"āŒ Error in ChatGPT corrective change: {str(e)}") + return None + + def _handle_general_change_chatgpt(self, file_content: str, old_text: str, new_text: str, file_path: str) -> Optional[str]: + """Handle general changes using ChatGPT with enhanced targeting""" + self.logger.log("šŸŽÆ ChatGPT handling general change with enhanced targeting...") + + max_retries = 3 + base_delay = 2 + + for attempt in range(max_retries): + try: + import openai + client = openai.OpenAI(api_key=self.api_key) + + prompt = f"""You are helping make a specific text change in a documentation file. + +File: {file_path} +Change needed: {old_text} +New content: {new_text} + +CRITICAL: Make the SMALLEST possible change. Do not rewrite or reorganize content. + +Your task: +1. Find the specific section that needs changing +2. Make ONLY that change +3. Preserve everything else exactly as-is +4. Return the complete updated file + +Current file content: +{file_content}""" + + response = client.chat.completions.create( + model="gpt-4-turbo-preview", + messages=[ + {"role": "system", "content": "You are a precise file editor. Make minimal targeted changes only."}, + {"role": "user", "content": prompt} + ], + temperature=0.1 + ) + + updated_content = response.choices[0].message.content + + if new_text.strip() in updated_content: + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + + if changed_lines > 30: + self.logger.log(f"āš ļø ChatGPT change affected {changed_lines} lines - may be too broad") + else: + self.logger.log(f"āœ… ChatGPT general change successful ({changed_lines} lines affected)") + + return updated_content + else: + self.logger.log("āš ļø ChatGPT: New text not found in result") + return None + + except Exception as e: + if attempt < max_retries - 1: + delay = base_delay * (2 ** attempt) + self.logger.log(f"āš ļø ChatGPT retry {attempt + 1}/{max_retries} after {delay}s...") + time.sleep(delay) + continue + else: + self.logger.log(f"āŒ Error in ChatGPT general change: {str(e)}") + return None + + return None + + def _find_insertion_point(self, file_content: str, context: str, file_path: str) -> int: + """Find the best place to insert new content""" + lines = file_content.split('\n') + + # For markdown files, try to find a good section to add after + if file_path.endswith('.md'): + # Look for existing sections + for i, line in enumerate(lines): + if line.startswith('#') and i < len(lines) - 1: + # Insert after this section + continue + + # If no good sections found, add at the end + return len(lines) + + # For other files, add at the end + return len(lines) + + +class GitHubCopilotProvider(AIProvider): + """GitHub Copilot provider using GitHub Models API""" + + # GitHub Models API endpoint + GITHUB_MODELS_API_URL = "https://models.inference.ai.azure.com/chat/completions" + + def make_change(self, file_content: str, old_text: str, new_text: str, file_path: str, custom_instructions: str = None) -> Optional[str]: + """Use diff-based approach for surgical edits""" + + # Step 1: Always try direct string replacement first (most accurate) + if old_text and old_text.strip() in file_content: + self.logger.log("āœ… Making direct string replacement (most precise)") + updated_content = file_content.replace(old_text.strip(), new_text.strip()) + if updated_content != file_content: + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + self.logger.log(f"āœ… Direct replacement successful ({changed_lines} lines changed)") + return updated_content + + # Step 2: Use AI to generate full document with targeted changes + self.logger.log("šŸ“ Using GitHub Copilot to modify the document...") + return self._generate_updated_document_copilot(file_content, old_text, new_text, file_path, custom_instructions) + + def _generate_updated_document_copilot(self, file_content: str, old_text: str, new_text: str, file_path: str, custom_instructions: str = None) -> Optional[str]: + """Generate updated document content using GitHub Copilot""" + + try: + import requests + + url = self.GITHUB_MODELS_API_URL + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + } + + # Build custom instructions text + if custom_instructions and custom_instructions.strip(): + custom_instructions_text = f""" + +**Additional Custom Instructions:** +{custom_instructions.strip()} + +""" + else: + custom_instructions_text = "" + + # Handle blank new_text field with dynamic prompt + if not new_text or not new_text.strip(): + # General improvement request when new text is blank + prompt = f"""**Instructions:** + +Task: Review and improve the documentation file based on the reference context provided. + +Steps to complete: + +1. Review the current file content below +2. Look at the reference context: "{old_text}" +3. Improve the relevant sections based on Microsoft documentation standards +4. Maintain the existing formatting, indentation, and markdown structure +5. Return the complete updated file content + +> [!IMPORTANT] +> OUTPUT REQUIREMENTS: +> - Return ONLY the complete file content - no explanatory text, dialog, or commentary +> - Do NOT add any text before or after the file content +> - Do NOT wrap output in markdown code blocks (```), just return the raw content +> - Return the ENTIRE document - no truncation, no placeholders like [Rest of the document here...] +> - Every single line of the original document must be present in your response +> - Focus on areas related to: {old_text} +> - Preserve all markdown formatting, links, and code blocks exactly +> - Please ensure improvements align with Microsoft documentation standards +> - Only make improvements - do not remove existing content unless it's redundant + +{custom_instructions_text} + +**Current File Content:** +``` +{file_content} +``` + +**Context for improvements:** +``` +{old_text} +``` + +Return the complete updated file content now (NO explanatory text):""" + + else: + # Specific replacement when new text is provided + prompt = f"""**Instructions:** + +Task: Update the documentation file with the changes requested. + +Steps to complete: + +1. Review the current file content below +2. Find the reference text that needs to be updated +3. Replace it with the suggested new content +4. Maintain the existing formatting, indentation, and markdown structure +5. Return the complete updated file content + +> [!IMPORTANT] +> OUTPUT REQUIREMENTS: +> - Return ONLY the complete file content - no explanatory text, dialog, or commentary +> - Do NOT add any text before or after the file content +> - Do NOT wrap output in markdown code blocks (```), just return the raw content +> - Return the ENTIRE document - no truncation, no placeholders like [Rest of the document here...] +> - Every single line of the original document must be present in your response +> - Only replace the specified text - do not make additional changes +> - Preserve all markdown formatting, links, and code blocks exactly +> - If the current text cannot be found exactly, search for similar text +> - Please ensure the changes align with Microsoft documentation standards +> - Do not remove any text unless the reference or suggested guidance indicates to do so + +{custom_instructions_text} + +**Current File Content:** +``` +{file_content} +``` + +**Reference text to find and replace:** +``` +{old_text} +``` + +**Suggested new content:** +``` +{new_text} +``` + +Return the complete updated file content now (NO explanatory text):""" + + data = { + "messages": [ + {"role": "system", "content": "You are a document editor. Return ONLY the complete updated file content - no explanatory text, no dialog, no code blocks, no truncation, no placeholders. Output must be the raw complete file content with requested changes."}, + {"role": "user", "content": prompt} + ], + "model": "gpt-4o", + "temperature": 0.1, + "max_tokens": 4096 + } + + response = requests.post(url, headers=headers, json=data, timeout=60) + response.raise_for_status() + + result = response.json() + updated_content = result['choices'][0]['message']['content'].strip() + + # Clean up code blocks if AI wrapped it + if updated_content.startswith('```'): + updated_content = '\n'.join(updated_content.split('\n')[1:-1]) + + # Basic validation - ensure content was actually changed + if updated_content and updated_content != file_content: + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + + self.logger.log(f"āœ… GitHub Copilot document update successful ({changed_lines} lines affected)") + return updated_content + else: + self.logger.log("āš ļø No changes detected in AI response") + return None + + except Exception as e: + self.logger.log(f"āŒ Error generating updated document with GitHub Copilot: {str(e)}") + return None + + def _validate_diff_patch(self, diff_patch: str, original_content: str, old_text: str, new_text: str) -> bool: + """Validate that the AI-generated diff is safe and appropriate""" + try: + # Check for common problems + lines = diff_patch.split('\n') + + # Problem 0: Check for proper diff structure + has_hunk_header = any(line.startswith('@@') for line in lines) + if not has_hunk_header: + self.logger.log("āŒ Invalid diff: Missing @@ hunk headers") + return False + + # Problem 1: Check for duplicate +++ lines + plus_count = sum(1 for line in lines if line.startswith('+++')) + if plus_count > 1: + self.logger.log("āŒ Invalid diff: Multiple +++ lines detected") + return False + + # Problem 2: Check for removal of metadata (title, author, etc.) + for line in lines: + if line.startswith('-') and not line.startswith('---'): + removed_content = line[1:].strip() + # Check if removing metadata + if any(keyword in removed_content.lower() for keyword in ['title:', 'author:', 'description:', 'ms.author:', 'ms.date:']): + self.logger.log(f"āŒ Invalid diff: Attempting to remove metadata: {removed_content}") + return False + + # Problem 3: Check if diff is too large (indicates rewrite) + removed_lines = len([line for line in lines if line.startswith('-') and not line.startswith('---')]) + added_lines = len([line for line in lines if line.startswith('+') and not line.startswith('+++')]) + + if removed_lines > 10: # Too many removals for an additive change + self.logger.log(f"āŒ Invalid diff: Too many removals ({removed_lines} lines)") + return False + + return True + + except Exception as e: + self.logger.log(f"āŒ Error validating diff: {str(e)}") + return False + + def _create_safe_diff(self, file_content: str, old_text: str, new_text: str, file_path: str) -> Optional[str]: + """Create a safer, simpler diff that just adds content without removing anything""" + try: + # Strategy: Find the best location to add the new content and insert it there + lines = file_content.split('\n') + + # Look for common insertion points for adding sections + insertion_point = self._find_safe_insertion_point(lines, old_text, new_text) + + if insertion_point is None: + self.logger.log("āš ļø Could not find safe insertion point") + return None + + # Insert the new content at the found location + new_lines = lines[:insertion_point] + [new_text.strip(), ''] + lines[insertion_point:] + updated_content = '\n'.join(new_lines) + + self.logger.log(f"āœ… Created safe diff - inserting content at line {insertion_point}") + return updated_content + + except Exception as e: + self.logger.log(f"āŒ Error creating safe diff: {str(e)}") + return None + + def _find_safe_insertion_point(self, lines: list, old_text: str, new_text: str) -> Optional[int]: + """Find the best place to insert new content safely""" + try: + # Look for section headers to insert after + for i, line in enumerate(lines): + # If the old_text contains context about where to insert + if old_text and old_text.lower().strip() in line.lower(): + # Insert after this line + return i + 1 + + # Look for pattern where we should insert a new section + # Insert before conclusion, examples, or other sections + if line.strip().startswith('##') and any(keyword in line.lower() for keyword in ['example', 'conclusion', 'summary', 'next steps']): + return i + + # If no specific location found, insert before the last section + for i in range(len(lines) - 1, -1, -1): + if lines[i].strip().startswith('##'): + return i + + # Last resort: insert at 80% through the document + return int(len(lines) * 0.8) + + except Exception: + return None + + def _apply_diff_patch_copilot(self, original_content: str, diff_patch: str, file_path: str) -> Optional[str]: + """Apply a unified diff patch to the original content""" + try: + import tempfile + import subprocess + import os + + # Create temporary files + with tempfile.TemporaryDirectory() as temp_dir: + # Write original content to temp file + original_file = os.path.join(temp_dir, "original.txt") + with open(original_file, 'w', encoding='utf-8') as f: + f.write(original_content) + + # Write diff patch to temp file + patch_file = os.path.join(temp_dir, "changes.patch") + with open(patch_file, 'w', encoding='utf-8') as f: + f.write(diff_patch) + + # Apply patch using git apply + try: + subprocess.run(['git', 'apply', '--verbose', patch_file], + cwd=temp_dir, check=True, capture_output=True, text=True) + + # Read the result + with open(original_file, 'r', encoding='utf-8') as f: + return f.read() + + except subprocess.CalledProcessError: + # Fallback to manual patch application + self.logger.log("šŸ“ Git apply failed, trying manual diff application...") + return self._manual_diff_apply_copilot(original_content, diff_patch) + + except Exception as e: + self.logger.log(f"āš ļø GitHub Copilot patch application failed: {str(e)}") + return self._manual_diff_apply_copilot(original_content, diff_patch) + + def _manual_diff_apply_copilot(self, original_content: str, diff_patch: str) -> Optional[str]: + """Manually apply a diff patch when git apply fails""" + try: + # Detect original line ending style + has_crlf = '\r\n' in original_content + + original_lines = original_content.split('\n') + result_lines = original_lines.copy() + + # Parse the diff patch + diff_lines = diff_patch.split('\n') + current_original_line = 0 + + i = 0 + while i < len(diff_lines): + line = diff_lines[i] + + # Look for @@ headers + if line.startswith('@@'): + # Extract line numbers: @@ -start,count +start,count @@ + parts = line.split() + if len(parts) >= 3: + old_info = parts[1][1:] # Remove the - + if ',' in old_info: + start_line = int(old_info.split(',')[0]) - 1 # Convert to 0-based + else: + start_line = int(old_info) - 1 + + current_original_line = start_line + i += 1 + continue + + # Skip diff headers (must check before processing -/+ lines) + if line.startswith('---') or line.startswith('+++'): + i += 1 + continue + + # Process diff lines + if line.startswith('-'): + # Remove line + if current_original_line < len(result_lines): + del result_lines[current_original_line] + elif line.startswith('+'): + # Add line + new_line = line[1:] # Remove the + + # If original had CRLF and this line doesn't have \r, add it + if has_crlf and not new_line.endswith('\r'): + new_line = new_line + '\r' + result_lines.insert(current_original_line, new_line) + current_original_line += 1 + elif line.startswith(' '): + # Context line - advance + current_original_line += 1 + + i += 1 + + return '\n'.join(result_lines) + + except Exception as e: + self.logger.log(f"āŒ GitHub Copilot manual diff application failed: {str(e)}") + return None + + def _detect_change_type(self, old_text: str, new_text: str, file_path: str) -> str: + """Detect the type of change requested""" + old_lower = old_text.lower() + new_lower = new_text.lower() + + # Additive indicators + additive_keywords = ['add', 'include', 'incorporate', 'insert', 'create section', 'new section', 'best practices'] + if any(keyword in old_lower or keyword in new_lower for keyword in additive_keywords): + return "ADDITIVE" + + # Corrective indicators + corrective_keywords = ['correct', 'fix', 'grammar', 'spelling', 'typo', 'misspell', 'wrong', 'error'] + if any(keyword in old_lower or keyword in new_lower for keyword in corrective_keywords): + return "CORRECTIVE" + + # If new text is much longer than old text, likely additive + if len(new_text.strip()) > len(old_text.strip()) * 2: + return "ADDITIVE" + + # If similar length, likely corrective + if abs(len(new_text.strip()) - len(old_text.strip())) < 50: + return "CORRECTIVE" + + return "GENERAL" + + def _handle_additive_change_copilot(self, file_content: str, old_text: str, new_text: str, file_path: str) -> Optional[str]: + """Handle additive changes using GitHub Copilot""" + self.logger.log("šŸ”Ø GitHub Copilot handling additive change - generating new content...") + + try: + import requests + + url = self.GITHUB_MODELS_API_URL + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + } + + prompt = f"""You are helping add new content to a documentation file. + +File: {file_path} +Request: {old_text} +Content to add: {new_text} + +Your task: Generate ONLY the new content that should be added. Do not rewrite the existing file. + +Rules: +1. Generate ONLY the new section/content to be added +2. Use proper markdown formatting if it's a markdown file +3. Make it standalone - don't reference existing content +4. Do not include any existing file content in your response +5. Return only the new content, nothing else + +Generate the new content now:""" + + data = { + "messages": [ + {"role": "system", "content": "You are a content generator. Generate only new content, never rewrite existing content."}, + {"role": "user", "content": prompt} + ], + "model": "gpt-4o", + "temperature": 0.1, + "max_tokens": 2048 + } + + response = requests.post(url, headers=headers, json=data, timeout=60) + response.raise_for_status() + + result = response.json() + new_content = result['choices'][0]['message']['content'].strip() + + # Clean up markdown blocks if needed + if new_content.startswith("```") and new_content.endswith("```"): + lines = new_content.split('\n') + if len(lines) > 2: + new_content = '\n'.join(lines[1:-1]) + + # Find insertion point and insert + insertion_point = self._find_insertion_point(file_content, old_text, file_path) + lines = file_content.split('\n') + lines.insert(insertion_point, '\n' + new_content + '\n') + updated_content = '\n'.join(lines) + + # Count actual changes + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+')]) + + self.logger.log(f"āœ… GitHub Copilot added new content ({changed_lines} lines added)") + return updated_content + + except Exception as e: + self.logger.log(f"āŒ Error in GitHub Copilot additive change: {str(e)}") + return None + + def _handle_corrective_change_copilot(self, file_content: str, old_text: str, new_text: str, file_path: str) -> Optional[str]: + """Handle corrective changes using GitHub Copilot""" + self.logger.log("šŸ” GitHub Copilot handling corrective change - finding specific issues...") + + try: + import requests + + url = self.GITHUB_MODELS_API_URL + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + } + + prompt = f"""You are helping fix a specific issue in a documentation file. + +Issue: {old_text} +Fix: {new_text} +File: {file_path} + +Your task: Find the EXACT text that needs to be corrected and provide the EXACT replacement. + +Return your response in this format: +OLD: [exact text to find] +NEW: [exact replacement text] + +Be very specific - find the minimal text that needs changing. For example: +- If fixing "Microsft" → return OLD: Microsft, NEW: Microsoft +- If fixing grammar → return OLD: [the incorrect phrase], NEW: [corrected phrase] + +File content to search: +{file_content} + +Find the exact text to correct:""" + + data = { + "messages": [ + {"role": "system", "content": "You are a precise error detector. Find exact text that needs correction."}, + {"role": "user", "content": prompt} + ], + "model": "gpt-4o", + "temperature": 0.1, + "max_tokens": 1024 + } + + response = requests.post(url, headers=headers, json=data, timeout=60) + response.raise_for_status() + + result = response.json() + response_text = result['choices'][0]['message']['content'].strip() + + # Parse OLD and NEW + old_match = None + new_match = None + + for line in response_text.split('\n'): + if line.startswith('OLD:'): + old_match = line[4:].strip() + elif line.startswith('NEW:'): + new_match = line[4:].strip() + + if old_match and new_match and old_match in file_content: + updated_content = file_content.replace(old_match, new_match) + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + self.logger.log(f"āœ… GitHub Copilot corrective change successful ({changed_lines} lines affected)") + return updated_content + else: + self.logger.log(f"āš ļø GitHub Copilot could not find exact text to correct") + return None + + except Exception as e: + self.logger.log(f"āŒ Error in GitHub Copilot corrective change: {str(e)}") + return None + + def _handle_general_change_copilot(self, file_content: str, old_text: str, new_text: str, file_path: str) -> Optional[str]: + """Handle general changes using GitHub Copilot with enhanced targeting""" + self.logger.log("šŸŽÆ GitHub Copilot handling general change with enhanced targeting...") + + try: + import requests + + url = self.GITHUB_MODELS_API_URL + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + } + + prompt = f"""You are helping make a specific text change in a documentation file. + +File: {file_path} +Change needed: {old_text} +New content: {new_text} + +CRITICAL: Make the SMALLEST possible change. Do not rewrite or reorganize content. + +Your task: +1. Find the specific section that needs changing +2. Make ONLY that change +3. Preserve everything else exactly as-is +4. Return the complete updated file + +Current file content: +{file_content}""" + + data = { + "messages": [ + {"role": "system", "content": "You are a precise file editor. Make minimal targeted changes only."}, + {"role": "user", "content": prompt} + ], + "model": "gpt-4o", + "temperature": 0.1, + "max_tokens": 8000 + } + + response = requests.post(url, headers=headers, json=data, timeout=60) + response.raise_for_status() + + result = response.json() + updated_content = result['choices'][0]['message']['content'].strip() + + # Clean up markdown code blocks + if updated_content.startswith("```"): + lines = updated_content.split('\n') + if len(lines) > 2: + if lines[0].startswith("```") and lines[-1].strip() == "```": + updated_content = '\n'.join(lines[1:-1]) + + if new_text.strip() in updated_content: + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + + if changed_lines > 30: + self.logger.log(f"āš ļø GitHub Copilot change affected {changed_lines} lines - may be too broad") + else: + self.logger.log(f"āœ… GitHub Copilot general change successful ({changed_lines} lines affected)") + + return updated_content + else: + self.logger.log("āš ļø GitHub Copilot: New text not found in result") + return None + + except Exception as e: + self.logger.log(f"āŒ Error in GitHub Copilot general change: {str(e)}") + return None + + def _find_insertion_point(self, file_content: str, context: str, file_path: str) -> int: + """Find the best place to insert new content""" + lines = file_content.split('\n') + + # For markdown files, try to find a good section to add after + if file_path.endswith('.md'): + # Look for existing sections + for i, line in enumerate(lines): + if line.startswith('#') and i < len(lines) - 1: + # Insert after this section + continue + + # If no good sections found, add at the end + return len(lines) + + # For other files, add at the end + return len(lines) + + +class LocalGitManager: + """Manages local git operations for making changes before creating PRs""" + + def __init__(self, logger: Logger, github_token: str): + self.logger = logger + self.github_token = github_token + self.last_diff_content = "" # Store the last generated diff content + + def get_repo_path(self, owner: str, repo: str, local_path: Optional[str] = None) -> Path: + """Get or create local repository path + + Args: + owner: Repository owner + repo: Repository name + local_path: Base path from LOCAL_REPO_PATH setting + + Returns: + Full path to the repository (base/owner/repo) + """ + # If LOCAL_REPO_PATH is configured, use it as the base directory + if local_path and local_path.strip(): + base_path = Path(local_path.strip()) + + # Warn if OneDrive path detected + if 'OneDrive' in str(base_path): + self.logger.log("āš ļø WARNING: Local Repo Path is in a OneDrive folder") + self.logger.log(" OneDrive sync can cause file locking issues with git operations") + self.logger.log(" Consider using a non-OneDrive location (e.g., C:\\git\\repos)") + + # Create base directory if it doesn't exist + if not base_path.exists(): + self.logger.log(f"Creating local repo directory: {base_path}") + try: + base_path.mkdir(parents=True, exist_ok=True) + except Exception as e: + self.logger.log(f"āš ļø Could not create directory {base_path}: {e}") + self.logger.log(" Falling back to default location") + # Fall through to default + else: + # Successfully created or exists, use it + repo_path = base_path / owner / repo + return repo_path + else: + # Base path exists, use it + repo_path = base_path / owner / repo + return repo_path + + # Default: Use Downloads folder (typically not in OneDrive) + downloads = Path.home() / "Downloads" + repo_path = downloads / "github_repos" / owner / repo + return repo_path + + def clone_or_pull_repo(self, owner: str, repo: str, local_path: Optional[str] = None) -> Optional[Path]: + """Clone repository if it doesn't exist, or pull latest changes if it does""" + try: + import git + import gc + + repo_path = self.get_repo_path(owner, repo, local_path) + repo_url = f"https://{self.github_token}@github.com/{owner}/{repo}.git" + + if repo_path.exists() and (repo_path / ".git").exists(): + # Repository exists, try to update it + self.logger.log(f"Repository exists at {repo_path}, updating...") + git_repo = None + try: + git_repo = git.Repo(repo_path) + + # Try alternative update methods that are more reliable + try: + # Method 1: Fetch and reset (more reliable than pull) + self.logger.log("Fetching latest changes...") + git_repo.git.fetch('origin') + + # Make sure we're on main/master + try: + git_repo.git.checkout('main') + git_repo.git.reset('--hard', 'origin/main') + except: + git_repo.git.checkout('master') + git_repo.git.reset('--hard', 'origin/master') + + self.logger.log("āœ… Repository updated successfully") + return repo_path + except Exception as fetch_error: + self.logger.log(f"āš ļø Fetch/reset failed: {fetch_error}") + # Try simple pull as fallback + origin = git_repo.remotes.origin + origin.pull() + self.logger.log("āœ… Pulled latest changes") + return repo_path + + except Exception as e: + self.logger.log(f"āš ļø Error updating repo: {e}") + self.logger.log("Repository will be reused as-is for this operation") + + # Don't try to delete - just reuse the existing repo + # This avoids file locking issues + return repo_path + finally: + # Always clean up + if git_repo: + try: + git_repo.close() + git_repo.__del__() + except: + pass + git_repo = None + gc.collect() + + # Clone repository + self.logger.log(f"Cloning repository to {repo_path}...") + repo_path.parent.mkdir(parents=True, exist_ok=True) + git.Repo.clone_from(repo_url, repo_path) + self.logger.log("āœ… Repository cloned successfully") + return repo_path + + except ImportError: + self.logger.log("āŒ GitPython not installed. Run: pip install GitPython") + return None + except Exception as e: + self.logger.log(f"āŒ Error with git operations: {str(e)}") + return None + + def _safe_remove_tree(self, path: Path, max_retries: int = 3) -> bool: + """Safely remove a directory tree with retry logic for Windows file locking""" + import gc + + for attempt in range(max_retries): + try: + if path.exists(): + # On Windows, make files writable before deletion + if sys.platform == 'win32': + for root, _, files in os.walk(str(path)): + for fname in files: + fpath = os.path.join(root, fname) + try: + os.chmod(fpath, 0o777) + except: + pass + + shutil.rmtree(path, ignore_errors=False) + self.logger.log(f"āœ… Removed directory: {path}") + return True + except Exception as e: + if attempt < max_retries - 1: + self.logger.log(f"āš ļø Attempt {attempt + 1} failed to remove {path}: {e}") + gc.collect() # Force garbage collection + time.sleep(1) # Wait longer between retries + else: + self.logger.log(f"āŒ Failed to remove {path} after {max_retries} attempts: {e}") + self.logger.log(f"šŸ’” TIP: Close any file explorers or editors that might have this folder open") + return False + return False + + def apply_diff_and_commit(self, repo_path: Path, branch_name: str, + file_path: str, diff_patch: str, commit_message: str) -> bool: + """Apply diff patch using git apply and commit changes + + This is the preferred method as it uses native git to apply patches, + which properly handles line endings, whitespace, and other edge cases. + """ + git_repo = None + try: + import git + import tempfile + import os + + git_repo = git.Repo(repo_path) + + # Create new branch from main + self.logger.log(f"Creating branch {branch_name}...") + try: + git_repo.git.checkout('main') + git_repo.git.pull() + except: + git_repo.git.checkout('master') + git_repo.git.pull() + + git_repo.git.checkout('-b', branch_name) + self.logger.log(f"āœ… Branch {branch_name} created") + + # Write diff patch to temp file + # Ensure patch ends with newline for git apply compatibility + patch_content = diff_patch if diff_patch.endswith('\n') else diff_patch + '\n' + + with tempfile.NamedTemporaryFile(mode='w', suffix='.patch', delete=False, encoding='utf-8', newline='\n') as patch_file: + patch_file.write(patch_content) + patch_file_path = patch_file.name + + try: + # Apply patch using git apply + self.logger.log(f"Applying diff patch to {file_path}...") + self.logger.log(f"Patch file: {patch_file_path}") + + try: + git_repo.git.apply('--verbose', '--whitespace=nowarn', patch_file_path) + self.logger.log("āœ… Diff patch applied successfully using git apply") + except Exception as apply_error: + self.logger.log(f"āš ļø git apply failed: {str(apply_error)}") + + # Log the patch content for debugging + self.logger.log("šŸ“„ Patch content (first 1000 chars):") + self.logger.log(patch_content[:1000]) + + self.logger.log("šŸ“ Attempting to apply patch with --3way merge...") + try: + # Try with 3-way merge which is more forgiving + git_repo.git.apply('--3way', '--whitespace=nowarn', patch_file_path) + self.logger.log("āœ… Diff patch applied using 3-way merge") + except Exception as merge_error: + self.logger.log(f"āš ļø 3-way merge also failed: {str(merge_error)}") + + # Try one more time with --ignore-whitespace + self.logger.log("šŸ“ Attempting with --ignore-whitespace...") + try: + git_repo.git.apply('--ignore-whitespace', '--whitespace=nowarn', patch_file_path) + self.logger.log("āœ… Diff patch applied with --ignore-whitespace") + except: + self.logger.log("āŒ All git apply methods failed") + # Keep the patch file for debugging + self.logger.log(f"šŸ’¾ Patch file saved for debugging: {patch_file_path}") + raise + + # Stage and commit + git_repo.index.add([file_path]) + git_repo.index.commit(commit_message) + self.logger.log("āœ… Changes committed") + + return True + + finally: + # Clean up temp patch file only on success + if git_repo and git_repo.head.is_valid(): + try: + os.unlink(patch_file_path) + except: + pass + + except Exception as e: + self.logger.log(f"āŒ Error applying diff and committing: {str(e)}") + self.logger.log("šŸ’” This may indicate the file has changed since it was fetched") + return False + finally: + if git_repo: + try: + git_repo.close() + git_repo.__del__() + except: + pass + import gc + gc.collect() + + def create_branch_and_commit(self, repo_path: Path, branch_name: str, + file_path: str, updated_content: str, + commit_message: str, line_ending: str = '\n') -> bool: + """Create branch, update file, and commit + + Args: + line_ending: Original line ending style to preserve ('\n' or '\r\n') + + NOTE: This method is deprecated in favor of apply_diff_and_commit which uses git apply. + """ + git_repo = None + try: + import git + import gc + + git_repo = git.Repo(repo_path) + + # Create new branch from main + self.logger.log(f"Creating branch {branch_name}...") + try: + git_repo.git.checkout('main') + git_repo.git.pull() + except: + git_repo.git.checkout('master') + git_repo.git.pull() + + git_repo.git.checkout('-b', branch_name) + self.logger.log(f"āœ… Branch {branch_name} created") + + # Update the file + full_file_path = repo_path / file_path + if not full_file_path.exists(): + self.logger.log(f"āŒ File not found: {file_path}") + return False + + self.logger.log(f"Writing changes to {file_path}...") + + # Preserve original line endings + if line_ending == '\r\n': + # Normalize to CRLF if original had CRLF + content_to_write = updated_content.replace('\r\n', '\n').replace('\n', '\r\n') + self.logger.log(f"āœ… Preserving CRLF line endings") + else: + content_to_write = updated_content + + full_file_path.write_text(content_to_write, encoding='utf-8', newline='') + + # Stage and commit + git_repo.index.add([file_path]) + git_repo.index.commit(commit_message) + self.logger.log("āœ… Changes committed") + + return True + + except Exception as e: + self.logger.log(f"āŒ Error creating branch and committing: {str(e)}") + return False + finally: + if git_repo: + try: + git_repo.close() + git_repo.__del__() + except: + pass + import gc + gc.collect() # Force garbage collection to release file handles + + def push_branch(self, repo_path: Path, branch_name: str) -> bool: + """Push branch to remote""" + git_repo = None + try: + import git + import gc + + self.logger.log(f"Pushing branch {branch_name} to remote...") + git_repo = git.Repo(repo_path) + origin = git_repo.remotes.origin + origin.push(branch_name) + self.logger.log("āœ… Branch pushed to remote") + return True + + except Exception as e: + self.logger.log(f"āŒ Error pushing branch: {str(e)}") + return False + finally: + if git_repo: + try: + git_repo.close() + git_repo.__del__() + except: + pass + import gc + gc.collect() # Force garbage collection to release file handles + + def make_ai_assisted_change(self, owner: str, repo: str, branch_name: str, + file_path: str, old_text: str, new_text: str, + commit_message: str, ai_provider: AIProvider, + local_path: Optional[str] = None, custom_instructions: str = None) -> Tuple[bool, Optional[str]]: + """ + Complete workflow: clone, make TARGETED changes, commit, and push + This uses direct string replacement to avoid AI rewriting entire files + + Returns: + (success: bool, error_message: Optional[str]) + """ + try: + # Step 1: Clone or pull repository + repo_path = self.clone_or_pull_repo(owner, repo, local_path) + if not repo_path: + return False, "Failed to clone/pull repository" + + # Step 2: Read the current file + full_file_path = repo_path / file_path + if not full_file_path.exists(): + return False, f"File not found: {file_path}" + + self.logger.log(f"Reading file: {file_path}") + # Read in binary mode to detect and preserve line endings + raw_bytes = full_file_path.read_bytes() + current_content = raw_bytes.decode('utf-8') + + # Detect original line ending style + original_line_ending = '\r\n' if b'\r\n' in raw_bytes else '\n' + self.logger.log(f"šŸ“ Detected line endings: {'CRLF' if original_line_ending == '\\r\\n' else 'LF'}") + + # Normalize everything to LF for consistent processing + normalized_content = current_content.replace('\r\n', '\n') + normalized_old = old_text.replace('\r\n', '\n') + normalized_new = new_text.replace('\r\n', '\n') + + # Step 3: Make TARGETED change + updated_content = None + + # Strategy 1: Very conservative direct replacement (only for exact, specific content) + # Only use this for replacements where old_text is substantial and very specific + use_direct_replacement = ( + normalized_old.strip() and + len(normalized_old.strip()) > 20 and # Must be substantial content + normalized_old.strip().count('\n') >= 2 and # Must be multi-line + normalized_old.strip() in normalized_content and + normalized_content.count(normalized_old.strip()) == 1 # Must be unique match + ) + + if use_direct_replacement: + self.logger.log("āœ… Making very targeted direct replacement") + updated_content = normalized_content.replace(normalized_old.strip(), normalized_new.strip()) + + # Verify the replacement worked and was targeted + original_lines = normalized_content.split('\n') + updated_lines = updated_content.split('\n') + + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + + if changed_lines > 20: # If too many changes, something went wrong + self.logger.log(f"āš ļø Direct replacement affected {changed_lines} lines - falling back to AI") + updated_content = None # Fall back to AI + else: + self.logger.log(f"āœ… Direct replacement successful ({changed_lines} lines changed)") + + # Strategy 2: Use AI to generate complete updated document + if not updated_content: + self.logger.log("Using AI to modify complete document...") + self.logger.log(f"AI Provider type: {type(ai_provider).__name__}") + self.logger.log(f"Old text preview: {normalized_old[:100]}...") + self.logger.log(f"New text preview: {normalized_new[:100]}...") + + # Pass normalized versions to AI provider + try: + updated_content = ai_provider.make_change(normalized_content, normalized_old, normalized_new, file_path, custom_instructions) + if not updated_content: + self.logger.log("āŒ AI provider returned None or empty content") + return False, "AI failed to make the change - provider returned no content" + else: + self.logger.log(f"āœ… AI provider returned content ({len(updated_content)} characters)") + except Exception as e: + self.logger.log(f"āŒ AI provider threw exception: {str(e)}") + return False, f"AI failed to make the change - error: {str(e)}" + + # Step 4: Apply changes directly using file write method + # Restore original line endings in the updated content before writing + if original_line_ending == '\r\n': + updated_content_with_endings = updated_content.replace('\n', '\r\n') + else: + updated_content_with_endings = updated_content + + if not self.create_branch_and_commit(repo_path, branch_name, file_path, + updated_content_with_endings, commit_message): + return False, "Failed to apply changes using direct file write method" + + self.logger.log("āœ… Changes applied using direct file write method") + + # Step 5: Push to remote + if not self.push_branch(repo_path, branch_name): + return False, "Failed to push branch to remote" + + self.logger.log("āœ… AI-assisted changes completed successfully") + return True, None + + except Exception as e: + error_msg = f"Error in AI-assisted change workflow: {str(e)}" + self.logger.log(f"āŒ {error_msg}") + return False, error_msg + + def get_last_diff_content(self) -> str: + """Get the last generated diff content for display in the UI""" + return self.last_diff_content + + def clear_diff_content(self): + """Clear the stored diff content""" + self.last_diff_content = "" + + def get_git_diff_from_repo(self, repo_path: str, branch_name: str) -> str: + """Get the actual git diff from the repository for the specified branch""" + try: + import subprocess + import os + + self.logger.log(f"šŸ” Getting git diff from: {repo_path}") + self.logger.log(f"šŸ” Branch: {branch_name}") + + # Change to repo directory + original_dir = os.getcwd() + + if not os.path.exists(repo_path): + self.logger.log(f"āŒ Repository path does not exist: {repo_path}") + return "" + + os.chdir(repo_path) + self.logger.log(f"šŸ” Changed to directory: {os.getcwd()}") + + try: + diff_content = "" + + # Check current git status first + try: + result = subprocess.run(['git', 'status', '--porcelain'], + capture_output=True, text=True, check=True, encoding='utf-8', errors='replace') + if result.stdout.strip(): + self.logger.log(f"šŸ” Git status shows changes: {result.stdout.strip()[:100]}...") + else: + self.logger.log("šŸ” Git status shows no uncommitted changes") + except Exception as e: + self.logger.log(f"āš ļø Could not check git status: {e}") + + # Check current branch + try: + result = subprocess.run(['git', 'branch', '--show-current'], + capture_output=True, text=True, check=True, encoding='utf-8', errors='replace') + current_branch = result.stdout.strip() + self.logger.log(f"šŸ” Current branch: {current_branch}") + except Exception as e: + self.logger.log(f"āš ļø Could not get current branch: {e}") + + # First, try to get diff from the current commit against main/master + try: + self.logger.log("šŸ” Trying: git diff main HEAD") + result = subprocess.run(['git', 'diff', 'main', 'HEAD'], + capture_output=True, text=True, check=True, encoding='utf-8', errors='replace') + diff_content = result.stdout + if diff_content: + self.logger.log(f"āœ… Retrieved git diff against main ({len(diff_content)} characters)") + else: + self.logger.log("šŸ” No diff found against main") + except subprocess.CalledProcessError as e: + self.logger.log(f"šŸ” git diff main HEAD failed: {e}") + try: + self.logger.log("šŸ” Trying: git diff master HEAD") + result = subprocess.run(['git', 'diff', 'master', 'HEAD'], + capture_output=True, text=True, check=True, encoding='utf-8', errors='replace') + diff_content = result.stdout + if diff_content: + self.logger.log(f"āœ… Retrieved git diff against master ({len(diff_content)} characters)") + else: + self.logger.log("šŸ” No diff found against master") + except subprocess.CalledProcessError as e: + self.logger.log(f"šŸ” git diff master HEAD failed: {e}") + + # If still no diff, try against previous commit (only if it exists) + if not diff_content: + try: + self.logger.log("šŸ” Trying: git rev-parse --verify HEAD~1") + subprocess.run(['git', 'rev-parse', '--verify', 'HEAD~1'], + capture_output=True, check=True, encoding='utf-8', errors='replace') + # If we get here, HEAD~1 exists + self.logger.log("šŸ” Trying: git diff HEAD~1 HEAD") + result = subprocess.run(['git', 'diff', 'HEAD~1', 'HEAD'], + capture_output=True, text=True, check=True, encoding='utf-8', errors='replace') + diff_content = result.stdout + if diff_content: + self.logger.log(f"āœ… Retrieved git diff against HEAD~1 ({len(diff_content)} characters)") + else: + self.logger.log("šŸ” No diff found against HEAD~1") + except subprocess.CalledProcessError as e: + self.logger.log(f"šŸ” HEAD~1 doesn't exist or diff failed: {e}") + + # If still no diff, try to get the diff of all changes in the current commit + if not diff_content: + try: + self.logger.log("šŸ” Trying: git show --format= HEAD") + result = subprocess.run(['git', 'show', '--format=', 'HEAD'], + capture_output=True, text=True, check=True, encoding='utf-8', errors='replace') + diff_content = result.stdout + if diff_content: + self.logger.log(f"āœ… Retrieved git show for HEAD commit ({len(diff_content)} characters)") + else: + self.logger.log("šŸ” No content from git show HEAD") + except subprocess.CalledProcessError as e: + self.logger.log(f"šŸ” git show HEAD failed: {e}") + + # If we have diff content, save it to a .diff file + if diff_content: + self._save_diff_to_file(diff_content, repo_path, branch_name) + else: + self.logger.log("āŒ No diff content found using any method") + + return diff_content + + finally: + os.chdir(original_dir) + self.logger.log(f"šŸ” Changed back to: {os.getcwd()}") + + except Exception as e: + self.logger.log(f"āŒ Error getting git diff from repository: {str(e)}") + import traceback + self.logger.log(f"āŒ Traceback: {traceback.format_exc()}") + return "" + + def _save_diff_to_file(self, diff_content: str, repo_path: str, branch_name: str) -> None: + """Save the diff content to a .diff file in the repository""" + try: + import os + from datetime import datetime + + # Create a filename with timestamp and branch name + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + safe_branch_name = branch_name.replace('/', '_').replace(':', '_') + diff_filename = f"changes_{safe_branch_name}_{timestamp}.diff" + diff_filepath = os.path.join(repo_path, diff_filename) + + # Write the diff content to file + with open(diff_filepath, 'w', encoding='utf-8') as f: + f.write(diff_content) + + self.logger.log(f"šŸ’¾ Saved diff to: {diff_filename}") + + except Exception as e: + self.logger.log(f"āŒ Error saving diff to file: {str(e)}") + + +def create_ai_provider(provider_name: str, api_key: str, logger: Logger, ollama_url: str = None, ollama_model: str = None) -> Optional[AIProvider]: + """Factory function to create AI provider instances""" + if provider_name.lower() == 'claude': + return ClaudeProvider(api_key, logger) + elif provider_name.lower() in ['chatgpt', 'openai', 'gpt']: + return ChatGPTProvider(api_key, logger) + elif provider_name.lower() in ['github-copilot', 'copilot', 'github_copilot']: + return GitHubCopilotProvider(api_key, logger) + elif provider_name.lower() == 'ollama': + # For Ollama, api_key is optional (can be empty string) + return OllamaProvider(api_key or "", logger, ollama_url, ollama_model) + else: + logger.log(f"āš ļø Unknown AI provider: {provider_name}") + return None + + +def get_detailed_python_environment_info() -> dict: + """Get detailed information about the current Python environment + + Returns: + dict: Environment information including venv status, Python version, etc. + """ + import sys + import os + + # Detect virtual environment + in_venv = (hasattr(sys, 'real_prefix') or + (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix) or + os.environ.get('VIRTUAL_ENV') is not None) + + env_info = { + 'in_venv': in_venv, + 'python_version': f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", + 'python_executable': sys.executable, + } + + if in_venv: + venv_path = os.environ.get('VIRTUAL_ENV', sys.prefix) + env_info['venv_name'] = os.path.basename(venv_path) + env_info['venv_path'] = venv_path + else: + env_info['venv_name'] = None + env_info['venv_path'] = None + + return env_info + + +def install_ai_packages_enhanced(packages: List[str], parent_window=None) -> bool: + """Enhanced AI provider package installation with better error handling + + Returns: + bool: True if installation successful or user declined, False if failed + """ + import subprocess + import sys + import os + + if not packages: + return True + + # Detect virtual environment + in_venv = (hasattr(sys, 'real_prefix') or + (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix) or + os.environ.get('VIRTUAL_ENV') is not None) + + venv_info = "" + install_location = "" + + if in_venv: + venv_path = os.environ.get('VIRTUAL_ENV', sys.prefix) + venv_name = os.path.basename(venv_path) + install_location = f"virtual environment '{venv_name}'" + venv_info = f"\n🌐 Virtual environment detected: {venv_name}" + else: + install_location = "system-wide (may require administrator rights)" + venv_info = f"\nāš ļø No virtual environment detected - installing system-wide" + + # Create confirmation message + package_list = ', '.join(packages) + message = (f"The following packages are required for AI functionality:\n\n" + f"{package_list}\n\n" + f"Installation location: {install_location}" + f"{venv_info}\n\n" + f"Would you like to install them now?\n\n" + f"This will run: pip install {' '.join(packages)}") + + +def validate_ai_provider_setup(config: dict, parent_window=None) -> bool: + """Validate AI provider setup and offer to install missing modules + + Returns: + bool: True if setup is valid or user handled the issue + """ + ai_provider = config.get('AI_PROVIDER', '').lower() + + if not ai_provider or ai_provider == 'none': + return True # No AI provider selected, nothing to validate + + # Create a temporary AI manager to check modules + temp_manager = AIManager() + + # Check if modules are available + available, missing = temp_manager.check_ai_module_availability(ai_provider) + + if available: + return True # All modules available + + print(f"āš ļø AI Provider '{ai_provider}' selected but missing required packages: {', '.join(missing)}") + + # Offer to install missing packages using enhanced installer + success = install_ai_packages_enhanced(missing, parent_window) + + if success: + # Re-check availability after installation + available, still_missing = temp_manager.check_ai_module_availability(ai_provider) + if available: + print(f"āœ… AI Provider '{ai_provider}' is now ready to use") + return True + else: + print(f"āš ļø Some packages may still be missing: {', '.join(still_missing)}") + print("Please restart the application after installation completes") + return False + + return False + + +class OllamaProvider(AIProvider): + """Ollama AI provider for self-hosted models""" + + def __init__(self, api_key: str, logger: Logger, ollama_url: str = None, model: str = None): + super().__init__(api_key, logger) + self.ollama_url = ollama_url or "http://localhost:11434" + self.model = model or "llama2" + + # Normalize URL + if not self.ollama_url.startswith('http'): + self.ollama_url = f"http://{self.ollama_url}" + + def make_change(self, file_content: str, old_text: str, new_text: str, file_path: str, custom_instructions: str = None) -> Optional[str]: + """Make targeted changes using Ollama""" + + # Step 1: Try direct string replacement first + if old_text and old_text.strip() in file_content: + self.logger.log("āœ… Making direct string replacement (reference text found exactly)") + updated_content = file_content.replace(old_text.strip(), new_text.strip()) + if updated_content != file_content: + original_lines = file_content.split('\n') + updated_lines = updated_content.split('\n') + import difflib + diff = list(difflib.unified_diff(original_lines, updated_lines, lineterm='')) + changed_lines = len([line for line in diff if line.startswith('+') or line.startswith('-')]) + self.logger.log(f"āœ… Direct replacement successful ({changed_lines} lines changed)") + return updated_content + + # Step 2: Use Ollama to generate full document with targeted changes + self.logger.log(f"šŸ“ Using Ollama ({self.model}) to modify the document...") + return self._generate_updated_document(file_content, old_text, new_text, file_path, custom_instructions) + + def _generate_updated_document(self, file_content: str, old_text: str, new_text: str, file_path: str, custom_instructions: str = None) -> Optional[str]: + """Generate updated document content using Ollama""" + + try: + import requests + + # Build custom instructions text + if custom_instructions and custom_instructions.strip(): + custom_instructions_text = f""" +**Additional Custom Instructions:** +{custom_instructions.strip()} + +""" + else: + custom_instructions_text = "" + + # Handle case where new_text is empty or just guidance + if new_text and new_text.strip() and not new_text.strip().lower().startswith(' [!IMPORTANT] +> OUTPUT REQUIREMENTS: +> - Return ONLY the complete file content - no explanatory text, dialog, or commentary +> - Do NOT add any text before or after the file content +> - Do NOT wrap output in markdown code blocks (```), just return the raw content +> - Return the ENTIRE document - no truncation, no placeholders like [Rest of the document here...] +> - Every single line of the original document must be present in your response +> - Preserve all markdown formatting, links, and code blocks exactly +> - Only make changes that fulfill the specified request + +{custom_instructions_text} + +**Current File Content:** +``` +{file_content} +``` + +{guidance_text} + +Return the complete updated file content now (NO explanatory text):""" + + # Prepare request headers + headers = { + "Content-Type": "application/json" + } + if self.api_key: + headers["Authorization"] = f"Bearer {self.api_key}" + + # Prepare request payload + payload = { + "model": self.model, + "prompt": prompt, + "stream": False, + "options": { + "temperature": 0.3, # Lower temperature for more consistent output + "num_predict": -1, # Generate as many tokens as needed + } + } + + # Make request to Ollama + self.logger.log(f"šŸ”„ Sending request to Ollama at {self.ollama_url}...") + response = requests.post( + f"{self.ollama_url}/api/generate", + json=payload, + headers=headers, + timeout=300 # 5 minute timeout for large documents + ) + response.raise_for_status() + + result = response.json() + updated_content = result.get("response", "").strip() + + if not updated_content: + self.logger.log("āŒ Ollama returned empty response") + return None + + # Clean up response + updated_content = self._clean_ai_response(updated_content) + + # Validate that we got the full document back + original_line_count = len(file_content.split('\n')) + updated_line_count = len(updated_content.split('\n')) + + if updated_line_count < original_line_count * 0.5: # Less than 50% of original lines + self.logger.log(f"āš ļø Warning: Updated document seems truncated ({updated_line_count} vs {original_line_count} lines)") + self.logger.log("āŒ AI may have truncated the document - using fallback") + return None + + self.logger.log(f"āœ… Successfully generated updated document ({updated_line_count} lines)") + return updated_content + + except requests.exceptions.ConnectionError: + self.logger.log(f"āŒ Could not connect to Ollama server at {self.ollama_url}") + self.logger.log(" Make sure Ollama is running and the URL is correct") + return None + except requests.exceptions.Timeout: + self.logger.log("āŒ Request to Ollama server timed out") + return None + except requests.exceptions.HTTPError as e: + if e.response.status_code == 401: + self.logger.log("āŒ Authentication failed - check your Ollama API key") + elif e.response.status_code == 404: + self.logger.log(f"āŒ Model '{self.model}' not found on Ollama server") + self.logger.log(f" Use 'ollama pull {self.model}' to download it") + else: + self.logger.log(f"āŒ HTTP error from Ollama: {e}") + return None + except Exception as e: + self.logger.log(f"āŒ Error calling Ollama: {str(e)}") + import traceback + traceback.print_exc() + return None + + def _clean_ai_response(self, response: str) -> str: + """Clean up AI response by removing markdown code blocks and explanatory text""" + + # Remove markdown code blocks if present + if response.startswith('```'): + lines = response.split('\n') + # Remove first line if it's a code fence + if lines[0].startswith('```'): + lines = lines[1:] + # Remove last line if it's a code fence + if lines and lines[-1].strip() == '```': + lines = lines[:-1] + response = '\n'.join(lines) + + return response.strip() + + +# AI Providers availability flag - now always True since they're included +AI_PROVIDERS_AVAILABLE = True + + +class AIManager: + """Manages AI providers and module installations""" + + def __init__(self, logger=None): + self.logger = logger + self.last_diff_content = "" # Store the last generated diff content + + def log(self, message: str) -> None: + """Log a message with Unicode support""" + if self.logger: + self.logger.log(message) + else: + try: + print(message) + except UnicodeEncodeError: + # Fallback: replace Unicode emojis with ASCII equivalents + safe_message = message.replace('āœ…', '[SUCCESS]').replace('āŒ', '[ERROR]').replace('āš ļø', '[WARNING]').replace('šŸ“‹', '[INFO]').replace('šŸ“„', '[FILE]').replace('šŸ“', '[LOCATION]').replace('šŸ“', '[EDIT]') + print(safe_message) + + def check_ai_module_availability(self, provider_name: str) -> Tuple[bool, List[str]]: + """Check if AI provider modules are available and return missing packages + + Args: + provider_name: 'chatgpt', 'claude', 'anthropic', 'github-copilot', or 'ollama' + + Returns: + tuple: (all_available, missing_packages) + """ + missing_packages = [] + + # Common packages needed for AI providers + required_common = ['GitPython'] + + # Provider-specific packages + if provider_name.lower() == 'chatgpt': + required_packages = required_common + ['openai'] + elif provider_name.lower() in ['claude', 'anthropic']: + required_packages = required_common + ['anthropic'] + elif provider_name.lower() in ['github-copilot', 'copilot', 'github_copilot']: + required_packages = required_common + ['requests'] + elif provider_name.lower() == 'ollama': + required_packages = required_common + ['requests'] + else: + return True, [] # Unknown provider, assume no check needed + + for package in required_packages: + try: + if package == 'GitPython': + import git + elif package == 'openai': + import openai + elif package == 'anthropic': + import anthropic + elif package == 'requests': + import requests + except ImportError: + missing_packages.append(package) + + all_available = len(missing_packages) == 0 + return all_available, missing_packages + + def get_python_environment_info(self) -> dict: + """Get information about the current Python environment""" + return get_detailed_python_environment_info() + + def install_ai_packages(self, packages: List[str], parent_window=None) -> bool: + """Install AI packages using pip""" + try: + env_info = self.get_python_environment_info() + install_location = f"virtual environment '{env_info['venv_name']}'" if env_info['in_venv'] else "system-wide" + + # Show confirmation dialog + if parent_window: + install_choice = messagebox.askyesno( + "Install AI Packages", + f"šŸ Python {env_info['python_version']}\n" + f"šŸ“¦ Location: {install_location}\n\n" + f"The following packages will be installed:\n" + f"• {', '.join(packages)}\n\n" + f"This will run: pip install {' '.join(packages)}\n\n" + f"Continue with installation?", + parent=parent_window + ) + + if not install_choice: + return False + + self.log(f"Installing packages: {', '.join(packages)}") + self.log(f"Installation location: {install_location}") + + # Run pip install + cmd = [sys.executable, '-m', 'pip', 'install'] + packages + self.log(f"Running: {' '.join(cmd)}") + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + + if result.returncode == 0: + self.log("āœ… Installation completed successfully!") + if parent_window: + messagebox.showinfo( + "Installation Complete", + f"āœ… Successfully installed: {', '.join(packages)}\n\n" + f"Location: {install_location}", + parent=parent_window + ) + return True + else: + error_msg = f"āŒ Installation failed!\n\nError: {result.stderr}" + self.log(error_msg) + if parent_window: + messagebox.showerror("Installation Failed", error_msg, parent=parent_window) + return False + + except subprocess.TimeoutExpired: + error_msg = "āŒ Installation timed out (>5 minutes)" + self.log(error_msg) + if parent_window: + messagebox.showerror("Installation Timeout", error_msg, parent=parent_window) + return False + except Exception as e: + error_msg = f"āŒ Installation error: {str(e)}" + self.log(error_msg) + if parent_window: + messagebox.showerror("Installation Error", error_msg, parent=parent_window) + return False + + def check_and_install_ai_modules(self, provider_name: str, parent_window=None) -> bool: + """Check AI modules and offer to install if missing""" + if not provider_name or provider_name.lower() in ['none', '']: + return True + + if provider_name.lower() not in ['chatgpt', 'claude', 'anthropic', 'github-copilot', 'copilot', 'github_copilot']: + return True + + # Check module availability + available, missing = self.check_ai_module_availability(provider_name) + + if available: + self.log(f"āœ… All required modules for {provider_name} are available") + return True + + # Modules are missing, offer to install + self.log(f"āš ļø Missing modules for {provider_name}: {', '.join(missing)}") + + return self.install_ai_packages(missing, parent_window) + + async def check_and_install_ai_modules_async(self, provider_name: str, page=None) -> bool: + """Async wrapper for check_and_install_ai_modules for Flet integration + + Args: + provider_name: AI provider name + page: Flet page instance for showing dialogs + + Returns: + bool: True if modules are available or successfully installed + """ + import asyncio + + # Run the sync method in a thread pool + result = await asyncio.to_thread( + self.check_and_install_ai_modules, + provider_name, + page + ) + return result + + def show_ai_modules_info(self, provider_name: str, parent_window=None) -> None: + """Show detailed AI modules information""" + try: + # Get environment information + env_info = self.get_python_environment_info() + env_status = f"šŸ Python {env_info['python_version']}" + + if env_info['in_venv']: + env_status += f" (venv: {env_info['venv_name']})" + else: + env_status += " (system-wide)" + + if not provider_name or provider_name.lower() in ['none', '']: + messagebox.showinfo("AI Modules Check", + f"{env_status}\n\n" + f"No AI provider selected.\n\n" + f"Available providers:\n" + f"• ChatGPT (requires 'openai' package)\n" + f"• Claude/Anthropic (requires 'anthropic' package)\n" + f"• GitHub Copilot (requires 'requests' package)\n" + f"• All require 'GitPython' package", + parent=parent_window) + return + + if provider_name.lower() not in ['chatgpt', 'claude', 'anthropic', 'github-copilot', 'copilot', 'github_copilot']: + messagebox.showinfo("AI Modules Check", + f"AI provider '{provider_name}' is not recognized.\n\n" + f"Supported providers: ChatGPT, Claude/Anthropic, GitHub Copilot", + parent=parent_window) + return + + # Check module availability + available, missing = self.check_ai_module_availability(provider_name) + + if available: + messagebox.showinfo("AI Modules Status", + f"{env_status}\n\n" + f"āœ… All required modules for '{provider_name}' are installed!\n\n" + f"AI-assisted features are ready to use.", + parent=parent_window) + return + + # Modules are missing, show detailed info and offer to install + missing_list = '\n'.join(f"• {pkg}" for pkg in missing) + install_location = f"virtual environment '{env_info['venv_name']}'" if env_info['in_venv'] else "system-wide" + + install_choice = messagebox.askyesno("Missing AI Modules", + f"{env_status}\n\n" + f"AI provider '{provider_name}' requires the following packages:\n\n" + f"{missing_list}\n\n" + f"Installation location: {install_location}\n\n" + f"Would you like to install them now?\n\n" + f"This will run: pip install {' '.join(missing)}", + parent=parent_window) + + if install_choice: + success = self.install_ai_packages(missing, parent_window) + + if success: + messagebox.showinfo("Installation Complete", + f"āœ… AI modules installed successfully!\n\n" + f"Provider: {provider_name}\n" + f"Location: {install_location}\n\n" + f"AI-assisted features are now ready to use.", + parent=parent_window) + else: + messagebox.showerror("Installation Failed", + f"āŒ Failed to install AI modules.\n\n" + f"Please try installing manually:\n" + f"pip install {' '.join(missing)}", + parent=parent_window) + else: + messagebox.showinfo("Installation Skipped", + f"AI modules were not installed.\n\n" + f"You can install them later with:\n" + f"pip install {' '.join(missing)}", + parent=parent_window) + + except Exception as e: + if parent_window: + messagebox.showerror("Error", + f"Error checking AI modules: {str(e)}", + parent=parent_window) + if self.logger: + self.logger.log(f"Error in AI modules check: {str(e)}") + + def create_ai_provider(self, provider_name: str, api_key: str, ollama_url: str = None, ollama_model: str = None): + """Create an AI provider instance""" + if not AI_PROVIDERS_AVAILABLE: + return None + + try: + ai_logger = Logger(self.log) + return create_ai_provider(provider_name, api_key, ai_logger, ollama_url, ollama_model) + except Exception as e: + self.log(f"Error creating AI provider: {e}") + return None + + def create_local_git_manager(self, github_token: str): + """Create a LocalGitManager instance""" + if not AI_PROVIDERS_AVAILABLE: + return None + + try: + ai_logger = Logger(self.log) + return LocalGitManager(ai_logger, github_token) + except Exception as e: + self.log(f"Error creating LocalGitManager: {e}") + return None + + def get_last_diff_content(self) -> str: + """Get the last generated diff content for display in the UI""" + return self.last_diff_content + + def clear_diff_content(self): + """Clear the stored diff content""" + self.last_diff_content = "" + + def generate_response(self, prompt: str, provider_name: str, config: dict) -> str: + """Generate a text response from an AI provider + + Args: + prompt: The prompt/question to send to the AI + provider_name: Name of the AI provider ('chatgpt', 'claude', 'ollama', etc.) + config: Configuration dictionary containing API keys and settings + + Returns: + str: The AI-generated response + """ + try: + provider_name = provider_name.lower() + + # OpenAI/ChatGPT + if provider_name in ['chatgpt', 'openai', 'gpt']: + api_key = config.get('OPENAI_API_KEY', '') + if not api_key: + return "Error: OpenAI API key not configured" + + try: + import openai + client = openai.OpenAI(api_key=api_key) + + response = client.chat.completions.create( + model=config.get('OPENAI_MODEL', 'gpt-4'), + messages=[ + {"role": "system", "content": "You are a helpful assistant that analyzes GitHub pull requests and issues."}, + {"role": "user", "content": prompt} + ], + max_tokens=2000, + temperature=0.7 + ) + + return response.choices[0].message.content.strip() + + except Exception as e: + self.log(f"Error calling OpenAI API: {e}") + return f"Error calling OpenAI API: {str(e)}" + + # Anthropic/Claude + elif provider_name in ['claude', 'anthropic']: + # Try both CLAUDE_API_KEY and ANTHROPIC_API_KEY for compatibility + api_key = config.get('CLAUDE_API_KEY', '') + if not api_key: + api_key = config.get('ANTHROPIC_API_KEY', '') + if not api_key: + return "Error: Claude API key not configured (tried both CLAUDE_API_KEY and ANTHROPIC_API_KEY)" + + try: + import anthropic + client = anthropic.Anthropic(api_key=api_key) + + response = client.messages.create( + model=config.get('ANTHROPIC_MODEL', 'claude-sonnet-4-5'), + max_tokens=2000, + messages=[ + {"role": "user", "content": prompt} + ] + ) + + return response.content[0].text.strip() + + except Exception as e: + self.log(f"Error calling Anthropic API: {e}") + return f"Error calling Anthropic API: {str(e)}" + + # Ollama + elif provider_name == 'ollama': + ollama_url = config.get('OLLAMA_URL', 'http://localhost:11434') + ollama_model = config.get('OLLAMA_MODEL', 'llama2') + + try: + import requests + + # Normalize URL + if not ollama_url.startswith('http'): + ollama_url = f"http://{ollama_url}" + + # Remove trailing slash + ollama_url = ollama_url.rstrip('/') + + api_url = f"{ollama_url}/api/generate" + + payload = { + "model": ollama_model, + "prompt": prompt, + "stream": False + } + + response = requests.post(api_url, json=payload, timeout=120) + response.raise_for_status() + + result = response.json() + return result.get('response', '').strip() + + except Exception as e: + self.log(f"Error calling Ollama API: {e}") + return f"Error calling Ollama API: {str(e)}" + + else: + return f"Error: Unknown AI provider '{provider_name}'" + + except Exception as e: + self.log(f"Error in generate_response: {e}") + return f"Error generating response: {str(e)}" \ No newline at end of file diff --git a/src/app_components/assets/flow-diagram.png b/src/app_components/assets/flow-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..f3aa6496f62e1c9ff1ff22619ca9f9b187c29ba7 GIT binary patch literal 130934 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%0A9bjmkwPqe=x^KbwWhP$jH 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 literal 0 HcmV?d00001 diff --git a/src/app_components/assets/github_pulse_img.png b/src/app_components/assets/github_pulse_img.png new file mode 100644 index 0000000000000000000000000000000000000000..8b5f9307f3d5c4fba92c4715be5e18b8ff1b78a2 GIT binary patch literal 93327 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 literal 0 HcmV?d00001 diff --git a/src/app_components/assets/pulse_logo_gray_no_bkg.png b/src/app_components/assets/pulse_logo_gray_no_bkg.png new file mode 100644 index 0000000000000000000000000000000000000000..8d03e89c606bf82d7119dc28e90a7192fd6ad32b GIT binary patch literal 25736 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 literal 0 HcmV?d00001 diff --git a/src/app_components/assets/pulse_logo_white_no_bkg.png b/src/app_components/assets/pulse_logo_white_no_bkg.png new file mode 100644 index 0000000000000000000000000000000000000000..6159727486ebd2d374db0aab7f9e2fadfa0c9a8d GIT binary patch literal 25961 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 h{@)(|_i(si)EOyZ#Tc%`98Bt^@A(Wj##!gZw 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 NFE?K#+vW82{{Uo str: + """Generate cache key from source type and identifier""" + # Use MD5 hash to create safe filename + key_str = f"{source_type}_{identifier}" + return md5(key_str.encode()).hexdigest() + + def _get_cache_path(self, cache_key: str) -> Path: + """Get full path to cache file""" + return self.cache_dir / f"{cache_key}.json" + + def is_cache_valid(self, source_type: str, identifier: str) -> bool: + """Check if cache exists and is still valid""" + cache_key = self._get_cache_key(source_type, identifier) + cache_path = self._get_cache_path(cache_key) + + if not cache_path.exists(): + return False + + # Check if cache has expired + file_age = time.time() - cache_path.stat().st_mtime + return file_age < self.cache_duration_seconds + + def load_from_cache(self, source_type: str, identifier: str) -> Optional[List[Dict[str, Any]]]: + """ + Load GitHub items from cache + + Args: + source_type: 'github_prs', 'github_issues', 'target_prs', 'fork_prs', etc. + identifier: repository identifier or config hash + + Returns: + List of items if cache is valid, None otherwise + """ + if not self.is_cache_valid(source_type, identifier): + return None + + cache_key = self._get_cache_key(source_type, identifier) + cache_path = self._get_cache_path(cache_key) + + try: + with open(cache_path, 'r', encoding='utf-8') as f: + cache_data = json.load(f) + + # Validate cache structure + if 'timestamp' not in cache_data or 'items' not in cache_data: + return None + + return cache_data['items'] + + except Exception as e: + print(f"Error loading cache: {e}") + return None + + def save_to_cache(self, source_type: str, identifier: str, items: List[Dict[str, Any]]) -> bool: + """ + Save GitHub items to cache + + Args: + source_type: 'github_prs', 'github_issues', 'target_prs', 'fork_prs', etc. + identifier: repository identifier or config hash + items: List of items to cache (PRs or Issues) + + Returns: + True if successful, False otherwise + """ + cache_key = self._get_cache_key(source_type, identifier) + cache_path = self._get_cache_path(cache_key) + + try: + cache_data = { + 'timestamp': time.time(), + 'source_type': source_type, + 'identifier': identifier, + 'items': items + } + + with open(cache_path, 'w', encoding='utf-8') as f: + json.dump(cache_data, f, indent=2, ensure_ascii=False) + + return True + + except Exception as e: + print(f"Error saving cache: {e}") + return False + + def invalidate_cache(self, source_type: str = None, identifier: str = None): + """ + Invalidate (delete) cache + + Args: + source_type: If specified, only invalidate this source type + identifier: If specified, only invalidate this specific cache + """ + if source_type and identifier: + # Invalidate specific cache + cache_key = self._get_cache_key(source_type, identifier) + cache_path = self._get_cache_path(cache_key) + if cache_path.exists(): + cache_path.unlink() + elif source_type: + # Invalidate all caches for this source type + for cache_file in self.cache_dir.glob("*.json"): + try: + with open(cache_file, 'r', encoding='utf-8') as f: + cache_data = json.load(f) + if cache_data.get('source_type') == source_type: + cache_file.unlink() + except: + pass + else: + # Invalidate all caches + for cache_file in self.cache_dir.glob("*.json"): + cache_file.unlink() + + def get_cache_info(self) -> Dict[str, Any]: + """Get information about cached items""" + cache_files = list(self.cache_dir.glob("*.json")) + + info = { + 'cache_dir': str(self.cache_dir), + 'total_files': len(cache_files), + 'total_size_bytes': sum(f.stat().st_size for f in cache_files), + 'caches': [] + } + + for cache_file in cache_files: + try: + with open(cache_file, 'r', encoding='utf-8') as f: + cache_data = json.load(f) + + file_age = time.time() - cache_file.stat().st_mtime + is_valid = file_age < self.cache_duration_seconds + + info['caches'].append({ + 'source_type': cache_data.get('source_type', 'unknown'), + 'item_count': len(cache_data.get('items', [])), + 'age_hours': round(file_age / 3600, 1), + 'is_valid': is_valid, + 'size_kb': round(cache_file.stat().st_size / 1024, 1) + }) + except: + pass + + return info + + def cleanup_expired(self): + """Remove expired cache files""" + current_time = time.time() + removed_count = 0 + + for cache_file in self.cache_dir.glob("*.json"): + file_age = current_time - cache_file.stat().st_mtime + if file_age >= self.cache_duration_seconds: + cache_file.unlink() + removed_count += 1 + + return removed_count diff --git a/src/app_components/config_manager.py b/src/app_components/config_manager.py new file mode 100644 index 0000000..d572328 --- /dev/null +++ b/src/app_components/config_manager.py @@ -0,0 +1,246 @@ +""" +Configuration Manager +Wrapper around SettingsManager for backward compatibility. +Now uses config.json + keyring instead of .env files. +""" + +import os +import json +from typing import Dict, Any, Optional +from pathlib import Path +from .settings_manager import SettingsManager + + +class ConfigManager: + """ + Manages application configuration using the new SettingsManager. + + Provides backward compatibility with old .env-based code while + using the modern config.json + keyring system underneath. + """ + + def __init__(self): + """Initialize with SettingsManager backend""" + # Initialize the modern settings system + self._settings = SettingsManager() + + # Check if .env exists and offer migration + env_path = Path('.env') + if env_path.exists() and not Path('application/config.json').exists(): + print("\n" + "="*60) + print("NOTICE: Legacy .env file detected!") + print("="*60) + print("Your app now uses a modern settings system with:") + print(" āœ“ Secure API key storage (Windows Credential Manager)") + print(" āœ“ Live settings updates (no restart needed)") + print(" āœ“ Better configuration management") + print() + print("Migrating settings from .env to new system...") + print() + + if self._settings.migrate_from_env(env_path): + print("āœ“ Migration successful!") + print(f" - Secrets → System keyring") + print(f" - Settings → {self._settings.config_file}") + print() + print("Your .env file is kept as backup.") + print("You can delete it once you verify everything works.") + else: + print("āœ— Migration failed. Using .env as fallback.") + print("="*60 + "\n") + + # Load configuration + self.config = self._settings.get_all() + + # Auto-default GITHUB_TOKEN to GITHUB_PAT if needed + self._apply_token_defaults() + + # Show configuration status + self._print_config_status() + + def _apply_token_defaults(self): + """Auto-default GITHUB_TOKEN to GITHUB_PAT if GITHUB_TOKEN is empty""" + github_token = self.config.get('GITHUB_TOKEN', '').strip() if self.config.get('GITHUB_TOKEN') else '' + github_pat = self.config.get('GITHUB_PAT', '').strip() if self.config.get('GITHUB_PAT') else '' + + if not github_token and github_pat: + self.config['GITHUB_TOKEN'] = github_pat + self._settings.set('GITHUB_TOKEN', github_pat, save=False) + + def _print_config_status(self): + """Print configuration load status""" + loaded_keys = [] + for key, value in self.config.items(): + if value and str(value).strip(): + # Don't show actual secret values + if key in SettingsManager.SECRET_KEYS: + loaded_keys.append(f"{key}: loaded") + else: + loaded_keys.append(f"{key}: loaded") + + if loaded_keys: + print(f"Configuration status: {', '.join(loaded_keys)}") + else: + print("No configuration values loaded - using defaults") + + def load_configuration(self) -> Dict[str, Any]: + """ + Load configuration from new system (config.json + keyring). + + Returns: + Dictionary of all settings + """ + self.config = self._settings.load() + self._apply_token_defaults() + return self.config + + def save_configuration(self, config_values: Dict[str, Any]) -> bool: + """ + Save configuration using new system. + + No restart required - changes apply immediately! + + Args: + config_values: Settings to save + + Returns: + True if successful + """ + # Save using new system + success = self._settings.save(config_values) + + if success: + # Reload to get updated values + self.config = self._settings.get_all() + self._apply_token_defaults() + print(f"Configuration saved to {self._settings.config_file}") + print("Settings updated (no restart needed!)") + else: + print("Failed to save configuration") + + return success + + def get_config(self) -> Dict[str, Any]: + """ + Get current configuration with automatic GITHUB_TOKEN defaulting. + + Returns: + Dictionary of all settings + """ + config = self.config.copy() + + # Auto-default GITHUB_TOKEN to GITHUB_PAT if needed + github_token = config.get('GITHUB_TOKEN', '').strip() if config.get('GITHUB_TOKEN') else '' + github_pat = config.get('GITHUB_PAT', '').strip() if config.get('GITHUB_PAT') else '' + + if not github_token and github_pat: + config['GITHUB_TOKEN'] = github_pat + + return config + + def get_value(self, key: str, default: Any = None) -> Any: + """ + Get a specific configuration value. + + Args: + key: Setting key + default: Default value if not found + + Returns: + Setting value or default + """ + return self._settings.get(key, default) + + def get(self, key: str, default: Any = None) -> Any: + """ + Get a specific configuration value (dictionary-like interface). + + Args: + key: Setting key + default: Default value if not found + + Returns: + Setting value or default + """ + return self._settings.get(key, default) + + def set_value(self, key: str, value: Any) -> None: + """ + Set a specific configuration value. + + Args: + key: Setting key + value: New value + """ + self._settings.set(key, value) + self.config[key] = value + + def register_listener(self, callback): + """ + Register a callback for settings changes (live updates). + + The callback will be called with (key, new_value) when a setting changes. + + Args: + callback: Function to call on settings change + + Example: + def on_settings_changed(key, value): + if key == 'THEME_MODE': + # Update theme immediately + page.theme_mode = ft.ThemeMode.DARK if value == 'dark' else ft.ThemeMode.LIGHT + page.update() + + config_manager.register_listener(on_settings_changed) + """ + self._settings.register_listener(callback) + + def unregister_listener(self, callback): + """ + Unregister a settings change callback. + + Args: + callback: Function to remove from listeners + """ + self._settings.unregister_listener(callback) + + # Legacy methods for PR counter (unchanged) + + def get_pr_counter_file(self) -> str: + """Get the path to the PR counter file""" + script_dir = os.path.dirname(os.path.abspath(__file__)) + return os.path.join(script_dir, '..', '.pr_counter.json') + + def load_pr_counter(self) -> Dict[str, int]: + """Load the PR counter from file""" + counter_file = self.get_pr_counter_file() + if os.path.exists(counter_file): + try: + with open(counter_file, 'r', encoding='utf-8') as f: + return json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + pass + return {'count': 0} + + def save_pr_counter(self, counter: Dict[str, int]) -> bool: + """Save the PR counter to file""" + counter_file = self.get_pr_counter_file() + try: + with open(counter_file, 'w', encoding='utf-8') as f: + json.dump(counter, f, indent=2) + return True + except Exception as e: + print(f"Error saving PR counter: {e}") + return False + + def increment_pr_counter(self) -> int: + """Increment and return the PR counter""" + counter = self.load_pr_counter() + counter['count'] = counter.get('count', 0) + 1 + self.save_pr_counter(counter) + return counter['count'] + + def get_pr_counter(self) -> int: + """Get the current PR counter value""" + counter = self.load_pr_counter() + return counter.get('count', 0) diff --git a/src/app_components/github_api.py b/src/app_components/github_api.py new file mode 100644 index 0000000..a3ab660 --- /dev/null +++ b/src/app_components/github_api.py @@ -0,0 +1,985 @@ +""" +GitHub API Manager +Handles GitHub GraphQL operations, PR/Issue creation, and Copilot interactions +""" + +import base64 +import difflib +import json +import requests +from typing import Optional, Tuple, Dict, Any, List +from urllib.parse import urlparse + +# Constants +GITHUB_GRAPHQL_ENDPOINT = "https://api.github.com/graphql" +USER_AGENT = "github-automation-tool/1.0" + + +class GitHubGQL: + """GitHub GraphQL API client for creating issues, PRs, and managing assignments""" + + def __init__(self, token: str, logger=None, dry_run: bool = False): + self.token = token + self.logger = logger + self.dry_run = dry_run + + def log(self, message: str) -> None: + """Log a message""" + if self.logger: + self.logger.log(message) + else: + print(message) + + def _headers(self): + """Get headers for GitHub API requests""" + return { + "Authorization": f"Bearer {self.token}", + "User-Agent": USER_AGENT, + "Content-Type": "application/json" + } + + def run(self, query: str, variables: dict | None = None) -> dict: + """Execute a GraphQL query""" + payload = {"query": query, "variables": variables or {}} + + if self.dry_run: + self.log("[DRY-RUN] Would POST GraphQL payload:") + pretty = json.dumps(payload, indent=2) + self.log(pretty) + return {"dryRun": True, "data": None} + + try: + resp = requests.post(GITHUB_GRAPHQL_ENDPOINT, headers=self._headers(), json=payload, timeout=60) + if resp.status_code != 200: + raise RuntimeError(f"GraphQL HTTP {resp.status_code}: {resp.text}") + + data = resp.json() + if "errors" in data and data["errors"]: + raise RuntimeError(f"GraphQL errors: {json.dumps(data['errors'], indent=2)}") + + return data + except requests.RequestException as e: + raise RuntimeError(f"Request failed: {str(e)}") + + def _make_rest_request(self, method: str, url: str, data: Dict[str, Any] = None) -> Dict[str, Any]: + """Make a REST API request to GitHub""" + headers = { + "Authorization": f"Bearer {self.token}", + "Accept": "application/vnd.github+json", + "User-Agent": USER_AGENT + } + + if self.dry_run: + self.log(f"[DRY-RUN] Would make {method} request to: {url}") + return {"number": 123, "html_url": "https://github.com/example/repo/pull/123"} + + response = requests.request(method, url, headers=headers, json=data, timeout=30) + response.raise_for_status() + + return response.json() + + def get_repo_id(self, owner: str, name: str) -> str: + """Get GitHub repository ID""" + self.log(f"Fetching repositoryId for {owner}/{name}...") + query = """ + query($owner:String!, $name:String!) { + repository(owner:$owner, name:$name) { + id + url + } + } + """ + data = self.run(query, {"owner": owner, "name": name}) + + if data.get("dryRun"): + return "DRY_RUN_REPO_ID" + + repo = data["data"]["repository"] + if not repo: + raise RuntimeError(f"Repository {owner}/{name} not found or token lacks access.") + + self.log(f"Repository ID: {repo['id']} ({repo['url']})") + return repo["id"] + + def get_copilot_actor_id(self, owner: str, name: str) -> tuple[str | None, str | None]: + """Find Copilot actor ID for assignment""" + self.log("Querying suggestedActors for CAN_BE_ASSIGNED...") + query = """ + query($owner:String!, $name:String!) { + repository(owner:$owner, name:$name) { + suggestedActors(capabilities:[CAN_BE_ASSIGNED], first:100) { + nodes { + login + __typename + ... on Bot { id } + ... on User { id } + } + } + } + } + """ + data = self.run(query, {"owner": owner, "name": name}) + + if data.get("dryRun"): + return ("DRY_RUN_ACTOR_ID", "copilot-swe-agent") + + nodes = data["data"]["repository"]["suggestedActors"]["nodes"] + if not nodes: + self.log("No suggestedActors returned.") + return (None, None) + + # Log all available actors for debugging + self.log(f"Available assignable actors ({len(nodes)}):") + for node in nodes: + self.log(f" - {node.get('login', 'N/A')} ({node.get('__typename', 'N/A')}) ID: {node.get('id', 'N/A')}") + + # Prefer known Copilot logins + preferred = ("copilot-swe-agent", "copilot", "github-copilot", "github-advanced-security") + chosen = None + + # First, try exact matches + for candidate in nodes: + login = candidate.get("login", "").lower() + if login in preferred: + chosen = candidate + break + + # If no exact match, try partial matches + if not chosen: + for candidate in nodes: + login = candidate.get("login", "").lower() + if "copilot" in login: + chosen = candidate + break + + if not chosen: + self.log("Copilot not found in suggestedActors list.") + self.log("Available actors: " + ", ".join([n.get("login", "N/A") for n in nodes])) + return (None, None) + + login = chosen["login"] + actor_id = chosen.get("id") + + if not actor_id: + self.log(f"Warning: No actor ID found for {login}") + return (None, None) + + self.log(f"Found assignable Copilot actor: {login} (id={actor_id})") + return (actor_id, login) + + def create_issue(self, repository_id: str, title: str, body: str) -> tuple[str, str, int]: + """Create a GitHub issue""" + self.log("Creating issue with createIssue mutation...") + mutation = """ + mutation($repositoryId:ID!, $title:String!, $body:String!) { + createIssue(input:{repositoryId:$repositoryId, title:$title, body:$body}) { + issue { + id + url + number + title + } + } + } + """ + data = self.run(mutation, {"repositoryId": repository_id, "title": title, "body": body}) + + if data.get("dryRun"): + return ("DRY_RUN_ISSUE_ID", "https://github.com/owner/repo/issues/123", 123) + + issue = data["data"]["createIssue"]["issue"] + self.log(f"Issue created: {issue['url']} (#{issue['number']})") + return (issue["id"], issue["url"], issue["number"]) + + def create_branch_from_main(self, owner: str, repo: str, branch_name: str) -> bool: + """Create a new branch from the main branch""" + self.log(f"Creating branch '{branch_name}' in {owner}/{repo}") + + try: + # Get the SHA of the main branch + main_ref_url = f"https://api.github.com/repos/{owner}/{repo}/git/ref/heads/main" + main_ref_response = self._make_rest_request("GET", main_ref_url) + main_sha = main_ref_response["object"]["sha"] + + self.log(f"Main branch SHA: {main_sha}") + + # Create new branch + new_ref_url = f"https://api.github.com/repos/{owner}/{repo}/git/refs" + new_ref_data = { + "ref": f"refs/heads/{branch_name}", + "sha": main_sha + } + + if self.dry_run: + self.log(f"🧪 DRY RUN: Would create branch '{branch_name}' from main ({main_sha})") + return True + + self._make_rest_request("POST", new_ref_url, new_ref_data) + self.log(f"āœ… Branch '{branch_name}' created successfully") + return True + + except Exception as e: + self.log(f"āŒ Failed to create branch: {str(e)}") + return False + + def get_user_forks(self, include_org_repos: bool = True) -> List[str]: + """Get list of user's forked repositories""" + self.log("Fetching user's forked repositories...") + + if self.dry_run: + # Return sample data for dry run + return [ + "username/repo_name", + ] + + try: + forks = [] + page = 1 + per_page = 100 + + while page <= 5: # Limit to 5 pages to avoid long waits + url = f"https://api.github.com/user/repos?type=forks&per_page={per_page}&page={page}" + + response = self._make_rest_request("GET", url) + repos = response if isinstance(response, list) else response.get('data', []) + + if not repos: + break + + for repo in repos: + if repo.get('fork', False): + forks.append(f"{repo['owner']['login']}/{repo['name']}") + + if len(repos) < per_page: + break + + page += 1 + + self.log(f"Found {len(forks)} forked repositories") + return forks + + except Exception as e: + self.log(f"āŒ Failed to fetch user forks: {str(e)}") + return [] + + def get_authenticated_user(self) -> Dict[str, Any]: + """Get authenticated user information""" + if self.dry_run: + return {"login": "dry-run-user", "name": "Dry Run User"} + + try: + return self._make_rest_request("GET", "https://api.github.com/user") + except Exception as e: + self.log(f"āŒ Failed to get user info: {str(e)}") + return {} + + def fork_repository(self, owner: str, repo: str, target_org: str = None) -> tuple[str, str]: + """Fork a repository to the authenticated user's account or specified organization""" + self.log(f"Forking repository {owner}/{repo}") + + fork_url = f"https://api.github.com/repos/{owner}/{repo}/forks" + fork_data = {} + + if target_org: + fork_data["organization"] = target_org + + if self.dry_run: + # Get authenticated user for dry run + user_url = "https://api.github.com/user" + try: + user_data = self._make_rest_request("GET", user_url) + fork_owner = target_org if target_org else user_data["login"] + self.log(f"🧪 DRY RUN: Would fork {owner}/{repo} to {fork_owner}/{repo}") + return fork_owner, repo + except: + self.log(f"🧪 DRY RUN: Would fork {owner}/{repo}") + return "dry-run-user", repo + + try: + fork_response = self._make_rest_request("POST", fork_url, fork_data) + fork_owner = fork_response["owner"]["login"] + fork_name = fork_response["name"] + + self.log(f"āœ… Repository forked to {fork_owner}/{fork_name}") + return fork_owner, fork_name + + except Exception as e: + self.log(f"āŒ Failed to fork repository: {str(e)}") + raise + + def check_repository_exists(self, owner: str, repo: str) -> bool: + """Check if a repository exists and is accessible""" + try: + url = f"https://api.github.com/repos/{owner}/{repo}" + response = self._make_rest_request("GET", url) + return bool(response.get('id')) + except: + return False + + def find_matching_repositories(self, target_repo: str, fork_repo: str) -> Dict[str, List[str]]: + """Find matching repositories to suggest alternatives for mismatched repos""" + self.log(f"Finding matching repositories for target: {target_repo}, fork: {fork_repo}") + + if self.dry_run: + return { + "target_alternatives": ["username/target_repo_name"], + "fork_alternatives": ["username/fork_repo_name"] + } + + try: + target_owner, target_name = target_repo.split('/', 1) if '/' in target_repo else ("", target_repo) + fork_owner, fork_name = fork_repo.split('/', 1) if '/' in fork_repo else ("", fork_repo) + + target_alternatives = [] + fork_alternatives = [] + + # Get authenticated user info + user_info = self.get_authenticated_user() + user_login = user_info.get('login', '') + + # Search for repositories with similar names + search_terms = [target_name, fork_name] + for term in search_terms: + if term: + # Clean up the search term (remove common suffixes) + clean_term = term.replace('-docs', '').replace('-pr', '').replace('_', ' ') + + # Search for repositories + search_url = f"https://api.github.com/search/repositories?q={clean_term}&per_page=20" + try: + search_response = self._make_rest_request("GET", search_url) + repositories = search_response.get('items', []) + + for repo_data in repositories: + repo_full_name = repo_data['full_name'] + repo_owner = repo_data['owner']['login'] + + # Check if this is a potential target alternative + if (repo_owner == target_owner and + repo_data['name'] != target_name and + repo_full_name not in target_alternatives): + target_alternatives.append(repo_full_name) + + # Check if this is a potential fork alternative + if (repo_owner == user_login and + repo_data['name'] != fork_name and + repo_data.get('fork', False) and + repo_full_name not in fork_alternatives): + fork_alternatives.append(repo_full_name) + + except Exception as e: + self.log(f"āŒ Search failed for term '{term}': {str(e)}") + + return { + "target_alternatives": target_alternatives[:5], # Limit to 5 suggestions + "fork_alternatives": fork_alternatives[:5] + } + + except Exception as e: + self.log(f"āŒ Failed to find matching repositories: {str(e)}") + return {"target_alternatives": [], "fork_alternatives": []} + + def make_documentation_change(self, owner: str, repo: str, branch_name: str, file_path: str, + old_text: str, new_text: str, commit_message: str) -> bool: + """Make actual documentation changes to a file in the repository + + This fetches the file, makes the text replacement, and commits it to the branch. + + Returns True if successful, False otherwise. + """ + if self.dry_run: + self.log(f"[DRY-RUN] Would update {file_path} in branch {branch_name}") + self.log(f"[DRY-RUN] Replace: {old_text[:50]}...") + self.log(f"[DRY-RUN] With: {new_text[:50]}...") + return True + + try: + rest_headers = { + "Authorization": f"Bearer {self.token}", + "Accept": "application/vnd.github+json", + "User-Agent": USER_AGENT + } + + # 1. Get the current file content from the branch + self.log(f"Fetching file: {file_path}") + file_url = f"https://api.github.com/repos/{owner}/{repo}/contents/{file_path}?ref={branch_name}" + resp = requests.get(file_url, headers=rest_headers, timeout=30) + + if resp.status_code == 404: + self.log(f"āŒ File not found: {file_path}") + self.log(f" The file path might be incorrect or the file doesn't exist") + return False + + resp.raise_for_status() + file_data = resp.json() + + # Decode the file content + current_content = base64.b64decode(file_data["content"]).decode('utf-8') + file_sha = file_data["sha"] + + self.log(f"āœ… File retrieved ({len(current_content)} bytes)") + + # Detect line ending style to preserve it + line_ending = '\r\n' if '\r\n' in current_content else '\n' + self.log(f"šŸ“ Detected line endings: {'CRLF' if line_ending == '\\r\\n' else 'LF'}") + + # Normalize everything to LF for consistent processing + normalized_content = current_content.replace('\r\n', '\n') + normalized_old = old_text.replace('\r\n', '\n') + normalized_new = new_text.replace('\r\n', '\n') + + # 2. Make the text replacement + if normalized_old not in normalized_content: + self.log(f"āš ļø Warning: Could not find exact text to replace in {file_path}") + self.log(f" Searching for similar text...") + + # Try to find similar text (case-insensitive, whitespace-flexible) + lines = normalized_content.split('\n') + old_lines = normalized_old.split('\n') + + # Find the best matching sequence + matcher = difflib.SequenceMatcher(None, old_lines, lines) + match = matcher.find_longest_match(0, len(old_lines), 0, len(lines)) + + if match.size > len(old_lines) * 0.7: # If we find 70% match + self.log(f" Found similar text at line {match.b + 1}") + self.log(f" Making best-effort replacement...") + # This is a simplified approach - in production you'd want more sophisticated matching + else: + self.log(f"āŒ Could not find text to replace. The document may have changed.") + self.log(f" Creating PR with instructions instead...") + return False + + # Replace the text (using normalized versions) + updated_content = normalized_content.replace(normalized_old, normalized_new) + + if updated_content == normalized_content: + self.log(f"āš ļø No changes made - text might not exist in file") + return False + + self.log(f"āœ… Text replacement successful") + + # Restore original line endings + if line_ending == '\r\n': + updated_content = updated_content.replace('\n', '\r\n') + self.log(f"āœ… Restored CRLF line endings") + + # 3. Commit the updated file + self.log(f"Committing changes to {file_path}...") + encoded_content = base64.b64encode(updated_content.encode('utf-8')).decode() + + update_payload = { + "message": commit_message, + "content": encoded_content, + "sha": file_sha, + "branch": branch_name + } + + update_url = f"https://api.github.com/repos/{owner}/{repo}/contents/{file_path}" + resp = requests.put(update_url, headers=rest_headers, json=update_payload, timeout=30) + resp.raise_for_status() + + self.log(f"āœ… Changes committed to branch {branch_name}") + return True + + except requests.HTTPError as e: + self.log(f"āŒ HTTP Error making changes: {e}") + if e.response.status_code == 403: + self.log(" Permission denied - token doesn't have write access") + elif e.response.status_code == 404: + self.log(f" File not found: {file_path}") + return False + except Exception as e: + self.log(f"āŒ Error making changes: {str(e)}") + return False + + def create_cross_repo_pull_request(self, source_owner: str, source_repo: str, target_owner: str, target_repo: str, + title: str, body: str, head_ref: str, base_ref: str = "main") -> tuple[str, str, int]: + """Create a pull request from source repo to target repo""" + self.log(f"Creating cross-repository PR from {source_owner}/{source_repo}:{head_ref} to {target_owner}/{target_repo}:{base_ref}") + + # Get target repository ID + target_repo_id = self.get_repo_id(target_owner, target_repo) + + # Format the head reference for cross-repo PR + head_ref_full = f"{source_owner}:{head_ref}" + + mutation = """ + mutation($repositoryId:ID!, $title:String!, $body:String!, $headRefName:String!, $baseRefName:String!) { + createPullRequest(input:{ + repositoryId:$repositoryId, + title:$title, + body:$body, + headRefName:$headRefName, + baseRefName:$baseRefName + }) { + pullRequest { + id + url + number + } + } + } + """ + + variables = { + "repositoryId": target_repo_id, + "title": title, + "body": body, + "headRefName": head_ref_full, + "baseRefName": base_ref + } + + if self.dry_run: + self.log(f"🧪 DRY RUN: Would create cross-repo PR '{title}' from {head_ref_full} to {base_ref}") + return "dry-run-pr-id", f"https://github.com/{target_owner}/{target_repo}/pull/0", 0 + + try: + data = self.run(mutation, variables) + pr_data = data["data"]["createPullRequest"]["pullRequest"] + + pr_id = pr_data["id"] + pr_url = pr_data["url"] + pr_number = pr_data["number"] + + self.log(f"āœ… Cross-repo pull request created: {pr_url}") + return pr_id, pr_url, pr_number + + except Exception as e: + self.log(f"āŒ Failed to create cross-repo pull request: {str(e)}") + raise + + def create_pull_request(self, repository_id: str, title: str, body: str, head_ref: str, base_ref: str = "main") -> tuple[str, str, int]: + """Create a pull request""" + self.log(f"Creating pull request with createPullRequest mutation from {head_ref} to {base_ref}...") + mutation = """ + mutation($repositoryId:ID!, $title:String!, $body:String!, $headRefName:String!, $baseRefName:String!) { + createPullRequest(input:{ + repositoryId:$repositoryId, + title:$title, + body:$body, + headRefName:$headRefName, + baseRefName:$baseRefName + }) { + pullRequest { + id + url + number + title + } + } + } + """ + variables = { + "repositoryId": repository_id, + "title": title, + "body": body, + "headRefName": head_ref, + "baseRefName": base_ref + } + data = self.run(mutation, variables) + if data.get("dryRun"): + return ("DRY_RUN_PR_ID", "https://github.com/owner/repo/pull/456", 456) + pr = data["data"]["createPullRequest"]["pullRequest"] + self.log(f"Pull request created: {pr['url']} (#{pr['number']})") + return (pr["id"], pr["url"], pr["number"]) + + def assign_to_copilot(self, assignable_id: str, actor_ids: list[str]) -> bool: + """Assign issue to Copilot + + Returns True if successful, False otherwise. + """ + self.log("Assigning with replaceActorsForAssignable mutation...") + mutation = """ + mutation($assignableId:ID!, $actorIds:[ID!]!) { + replaceActorsForAssignable(input:{assignableId:$assignableId, actorIds:$actorIds}) { + assignable { + ... on Issue { + id + title + assignees(first:10) { nodes { login } } + url + } + ... on PullRequest { + id + title + assignees(first:10) { nodes { login } } + url + } + } + } + } + """ + try: + data = self.run(mutation, {"assignableId": assignable_id, "actorIds": actor_ids}) + + if data.get("dryRun"): + self.log("[DRY-RUN] Would have assigned Copilot.") + return True + + assigned = data["data"]["replaceActorsForAssignable"]["assignable"]["assignees"]["nodes"] + assignees = ", ".join([n["login"] for n in assigned]) or "(none)" + self.log(f"Current assignees: {assignees}") + return True + except Exception as e: + error_message = str(e) + self.log(f"Error assigning Copilot: {error_message}") + + # Provide specific guidance for common permission issues + if "FORBIDDEN" in error_message and "ReplaceActorsForAssignable" in error_message: + self.log("") + self.log("šŸ“‹ Permission Issue: Cannot assign GitHub Copilot") + self.log(" This is a repository permission limitation, not an application error.") + self.log("") + self.log(" Possible solutions:") + self.log(" 1. Repository admin can assign Copilot manually to the PR") + self.log(" 2. Repository admin can grant assignment permissions") + self.log(" 3. The @copilot comment will still notify Copilot to work on the PR") + self.log("") + self.log(" āœ… The PR was created successfully with @copilot instructions") + self.log(" āœ… Copilot can still see and act on the @copilot comment") + elif "NOT_FOUND" in error_message: + self.log("") + self.log("šŸ“‹ Copilot Actor Not Found") + self.log(" This repository may not have GitHub Copilot enabled or available.") + self.log(" The @copilot comment was still added to notify available Copilot services.") + + return False + + def add_copilot_comment(self, owner: str, repo: str, pr_number: int, + file_path: str, old_text: str, new_text: str, branch_name: str, + work_item_id: str = None, item_source: str = None, doc_url: str = None, + custom_instructions: str = None) -> bool: + """Add a comment mentioning @copilot with explicit instructions to work on THIS PR + + This tells Copilot to make changes in the current PR's branch, not create a new PR. + + Args: + owner: Repository owner + repo: Repository name + pr_number: Pull request number + file_path: Path to the file to modify + old_text: Text to find and replace + new_text: New text to replace with + branch_name: Branch name for this PR + work_item_id: Reference ID for tracking (optional) + item_source: Source of the item (optional) + + Returns True if successful, False otherwise. + """ + if self.dry_run: + self.log(f"[DRY-RUN] Would add @copilot comment to PR #{pr_number}") + return True + + try: + rest_headers = { + "Authorization": f"Bearer {self.token}", + "Accept": "application/vnd.github+json", + "User-Agent": USER_AGENT + } + + # Build reference ID if provided + if work_item_id: + reference_id = f"**Reference ID:** {work_item_id}\n" + else: + reference_id = "" + + # Build document reference + if file_path and not file_path.startswith("File path not specified"): + doc_ref = f"**Document to modify:** `{file_path}`\n" + file_instruction = f"2. Locate the file: `{file_path}`" + elif doc_url: + doc_ref = f"**Document URL:** {doc_url}\n" + file_instruction = f"2. Locate the file from this document URL: {doc_url}" + else: + doc_ref = "**Note:** File path not specified\n" + file_instruction = "2. Review the PR description to identify the file(s) that need to be modified" + + # Build custom instructions section + if custom_instructions and custom_instructions.strip(): + custom_instructions_section = f""" +**Custom AI Instructions:** +{custom_instructions.strip()} + +""" + else: + custom_instructions_section = "" + + # Create a comment mentioning @copilot with VERY explicit instructions + comment_body = f"""@copilot + +{reference_id}{doc_ref} + +**Instructions:** + +Task: Update the file with the changes requested above. + +Steps to complete: + +Locate the file containing the reference shown below. +Find the reference text within the file +Replace it with the 'Proposed New Text' shown above or use the reference as guidance +Maintain the existing formatting, indentation, and structure +Ensure no other content in the file is modified + +> [!IMPORTANT] +> Only replace the specified text - do not make additional changes. +> Preserve all formatting, links, and code blocks. +> If the current text cannot be found exactly, search for similar text. +> Do not remove any text unless the reference or suggested guidance indicates to do so, if the text is obsolete or incorrect. + +1. Make changes to `{branch_name}` branch for this pull request. + +{file_instruction} + +3. Find this reference in the content: +``` +{old_text} +``` + +4. Use this text as guidance for the new content: +``` +{new_text} +``` + +5. Ensure the changes align with the context provided. + +6. Do a freshness check to ensure the file content is up-to-date before making changes. + +7. Commit the changes to the `{branch_name}` branch + +> [!NOTE] +> If guidance is empty, follow the reference to make changes. + +{custom_instructions_section} +Thank you! +""" + + # Post the comment to the PR + comments_url = f"https://api.github.com/repos/{owner}/{repo}/issues/{pr_number}/comments" + comment_data = {"body": comment_body} + + resp = requests.post(comments_url, headers=rest_headers, json=comment_data, timeout=30) + + if resp.status_code == 403: + self.log("āŒ Permission denied when adding comment") + return False + + resp.raise_for_status() + self.log(f"āœ… Added @copilot comment to PR #{pr_number}") + self.log(" Copilot has been instructed to work on THIS PR's branch") + return True + + except requests.HTTPError as e: + self.log(f"āŒ HTTP Error adding comment: {e}") + return False + except Exception as e: + self.log(f"āŒ Error adding comment: {str(e)}") + return False + + def add_pr_suggestion(self, owner: str, repo: str, pr_number: int, file_path: str, + old_text: str, new_text: str) -> bool: + """Add a suggested change comment to a PR + + This creates a review comment with a code suggestion that can be applied + with one click, keeping everything in the same PR. + + Returns True if successful, False otherwise. + """ + if self.dry_run: + self.log(f"[DRY-RUN] Would add suggested change to PR #{pr_number}") + return True + + try: + # Use REST API to create a review comment with suggestion + rest_headers = { + "Authorization": f"Bearer {self.token}", + "Accept": "application/vnd.github+json", + "User-Agent": USER_AGENT + } + + # First, get the latest commit SHA from the PR + pr_url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}" + resp = requests.get(pr_url, headers=rest_headers, timeout=30) + resp.raise_for_status() + pr_data = resp.json() + commit_sha = pr_data["head"]["sha"] + + self.log(f"Latest commit SHA: {commit_sha}") + + # Get the file content to find line numbers + file_url = f"https://api.github.com/repos/{owner}/{repo}/contents/{file_path}?ref={commit_sha}" + resp = requests.get(file_url, headers=rest_headers, timeout=30) + + if resp.status_code == 404: + self.log(f"āš ļø File not found in PR: {file_path}") + return False + + resp.raise_for_status() + file_data = resp.json() + + content = base64.b64decode(file_data["content"]).decode('utf-8') + lines = content.split('\n') + + # Find the line number where the old text appears + old_text_lines = old_text.split('\n') + start_line = None + + for i in range(len(lines) - len(old_text_lines) + 1): + if '\n'.join(lines[i:i+len(old_text_lines)]) == old_text: + start_line = i + 1 # Line numbers are 1-based + break + + if not start_line: + self.log("āš ļø Could not find text in file to create suggestion") + return False + + end_line = start_line + len(old_text_lines) - 1 + + # Create a review comment with suggested change + suggestion_body = f"""```suggestion +{new_text} +``` + +**Automated Suggestion:** This change was requested. + +Click "Commit suggestion" above to apply this change directly to the PR.""" + + comment_data = { + "body": suggestion_body, + "commit_id": commit_sha, + "path": file_path, + "line": end_line, + "start_line": start_line if start_line != end_line else None, + "start_side": "RIGHT" + } + + # Remove start_line if it's the same as line (single-line comment) + if start_line == end_line: + del comment_data["start_line"] + + comments_url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/comments" + resp = requests.post(comments_url, headers=rest_headers, json=comment_data, timeout=30) + + if resp.status_code == 403: + self.log("āŒ Permission denied when adding suggestion") + return False + + resp.raise_for_status() + self.log(f"āœ… Added suggested change comment to PR #{pr_number}") + self.log(" User can click 'Commit suggestion' to apply it") + return True + + except requests.HTTPError as e: + self.log(f"āŒ HTTP Error adding suggestion: {e}") + if hasattr(e, 'response') and e.response is not None: + self.log(f" Response: {e.response.text[:200]}") + return False + except Exception as e: + self.log(f"āŒ Error adding suggestion: {str(e)}") + return False + + def create_branch_with_placeholder(self, owner: str, repo: str, branch_name: str, instructions: str) -> bool: + """Create a branch with a placeholder commit using REST API + + This creates a branch from main and adds a .copilot-instructions.md file + so that the branch has at least one commit, allowing PR creation. + + Returns True if successful, False otherwise. + """ + if self.dry_run: + self.log(f"[DRY-RUN] Would create branch {branch_name} with placeholder commit") + return True + + try: + # Use REST API for branch/file creation + rest_headers = { + "Authorization": f"Bearer {self.token}", + "Accept": "application/vnd.github+json", + "User-Agent": USER_AGENT + } + + # 1. Get the SHA of the main branch + self.log(f"Getting SHA of main branch...") + ref_url = f"https://api.github.com/repos/{owner}/{repo}/git/refs/heads/main" + resp = requests.get(ref_url, headers=rest_headers, timeout=30) + resp.raise_for_status() + main_sha = resp.json()["object"]["sha"] + self.log(f"Main branch SHA: {main_sha}") + + # 2. Create new branch from main + self.log(f"Creating branch {branch_name}...") + create_ref_url = f"https://api.github.com/repos/{owner}/{repo}/git/refs" + create_ref_payload = { + "ref": f"refs/heads/{branch_name}", + "sha": main_sha + } + resp = requests.post(create_ref_url, headers=rest_headers, json=create_ref_payload, timeout=30) + + # Check for permission errors + if resp.status_code == 403: + self.log("āŒ Permission denied: GitHub token doesn't have write access to this repository") + self.log(f" Repository: {owner}/{repo}") + self.log(" Required permission: 'repo' scope with write access") + self.log("") + self.log(" Please verify:") + self.log(" 1. Your token has the 'repo' scope enabled") + self.log(" 2. You have write/push access to this repository") + self.log(" 3. The repository exists and the name is correct") + self.log("") + self.log(" TIP: You can still create Issues (uncheck the PR checkbox)") + return False + + # Branch might already exist, that's okay + if resp.status_code == 422: + error_detail = resp.json() + if "already exists" in str(error_detail).lower(): + self.log(f"Branch {branch_name} already exists, using existing branch") + return True + else: + self.log(f"Error creating branch: {error_detail}") + + resp.raise_for_status() + self.log(f"āœ… Branch {branch_name} created") + + # 3. Create a placeholder file with instructions + self.log("Creating placeholder commit with Copilot instructions...") + file_content = f"""# Copilot Instructions + +This is a placeholder file created to allow PR creation. + +## Task +{instructions} + +Please process the instructions above and make the necessary changes to the documentation. + +Once you've made the changes, you can delete this file. +""" + + encoded_content = base64.b64encode(file_content.encode('utf-8')).decode() + + file_payload = { + "message": f"Add Copilot instructions for {branch_name}", + "content": encoded_content, + "branch": branch_name + } + + file_url = f"https://api.github.com/repos/{owner}/{repo}/contents/.copilot-instructions.md" + resp = requests.put(file_url, headers=rest_headers, json=file_payload, timeout=30) + resp.raise_for_status() + + self.log(f"āœ… Placeholder commit created in branch {branch_name}") + return True + + except requests.HTTPError as e: + self.log(f"āŒ HTTP Error creating branch with placeholder: {e}") + if e.response.status_code == 403: + self.log(" Permission denied - token doesn't have write access") + return False + except Exception as e: + self.log(f"āŒ Error creating branch with placeholder: {str(e)}") + return False + + +# Backward compatibility alias +GitHubAPI = GitHubGQL \ No newline at end of file diff --git a/src/app_components/main_gui.py b/src/app_components/main_gui.py new file mode 100644 index 0000000..7739f8a --- /dev/null +++ b/src/app_components/main_gui.py @@ -0,0 +1,3123 @@ +""" +Main GUI Interface (Flet version) +The primary user interface for the application +""" + +import flet as ft +# Compatibility fix for Flet 0.28+ (Icons vs icons, Colors vs colors) +ft.icons = ft.Icons +ft.colors = ft.Colors +import os +import threading +import webbrowser +import asyncio +from typing import List, Dict, Any, Optional +from pathlib import Path + +from .utils import Logger +from .settings_dialog import SettingsDialog +from .processing_log_dialog import ProcessingLogDialog + + +class DryRunVar: + """Compatibility class for dry run variable""" + + def __init__(self, app): + self.app = app + + def get(self): + return self.app.dry_run_enabled + + def set(self, value): + self.app.dry_run_enabled = bool(value) + + +class MainGUI: + """Main GUI interface for the application""" + + def __init__(self, page: ft.Page, config_manager, ai_manager, app): + self.page = page + self.config_manager = config_manager + self.ai_manager = ai_manager + self.app = app + + # Application state + self.current_work_items = [] + self.current_item_index = 0 + self.current_organization = None + self.edit_mode = False + self.workflow_items = {} + self.current_workflow_items = [] + self.active_workflow_item = None # Currently selected item from All Items list + + # Repository data + self.target_repos = [] + self.forked_repos = {'local': [], 'github': []} + + # Create dry run compatibility wrapper + self.dry_run_var = DryRunVar(app) + + # UI References + self.status_text_ref = ft.Ref[ft.Text]() + self.progress_bar_ref = ft.Ref[ft.ProgressBar]() + self.work_item_id_ref = ft.Ref[ft.Text]() + self.nature_text_ref = ft.Ref[ft.TextField]() + self.live_doc_url_ref = ft.Ref[ft.TextField]() + self.text_to_change_ref = ft.Ref[ft.TextField]() + self.proposed_new_text_ref = ft.Ref[ft.TextField]() + self.custom_instructions_ref = ft.Ref[ft.TextField]() + self.diff_text_ref = ft.Ref[ft.TextField]() + self.log_text_ref = ft.Ref[ft.TextField]() + self.edit_button_ref = ft.Ref[ft.IconButton]() + self.go_button_ref = ft.Ref[ft.ElevatedButton]() + + # Mode and filter refs + self.tools_mode_ref = ft.Ref[ft.RadioGroup]() + self.repo_source_ref = ft.Ref[ft.RadioGroup]() + self.item_type_ref = ft.Ref[ft.RadioGroup]() + self.create_type_ref = ft.Ref[ft.RadioGroup]() + self.target_repo_dropdown_ref = ft.Ref[ft.Dropdown]() + self.forked_repo_dropdown_ref = ft.Ref[ft.Dropdown]() + self.workflow_item_dropdown_ref = ft.Ref[ft.Dropdown]() + self.active_item_display_ref = ft.Ref[ft.Container]() + self.item_counter_ref = ft.Ref[ft.Text]() + + # DataTable ref for all items + self.items_table_ref = ft.Ref[ft.DataTable]() + + # All items display + self.all_items_container_ref = ft.Ref[ft.Column]() + self.all_items_search_ref = ft.Ref[ft.TextField]() + self.all_items_type_filter_ref = ft.Ref[ft.RadioGroup]() + self.all_items_repo_filter_ref = ft.Ref[ft.RadioGroup]() + self.item_detail_dialog_ref = ft.Ref[ft.AlertDialog]() + + # Sidebar state + self.sidebar_visible = True + self.sidebar_ref = ft.Ref[ft.Container]() + self.tools_content_ref = ft.Ref[ft.Column]() + + # Initialize cache manager + from .cache_manager import CacheManager + self.cache_manager = CacheManager(cache_duration_hours=24) + + # Initialize logger + self.logger = None # Will be set after UI is created + + # AI Action Plan state + self.current_action_plan = None + self.plan_display_ref = ft.Ref[ft.Column]() + self.plan_progress_ref = ft.Ref[ft.ProgressBar]() + self.plan_status_ref = ft.Ref[ft.Text]() + self.execute_plan_button_ref = ft.Ref[ft.ElevatedButton]() + self.generate_plan_button_ref = ft.Ref[ft.ElevatedButton]() + self.ai_instructions_ref = ft.Ref[ft.TextField]() + + # Register settings change listener for live updates + self.config_manager.register_listener(self._on_settings_changed) + + def build(self) -> ft.Container: + """Build and return the main UI with VS Code-style layout""" + # Top navigation bar with branding and buttons + top_nav = ft.Container( + content=ft.Row( + [ + ft.IconButton( + icon=ft.icons.MENU, + tooltip="Toggle GitHub Tools", + on_click=self._toggle_sidebar, + ), + ft.Icon(ft.icons.BOLT, color="blue", size=24), + ft.Text( + "GitHub Pulse", + size=20, + weight=ft.FontWeight.BOLD, + color="blue", + ), + ft.Container(expand=True), + ft.IconButton( + icon=ft.icons.LIST_ALT, + tooltip="Processing Log", + on_click=self._open_processing_log, + ), + ft.IconButton( + icon=ft.icons.SETTINGS, + tooltip="Settings", + on_click=self._open_settings, + ), + ], + alignment=ft.MainAxisAlignment.START, + ), + padding=15, + bgcolor=ft.colors.BLUE_GREY_900, + ) + + # Create sidebar (GitHub Tools) - collapsible + sidebar = ft.Container( + ref=self.sidebar_ref, + content=self._create_sidebar_content(), + width=350, + bgcolor=ft.colors.BLUE_GREY_900, + padding=15, + ) + + # Create main content area (tabs + status) + main_content = ft.Column( + [ + self._create_status_section(), + self._create_tabs_section(), + ], + spacing=10, + expand=True, + ) + + # Bottom section: Sidebar on left, content on right + bottom_section = ft.Row( + [ + sidebar, + ft.VerticalDivider(width=1), + ft.Container( + content=main_content, + expand=True, + padding=20, + ), + ], + spacing=0, + expand=True, + vertical_alignment=ft.CrossAxisAlignment.STRETCH, + ) + + # Create hidden log text field for the processing log dialog + hidden_log_text = ft.TextField( + ref=self.log_text_ref, + multiline=True, + read_only=True, + text_style=ft.TextStyle(font_family="Courier New"), + visible=False, # Hidden from main UI + ) + + # Overall layout: Top nav + bottom section + hidden log field + app_layout = ft.Column( + [ + top_nav, + ft.Divider(height=1), + bottom_section, + hidden_log_text, # Hidden but accessible for dialog + ], + spacing=0, + expand=True, + ) + + # Initialize logger after UI is created + if self.log_text_ref.current: + self.logger = Logger(self.log_text_ref.current) + + # Start async initialization + self.page.run_task(self._async_init) + + return ft.Container( + content=app_layout, + expand=True, + ) + + async def _async_init(self): + """Async initialization""" + await asyncio.sleep(0.5) + await self._load_custom_instructions() + await self._init_load_repos() + # Auto-load cached items after repos are loaded + await self._auto_load_cached_items() + + def _toggle_sidebar(self, e): + """Toggle sidebar visibility""" + self.sidebar_visible = not self.sidebar_visible + if self.sidebar_ref.current: + if self.sidebar_visible: + self.sidebar_ref.current.width = 350 + self.sidebar_ref.current.visible = True + else: + self.sidebar_ref.current.width = 0 + self.sidebar_ref.current.visible = False + self.page.update() + + def _create_title_section(self) -> ft.Container: + """Create the title section with buttons""" + return ft.Container( + content=ft.Row( + [ + ft.Container(expand=True), + ft.IconButton( + icon=ft.icons.SETTINGS, + tooltip="Settings", + on_click=self._open_settings, + ), + ], + alignment=ft.MainAxisAlignment.END, + ), + padding=ft.padding.only(bottom=10), + ) + + def _create_sidebar_content(self) -> ft.Column: + """Create the controls section""" + # Mode selection + mode_controls = ft.RadioGroup( + ref=self.tools_mode_ref, + content=ft.Row([ + ft.Radio(value="create", label="Create PR/Issue"), + ft.Radio(value="action", label="Action Existing PR/Issue"), + ]), + value="action", + on_change=self._on_mode_changed, + ) + + # Target Repository + target_repo_row = ft.Row( + [ + ft.Dropdown( + ref=self.target_repo_dropdown_ref, + label="Target Repository", + hint_text="Select target repository", + options=[], + expand=True, + on_change=self._on_repo_selection_changed, + ), + ft.IconButton( + icon=ft.icons.REFRESH, + tooltip="Refresh", + on_click=lambda e: self.page.run_task(self._refresh_target_repos_async), + ), + ft.IconButton( + icon=ft.icons.SEARCH, + tooltip="Search", + on_click=lambda e: self.page.run_task(self._search_target_repos_async), + ), + ], + spacing=5, + ) + + # Forked Repository + forked_repo_row = ft.Row( + [ + ft.Dropdown( + ref=self.forked_repo_dropdown_ref, + label="Forked Repository", + hint_text="Select forked repository", + options=[], + expand=True, + on_change=self._on_repo_selection_changed, + ), + ft.IconButton( + icon=ft.icons.REFRESH, + tooltip="Refresh", + on_click=lambda e: self.page.run_task(self._refresh_forked_repos_async), + ), + ft.IconButton( + icon=ft.icons.DOWNLOAD, + tooltip="Clone", + on_click=self._clone_forked_repo, + ), + ], + spacing=5, + ) + + # Action controls (for action mode) + action_controls = ft.Column( + [ + ft.Text("Active Item", weight=ft.FontWeight.BOLD, size=14), + ft.Row([ + ft.Container( + ref=self.active_item_display_ref, + content=ft.Text( + "No item selected", + color=ft.colors.GREY_500, + italic=True, + text_align=ft.TextAlign.CENTER, + ), + padding=10, + border=ft.border.all(1, ft.colors.OUTLINE), + border_radius=8, + bgcolor=ft.colors.GREY_900, + expand=True, + ), + ], spacing=5), + ft.Divider(height=10), + ft.Text("All Items", weight=ft.FontWeight.BOLD, size=14), + ft.Row([ + ft.ElevatedButton( + "šŸ“„ Pull PRs/Issues", + on_click=lambda e: self.page.run_task(self._load_workflow_items_async), + ), + ft.Text(ref=self.item_counter_ref, value="No items loaded"), + ]), + ft.TextField( + ref=self.all_items_search_ref, + hint_text="Search items...", + prefix_icon=ft.icons.SEARCH, + dense=True, + on_change=self._on_all_items_search_changed, + border_radius=8, + ), + ft.Text("Source Repo", weight=ft.FontWeight.BOLD), + ft.RadioGroup( + ref=self.all_items_type_filter_ref, + content=ft.Row([ + ft.Radio(value="both", label="Both"), + ft.Radio(value="prs", label="PRs"), + ft.Radio(value="issues", label="Issues"), + ], spacing=5), + value="both", + on_change=self._on_all_items_filter_changed, + ), + ft.Text("Item Type", weight=ft.FontWeight.BOLD), + ft.RadioGroup( + ref=self.all_items_repo_filter_ref, + content=ft.Row([ + ft.Radio(value="both", label="Both"), + ft.Radio(value="target", label="Target"), + ft.Radio(value="fork", label="Fork"), + ], spacing=5), + value="both", + on_change=self._on_all_items_filter_changed, + ), + ft.Container( + content=ft.Column( + ref=self.all_items_container_ref, + controls=[ + ft.Text("No items loaded", color=ft.colors.GREY_500, italic=True, text_align=ft.TextAlign.CENTER) + ], + spacing=10, + scroll=ft.ScrollMode.AUTO, + horizontal_alignment=ft.CrossAxisAlignment.STRETCH, + ), + height=300, + border=ft.border.all(1, ft.colors.OUTLINE), + border_radius=8, + padding=5, + ), + ], + spacing=10, + ) + + # Create controls (for create mode) + create_controls = ft.Column( + [ + ft.Text("Create Type", weight=ft.FontWeight.BOLD), + ft.RadioGroup( + ref=self.create_type_ref, + content=ft.Row([ + ft.Radio(value="pull_request", label="Pull Request"), + ft.Radio(value="issue", label="Issue"), + ]), + value="pull_request", + ), + ft.ElevatedButton( + "āœļø Create New", + on_click=self._create_new_item, + ), + ], + spacing=10, + visible=False, + ) + + # GitHub Tools content + return ft.Column( + [ + ft.Row([ + ft.Icon(ft.icons.SOURCE, size=20), + ft.Text("GitHub Tools", size=18, weight=ft.FontWeight.BOLD), + ]), + ft.Divider(height=20), + mode_controls, + ft.Divider(height=10), + target_repo_row, + forked_repo_row, + ft.Divider(height=10), + action_controls, + create_controls, + ], + spacing=10, + scroll=ft.ScrollMode.AUTO, + expand=True, # Make column expand to fill available space + ) + + def _create_status_section(self) -> ft.Container: + """Create the status section""" + return ft.Container( + content=ft.Column([ + ft.ProgressBar(ref=self.progress_bar_ref, visible=False), + ft.Text(ref=self.status_text_ref, value="Ready", size=14), + ]), + padding=ft.padding.symmetric(vertical=10), + ) + + def _create_tabs_section(self) -> ft.Container: + """Create the tabbed interface""" + tabs = ft.Tabs( + selected_index=0, + animation_duration=300, + tabs=[ + ft.Tab( + text="Current Item", + icon=ft.icons.DESCRIPTION, + content=self._create_current_item_tab() + ), + ft.Tab( + text="View Diff", + icon=ft.icons.DIFFERENCE, + content=self._create_diff_tab() + ), + ft.Tab( + text="AI Action Plan", + icon=ft.icons.AUTO_AWESOME, + content=self._create_ai_plan_tab() + ), + ], + expand=True, + ) + + return ft.Container( + content=tabs, + expand=True, + ) + + def _create_current_item_tab(self) -> ft.Container: + """Create the current item tab""" + # Create a container to hold the dynamic content + self.current_item_content_ref = ft.Ref[ft.Column]() + + # Default empty state + default_content = ft.Column( + [ + ft.Container( + content=ft.Column([ + ft.Icon(ft.icons.INBOX, size=64, color=ft.colors.GREY_500), + ft.Text( + "No item selected", + size=18, + weight=ft.FontWeight.BOLD, + color=ft.colors.GREY_500, + ), + ft.Text( + "Select a PR or Issue from the sidebar to view details", + size=14, + color=ft.colors.GREY_600, + text_align=ft.TextAlign.CENTER, + ), + ], horizontal_alignment=ft.CrossAxisAlignment.CENTER, spacing=10), + alignment=ft.alignment.center, + expand=True, + ), + ], + ref=self.current_item_content_ref, + spacing=15, + scroll=ft.ScrollMode.AUTO, + ) + + return ft.Container( + content=ft.ListView( + controls=[default_content], + spacing=0, + padding=20, + ), + expand=True, + ) + + def _create_diff_tab(self) -> ft.Container: + """Create the diff view tab""" + diff_buttons = ft.Row( + [ + ft.ElevatedButton( + "Find .diff Files", + icon=ft.icons.SEARCH, + on_click=self.find_and_load_diff_files, + ), + ft.ElevatedButton( + "Clear Diff", + icon=ft.icons.CLEAR, + on_click=self.clear_diff_display, + ), + ], + spacing=10, + ) + + diff_text = ft.TextField( + ref=self.diff_text_ref, + multiline=True, + read_only=True, + expand=True, + text_style=ft.TextStyle(font_family="Courier New"), + ) + + return ft.Container( + content=ft.Column([ + diff_buttons, + diff_text, + ], spacing=10, expand=True), + padding=20, + expand=True, + ) + + def _create_all_items_tab(self) -> ft.Container: + """Create the all items tab""" + # DataTable for items + items_table = ft.DataTable( + ref=self.items_table_ref, + columns=[ + ft.DataColumn(ft.Text("Repo")), + ft.DataColumn(ft.Text("Type")), + ft.DataColumn(ft.Text("ID")), + ft.DataColumn(ft.Text("Title")), + ft.DataColumn(ft.Text("Author")), + ft.DataColumn(ft.Text("Status")), + ], + rows=[], + border=ft.border.all(1, ft.colors.OUTLINE), + border_radius=8, + heading_row_color=ft.colors.BLUE_GREY_100, + ) + + set_current_button = ft.ElevatedButton( + "Set as Current Item", + icon=ft.icons.CHECK_CIRCLE, + on_click=self._select_current_item, + ) + + return ft.Container( + content=ft.Column([ + set_current_button, + ft.ListView( + controls=[items_table], + expand=True, + ), + ], spacing=10, expand=True), + padding=20, + expand=True, + ) + + # ===== Event Handlers ===== + + def _on_settings_changed(self, key: str, value: any): + """ + Handle settings changes from settings dialog (live updates). + + Args: + key: Setting key that changed + value: New value + """ + # Update repository dropdowns when repos change in settings + if key == 'GITHUB_REPO': + if self.target_repo_dropdown_ref.current: + self.target_repo_dropdown_ref.current.value = value + self.page.update() + print(f"āœ“ Main GUI: Target repo updated to {value}") + + elif key == 'FORKED_REPO': + if self.forked_repo_dropdown_ref.current: + self.forked_repo_dropdown_ref.current.value = value + self.page.update() + print(f"āœ“ Main GUI: Forked repo updated to {value}") + + def _on_mode_changed(self, e): + """Handle mode change between create and action""" + # This would toggle visibility of create vs action controls + # Implementation depends on UI structure + pass + + def _on_repo_selection_changed(self, e): + """Handle repository selection change""" + # Save selected repos to settings + config = self.config_manager.get_config() + + if self.target_repo_dropdown_ref.current and self.target_repo_dropdown_ref.current.value: + target_value = self.target_repo_dropdown_ref.current.value + # Don't save separator headers + if not target_value.startswith('---'): + config['GITHUB_REPO'] = target_value + + if self.forked_repo_dropdown_ref.current and self.forked_repo_dropdown_ref.current.value: + forked_value = self.forked_repo_dropdown_ref.current.value + # Don't save separator headers + if not forked_value.startswith('---'): + config['FORKED_REPO'] = forked_value + + # Save to config + self.config_manager.save_configuration(config) + + # Clear workflow items when repos change + self.workflow_items = {} + self.current_workflow_items = [] + if self.workflow_item_dropdown_ref.current: + self.workflow_item_dropdown_ref.current.options = [] + self.page.update() + + # Auto-load cached items for the newly selected repos + self.page.run_task(self._auto_load_cached_items_on_repo_change) + + def _on_workflow_item_selected(self, e): + """Handle workflow item selection""" + if not self.workflow_item_dropdown_ref.current: + return + + selected = self.workflow_item_dropdown_ref.current.value + if selected: + # Find the item and display it + for item in self.current_workflow_items: + if hasattr(item, 'title') and item.title == selected: + self._display_workflow_item(item) + break + + def _on_all_items_search_changed(self, e): + """Handle search field change in All Items list""" + if not self.all_items_search_ref.current: + return + + search_query = self.all_items_search_ref.current.value or "" + type_filter = self.all_items_type_filter_ref.current.value if self.all_items_type_filter_ref.current else "both" + repo_filter = self.all_items_repo_filter_ref.current.value if self.all_items_repo_filter_ref.current else "both" + self._populate_all_items(search_query, type_filter, repo_filter) + + def _on_all_items_filter_changed(self, e): + """Handle filter change in All Items list (type or repo source)""" + search_query = self.all_items_search_ref.current.value if self.all_items_search_ref.current else "" + type_filter = self.all_items_type_filter_ref.current.value if self.all_items_type_filter_ref.current else "both" + repo_filter = self.all_items_repo_filter_ref.current.value if self.all_items_repo_filter_ref.current else "both" + self._populate_all_items(search_query, type_filter, repo_filter) + + def _filter_workflow_items(self): + """Collect all workflow items (no filtering since toggles were removed)""" + print("=" * 60) + print("COLLECTING WORKFLOW ITEMS") + print("=" * 60) + + # Collect all items from all categories since filter toggles are removed + all_items = [] + for key, items in self.workflow_items.items(): + all_items.extend(items) + + self.current_workflow_items = all_items + print(f"DEBUG: Collected {len(all_items)} total items") + print(f"DEBUG: Available keys in workflow_items: {list(self.workflow_items.keys())}") + + if self.logger: + self.logger.log(f"Collected {len(all_items)} workflow items from all categories") + self.logger.log(f"Available workflow item keys: {list(self.workflow_items.keys())}") + + # Update item counter if it exists + if self.item_counter_ref.current: + count_text = f"{len(all_items)} item(s) loaded" + self.item_counter_ref.current.value = count_text + print(f"DEBUG: Counter text set to: {count_text}") + + print("DEBUG: Calling page.update()...") + self.page.update() + print("DEBUG: page.update() completed") + + def _display_workflow_item(self, item): + """Display a workflow item in the Current Item tab""" + if not self.current_item_content_ref.current: + return + + # Get repo string based on source + config = self.config_manager.get_config() + if item.repo_source == "target": + repo_str = config.get('GITHUB_REPO', '') + else: + repo_str = config.get('FORKED_REPO', '') + + # Fetch comments + comments = [] + pr_files = [] + try: + workflow_manager = self._get_workflow_manager() + comments = workflow_manager.fetch_comments(repo_str, item.number, item.item_type == "pull_request") + + # Fetch PR files if this is a pull request + if item.item_type == "pull_request": + pr_files = workflow_manager.fetch_pr_files(repo_str, item.number) + except Exception as e: + print(f"Error fetching item details: {e}") + if self.logger: + self.logger.log(f"Error fetching item details: {e}") + + # Build the display + controls = [] + + # Header section + header = ft.Container( + content=ft.Column([ + ft.Row([ + ft.Container( + content=ft.Text( + "PR" if item.item_type == "pull_request" else "Issue", + size=12, + weight=ft.FontWeight.BOLD, + color=ft.colors.WHITE, + ), + bgcolor=ft.colors.GREEN if item.item_type == "pull_request" else ft.colors.ORANGE, + padding=ft.padding.symmetric(horizontal=8, vertical=4), + border_radius=4, + ), + ft.Text(f"#{item.number}", size=18, weight=ft.FontWeight.BOLD), + ft.Container(expand=True), + ft.IconButton( + icon=ft.icons.OPEN_IN_BROWSER, + tooltip="Open in GitHub", + on_click=lambda e: self.page.launch_url(item.url), + ), + ], alignment=ft.MainAxisAlignment.START), + ft.Text(item.title, size=20, weight=ft.FontWeight.BOLD), + ], spacing=8), + padding=15, + bgcolor=ft.colors.BLUE_GREY_900, + border_radius=8, + ) + controls.append(header) + + # Basic Info section + info_items = [ + ft.Row([ + ft.Icon(ft.icons.PERSON, size=16, color=ft.colors.BLUE_400), + ft.Text("Created by:", weight=ft.FontWeight.BOLD, size=14), + ft.Text(f"@{item.author}", size=14, color=ft.colors.BLUE_300), + ], spacing=5), + ft.Row([ + ft.Icon(ft.icons.CALENDAR_TODAY, size=16, color=ft.colors.BLUE_400), + ft.Text("Created:", weight=ft.FontWeight.BOLD, size=14), + ft.Text(item.created_at[:10] if item.created_at else 'Unknown', size=14), + ], spacing=5), + ft.Row([ + ft.Icon(ft.icons.UPDATE, size=16, color=ft.colors.BLUE_400), + ft.Text("Last Updated:", weight=ft.FontWeight.BOLD, size=14), + ft.Text(item.updated_at[:10] if item.updated_at else 'Unknown', size=14), + ], spacing=5), + ft.Row([ + ft.Icon(ft.icons.CIRCLE, size=16, color=ft.colors.GREEN if item.state == "open" else ft.colors.PURPLE), + ft.Text("Status:", weight=ft.FontWeight.BOLD, size=14), + ft.Text(item.state.capitalize(), size=14, color=ft.colors.GREEN if item.state == "open" else ft.colors.PURPLE), + ], spacing=5), + ] + + # Add assignees with assign-to-self button + if item.assignees: + assignees_text = ", ".join([f"@{a}" for a in item.assignees]) + info_items.append( + ft.Row([ + ft.Icon(ft.icons.ASSIGNMENT_IND, size=16, color=ft.colors.BLUE_400), + ft.Text("Assigned to:", weight=ft.FontWeight.BOLD, size=14), + ft.Text(assignees_text, size=14, color=ft.colors.BLUE_300), + ft.IconButton( + icon=ft.icons.PERSON_ADD, + icon_size=16, + tooltip="Assign to me", + on_click=lambda _: self._assign_to_self(item, repo_str), + ), + ], spacing=5) + ) + else: + info_items.append( + ft.Row([ + ft.Icon(ft.icons.ASSIGNMENT_IND, size=16, color=ft.colors.GREY_600), + ft.Text("Assigned to:", weight=ft.FontWeight.BOLD, size=14), + ft.Text("Unassigned", size=14, color=ft.colors.GREY_500, italic=True), + ft.IconButton( + icon=ft.icons.PERSON_ADD, + icon_size=16, + tooltip="Assign to me", + on_click=lambda _: self._assign_to_self(item, repo_str), + ), + ], spacing=5) + ) + + # PR-specific info + if item.item_type == "pull_request": + merge_status_color = ft.colors.GREEN if item.merged else (ft.colors.ORANGE if item.state == "open" else ft.colors.GREY_600) + merge_status_text = "Merged" if item.merged else ("Pending Merge" if item.state == "open" else "Closed without merge") + info_items.append( + ft.Row([ + ft.Icon(ft.icons.MERGE_TYPE, size=16, color=merge_status_color), + ft.Text("Merge Status:", weight=ft.FontWeight.BOLD, size=14), + ft.Text(merge_status_text, size=14, color=merge_status_color), + ], spacing=5) + ) + + info_section = ft.Container( + content=ft.Column(info_items, spacing=8), + padding=15, + border=ft.border.all(1, ft.colors.OUTLINE), + border_radius=8, + ) + controls.append(info_section) + + # Description section (collapsible, collapsed by default) + description_section = ft.ExpansionTile( + title=ft.Text("Description", size=16, weight=ft.FontWeight.BOLD), + subtitle=ft.Text("Click to expand", size=12, color=ft.colors.GREY_500), + initially_expanded=False, + controls=[ + ft.Container( + content=ft.Container( + content=ft.Row([ + ft.Text( + item.body if item.body else "No description provided", + size=14, + selectable=True, + ), + ], spacing=5), + padding=10, + border=ft.border.all(1, ft.colors.OUTLINE), + border_radius=4, + bgcolor=ft.colors.GREY_900, + ), + margin=ft.margin.only(left=10, right=10, bottom=10), + ), + ], + ) + controls.append( + ft.Container( + content=description_section, + border=ft.border.all(1, ft.colors.OUTLINE), + border_radius=8, + ) + ) + + # PR Files section + if item.item_type == "pull_request" and pr_files: + files_widgets = [] + for file in pr_files: + status_color = { + 'added': ft.colors.GREEN, + 'removed': ft.colors.RED, + 'modified': ft.colors.ORANGE, + 'renamed': ft.colors.BLUE, + }.get(file['status'], ft.colors.GREY_400) + + files_widgets.append( + ft.Container( + content=ft.Row([ + ft.Icon(ft.icons.INSERT_DRIVE_FILE, size=16, color=status_color), + ft.Text(file['filename'], size=13, expand=True), + ft.Container( + content=ft.Text(file['status'], size=11, color=ft.colors.WHITE), + bgcolor=status_color, + padding=ft.padding.symmetric(horizontal=6, vertical=2), + border_radius=3, + ), + ft.Text(f"+{file['additions']} -{file['deletions']}", + size=12, + color=ft.colors.GREY_400), + ], spacing=8), + padding=8, + border=ft.border.all(1, ft.colors.OUTLINE), + border_radius=4, + bgcolor=ft.colors.GREY_900, + ) + ) + + files_section = ft.Container( + content=ft.Column([ + ft.Text(f"Modified Files ({len(pr_files)})", size=16, weight=ft.FontWeight.BOLD), + ft.Column( + controls=files_widgets, + spacing=5, + scroll=ft.ScrollMode.AUTO, + height=min(200, len(pr_files) * 50), + ), + ], spacing=8), + padding=15, + border=ft.border.all(1, ft.colors.OUTLINE), + border_radius=8, + ) + controls.append(files_section) + + # Comments section (collapsible, collapsed by default) + comments_widgets = [] + if comments: + for comment in comments: + comments_widgets.append( + ft.Container( + content=ft.Column([ + ft.Row([ + ft.Icon(ft.icons.PERSON, size=14), + ft.Text(f"@{comment['user']}", weight=ft.FontWeight.BOLD, size=13), + ft.Text( + comment['created_at'][:10] if comment.get('created_at') else '', + size=11, + color=ft.colors.GREY_600 + ), + ], spacing=5), + ft.Text(comment['body'], size=13, selectable=True), + ], spacing=5), + padding=10, + border=ft.border.all(1, ft.colors.OUTLINE), + border_radius=4, + bgcolor=ft.colors.GREY_900, + ) + ) + else: + comments_widgets.append( + ft.Text("No comments yet", italic=True, color=ft.colors.GREY_500, size=13) + ) + + comments_section = ft.ExpansionTile( + title=ft.Text(f"Comments ({len(comments)})", size=16, weight=ft.FontWeight.BOLD), + subtitle=ft.Text("Click to expand", size=12, color=ft.colors.GREY_500), + initially_expanded=False, + controls=[ + ft.Container( + content=ft.Column( + controls=comments_widgets, + spacing=8, + scroll=ft.ScrollMode.AUTO, + height=min(250, max(100, len(comments) * 80)), + ), + margin=ft.margin.only(left=10, right=10, bottom=10), + ), + ], + ) + controls.append( + ft.Container( + content=comments_section, + border=ft.border.all(1, ft.colors.OUTLINE), + border_radius=8, + ) + ) + + # AI Analysis section (placeholder for now) + self.ai_analysis_result_ref = ft.Ref[ft.Column]() + ai_section = self._create_ai_analysis_section(item, repo_str, pr_files, comments) + controls.append(ai_section) + + # Update the content + self.current_item_content_ref.current.controls = controls + self.page.update() + + def _create_ai_analysis_section(self, item, repo_str, pr_files, comments): + """Create the AI Analysis section""" + # Check if AI provider is configured + config = self.config_manager.get_config() + ai_provider = config.get('AI_PROVIDER', 'none').lower() + ai_configured = ai_provider and ai_provider != 'none' + + # Create result container + ai_result_container = ft.Column( + ref=self.ai_analysis_result_ref, + controls=[], + spacing=10, + ) + + # Create analyze button or disabled message + if ai_configured: + # Create a wrapper function that captures the parameters + async def run_analysis_wrapper(): + await self._run_ai_analysis_async(item, repo_str, pr_files, comments) + + analyze_button = ft.ElevatedButton( + "Run AI Analysis", + icon=ft.icons.AUTO_AWESOME, + on_click=lambda _: self.page.run_task(run_analysis_wrapper), + ) + button_row = ft.Row([ + analyze_button, + ft.Text( + f"Using {ai_provider.upper()}", + size=12, + color=ft.colors.BLUE_300, + italic=True, + ), + ], spacing=10) + else: + button_row = ft.Container( + content=ft.Row([ + ft.Icon(ft.icons.INFO_OUTLINE, size=16, color=ft.colors.ORANGE), + ft.Text( + "AI Analysis is not available. Please configure an AI provider in Settings.", + size=13, + color=ft.colors.ORANGE, + ), + ], spacing=8), + padding=10, + border=ft.border.all(1, ft.colors.ORANGE), + border_radius=4, + bgcolor=ft.colors.GREY_900, + ) + + ai_section = ft.Container( + content=ft.Column([ + ft.Text("AI Analysis", size=16, weight=ft.FontWeight.BOLD), + ft.Text( + "For PRs: Analyze changes and create a summary. For Issues: Find relevant files and suggest fixes.", + size=12, + color=ft.colors.GREY_400, + ), + button_row, + ai_result_container, + ], spacing=10), + padding=15, + border=ft.border.all(1, ft.colors.OUTLINE), + border_radius=8, + ) + + return ai_section + + async def _run_ai_analysis_async(self, item, repo_str, pr_files, comments): + """Run AI analysis on the selected item""" + if not self.ai_analysis_result_ref.current: + return + + # Show loading state + self.ai_analysis_result_ref.current.controls = [ + ft.Container( + content=ft.Row([ + ft.ProgressRing(width=16, height=16), + ft.Text("Analyzing...", size=14), + ], spacing=10), + padding=10, + ) + ] + self.page.update() + + def run_analysis(): + try: + config = self.config_manager.get_config() + ai_provider = config.get('AI_PROVIDER', 'none').lower() + + if item.item_type == "pull_request": + # PR Analysis: Summarize changes + result = self._analyze_pr(item, repo_str, pr_files, comments, ai_provider, config) + else: + # Issue Analysis: Find files and suggest fixes + result = self._analyze_issue(item, repo_str, comments, ai_provider, config) + + return result + + except Exception as e: + error_msg = f"Error during AI analysis: {str(e)}" + if self.logger: + self.logger.log(error_msg) + return { + 'success': False, + 'error': error_msg + } + + # Run in thread + result = await asyncio.to_thread(run_analysis) + + # Display results + if result.get('success'): + result_widgets = [ + ft.Container( + content=ft.Column([ + ft.Row([ + ft.Icon(ft.icons.CHECK_CIRCLE, size=16, color=ft.colors.GREEN), + ft.Text("Analysis Complete", weight=ft.FontWeight.BOLD, size=14, color=ft.colors.GREEN), + ], spacing=5), + ft.Divider(height=10), + ft.Text(result.get('summary', ''), size=13, selectable=True), + ], spacing=10), + padding=15, + border=ft.border.all(1, ft.colors.GREEN), + border_radius=8, + bgcolor=ft.colors.GREY_900, + ) + ] + + # Add suggested files for issues + if item.item_type == "issue" and result.get('suggested_files'): + result_widgets.append( + ft.Container( + content=ft.Column([ + ft.Text("Suggested Files to Modify:", weight=ft.FontWeight.BOLD, size=14), + ft.Column([ + ft.Text(f"• {file}", size=13, color=ft.colors.BLUE_300) + for file in result['suggested_files'] + ], spacing=5), + ], spacing=8), + padding=15, + border=ft.border.all(1, ft.colors.BLUE), + border_radius=8, + bgcolor=ft.colors.GREY_900, + ) + ) + + # Add "Create PR with AI Fix" button + result_widgets.append( + ft.ElevatedButton( + "Create PR with AI-Suggested Fix", + icon=ft.icons.AUTO_FIX_HIGH, + on_click=lambda _: self._create_pr_from_ai_fix(item, result), + ) + ) + + self.ai_analysis_result_ref.current.controls = result_widgets + else: + # Show error + self.ai_analysis_result_ref.current.controls = [ + ft.Container( + content=ft.Row([ + ft.Icon(ft.icons.ERROR_OUTLINE, size=16, color=ft.colors.RED), + ft.Text( + result.get('error', 'Unknown error occurred'), + size=13, + color=ft.colors.RED, + ), + ], spacing=8), + padding=10, + border=ft.border.all(1, ft.colors.RED), + border_radius=4, + bgcolor=ft.colors.GREY_900, + ) + ] + + self.page.update() + + def _analyze_pr(self, item, repo_str, pr_files, comments, ai_provider, config): + """Analyze a Pull Request using AI""" + try: + # Build context for AI + context = f"""Pull Request Analysis Request + +Repository: {repo_str} +PR Number: #{item.number} +Title: {item.title} +State: {item.state} +Merged: {item.merged} + +Description: +{item.body if item.body else 'No description provided'} + +Modified Files ({len(pr_files)}): +""" + for file in pr_files: + context += f"\n- {file['filename']} ({file['status']}) [+{file['additions']} -{file['deletions']}]" + + if comments: + context += f"\n\nComments ({len(comments)}):\n" + for comment in comments[:5]: # Limit to first 5 comments + context += f"\n@{comment['user']}: {comment['body'][:200]}...\n" + + context += "\n\nPlease provide a comprehensive summary of this pull request, including:\n" + context += "1. What changes were made\n" + context += "2. The purpose and impact of these changes\n" + context += "3. Any notable patterns or concerns from the comments\n" + context += "4. Overall assessment of the PR" + + # Call AI manager + summary = self.ai_manager.generate_response(context, ai_provider, config) + + if self.logger: + self.logger.log(f"AI PR Analysis completed for PR #{item.number}") + + return { + 'success': True, + 'summary': summary + } + + except Exception as e: + if self.logger: + self.logger.log(f"Error in PR analysis: {e}") + return { + 'success': False, + 'error': str(e) + } + + def _analyze_issue(self, item, repo_str, comments, ai_provider, config): + """Analyze an Issue using AI to suggest fixes""" + try: + # Build context for AI + context = f"""GitHub Issue Analysis Request + +Repository: {repo_str} +Issue Number: #{item.number} +Title: {item.title} +State: {item.state} + +Description: +{item.body if item.body else 'No description provided'} +""" + + if comments: + context += f"\n\nComments ({len(comments)}):\n" + for comment in comments[:5]: # Limit to first 5 comments + context += f"\n@{comment['user']}: {comment['body'][:200]}...\n" + + context += "\n\nPlease analyze this issue and provide:\n" + context += "1. A summary of the issue\n" + context += "2. Suggested files or components that might be causing this issue\n" + context += "3. Recommended approach to fix the issue\n" + context += "4. Any additional considerations\n" + context += "\nFor the suggested files, please list them in a clear format like:\n" + context += "SUGGESTED_FILES: file1.py, file2.js, file3.tsx" + + # Call AI manager + analysis = self.ai_manager.generate_response(context, ai_provider, config) + + # Try to extract suggested files from the response + suggested_files = [] + if "SUGGESTED_FILES:" in analysis: + files_line = analysis.split("SUGGESTED_FILES:")[1].split("\n")[0] + suggested_files = [f.strip() for f in files_line.split(",") if f.strip()] + + if self.logger: + self.logger.log(f"AI Issue Analysis completed for Issue #{item.number}") + + return { + 'success': True, + 'summary': analysis, + 'suggested_files': suggested_files + } + + except Exception as e: + if self.logger: + self.logger.log(f"Error in Issue analysis: {e}") + return { + 'success': False, + 'error': str(e) + } + + def _create_pr_from_ai_fix(self, item, _analysis_result): + """Create a PR with AI-suggested fix for an issue""" + # TODO: Implement PR creation with AI fix + # The analysis_result will contain suggested files and fix recommendations + self._show_snackbar("PR creation with AI fix - Coming soon!", error=False) + if self.logger: + self.logger.log(f"PR creation requested for Issue #{item.number}") + + def _assign_to_self(self, item, repo_str): + """Assign the current PR or Issue to the authenticated user""" + try: + # Get GitHub token + config = self.config_manager.get_config() + github_token = config.get('GITHUB_PAT', '') + + if not github_token: + self._show_snackbar("GitHub token not configured", error=True) + return + + # Parse repository + if '/' not in repo_str: + self._show_snackbar("Invalid repository format", error=True) + return + + owner, repo = repo_str.split('/', 1) + + # Get authenticated user + import requests + headers = { + "Authorization": f"Bearer {github_token}", + "Accept": "application/vnd.github+json", + "User-Agent": "github-pulse/1.0" + } + + # First, get the authenticated user's username + user_response = requests.get("https://api.github.com/user", headers=headers, timeout=10) + user_response.raise_for_status() + username = user_response.json().get('login') + + if not username: + self._show_snackbar("Could not get authenticated user", error=True) + return + + # Assign to self using the GitHub API + # For both PRs and Issues, we use the issues endpoint + url = f"https://api.github.com/repos/{owner}/{repo}/issues/{item.number}/assignees" + + # Add the authenticated user to assignees + payload = { + "assignees": [username] + } + + response = requests.post(url, headers=headers, json=payload, timeout=10) + response.raise_for_status() + + # Update the item in memory + if username not in item.assignees: + item.assignees.append(username) + + # Refresh the display + self._display_workflow_item(item) + + self._show_snackbar(f"Successfully assigned to @{username}", error=False) + + if self.logger: + self.logger.log(f"Assigned {item.item_type} #{item.number} to @{username}") + + except requests.exceptions.RequestException as e: + error_msg = f"Error assigning to self: {str(e)}" + self._show_snackbar(error_msg, error=True) + if self.logger: + self.logger.log(error_msg) + except Exception as e: + error_msg = f"Unexpected error: {str(e)}" + self._show_snackbar(error_msg, error=True) + if self.logger: + self.logger.log(error_msg) + + def _populate_all_items(self, search_query: str = "", type_filter: str = "both", repo_filter: str = "both"): + """Populate the all items list with all loaded PRs and Issues + + Args: + search_query: Optional search string to filter items + type_filter: Filter by item type - "both", "prs", or "issues" + repo_filter: Filter by repo source - "both", "target", or "fork" + """ + if not self.all_items_container_ref.current: + return + + # Collect all items from workflow_items + all_items = [] + for key, items in self.workflow_items.items(): + all_items.extend(items) + + # Apply repo source filter + if repo_filter == "target": + all_items = [item for item in all_items if item.repo_source == "target"] + elif repo_filter == "fork": + all_items = [item for item in all_items if item.repo_source == "fork"] + # "both" shows everything, no filtering needed + + # Apply type filter + if type_filter == "prs": + all_items = [item for item in all_items if item.item_type == "pull_request"] + elif type_filter == "issues": + all_items = [item for item in all_items if item.item_type == "issue"] + # "both" shows everything, no filtering needed + + # Apply search filter if provided + if search_query: + search_lower = search_query.lower() + filtered_items = [] + for item in all_items: + # Search in title, number, state, author, and labels + if (search_lower in item.title.lower() or + search_lower in str(item.number) or + search_lower in item.state.lower() or + (item.author and search_lower in item.author.lower()) or + any(search_lower in label.lower() for label in (item.labels or []))): + filtered_items.append(item) + all_items = filtered_items + + if not all_items: + if search_query or type_filter != "both" or repo_filter != "both": + filter_desc = [] + if search_query: + filter_desc.append(f"matching '{search_query}'") + if type_filter == "prs": + filter_desc.append("PRs only") + elif type_filter == "issues": + filter_desc.append("Issues only") + if repo_filter == "target": + filter_desc.append("Target repo only") + elif repo_filter == "fork": + filter_desc.append("Fork repo only") + + msg = "No items " + " and ".join(filter_desc) if filter_desc else "No items loaded" + self.all_items_container_ref.current.controls = [ + ft.Text(msg, color=ft.colors.GREY_500, italic=True) + ] + else: + self.all_items_container_ref.current.controls = [ + ft.Text("No items loaded", color=ft.colors.GREY_500, italic=True) + ] + else: + # Sort by updated_at (most recent first) + all_items.sort(key=lambda x: x.updated_at if hasattr(x, 'updated_at') else '', reverse=True) + + # Create item cards + cards = [] + for item in all_items: + cards.append(self._create_item_card(item)) + + self.all_items_container_ref.current.controls = cards + + self.page.update() + + def _create_item_card(self, item): + """Create a card for a workflow item""" + # Determine repo source label + repo_label = "Target" if item.repo_source == "target" else "Fork" + repo_color = ft.colors.BLUE if item.repo_source == "target" else ft.colors.PURPLE + + # Determine type label + type_label = "PR" if item.item_type == "pull_request" else "Issue" + type_color = ft.colors.GREEN if item.item_type == "pull_request" else ft.colors.ORANGE + + # Create card + return ft.Container( + content=ft.Row( + [ + # Repo source badge + ft.Container( + content=ft.Text(repo_label, size=10, weight=ft.FontWeight.BOLD), + bgcolor=repo_color, + padding=ft.padding.symmetric(horizontal=8, vertical=4), + border_radius=4, + ), + # Type badge + ft.Container( + content=ft.Text(type_label, size=10, weight=ft.FontWeight.BOLD), + bgcolor=type_color, + padding=ft.padding.symmetric(horizontal=8, vertical=4), + border_radius=4, + ), + # Title + ft.Text( + f"#{item.number}: {item.title}", + size=12, + expand=True, + overflow=ft.TextOverflow.ELLIPSIS, + ), + # Select button + ft.IconButton( + icon=ft.icons.CHECK_CIRCLE_OUTLINE, + icon_size=16, + tooltip="Select as current item", + on_click=lambda e, it=item: self._select_item_as_current(it), + ), + # View details button + ft.IconButton( + icon=ft.icons.OPEN_IN_NEW, + icon_size=16, + tooltip="View details", + on_click=lambda e, it=item: self._show_item_detail(it), + ), + ], + spacing=8, + alignment=ft.MainAxisAlignment.START, + ), + padding=8, + border=ft.border.all(1, ft.colors.OUTLINE), + border_radius=4, + bgcolor=ft.colors.GREY_800, + ) + + def _populate_all_items_table(self): + """Populate the DataTable in the All Items tab with all loaded PRs and Issues""" + if not self.items_table_ref.current: + return + + # Collect all items from workflow_items + all_items = [] + for key, items in self.workflow_items.items(): + all_items.extend(items) + + if not all_items: + self.items_table_ref.current.rows = [] + else: + # Sort by updated_at (most recent first) + all_items.sort(key=lambda x: x.updated_at if hasattr(x, 'updated_at') else '', reverse=True) + + # Create table rows + rows = [] + for item in all_items: + # Determine repo source and type + repo_source = "Target" if item.repo_source == "target" else "Fork" + item_type = "PR" if item.item_type == "pull_request" else "Issue" + + # Get author (item.author is already a string, not a dict) + author = item.author if item.author else 'Unknown' + + # Get state + state = item.state if hasattr(item, 'state') else 'unknown' + + # Get repo name + config = self.config_manager.get_config() + if item.repo_source == "target": + repo_name = config.get('GITHUB_REPO', '') + else: + repo_name = config.get('FORKED_REPO', '') + + # Create row with clickable button + row = ft.DataRow( + cells=[ + ft.DataCell(ft.Text(f"{repo_source}: {repo_name.split('/')[-1] if '/' in repo_name else repo_name}", size=12)), + ft.DataCell(ft.Text(item_type, size=12)), + ft.DataCell(ft.Text(f"#{item.number}", size=12)), + ft.DataCell(ft.Text(item.title[:50] + "..." if len(item.title) > 50 else item.title, size=12)), + ft.DataCell(ft.Text(author, size=12)), + ft.DataCell(ft.Text(state, size=12)), + ], + on_select_changed=lambda e, it=item: self._show_item_detail(it) if e.control.selected else None, + ) + rows.append(row) + + self.items_table_ref.current.rows = rows + + self.page.update() + + def _select_item_as_current(self, item): + """Select an item as the current active workflow item""" + if not self.active_item_display_ref.current: + return + + # Store the active item + self.active_workflow_item = item + + # Enable the Generate Plan button when an item is selected + if self.generate_plan_button_ref.current: + self.generate_plan_button_ref.current.disabled = False + + # Determine display labels + repo_label = "Target" if item.repo_source == "target" else "Fork" + repo_color = ft.colors.BLUE if item.repo_source == "target" else ft.colors.PURPLE + type_label = "PR" if item.item_type == "pull_request" else "Issue" + type_color = ft.colors.GREEN if item.item_type == "pull_request" else ft.colors.ORANGE + + # Update the active item display with a nice card + self.active_item_display_ref.current.content = ft.Column([ + ft.Row([ + # Repo badge + ft.Container( + content=ft.Text(repo_label, size=10, weight=ft.FontWeight.BOLD, color=ft.colors.WHITE), + bgcolor=repo_color, + padding=ft.padding.symmetric(horizontal=6, vertical=2), + border_radius=4, + ), + # Type badge + ft.Container( + content=ft.Text(type_label, size=10, weight=ft.FontWeight.BOLD, color=ft.colors.WHITE), + bgcolor=type_color, + padding=ft.padding.symmetric(horizontal=6, vertical=2), + border_radius=4, + ), + ft.Container(expand=True), + # Clear button + ft.IconButton( + icon=ft.icons.CLOSE, + icon_size=16, + tooltip="Clear selection", + on_click=self._clear_active_item, + ), + ], spacing=5), + ft.Text( + f"#{item.number}: {item.title}", + size=12, + weight=ft.FontWeight.BOLD, + ), + ], spacing=5) + + # Collect workflow items (filter toggles were removed, so this just collects all items) + self._filter_workflow_items() + + # Display the item + self._display_workflow_item(item) + + # Update the page + self.page.update() + + # Show confirmation + item_type_label = "PR" if item.item_type == "pull_request" else "Issue" + repo_label = "Target" if item.repo_source == "target" else "Fork" + self._show_snackbar(f"Selected {item_type_label} from {repo_label}: {item.title}", error=False) + + def _clear_active_item(self, e=None): + """Clear the active item selection""" + if not self.active_item_display_ref.current: + return + + # Clear the stored active item + self.active_workflow_item = None + + # Reset the display to default "No item selected" + self.active_item_display_ref.current.content = ft.Text( + "No item selected", + color=ft.colors.GREY_500, + italic=True, + text_align=ft.TextAlign.CENTER, + ) + + # Update the page + self.page.update() + + # Show confirmation + self._show_snackbar("Active item cleared", error=False) + + def _show_item_detail(self, item): + """Show detail dialog for a workflow item""" + # Get repo string for fetching comments + config = self.config_manager.get_config() + if item.repo_source == "target": + repo_str = config.get('GITHUB_REPO', '') + else: + repo_str = config.get('FORKED_REPO', '') + + # Build the dialog + dialog = self._build_item_detail_dialog(item, repo_str) + + # Use Flet 0.28+ API: page.open() instead of page.dialog + self.page.open(dialog) + + def _build_item_detail_dialog(self, item, repo_str): + """Build the detail dialog with tabs for Main (Preview) and System (extracted data)""" + + # Get repo name for display + config = self.config_manager.get_config() + if item.repo_source == "target": + repo_name = config.get('GITHUB_REPO', '') + else: + repo_name = config.get('FORKED_REPO', '') + + # Create header with repo and item info + header = ft.Container( + content=ft.Column([ + ft.Row([ + ft.Icon(ft.icons.SOURCE, size=16), + ft.Text(repo_name, size=12, weight=ft.FontWeight.BOLD), + ft.Container( + content=ft.Text( + "PR" if item.item_type == "pull_request" else "Issue", + size=10, + color=ft.colors.WHITE, + ), + bgcolor=ft.colors.GREEN if item.item_type == "pull_request" else ft.colors.ORANGE, + padding=ft.padding.symmetric(horizontal=8, vertical=2), + border_radius=4, + ), + ft.Text(f"#{item.number}", size=12, color=ft.colors.GREY_400), + ], spacing=8), + ft.Text(item.title, size=14, weight=ft.FontWeight.BOLD), + ft.Row([ + ft.Text( + f"by @{item.author if item.author else 'Unknown'}", + size=11, + color=ft.colors.GREY_400, + ), + ft.Text( + f"• {item.state}", + size=11, + color=ft.colors.GREEN if item.state == "open" else ft.colors.PURPLE, + ), + ], spacing=5), + ], spacing=5), + padding=10, + bgcolor=ft.colors.GREY_900, + border_radius=8, + ) + + # Create body preview + body_preview = ft.Container( + content=ft.Column([ + ft.Text("Description", size=12, weight=ft.FontWeight.BOLD), + ft.Container( + content=ft.Text( + item.body if item.body else "No description provided", + size=11, + selectable=True, + ), + padding=10, + border=ft.border.all(1, ft.colors.OUTLINE), + border_radius=4, + bgcolor=ft.colors.GREY_900, + ), + ], spacing=5), + ) + + # Fetch comments + comments = [] + if repo_str: + try: + workflow_manager = self._get_workflow_manager() + comments = workflow_manager.fetch_comments(repo_str, item.number, item.item_type == "pull_request") + print(f"Fetched {len(comments)} comments for {item.item_type} #{item.number}") + except Exception as e: + print(f"Error fetching comments: {e}") + if self.logger: + self.logger.log(f"Error fetching comments: {e}") + + # Build comments display + comments_widgets = [] + if comments: + for comment in comments: + comments_widgets.append( + ft.Container( + content=ft.Column( + [ + ft.Row([ + ft.Text(f"@{comment['user']}", weight=ft.FontWeight.BOLD, size=12), + ft.Text(comment['created_at'][:10] if comment.get('created_at') else '', size=10, color=ft.colors.GREY_600), + ]), + ft.Text(comment['body'], size=11, selectable=True), + ], + spacing=5, + ), + padding=8, + margin=ft.margin.only(bottom=8), + border=ft.border.all(1, ft.colors.OUTLINE), + border_radius=4, + bgcolor=ft.colors.GREY_800, + ) + ) + else: + comments_widgets.append(ft.Text("No comments yet", italic=True, color=ft.colors.GREY_500, size=11)) + + # Comments section + comments_section = ft.Container( + content=ft.Column([ + ft.Text(f"Comments ({len(comments)})", size=12, weight=ft.FontWeight.BOLD), + ft.Column( + controls=comments_widgets, + spacing=5, + scroll=ft.ScrollMode.AUTO, + ), + ], spacing=5), + ) + + # Main content (no tabs, just single scrollable content) + main_content = ft.Container( + content=ft.Column( + [ + header, + body_preview, + comments_section, + ft.Row([ + ft.ElevatedButton( + "Open in GitHub", + icon=ft.icons.OPEN_IN_BROWSER, + on_click=lambda e: self.page.launch_url(item.url), + ), + ft.TextButton( + "Copy URL", + icon=ft.icons.COPY, + on_click=lambda e: self._copy_to_clipboard(item.url), + ), + ], spacing=10), + ], + spacing=15, + scroll=ft.ScrollMode.AUTO, + ), + padding=10, + expand=True, + ) + + # Create close handler that will close this specific dialog + def close_handler(e): + self.page.close(dialog) + + # Create dialog + dialog = ft.AlertDialog( + modal=True, + title=ft.Text(f"{item.item_type.upper()} #{item.number}: {item.title}"), + content=ft.Container( + content=main_content, + width=800, + height=600, + ), + actions=[ + ft.TextButton("Close", on_click=close_handler), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + + return dialog + + def _copy_to_clipboard(self, text): + """Copy text to clipboard and show notification""" + self.page.set_clipboard(text) + self._show_snackbar("URL copied to clipboard!", error=False) + + def _get_workflow_manager(self): + """Get or create a WorkflowManager instance""" + github_token = self.config_manager.get_config().get('GITHUB_PAT', '') + if not github_token: + raise ValueError("GitHub token not configured") + + from .workflow import WorkflowManager + return WorkflowManager(github_token, self.logger) + + def _previous_item(self, e): + """Navigate to previous item""" + if self.current_item_index > 0: + self.current_item_index -= 1 + self._display_current_item() + self._update_navigation_buttons() + + def _toggle_edit_mode(self, e): + """Toggle edit mode for proposed new text""" + if not self.proposed_new_text_ref.current or not self.edit_button_ref.current: + return + + self.edit_mode = not self.edit_mode + + if self.edit_mode: + self.proposed_new_text_ref.current.read_only = False + self.edit_button_ref.current.icon = ft.icons.SAVE + self.edit_button_ref.current.tooltip = "Save" + else: + # Save the changes + if self.current_work_items and self.current_item_index < len(self.current_work_items): + self.current_work_items[self.current_item_index]['new_text'] = \ + self.proposed_new_text_ref.current.value + + self.proposed_new_text_ref.current.read_only = True + self.edit_button_ref.current.icon = ft.icons.EDIT + self.edit_button_ref.current.tooltip = "Edit" + + self.page.update() + + def save_custom_instructions(self, e): + """Save custom AI instructions""" + if not self.custom_instructions_ref.current: + return + + instructions = self.custom_instructions_ref.current.value + config_values = {'CUSTOM_INSTRUCTIONS': instructions} + success = self.config_manager.save_configuration(config_values) + + if success: + self._show_snackbar("Custom instructions saved successfully!") + else: + self._show_snackbar("Failed to save custom instructions", error=True) + + def clear_custom_instructions(self, e): + """Clear custom instructions""" + if self.custom_instructions_ref.current: + self.custom_instructions_ref.current.value = "" + self.page.update() + + def _create_github_resource(self, e): + """Create GitHub resource (PR or Issue)""" + # Implementation would handle GitHub resource creation + self._show_snackbar("Creating GitHub resource...") + + def _create_new_item(self, e): + """Create new PR/Issue""" + # Implementation for creating new items + pass + + def _select_current_item(self, e): + """Set selected item as current from table""" + # Implementation to set current item from table selection + pass + + def find_and_load_diff_files(self, e): + """Find and load .diff files""" + # Implementation to find and load diff files + pass + + def clear_diff_display(self, e): + """Clear the diff display""" + if self.diff_text_ref.current: + self.diff_text_ref.current.value = "" + self.page.update() + + # ===== Async Operations ===== + + async def _auto_load_cached_items(self): + """Auto-load cached items on startup if available""" + print("=" * 60) + print("šŸ”„ Auto-loading cached items on startup...") + print("=" * 60) + + def load_cached(): + try: + # Get configured repos + target_repo = self.target_repo_dropdown_ref.current.value if self.target_repo_dropdown_ref.current else None + forked_repo = self.forked_repo_dropdown_ref.current.value if self.forked_repo_dropdown_ref.current else None + + if not target_repo and not forked_repo: + print("No repositories configured, skipping auto-load") + return + + github_token = self.config_manager.get_config().get('GITHUB_PAT', '') + if not github_token: + print("No GitHub token configured, skipping auto-load") + return + + items_loaded = False + + # Try to load target repo items from cache + if target_repo and not target_repo.startswith('---') and '/' in target_repo: + cached_prs = self.cache_manager.load_from_cache('target_prs', target_repo) if self.cache_manager else None + cached_issues = self.cache_manager.load_from_cache('target_issues', target_repo) if self.cache_manager else None + + if cached_prs is not None: + from .workflow import WorkflowItem + self.workflow_items['target_prs'] = [WorkflowItem.from_dict(item) for item in cached_prs] + print(f"āœ“ Auto-loaded {len(cached_prs)} PRs from cache (target)") + if self.logger: + self.logger.log(f"āœ… Auto-loaded {len(cached_prs)} PRs from cache (target)") + items_loaded = True + + if cached_issues is not None: + from .workflow import WorkflowItem + self.workflow_items['target_issues'] = [WorkflowItem.from_dict(item) for item in cached_issues] + print(f"āœ“ Auto-loaded {len(cached_issues)} issues from cache (target)") + if self.logger: + self.logger.log(f"āœ… Auto-loaded {len(cached_issues)} issues from cache (target)") + items_loaded = True + + # Try to load fork repo items from cache + if forked_repo and not forked_repo.startswith('---') and '/' in forked_repo: + cached_fork_prs = self.cache_manager.load_from_cache('fork_prs', forked_repo) if self.cache_manager else None + cached_fork_issues = self.cache_manager.load_from_cache('fork_issues', forked_repo) if self.cache_manager else None + + if cached_fork_prs is not None: + from .workflow import WorkflowItem + self.workflow_items['fork_prs'] = [WorkflowItem.from_dict(item) for item in cached_fork_prs] + print(f"āœ“ Auto-loaded {len(cached_fork_prs)} PRs from cache (fork)") + if self.logger: + self.logger.log(f"āœ… Auto-loaded {len(cached_fork_prs)} PRs from cache (fork)") + items_loaded = True + + if cached_fork_issues is not None: + from .workflow import WorkflowItem + self.workflow_items['fork_issues'] = [WorkflowItem.from_dict(item) for item in cached_fork_issues] + print(f"āœ“ Auto-loaded {len(cached_fork_issues)} issues from cache (fork)") + if self.logger: + self.logger.log(f"āœ… Auto-loaded {len(cached_fork_issues)} issues from cache (fork)") + items_loaded = True + + if items_loaded: + # Filter and update UI + self.page.run_task(self._filter_workflow_items_async) + + # Populate all items list in sidebar + self._populate_all_items() + + print("āœ… Auto-load completed successfully") + else: + print("No cached items found, waiting for manual load") + + except Exception as e: + print(f"Error during auto-load: {e}") + if self.logger: + self.logger.log(f"Error during auto-load: {e}") + + await asyncio.to_thread(load_cached) + + async def _auto_load_cached_items_on_repo_change(self): + """Auto-load cached items when repository selection changes""" + print("šŸ”„ Repository changed - checking for cached items...") + + def load_cached(): + try: + # Get configured repos + target_repo = self.target_repo_dropdown_ref.current.value if self.target_repo_dropdown_ref.current else None + forked_repo = self.forked_repo_dropdown_ref.current.value if self.forked_repo_dropdown_ref.current else None + + github_token = self.config_manager.get_config().get('GITHUB_PAT', '') + if not github_token: + print("No GitHub token configured") + return + + items_loaded = False + + # Try to load target repo items from cache + if target_repo and not target_repo.startswith('---') and '/' in target_repo: + cached_prs = self.cache_manager.load_from_cache('target_prs', target_repo) if self.cache_manager else None + cached_issues = self.cache_manager.load_from_cache('target_issues', target_repo) if self.cache_manager else None + + if cached_prs is not None: + from .workflow import WorkflowItem + self.workflow_items['target_prs'] = [WorkflowItem.from_dict(item) for item in cached_prs] + print(f"āœ“ Loaded {len(cached_prs)} cached PRs for target: {target_repo}") + if self.logger: + self.logger.log(f"āœ… Loaded {len(cached_prs)} cached PRs for target: {target_repo}") + items_loaded = True + + if cached_issues is not None: + from .workflow import WorkflowItem + self.workflow_items['target_issues'] = [WorkflowItem.from_dict(item) for item in cached_issues] + print(f"āœ“ Loaded {len(cached_issues)} cached issues for target: {target_repo}") + if self.logger: + self.logger.log(f"āœ… Loaded {len(cached_issues)} cached issues for target: {target_repo}") + items_loaded = True + + # Try to load fork repo items from cache + if forked_repo and not forked_repo.startswith('---') and '/' in forked_repo: + cached_fork_prs = self.cache_manager.load_from_cache('fork_prs', forked_repo) if self.cache_manager else None + cached_fork_issues = self.cache_manager.load_from_cache('fork_issues', forked_repo) if self.cache_manager else None + + if cached_fork_prs is not None: + from .workflow import WorkflowItem + self.workflow_items['fork_prs'] = [WorkflowItem.from_dict(item) for item in cached_fork_prs] + print(f"āœ“ Loaded {len(cached_fork_prs)} cached PRs for fork: {forked_repo}") + if self.logger: + self.logger.log(f"āœ… Loaded {len(cached_fork_prs)} cached PRs for fork: {forked_repo}") + items_loaded = True + + if cached_fork_issues is not None: + from .workflow import WorkflowItem + self.workflow_items['fork_issues'] = [WorkflowItem.from_dict(item) for item in cached_fork_issues] + print(f"āœ“ Loaded {len(cached_fork_issues)} cached issues for fork: {forked_repo}") + if self.logger: + self.logger.log(f"āœ… Loaded {len(cached_fork_issues)} cached issues for fork: {forked_repo}") + items_loaded = True + + if items_loaded: + # Filter and update UI + self.page.run_task(self._filter_workflow_items_async) + + # Populate all items list in sidebar + self._populate_all_items() + + print("āœ… Cached items loaded for selected repositories") + if self.logger: + self.logger.log("āœ… Cached items loaded for selected repositories") + else: + print("No cached items found for selected repositories") + + except Exception as e: + print(f"Error loading cached items on repo change: {e}") + if self.logger: + self.logger.log(f"Error loading cached items on repo change: {e}") + + await asyncio.to_thread(load_cached) + + async def _load_custom_instructions(self): + """Load custom instructions from config""" + try: + config = self.config_manager.get_config() + instructions = config.get('CUSTOM_INSTRUCTIONS', '') + + if self.custom_instructions_ref.current: + self.custom_instructions_ref.current.value = instructions + self.page.update() + except Exception as e: + print(f"Error loading custom instructions: {e}") + + async def _init_load_repos(self): + """Initialize repository loading""" + await self._load_target_repos_async() + await self._load_forked_repos_async() + + async def _load_target_repos_async(self): + """Load target repositories""" + def load_repos(): + try: + github_token = self.config_manager.get_config().get('GITHUB_PAT', '') + if not github_token: + return + + from .workflow import GitHubRepoFetcher + repo_fetcher = GitHubRepoFetcher(github_token, self.logger) + repos = repo_fetcher.fetch_repos_with_permissions(min_permission='push') + self.target_repos = repo_fetcher.get_repo_names(repos) + + # Update UI + if self.target_repo_dropdown_ref.current: + self.page.run_task(self._update_target_dropdown_async) + + except Exception as e: + if self.logger: + self.logger.log(f"Error loading target repos: {e}") + + await asyncio.to_thread(load_repos) + + async def _update_target_dropdown_async(self): + """Update target repository dropdown""" + if not self.target_repo_dropdown_ref.current: + return + + options = [] + if self.target_repos: + options.append(ft.dropdown.Option("--- Your Repos (with edit access) ---", disabled=True)) + options.extend([ft.dropdown.Option(repo) for repo in self.target_repos]) + + self.target_repo_dropdown_ref.current.options = options + + # Set value from saved settings + saved_repo = self.config_manager.get_config().get('GITHUB_REPO', '') + if saved_repo: + self.target_repo_dropdown_ref.current.value = saved_repo + + self.page.update() + + async def _refresh_target_repos_async(self): + """Refresh target repositories""" + await self._load_target_repos_async() + + async def _search_target_repos_async(self): + """Search for repositories on GitHub""" + # Create search dialog + search_input = ft.TextField( + label="Search for repository", + hint_text="Enter owner/repo or search term", + expand=True, + autofocus=True, + ) + + results_list = ft.ListView( + expand=True, + spacing=5, + padding=10, + ) + + def perform_search(e): + search_term = search_input.value.strip() + if not search_term: + return + + # Clear previous results + results_list.controls.clear() + results_list.controls.append( + ft.Text("Searching...", color=ft.colors.GREY_400, italic=True) + ) + self.page.update() + + # Search GitHub + try: + github_token = self.config_manager.get_config().get('GITHUB_PAT', '') + if not github_token: + results_list.controls.clear() + results_list.controls.append( + ft.Text("GitHub token not configured", color=ft.colors.RED) + ) + self.page.update() + return + + from .workflow import GitHubRepoFetcher + repo_fetcher = GitHubRepoFetcher(github_token, self.logger) + + # Check if it's a direct repo reference (owner/repo) + if '/' in search_term and len(search_term.split('/')) == 2: + # Try to get the specific repo + repos = repo_fetcher.search_repositories(search_term, per_page=1) + if repos: + results_list.controls.clear() + for repo in repos: + repo_name = repo_fetcher.get_repo_names([repo])[0] if repo_fetcher.get_repo_names([repo]) else None + if repo_name: + results_list.controls.append( + self._create_repo_result_item(repo_name, repo, search_dialog) + ) + else: + results_list.controls.clear() + results_list.controls.append( + ft.Text("Repository not found or you don't have access", color=ft.colors.ORANGE) + ) + else: + # Search for repos + repos = repo_fetcher.search_repositories(search_term, per_page=10) + results_list.controls.clear() + + if repos: + for repo in repos: + repo_name = repo_fetcher.get_repo_names([repo])[0] if repo_fetcher.get_repo_names([repo]) else None + if repo_name: + results_list.controls.append( + self._create_repo_result_item(repo_name, repo, search_dialog) + ) + else: + results_list.controls.append( + ft.Text("No repositories found", color=ft.colors.GREY_400) + ) + + self.page.update() + + except Exception as ex: + results_list.controls.clear() + results_list.controls.append( + ft.Text(f"Error searching: {str(ex)}", color=ft.colors.RED) + ) + self.page.update() + + # Create dialog + def close_dialog(e): + self.page.close(search_dialog) + + search_dialog = ft.AlertDialog( + modal=True, + title=ft.Text("Search GitHub Repositories"), + content=ft.Container( + content=ft.Column([ + ft.Row([ + search_input, + ft.IconButton( + icon=ft.icons.SEARCH, + tooltip="Search", + on_click=perform_search, + ), + ]), + ft.Divider(), + results_list, + ], spacing=10), + width=600, + height=400, + ), + actions=[ + ft.TextButton("Cancel", on_click=close_dialog), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + + # Handle Enter key in search input + search_input.on_submit = perform_search + + self.page.open(search_dialog) + + def _create_repo_result_item(self, repo_name, repo_data, dialog): + """Create a repository result item""" + # Get repo description + description = repo_data.get('description', 'No description') + if not description: + description = 'No description' + + # Get visibility + is_private = repo_data.get('private', False) + visibility_text = "Private" if is_private else "Public" + visibility_color = ft.colors.ORANGE if is_private else ft.colors.GREEN + + def select_repo(e): + # Add to dropdown options if not already there + if self.target_repo_dropdown_ref.current: + current_options = [opt.key for opt in self.target_repo_dropdown_ref.current.options] + if repo_name not in current_options: + self.target_repo_dropdown_ref.current.options.append( + ft.dropdown.Option(repo_name) + ) + + # Select this repo + self.target_repo_dropdown_ref.current.value = repo_name + + # Save to config + config = self.config_manager.get_config() + config['GITHUB_REPO'] = repo_name + self.config_manager.save_configuration(config) + + self.page.update() + + # Close dialog + self.page.close(dialog) + self._show_snackbar(f"Selected repository: {repo_name}", error=False) + + return ft.Container( + content=ft.Column([ + ft.Row([ + ft.Text(repo_name, weight=ft.FontWeight.BOLD, size=14), + ft.Container( + content=ft.Text(visibility_text, size=10, color=ft.colors.WHITE), + bgcolor=visibility_color, + padding=ft.padding.symmetric(horizontal=8, vertical=2), + border_radius=4, + ), + ], spacing=10), + ft.Text(description, size=12, color=ft.colors.GREY_400), + ], spacing=5), + padding=10, + border=ft.border.all(1, ft.colors.OUTLINE), + border_radius=4, + bgcolor=ft.colors.GREY_800, + on_click=select_repo, + ink=True, + ) + + async def _load_forked_repos_async(self): + """Load forked repositories""" + def load_forks(): + try: + # Load local repos + local_repo_path = self.config_manager.get_config().get('LOCAL_REPO_PATH', '') + if local_repo_path: + try: + from .utils import LocalRepositoryScanner + self.forked_repos['local'] = LocalRepositoryScanner.scan_local_repos(local_repo_path) + except Exception as e: + print(f"Error scanning local repos: {e}") + + # Load GitHub repos + github_token = self.config_manager.get_config().get('GITHUB_PAT', '') + if github_token: + from .workflow import GitHubRepoFetcher + repo_fetcher = GitHubRepoFetcher(github_token, self.logger) + repos = repo_fetcher.fetch_user_repos(repo_type='owner') + self.forked_repos['github'] = repo_fetcher.get_repo_names(repos) + + # Update UI + if self.forked_repo_dropdown_ref.current: + self.page.run_task(self._update_forked_dropdown_async) + + except Exception as e: + if self.logger: + self.logger.log(f"Error loading forked repos: {e}") + + await asyncio.to_thread(load_forks) + + async def _update_forked_dropdown_async(self): + """Update forked repository dropdown""" + if not self.forked_repo_dropdown_ref.current: + return + + options = [] + + # Add local repos + if self.forked_repos.get('local'): + options.append(ft.dropdown.Option("--- Local Repositories ---", disabled=True)) + options.extend([ft.dropdown.Option(repo) for repo in self.forked_repos['local']]) + + # Add GitHub repos + if self.forked_repos.get('github'): + options.append(ft.dropdown.Option("--- Your GitHub Repos ---", disabled=True)) + options.extend([ft.dropdown.Option(repo) for repo in self.forked_repos['github']]) + + self.forked_repo_dropdown_ref.current.options = options + + # Set value from saved settings + saved_repo = self.config_manager.get_config().get('FORKED_REPO', '') + if saved_repo: + self.forked_repo_dropdown_ref.current.value = saved_repo + + self.page.update() + + async def _refresh_forked_repos_async(self): + """Refresh forked repositories""" + await self._load_forked_repos_async() + + def _clone_forked_repo(self, e): + """Clone forked repository""" + # Implementation would clone the selected repo + pass + + async def _load_workflow_items_async(self): + """Load workflow items (PRs/Issues)""" + # Check if items are already loaded to determine if this is a refresh + items_already_loaded = any(len(items) > 0 for items in self.workflow_items.values()) + force_refresh = items_already_loaded + + if force_refresh: + print("=" * 60) + print("šŸ”„ Refreshing Items (forcing API fetch)...") + print("=" * 60) + if self.logger: + self.logger.log("=" * 60) + self.logger.log("šŸ”„ Refreshing Items - forcing fresh fetch from GitHub API") + self.logger.log("=" * 60) + else: + print("=" * 60) + print("šŸ”„ Load Items button clicked!") + print("=" * 60) + if self.logger: + self.logger.log("=" * 60) + self.logger.log("šŸ”„ Load Items button clicked - starting workflow item load") + self.logger.log("=" * 60) + + def load_items(): + try: + print(f"DEBUG: target_repo_dropdown exists: {self.target_repo_dropdown_ref.current is not None}") + print(f"DEBUG: forked_repo_dropdown exists: {self.forked_repo_dropdown_ref.current is not None}") + + if self.target_repo_dropdown_ref.current: + print(f"DEBUG: target_repo value = '{self.target_repo_dropdown_ref.current.value}'") + if self.forked_repo_dropdown_ref.current: + print(f"DEBUG: forked_repo value = '{self.forked_repo_dropdown_ref.current.value}'") + + if not self.target_repo_dropdown_ref.current and not self.forked_repo_dropdown_ref.current: + if self.logger: + self.logger.log("āŒ No repositories dropdown controls found") + print("ERROR: No repo dropdowns found!") + return + + github_token = self.config_manager.get_config().get('GITHUB_PAT', '') + if not github_token: + if self.logger: + self.logger.log("āŒ No GitHub token configured") + print("ERROR: No GitHub token!") + return + + from .workflow import WorkflowManager + workflow_manager = WorkflowManager(github_token, self.logger) + + # Load from target repo + target_repo = self.target_repo_dropdown_ref.current.value if self.target_repo_dropdown_ref.current else None + print(f"DEBUG: target_repo extracted = '{target_repo}'") + print(f"DEBUG: Validation checks:") + print(f" - target_repo is not None: {target_repo is not None}") + print(f" - not starts with '---': {not target_repo.startswith('---') if target_repo else 'N/A'}") + print(f" - contains '/': {'/' in target_repo if target_repo else 'N/A'}") + + # Filter out separator headers and None values + if target_repo and not target_repo.startswith('---') and '/' in target_repo: + print(f"āœ“ Validation PASSED for target repo: {target_repo}") + if self.logger: + self.logger.log(f"šŸ“„ Loading PRs and issues from target repo: {target_repo}") + + # Try to load from cache first (unless forcing refresh) + cached_prs = None if force_refresh else (self.cache_manager.load_from_cache('target_prs', target_repo) if self.cache_manager else None) + cached_issues = None if force_refresh else (self.cache_manager.load_from_cache('target_issues', target_repo) if self.cache_manager else None) + + if cached_prs is not None and not force_refresh: + # Convert cached dicts back to WorkflowItem objects + from .workflow import WorkflowItem + self.workflow_items['target_prs'] = [WorkflowItem.from_dict(item) for item in cached_prs] + print(f"āœ“ Loaded {len(cached_prs)} PRs from cache") + if self.logger: + self.logger.log(f"āœ… Loaded {len(cached_prs)} PRs from cache") + else: + print(f"Calling workflow_manager.fetch_pull_requests('{target_repo}')...") + self.workflow_items['target_prs'] = workflow_manager.fetch_pull_requests(target_repo, repo_source='target') + # Convert to dicts and save to cache + if self.cache_manager: + items_as_dicts = [item.to_dict() for item in self.workflow_items['target_prs']] + self.cache_manager.save_to_cache('target_prs', target_repo, items_as_dicts) + + if cached_issues is not None and not force_refresh: + # Convert cached dicts back to WorkflowItem objects + from .workflow import WorkflowItem + self.workflow_items['target_issues'] = [WorkflowItem.from_dict(item) for item in cached_issues] + print(f"āœ“ Loaded {len(cached_issues)} issues from cache") + if self.logger: + self.logger.log(f"āœ… Loaded {len(cached_issues)} issues from cache") + else: + print(f"Calling workflow_manager.fetch_issues('{target_repo}')...") + self.workflow_items['target_issues'] = workflow_manager.fetch_issues(target_repo, repo_source='target') + # Convert to dicts and save to cache + if self.cache_manager: + items_as_dicts = [item.to_dict() for item in self.workflow_items['target_issues']] + self.cache_manager.save_to_cache('target_issues', target_repo, items_as_dicts) + + pr_count = len(self.workflow_items.get('target_prs', [])) + issue_count = len(self.workflow_items.get('target_issues', [])) + print(f"āœ“ Loaded {pr_count} PRs and {issue_count} issues from target repo") + + if self.logger: + self.logger.log(f"āœ… Loaded {pr_count} PRs and {issue_count} issues from target repo") + else: + print(f"āœ— Validation FAILED for target repo: {target_repo}") + + # Load from forked repo + forked_repo = self.forked_repo_dropdown_ref.current.value if self.forked_repo_dropdown_ref.current else None + # Filter out separator headers and None values + if forked_repo and not forked_repo.startswith('---') and '/' in forked_repo: + if self.logger: + self.logger.log(f"Loading PRs and issues from forked repo: {forked_repo}") + + # Try to load from cache first (unless forcing refresh) + cached_fork_prs = None if force_refresh else (self.cache_manager.load_from_cache('fork_prs', forked_repo) if self.cache_manager else None) + cached_fork_issues = None if force_refresh else (self.cache_manager.load_from_cache('fork_issues', forked_repo) if self.cache_manager else None) + + if cached_fork_prs is not None and not force_refresh: + # Convert cached dicts back to WorkflowItem objects + from .workflow import WorkflowItem + self.workflow_items['fork_prs'] = [WorkflowItem.from_dict(item) for item in cached_fork_prs] + print(f"āœ“ Loaded {len(cached_fork_prs)} PRs from cache (fork)") + if self.logger: + self.logger.log(f"āœ… Loaded {len(cached_fork_prs)} PRs from cache (fork)") + else: + self.workflow_items['fork_prs'] = workflow_manager.fetch_pull_requests(forked_repo, repo_source='fork') + # Convert to dicts and save to cache + if self.cache_manager: + items_as_dicts = [item.to_dict() for item in self.workflow_items['fork_prs']] + self.cache_manager.save_to_cache('fork_prs', forked_repo, items_as_dicts) + + if cached_fork_issues is not None and not force_refresh: + # Convert cached dicts back to WorkflowItem objects + from .workflow import WorkflowItem + self.workflow_items['fork_issues'] = [WorkflowItem.from_dict(item) for item in cached_fork_issues] + print(f"āœ“ Loaded {len(cached_fork_issues)} issues from cache (fork)") + if self.logger: + self.logger.log(f"āœ… Loaded {len(cached_fork_issues)} issues from cache (fork)") + else: + self.workflow_items['fork_issues'] = workflow_manager.fetch_issues(forked_repo, repo_source='fork') + # Convert to dicts and save to cache + if self.cache_manager: + items_as_dicts = [item.to_dict() for item in self.workflow_items['fork_issues']] + self.cache_manager.save_to_cache('fork_issues', forked_repo, items_as_dicts) + + if self.logger: + self.logger.log(f"Loaded {len(self.workflow_items.get('fork_prs', []))} PRs and {len(self.workflow_items.get('fork_issues', []))} issues from forked repo") + + # Filter and update UI + self.page.run_task(self._filter_workflow_items_async) + + # Populate all items list in sidebar + self._populate_all_items() + + except Exception as e: + if self.logger: + self.logger.log(f"Error loading workflow items: {e}") + import traceback + self.logger.log(traceback.format_exc()) + + await asyncio.to_thread(load_items) + + async def _filter_workflow_items_async(self): + """Filter workflow items async""" + self._filter_workflow_items() + + # ===== Helper Methods ===== + + def _display_current_item(self): + """Display the current work item""" + if not self.current_work_items or self.current_item_index >= len(self.current_work_items): + return + + item = self.current_work_items[self.current_item_index] + + # Update UI fields + if self.work_item_id_ref.current: + self.work_item_id_ref.current.value = f"Work Item {item.get('id', 'N/A')}" + + if self.nature_text_ref.current: + self.nature_text_ref.current.value = item.get('nature', '') + + if self.live_doc_url_ref.current: + self.live_doc_url_ref.current.value = item.get('live_doc_url', '') + + if self.text_to_change_ref.current: + self.text_to_change_ref.current.value = item.get('old_text', '') + + if self.proposed_new_text_ref.current: + self.proposed_new_text_ref.current.value = item.get('new_text', '') + + self.page.update() + self._update_navigation_buttons() + + def update_status(self, message: str): + """Update status message""" + if self.status_text_ref.current: + self.status_text_ref.current.value = message + self.page.update() + + def _show_progress(self): + """Show progress bar""" + if self.progress_bar_ref.current: + self.progress_bar_ref.current.visible = True + self.page.update() + + def _hide_progress(self): + """Hide progress bar""" + if self.progress_bar_ref.current: + self.progress_bar_ref.current.visible = False + self.page.update() + + def _show_snackbar(self, message: str, error: bool = False): + """Show snackbar notification""" + self.page.snack_bar = ft.SnackBar( + content=ft.Text(message), + bgcolor="error" if error else "green", + ) + self.page.snack_bar.open = True + self.page.update() + + def _open_settings(self, e): + """Open settings dialog""" + try: + print("Settings button clicked!") + + # Use Flet 0.28+ API: page.open() instead of page.dialog + config = self.config_manager.get_config() + print(f"Got config: {config.keys() if config else 'None'}") + + settings_dialog = SettingsDialog( + self.page, + config, + self.config_manager, + self.cache_manager + ) + print("SettingsDialog created") + + def on_settings_result(result): + if result: + # Reload configuration + self.config_manager.load_configuration() + self._show_snackbar("Settings saved successfully!") + + print("Calling settings_dialog.show()...") + settings_dialog.show(on_result=on_settings_result) + print("settings_dialog.show() completed") + + except Exception as ex: + print(f"Error in _open_settings: {ex}") + import traceback + traceback.print_exc() + self._show_snackbar(f"Error opening settings: {ex}", error=True) + + def _open_processing_log(self, e): + """Open processing log dialog""" + try: + print("Processing Log button clicked!") + + processing_log_dialog = ProcessingLogDialog( + self.page, + self.log_text_ref + ) + print("ProcessingLogDialog created") + + processing_log_dialog.show() + print("ProcessingLogDialog.show() completed") + + except Exception as ex: + print(f"Error in _open_processing_log: {ex}") + import traceback + traceback.print_exc() + self._show_snackbar(f"Error opening processing log: {ex}", error=True) + + def _show_real_settings(self): + """Show the real settings dialog""" + try: + config = self.config_manager.get_config() + print(f"Got config: {config.keys() if config else 'None'}") + + settings_dialog = SettingsDialog( + self.page, + config, + self.config_manager, + self.cache_manager + ) + print("SettingsDialog created") + + def on_settings_result(result): + if result: + # Reload configuration + self.config_manager.load_configuration() + self._show_snackbar("Settings saved successfully!") + + print("Calling settings_dialog.show()...") + settings_dialog.show(on_result=on_settings_result) + print("settings_dialog.show() completed") + except Exception as ex: + print(f"Error in _show_real_settings: {ex}") + import traceback + traceback.print_exc() + self._show_snackbar(f"Error showing settings: {ex}", error=True) + + def _check_ai_modules_manual(self, e): + """Manually check AI modules""" + config = self.config_manager.get_config() + ai_provider = config.get('AI_PROVIDER', 'none').lower() + + if ai_provider and ai_provider != 'none': + self.page.run_task(lambda: self._check_ai_provider_async(ai_provider)) + else: + self._show_snackbar("No AI provider configured") + + async def _check_ai_provider_async(self, ai_provider: str): + """Check AI provider setup""" + try: + available, missing = self.ai_manager.check_ai_module_availability(ai_provider) + + if available: + self._show_snackbar(f"AI Provider ({ai_provider}): All modules available!") + else: + self._show_snackbar( + f"AI Provider ({ai_provider}): Missing packages: {', '.join(missing)}", + error=True + ) + except Exception as e: + self._show_snackbar(f"Error checking AI provider: {e}", error=True) + + def update_diff_display(self, diff_content: str): + """Update diff display""" + if self.diff_text_ref.current: + self.diff_text_ref.current.value = diff_content + self.page.update() + + def _create_ai_plan_tab(self) -> ft.Container: + """Create the AI Action Plan tab""" + + # Plan display area (initially empty) + plan_display = ft.Column( + ref=self.plan_display_ref, + controls=[ + ft.Container( + content=ft.Column([ + ft.Icon(ft.icons.AUTO_AWESOME, size=64, color=ft.colors.BLUE_400), + ft.Text( + "AI Action Plan", + size=24, + weight=ft.FontWeight.BOLD, + color=ft.colors.BLUE_400, + ), + ft.Text( + "Select a PR or Issue and generate an AI action plan", + size=14, + color=ft.colors.GREY_600, + text_align=ft.TextAlign.CENTER, + ), + ], horizontal_alignment=ft.CrossAxisAlignment.CENTER, spacing=10), + alignment=ft.alignment.center, + expand=True, + ) + ], + spacing=10, + scroll=ft.ScrollMode.AUTO, + expand=True, + ) + + # AI Instructions input + ai_instructions = ft.TextField( + ref=self.ai_instructions_ref, + label="Additional Instructions (Optional)", + hint_text="Add any specific requirements or context for the AI...", + multiline=True, + min_lines=2, + max_lines=4, + ) + + # Buttons row + buttons_row = ft.Row( + [ + ft.ElevatedButton( + ref=self.generate_plan_button_ref, + text="Generate Plan", + icon=ft.icons.PSYCHOLOGY, + on_click=self._on_generate_plan_click, + disabled=True, # Enable when item is selected + ), + ft.ElevatedButton( + ref=self.execute_plan_button_ref, + text="Execute Plan", + icon=ft.icons.PLAY_ARROW, + on_click=self._on_execute_plan_click, + disabled=True, # Enable when plan is generated + style=ft.ButtonStyle( + color=ft.colors.WHITE, + bgcolor=ft.colors.GREEN_700, + ), + ), + ], + spacing=10, + ) + + # Progress bar (indeterminate/animated when visible) + progress_bar = ft.ProgressBar( + ref=self.plan_progress_ref, + visible=False, + color=ft.colors.BLUE_400, + bgcolor=ft.colors.BLUE_GREY_800, + ) + + # Status text + status_text = ft.Text( + ref=self.plan_status_ref, + value="", + color=ft.colors.BLUE_400, + size=12, + ) + + # Main layout + return ft.Container( + content=ft.Column( + [ + ai_instructions, + buttons_row, + progress_bar, + status_text, + ft.Divider(), + plan_display, + ], + spacing=10, + expand=True, + ), + padding=20, + expand=True, + ) + + def _on_generate_plan_click(self, e): + """Generate AI action plan for current item""" + if not self.active_workflow_item: + self._show_snackbar("Please select a PR or Issue first", error=True) + return + + # Run in thread to avoid blocking UI + import threading + thread = threading.Thread(target=self._generate_plan_async) + thread.start() + + def _generate_plan_async(self): + """Generate plan asynchronously""" + try: + # Update UI - disable both buttons during generation + if self.generate_plan_button_ref.current: + self.generate_plan_button_ref.current.disabled = True + if self.execute_plan_button_ref.current: + self.execute_plan_button_ref.current.disabled = True + if self.plan_status_ref.current: + self.plan_status_ref.current.value = "šŸ¤– Generating action plan..." + if self.plan_progress_ref.current: + self.plan_progress_ref.current.visible = True + self.page.update() + + # Create action planner + from .ai_action_planner import AIActionPlanner + planner = AIActionPlanner(self.ai_manager, self.logger, self.config_manager) + + # Get custom instructions + custom_instructions = "" + if self.ai_instructions_ref.current: + custom_instructions = self.ai_instructions_ref.current.value or "" + + # Generate plan + plan = planner.generate_plan(self.active_workflow_item, custom_instructions) + + if plan: + self.current_action_plan = plan + self._display_action_plan(plan) + + if self.plan_status_ref.current: + self.plan_status_ref.current.value = f"āœ… Plan generated with {len(plan.steps)} steps" + if self.execute_plan_button_ref.current: + self.execute_plan_button_ref.current.disabled = False + else: + if self.plan_status_ref.current: + self.plan_status_ref.current.value = "āŒ Failed to generate plan" + + except Exception as ex: + import traceback + self.logger.log(f"āŒ Error generating plan: {str(ex)}") + self.logger.log(f"āŒ Traceback: {traceback.format_exc()}") + if self.plan_status_ref.current: + self.plan_status_ref.current.value = f"āŒ Error: {str(ex)}" + + finally: + # Re-enable Generate Plan button + if self.generate_plan_button_ref.current: + self.generate_plan_button_ref.current.disabled = False + # Only enable Execute Plan if we have a valid plan + if self.execute_plan_button_ref.current and self.current_action_plan: + self.execute_plan_button_ref.current.disabled = False + if self.plan_progress_ref.current: + self.plan_progress_ref.current.visible = False + self.page.update() + + def _display_action_plan(self, plan): + """Display the generated action plan""" + if not self.plan_display_ref.current: + return + + steps_widgets = [] + + # Plan header + steps_widgets.append( + ft.Container( + content=ft.Column([ + ft.Text(plan.title, size=18, weight=ft.FontWeight.BOLD), + ft.Text( + f"Context: {plan.context.get('item_type', 'unknown').upper()} #{plan.context.get('item_number', '?')}", + size=12, + color=ft.colors.GREY_600 + ), + ]), + padding=10, + bgcolor=ft.colors.BLUE_GREY_900, + border_radius=5, + ) + ) + + # Individual steps + for step in plan.steps: + step_num = step['step_number'] + description = step['description'] + file_path = step.get('file_path', 'N/A') + action_type = step.get('action_type', 'unknown') + status = step.get('status', 'pending') + + # Status icon and color based on step status + if status == 'completed': + icon = ft.icons.CHECK_CIRCLE + icon_color = ft.colors.GREEN + bg_color = None # No special background + elif status == 'failed': + icon = ft.icons.ERROR + icon_color = ft.colors.RED + bg_color = None + elif status == 'in_progress': + icon = ft.icons.TIMELAPSE # Animated-looking icon + icon_color = ft.colors.BLUE_400 + bg_color = ft.colors.BLUE_GREY_800 # Highlight in-progress steps + else: # pending + icon = ft.icons.RADIO_BUTTON_UNCHECKED + icon_color = ft.colors.GREY + bg_color = None + + step_widget = ft.Container( + content=ft.Row([ + ft.Icon(icon, color=icon_color, size=20), + ft.Column([ + ft.Text(f"Step {step_num}: {description}", weight=ft.FontWeight.BOLD, size=14), + ft.Text(f"Action: {action_type}", size=11, color=ft.colors.BLUE_400), + ft.Text(f"File: {file_path}", size=11, color=ft.colors.GREY_600) if file_path != 'N/A' else ft.Container(), + ], spacing=2, expand=True), + ], spacing=10), + padding=10, + border=ft.border.all(1, ft.colors.GREY_700), + border_radius=5, + bgcolor=bg_color, # Highlight in-progress steps + ) + steps_widgets.append(step_widget) + + # Update display + self.plan_display_ref.current.controls = steps_widgets + self.page.update() + + def _on_execute_plan_click(self, e): + """Execute the current action plan""" + if not self.current_action_plan: + self._show_snackbar("No plan to execute", error=True) + return + + # Run in thread + import threading + thread = threading.Thread(target=self._execute_plan_async) + thread.start() + + def _execute_plan_async(self): + """Execute plan asynchronously""" + try: + self.logger.log("šŸ”§ Starting _execute_plan_async...") + + # Update UI - disable both buttons during execution + if self.execute_plan_button_ref.current: + self.execute_plan_button_ref.current.disabled = True + if self.generate_plan_button_ref.current: + self.generate_plan_button_ref.current.disabled = True + if self.plan_status_ref.current: + self.plan_status_ref.current.value = "ā–¶ļø Executing plan..." + if self.plan_progress_ref.current: + self.plan_progress_ref.current.visible = True + self.page.update() + + # Get local repo path + config = self.config_manager.get_config() + local_repo_path = config.get('LOCAL_REPO_PATH', '') + self.logger.log(f"šŸ”§ Local repo path: {local_repo_path}") + + if not local_repo_path: + self.logger.log("āŒ LOCAL_REPO_PATH is not set") + self._show_snackbar("Please set LOCAL_REPO_PATH in settings", error=True) + if self.plan_status_ref.current: + self.plan_status_ref.current.value = "āŒ LOCAL_REPO_PATH not set in settings" + self.page.update() + return + + # Create action planner + from .ai_action_planner import AIActionPlanner + planner = AIActionPlanner(self.ai_manager, self.logger, self.config_manager) + self.logger.log(f"šŸ”§ Created AIActionPlanner, about to execute plan with {len(self.current_action_plan.steps)} steps") + + # Execute with progress callback + def progress_callback(current, total, message): + if self.plan_status_ref.current: + self.plan_status_ref.current.value = f"ā–¶ļø {message} ({current}/{total})" + # Update the plan display in real-time to show progress + if self.current_action_plan: + self._display_action_plan(self.current_action_plan) + self.page.update() + + # Execute with logging callback for thought process + def log_callback(message): + # Log to the processing log + self.logger.log(message) + # Also show in status if it's important + if any(keyword in message for keyword in ["šŸ¤–", "āœ…", "āŒ", "šŸ“", "šŸ”"]): + if self.plan_status_ref.current: + self.plan_status_ref.current.value = message + self.page.update() + + result = planner.execute_plan( + self.current_action_plan, + local_repo_path, + progress_callback, + log_callback + ) + + self.logger.log(f"šŸ”§ execute_plan returned: {result}") + + # Update display + self._display_action_plan(self.current_action_plan) + + # Show completion dialog + if result['success']: + self._show_completion_dialog(result) + else: + if self.plan_status_ref.current: + self.plan_status_ref.current.value = f"āš ļø Plan completed with errors: {result['failed']}/{result['total']} steps failed" + + except Exception as ex: + import traceback + self.logger.log(f"āŒ Error executing plan: {str(ex)}") + self.logger.log(f"āŒ Traceback: {traceback.format_exc()}") + if self.plan_status_ref.current: + self.plan_status_ref.current.value = f"āŒ Error: {str(ex)}" + + finally: + # Re-enable both buttons after execution + if self.execute_plan_button_ref.current: + self.execute_plan_button_ref.current.disabled = False + if self.generate_plan_button_ref.current: + self.generate_plan_button_ref.current.disabled = False + if self.plan_progress_ref.current: + self.plan_progress_ref.current.visible = False + self.page.update() + + def _show_completion_dialog(self, result): + """Show dialog after plan execution with options to push/create PR""" + context = self.current_action_plan.context + item_type = context.get('item_type', 'unknown') + item_number = context.get('item_number', '?') + + def close_dialog(e): + dialog.open = False + self.page.update() + + def push_changes(e): + dialog.open = False + self.page.update() + self._push_changes_async() + + def create_pr(e): + dialog.open = False + self.page.update() + self._create_pr_from_plan_async() + + dialog = ft.AlertDialog( + title=ft.Text("āœ… Plan Execution Complete!"), + content=ft.Column([ + ft.Text(f"Successfully completed {result['completed']}/{result['total']} steps"), + ft.Divider(), + ft.Text("What would you like to do next?", weight=ft.FontWeight.BOLD), + ft.Text(f"• Push changes to the {item_type} branch", size=12), + ft.Text(f"• Create/Update PR for these changes", size=12), + ft.Text(f"• Review changes manually", size=12), + ], tight=True, spacing=10), + actions=[ + ft.TextButton("Review Later", on_click=close_dialog), + ft.ElevatedButton( + "Push Changes", + icon=ft.icons.CLOUD_UPLOAD, + on_click=push_changes, + ), + ft.ElevatedButton( + "Create/Update PR", + icon=ft.icons.MERGE_TYPE, + on_click=create_pr, + style=ft.ButtonStyle(bgcolor=ft.colors.GREEN_700), + ), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + + self.page.dialog = dialog + dialog.open = True + self.page.update() + + def _push_changes_async(self): + """Push changes to git repository""" + # This will be implemented to push changes + self.logger.log("šŸš€ Pushing changes to repository...") + # TODO: Implement git push logic + self._show_snackbar("Push functionality coming soon!") + + def _create_pr_from_plan_async(self): + """Create or update PR from executed plan""" + # This will be implemented to create/update PR + self.logger.log("šŸ“ Creating/updating PR...") + # TODO: Implement PR creation logic + self._show_snackbar("PR creation functionality coming soon!") + + +class Logger: + """Logger class for Flet""" + + def __init__(self, text_field: ft.TextField): + self.text_field = text_field + + def log(self, message: str): + """Log a message""" + import datetime + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + log_message = f"[{timestamp}] {message}\n" + + if self.text_field: + current = self.text_field.value or "" + self.text_field.value = current + log_message + # Auto-scroll is handled by Flet diff --git a/src/app_components/processing_log_dialog.py b/src/app_components/processing_log_dialog.py new file mode 100644 index 0000000..998549f --- /dev/null +++ b/src/app_components/processing_log_dialog.py @@ -0,0 +1,125 @@ +""" +Processing Log Dialog +Displays the processing log in a separate dialog window +""" + +import flet as ft +# Compatibility fix for Flet 0.28+ (Icons vs icons, Colors vs colors) +ft.icons = ft.Icons +ft.colors = ft.Colors + + +class ProcessingLogDialog: + """Processing log display dialog""" + + def __init__(self, page: ft.Page, log_text_ref: ft.Ref): + self.page = page + self.log_text_ref = log_text_ref + self.dialog_ref = ft.Ref[ft.AlertDialog]() + self.log_display_ref = ft.Ref[ft.TextField]() + + def show(self): + """Show the processing log dialog""" + try: + print("ProcessingLogDialog.show() called") + + # Create the dialog + dialog = self._create_dialog() + self.dialog_ref.current = dialog + + # Sync the log content before showing + self._sync_log_content() + + # Open the dialog + self.page.open(dialog) + self.page.update() + + except Exception as ex: + print(f"Error in ProcessingLogDialog.show(): {ex}") + import traceback + traceback.print_exc() + + def _sync_log_content(self): + """Sync log content from main log to dialog display""" + if self.log_text_ref.current and self.log_display_ref.current: + self.log_display_ref.current.value = self.log_text_ref.current.value + if self.page: + self.page.update() + + def _create_dialog(self) -> ft.AlertDialog: + """Create the processing log dialog""" + # Create a display field that will show a copy of the log + # This is synced from the main log field + log_display = ft.TextField( + ref=self.log_display_ref, + value=self.log_text_ref.current.value if self.log_text_ref.current else "", + multiline=True, + read_only=True, + expand=True, + text_style=ft.TextStyle(font_family="Courier New"), + min_lines=20, + max_lines=30, + ) + + # Refresh button + refresh_button = ft.TextButton( + "Refresh", + icon=ft.icons.REFRESH, + on_click=self._refresh_log, + ) + + # Clear button + clear_button = ft.TextButton( + "Clear Log", + icon=ft.icons.DELETE_OUTLINE, + on_click=self._clear_log, + ) + + # Close button + close_button = ft.TextButton( + "Close", + on_click=self._close_clicked, + ) + + dialog = ft.AlertDialog( + ref=self.dialog_ref, + modal=True, + title=ft.Row( + [ + ft.Icon(ft.icons.LIST_ALT, color="blue"), + ft.Text("Processing Log", size=20, weight=ft.FontWeight.BOLD), + ], + alignment=ft.MainAxisAlignment.START, + ), + content=ft.Container( + content=log_display, + width=800, + height=500, + ), + actions=[ + refresh_button, + clear_button, + close_button, + ], + actions_alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + ) + + return dialog + + def _refresh_log(self, e): + """Refresh the log content from the main log""" + self._sync_log_content() + + def _clear_log(self, e): + """Clear the log""" + # Clear both the main log and the display + if self.log_text_ref.current: + self.log_text_ref.current.value = "" + if self.log_display_ref.current: + self.log_display_ref.current.value = "" + self.page.update() + + def _close_clicked(self, e): + """Handle close button click""" + if self.dialog_ref.current: + self.page.close(self.dialog_ref.current) diff --git a/src/app_components/settings_dialog.py b/src/app_components/settings_dialog.py new file mode 100644 index 0000000..ea53efe --- /dev/null +++ b/src/app_components/settings_dialog.py @@ -0,0 +1,1150 @@ +""" +Settings Dialog +GUI for configuring application settings (Flet version) +""" + +import flet as ft +# Compatibility fix for Flet 0.28+ (Icons vs icons, Colors vs colors) +ft.icons = ft.Icons +ft.colors = ft.Colors +from typing import Dict, Any, Optional, List, Tuple +import os +import asyncio +import sys +import subprocess + + +class SettingsDialog: + """Settings configuration dialog""" + + def __init__(self, page: ft.Page, config: Dict[str, Any], config_manager=None, cache_manager=None): + self.page = page + self.config = config.copy() + self.config_manager = config_manager + self.cache_manager = cache_manager + self.result = None + self.entries = {} + self.dialog_ref = ft.Ref[ft.AlertDialog]() + + # Dropdown refs + self.detected_repos_dropdown_ref = ft.Ref[ft.Dropdown]() + self.ollama_model_dropdown_ref = ft.Ref[ft.Dropdown]() + + # Package checker refs + self.package_status_ref = ft.Ref[ft.Container]() + + def show(self, on_result=None): + """Show the settings dialog""" + try: + print("SettingsDialog.show() called") + self.on_result = on_result + + # Create the dialog + print("Creating dialog...") + dialog = self._create_dialog() + print(f"Dialog created: {dialog}") + + # Always set the dialog ref to the current dialog instance + print("Setting dialog_ref.current to new dialog instance") + self.dialog_ref.current = dialog + + # Use Flet 0.28+ API: page.open() instead of page.dialog + print("Opening dialog with page.open()...") + self.page.open(dialog) + # Ensure UI updates immediately (useful when console is not visible) + try: + self.page.update() + except Exception: + pass + print("page.open() completed") + + # Start async initialization + print("Starting async initialization...") + self.page.run_task(self._init_async) + print("SettingsDialog.show() completed") + except Exception as ex: + print(f"Error in SettingsDialog.show(): {ex}") + import traceback + traceback.print_exc() + + async def _init_async(self): + """Initialize async operations""" + await asyncio.sleep(0.1) + await self._scan_repos_async() + # Load cached Ollama models + await self._load_cached_ollama_models() + # Check packages for current AI provider + await self._check_packages_for_current_provider() + + def _create_dialog(self) -> ft.AlertDialog: + """Create the settings dialog""" + # Create tabs + tabs = ft.Tabs( + selected_index=0, + animation_duration=300, + tabs=[ + ft.Tab( + text="General", + icon=ft.icons.SETTINGS, + content=self._create_general_tab() + ), + ft.Tab( + text="AI Providers", + icon=ft.icons.PSYCHOLOGY, + content=self._create_ai_tab() + ), + ], + expand=True, + ) + + # Action buttons + actions = ft.Row( + [ + ft.TextButton( + "Test Connection", + icon=ft.icons.CABLE, + on_click=self._test_connection + ), + ft.TextButton( + "Clear Cache", + icon=ft.icons.DELETE_SWEEP, + on_click=self._clear_cache + ), + ft.Container(expand=True), + ft.TextButton("Cancel", on_click=self._cancel_clicked), + ft.FilledButton("Save Settings", icon=ft.icons.SAVE, on_click=self._save_clicked), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + ) + + dialog = ft.AlertDialog( + ref=self.dialog_ref, + modal=True, + title=ft.Text("āš™ļø Settings", size=24, weight=ft.FontWeight.BOLD), + content=ft.Container( + content=tabs, + width=900, + height=700, + padding=10, + ), + actions=[actions], + actions_padding=ft.padding.all(20), + ) + + return dialog + + def _create_general_tab(self) -> ft.Container: + """Create general settings tab""" + controls = [] + + # GitHub Configuration Section + controls.append(self._create_section_header("šŸ™ GitHub Personal Access Token")) + + # GitHub PAT + github_pat = ft.TextField( + label="Personal Access Token", + password=True, + can_reveal_password=True, + value=self.config.get('GITHUB_PAT', ''), + hint_text="Enter your GitHub Personal Access Token", + expand=True, + ) + self.entries['GITHUB_PAT'] = github_pat + controls.append(github_pat) + + # General Options Section + controls.append(self._create_section_header("āš™ļø General Options")) + + # Dry Run Mode + dry_run_checkbox = ft.Checkbox( + label="🧪 Dry Run Mode (Test without making changes)", + value=str(self.config.get('DRY_RUN', 'false')).lower() in ('true', '1', 'yes', 'on'), + ) + self.entries['DRY_RUN'] = dry_run_checkbox + controls.append(dry_run_checkbox) + controls.append(ft.Text( + "ā„¹ļø Simulates operations without creating actual GitHub issues/PRs", + size=12, + color="grey400", + )) + + # Local Repo Path + local_repo_path = ft.TextField( + label="Local Repo Path", + value=self.config.get('LOCAL_REPO_PATH', ''), + hint_text="Path where repositories are cloned", + expand=True, + ) + self.entries['LOCAL_REPO_PATH'] = local_repo_path + controls.append(local_repo_path) + + # Detected Repos + controls.append(ft.Text("Detected Repos", weight=ft.FontWeight.BOLD, size=14)) + detected_repos_row = ft.Row( + [ + ft.Dropdown( + ref=self.detected_repos_dropdown_ref, + label="Detected Repositories", + value="Scanning...", + options=[], + hint_text="Scanned local repositories", + expand=True, + ), + ft.ElevatedButton( + "šŸ”„ Scan", + on_click=lambda e: self.page.run_task(self._scan_repos_async), + ), + ], + spacing=5, + ) + controls.append(detected_repos_row) + + # Help text + controls.append(ft.Container( + content=ft.Text( + "šŸ’” Repository Setup Guide:\n" + " • Local Repo Path: Where your fork repos are cloned (e.g., C:\\git\\repos)\n" + " • Detected Repos: Shows your local fork (e.g., yourname/repo)\n" + " • Note: Target and Fork repositories are configured in the main GUI", + size=12, + color="grey400", + ), + padding=ft.padding.all(10), + bgcolor="surfacevariant", + border_radius=5, + margin=ft.margin.only(top=10), + )) + + controls.append(ft.Container( + content=ft.Text( + "šŸ’” Getting Started:\n" + "1. Create a GitHub Personal Access Token\n" + "2. Configure GitHub repositories in the main GUI\n" + "3. Set Local Repo Path for automatic repository detection\n" + "4. Configure AI provider in the AI tab (optional)\n" + "5. Test your connection before processing items", + size=12, + color="blue400", + ), + padding=ft.padding.all(10), + bgcolor="blue900", + border_radius=5, + margin=ft.margin.only(top=10), + )) + + return ft.Container( + content=ft.ListView( + controls=controls, + spacing=15, + padding=20, + ), + expand=True, + ) + + def _create_ai_tab(self) -> ft.Container: + """Create AI settings tab""" + controls = [] + + # Package Status Section (at the top) + controls.append(ft.Container( + content=ft.Column([ + ft.Row([ + ft.Text("Package Status", size=16, weight=ft.FontWeight.BOLD), + ft.IconButton( + icon=ft.icons.REFRESH, + tooltip="Refresh package status", + on_click=lambda e: self.page.run_task(self._check_packages_for_current_provider), + ), + ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN), + ft.Container( + ref=self.package_status_ref, + content=ft.Row([ + ft.ProgressRing(width=20, height=20), + ft.Text("Checking packages...", color=ft.colors.BLUE), + ]), + padding=10, + bgcolor=ft.colors.BLUE_100, + border_radius=5, + ), + ], spacing=10), + padding=ft.padding.only(bottom=10), + )) + + # AI Provider Section + controls.append(self._create_section_header("šŸ¤– AI Provider Configuration")) + + # Provider dropdown + ai_provider = ft.Dropdown( + label="AI Provider", + value=self.config.get('AI_PROVIDER', 'none'), + options=[ + ft.dropdown.Option("none", "None"), + ft.dropdown.Option("claude", "Claude"), + ft.dropdown.Option("chatgpt", "ChatGPT"), + ft.dropdown.Option("github-copilot", "GitHub Copilot"), + ft.dropdown.Option("ollama", "Ollama"), + ], + expand=True, + on_change=lambda e: self.page.run_task(self._check_packages_for_current_provider), + ) + self.entries['AI_PROVIDER'] = ai_provider + controls.append(ai_provider) + + # API Keys + claude_key = ft.TextField( + label="Claude API Key", + password=True, + can_reveal_password=True, + value=self.config.get('CLAUDE_API_KEY', ''), + hint_text="Get key at console.anthropic.com", + expand=True, + ) + self.entries['CLAUDE_API_KEY'] = claude_key + controls.append(claude_key) + + chatgpt_key = ft.TextField( + label="ChatGPT API Key", + password=True, + can_reveal_password=True, + value=self.config.get('OPENAI_API_KEY', ''), + hint_text="Get key at platform.openai.com/api-keys", + expand=True, + ) + self.entries['OPENAI_API_KEY'] = chatgpt_key + controls.append(chatgpt_key) + + github_token = ft.TextField( + label="GitHub Token (for Copilot) [defaults to GitHub PAT]", + password=True, + can_reveal_password=True, + value=self.config.get('GITHUB_TOKEN', ''), + hint_text="Defaults to GitHub PAT if empty", + expand=True, + ) + self.entries['GITHUB_TOKEN'] = github_token + controls.append(github_token) + + # Ollama Configuration + controls.append(self._create_section_header("šŸ¦™ Ollama Configuration")) + + ollama_url = ft.TextField( + label="Ollama Server URL", + value=self.config.get('OLLAMA_URL', ''), + hint_text="http://localhost:11434", + expand=True, + ) + self.entries['OLLAMA_URL'] = ollama_url + controls.append(ollama_url) + + ollama_api_key = ft.TextField( + label="Ollama API Key (optional)", + password=True, + can_reveal_password=True, + value=self.config.get('OLLAMA_API_KEY', ''), + expand=True, + ) + self.entries['OLLAMA_API_KEY'] = ollama_api_key + controls.append(ollama_api_key) + + # Ollama Model + ollama_model = ft.Dropdown( + ref=self.ollama_model_dropdown_ref, + label="Ollama Model", + value=self.config.get('OLLAMA_MODEL', ''), + options=[], + hint_text="Click scan to load models", + expand=True, + ) + self.entries['OLLAMA_MODEL'] = ollama_model + + ollama_model_row = ft.Row( + [ + ollama_model, + ft.ElevatedButton( + "šŸ” Scan", + on_click=lambda e: self.page.run_task(self._scan_ollama_models_async), + ), + ], + spacing=5, + ) + controls.append(ollama_model_row) + controls.append(ft.Text( + "ā„¹ļø Click šŸ” to scan available models from your Ollama server.", + size=12, + color="grey400", + )) + + # Help text + controls.append(ft.Container( + content=ft.Text( + "šŸ’” Tips:\n" + "• Provider: Choose 'none' to disable AI (uses Copilot workflow)\n" + "• Claude: Get key at console.anthropic.com\n" + "• ChatGPT: Get key at platform.openai.com/api-keys\n" + "• GitHub Copilot: Uses GitHub Models API (requires GitHub token)\n" + "• GitHub Token: Auto-defaults to GitHub PAT if left empty\n" + "• Ollama: Self-hosted AI (requires Ollama server running)\n" + "• Cost: ~$0.01-0.05 per PR with AI, free with 'none' and Ollama\n" + "• AI providers clone repos locally to make changes before pushing", + size=12, + color="blue400", + ), + padding=ft.padding.all(10), + bgcolor="blue900", + border_radius=5, + margin=ft.margin.only(top=10), + )) + + return ft.Container( + content=ft.ListView( + controls=controls, + spacing=15, + padding=20, + ), + expand=True, + ) + + def _create_section_header(self, text: str) -> ft.Container: + """Create a section header""" + return ft.Container( + content=ft.Column( + [ + ft.Text(text, size=18, weight=ft.FontWeight.BOLD), + ft.Divider(thickness=2, color="primary"), + ], + spacing=5, + ), + padding=ft.padding.only(top=20, bottom=10), + ) + + def _check_ai_packages(self, provider_name: str) -> Tuple[bool, List[str]]: + """Check if required packages for AI provider are installed""" + try: + from .ai_manager import AIManager + ai_manager = AIManager() + available, missing = ai_manager.check_ai_module_availability(provider_name) + return available, missing + except Exception as e: + print(f"Error checking AI packages: {e}") + return False, [] + + def _detect_environment(self) -> Tuple[bool, str]: + """Detect if running in virtual environment""" + in_venv = (hasattr(sys, 'real_prefix') or + (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix) or + os.environ.get('VIRTUAL_ENV') is not None) + + if in_venv: + venv_path = os.environ.get('VIRTUAL_ENV', sys.prefix) + venv_name = os.path.basename(venv_path) + return True, venv_name + else: + return False, "system-wide" + + async def _load_cached_ollama_models(self): + """Load cached Ollama models on dialog open""" + if not self.cache_manager: + return + + try: + # Get Ollama URL to use as cache identifier + ollama_url = self.config.get('OLLAMA_URL', '').strip() + if not ollama_url: + return + + # Load cached models + def load_cache(): + cached_data = self.cache_manager.load_from_cache('ollama_models', ollama_url) + if cached_data: + # Extract model names from cache format + return [item['name'] for item in cached_data if 'name' in item] + return [] + + cached_models = await asyncio.to_thread(load_cache) + + if cached_models and self.ollama_model_dropdown_ref.current: + # Update dropdown with cached models + self.ollama_model_dropdown_ref.current.options = [ + ft.dropdown.Option(model) for model in cached_models + ] + + # Restore saved selection + saved_model = self.config.get('OLLAMA_MODEL', '') + if saved_model and saved_model in cached_models: + self.ollama_model_dropdown_ref.current.value = saved_model + elif cached_models: + # If saved model not in list, select first one + self.ollama_model_dropdown_ref.current.value = cached_models[0] + + self.page.update() + print(f"Loaded {len(cached_models)} cached Ollama models") + + except Exception as e: + print(f"Error loading cached Ollama models: {e}") + + async def _check_packages_for_current_provider(self): + """Check packages for the currently selected AI provider""" + if not self.package_status_ref.current: + return + + # Get current provider selection + ai_provider_dropdown = self.entries.get('AI_PROVIDER') + if not ai_provider_dropdown: + return + + provider = ai_provider_dropdown.value + if not provider or provider == 'none': + self.package_status_ref.current.content = ft.Container( + content=ft.Row([ + ft.Icon(ft.icons.INFO, color=ft.colors.BLUE), + ft.Text("No AI provider selected", color=ft.colors.BLUE), + ]), + padding=10, + bgcolor=ft.colors.BLUE_100, + border_radius=5, + ) + self.page.update() + return + + # Check packages in background thread + def check_packages(): + return self._check_ai_packages(provider) + + available, missing = await asyncio.to_thread(check_packages) + + # Update UI with results + if available: + self.package_status_ref.current.content = ft.Container( + content=ft.Row([ + ft.Icon(ft.icons.CHECK_CIRCLE, color=ft.colors.GREEN), + ft.Text(f"All required packages for {provider} are installed", color=ft.colors.GREEN), + ]), + padding=10, + bgcolor=ft.colors.GREEN_100, + border_radius=5, + ) + else: + in_venv, env_name = self._detect_environment() + env_text = f"Virtual environment: {env_name}" if in_venv else "System-wide installation" + + self.package_status_ref.current.content = ft.Container( + content=ft.Column([ + ft.Row([ + ft.Icon(ft.icons.WARNING, color=ft.colors.ORANGE), + ft.Text(f"Missing packages for {provider}", color=ft.colors.ORANGE, weight=ft.FontWeight.BOLD), + ]), + ft.Text(f"Required: {', '.join(missing)}", size=12), + ft.Text(f"Environment: {env_text}", size=12, italic=True), + ft.ElevatedButton( + "Install Packages", + icon=ft.icons.DOWNLOAD, + on_click=lambda e: self._install_packages(missing, provider), + ), + ], spacing=5), + padding=10, + bgcolor=ft.colors.ORANGE_100, + border_radius=5, + ) + + self.page.update() + + def _install_packages(self, packages: List[str], provider: str): + """Install missing packages""" + in_venv, env_name = self._detect_environment() + env_text = f"virtual environment '{env_name}'" if in_venv else "system-wide (may require administrator rights)" + + # Create confirmation dialog + package_list = ', '.join(packages) + message = (f"Install the following packages for {provider}?\n\n" + f"Packages: {package_list}\n\n" + f"Installation location: {env_text}\n\n" + f"Command: pip install {' '.join(packages)}") + + def handle_install(e): + self.page.close(install_dialog) + # Run installation in background + self.page.run_task(lambda: self._do_install_packages(packages, provider)) + + def handle_cancel(e): + self.page.close(install_dialog) + + install_dialog = ft.AlertDialog( + modal=True, + title=ft.Text("Install AI Packages"), + content=ft.Text(message), + actions=[ + ft.TextButton("Cancel", on_click=handle_cancel), + ft.FilledButton("Install", on_click=handle_install), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + + self.page.open(install_dialog) + + async def _do_install_packages(self, packages: List[str], provider: str): + """Actually install the packages""" + in_venv, env_name = self._detect_environment() + + # Update status to show installation in progress + if self.package_status_ref.current: + self.package_status_ref.current.content = ft.Container( + content=ft.Row([ + ft.ProgressRing(width=20, height=20), + ft.Text(f"Installing packages for {provider}...", color=ft.colors.BLUE), + ]), + padding=10, + bgcolor=ft.colors.BLUE_100, + border_radius=5, + ) + self.page.update() + + # Install packages in background thread + def install(): + try: + for package in packages: + print(f"Installing {package}...") + pip_cmd = [sys.executable, '-m', 'pip', 'install', package] + result = subprocess.run(pip_cmd, capture_output=True, text=True, timeout=300) + + # If direct install fails and we're not in venv, try with --user flag + if result.returncode != 0 and not in_venv: + print(f" Direct installation failed, trying with --user flag...") + pip_cmd_user = [sys.executable, '-m', 'pip', 'install', '--user', package] + result = subprocess.run(pip_cmd_user, capture_output=True, text=True, timeout=300) + + if result.returncode != 0: + return False, f"Failed to install {package}: {result.stderr}" + + return True, "All packages installed successfully" + + except subprocess.TimeoutExpired: + return False, "Installation timed out" + except Exception as e: + return False, f"Error installing packages: {str(e)}" + + success, message = await asyncio.to_thread(install) + + # Show result and offer to restart + if success: + def handle_restart(e): + self.page.close(result_dialog) + self._restart_application() + + def handle_later(e): + self.page.close(result_dialog) + # Re-check packages after installation + self.page.run_task(self._check_packages_for_current_provider) + + result_dialog = ft.AlertDialog( + modal=True, + title=ft.Text("Installation Complete"), + content=ft.Text(f"{message}\n\nThe application needs to restart to use the newly installed packages."), + actions=[ + ft.TextButton("Restart Later", on_click=handle_later), + ft.FilledButton("Restart Now", on_click=handle_restart), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + + self.page.open(result_dialog) + else: + self._show_alert("Installation Failed", message) + # Re-check packages to update status + await self._check_packages_for_current_provider() + + def _restart_application(self): + """Restart the application""" + try: + # Close the dialog first + if self.dialog_ref.current: + self.page.close(self.dialog_ref.current) + + # Show restart message + restart_msg = ft.SnackBar( + content=ft.Text("Restarting application..."), + bgcolor=ft.colors.BLUE, + ) + self.page.open(restart_msg) + self.page.update() + + # Restart the application + python = sys.executable + os.execl(python, python, *sys.argv) + except Exception as e: + self._show_alert("Restart Failed", f"Could not restart application: {str(e)}\n\nPlease restart manually.") + + async def _install_and_save(self, packages: List[str], provider: str, config_values: Dict[str, Any]): + """Install packages and then save configuration""" + # Install packages + in_venv, _ = self._detect_environment() + + def install(): + try: + for package in packages: + print(f"Installing {package}...") + pip_cmd = [sys.executable, '-m', 'pip', 'install', package] + result = subprocess.run(pip_cmd, capture_output=True, text=True, timeout=300) + + # If direct install fails and we're not in venv, try with --user flag + if result.returncode != 0 and not in_venv: + print(f" Direct installation failed, trying with --user flag...") + pip_cmd_user = [sys.executable, '-m', 'pip', 'install', '--user', package] + result = subprocess.run(pip_cmd_user, capture_output=True, text=True, timeout=300) + + if result.returncode != 0: + return False, f"Failed to install {package}: {result.stderr}" + + return True, "All packages installed successfully" + + except subprocess.TimeoutExpired: + return False, "Installation timed out" + except Exception as e: + return False, f"Error installing packages: {str(e)}" + + success, message = await asyncio.to_thread(install) + + if success: + # Save configuration after successful installation + self._do_save(config_values) + + # Offer to restart + def handle_restart(e): + self.page.close(restart_dialog) + self._restart_application() + + def handle_later(e): + self.page.close(restart_dialog) + + restart_dialog = ft.AlertDialog( + modal=True, + title=ft.Text("Installation Complete"), + content=ft.Text( + f"Packages installed successfully!\n" + f"Settings have been saved.\n\n" + f"The application needs to restart to use the newly installed packages." + ), + actions=[ + ft.TextButton("Restart Later", on_click=handle_later), + ft.FilledButton("Restart Now", on_click=handle_restart), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + + self.page.open(restart_dialog) + else: + self._show_alert("Installation Failed", f"{message}\n\nSettings were not saved.") + + def _do_save(self, config_values: Dict[str, Any]): + """Actually save the configuration""" + try: + # Save configuration + if self.config_manager: + success = self.config_manager.save_configuration(config_values) + else: + success = self._save_to_env_file(config_values) + + if success: + self.result = config_values + self._show_alert( + "Settings Saved", + "Settings saved successfully!\n\nChanges applied immediately - no restart needed! ✨" + ) + self._close_dialog() + else: + self._show_alert("Save Error", "Failed to save settings to .env file.") + + except Exception as e: + self._show_alert("Save Error", f"Error saving settings:\n{str(e)}") + + async def _scan_repos_async(self): + """Scan for git repositories in the local repo path""" + try: + from pathlib import Path + + # Get the local repo path + local_path_field = self.entries.get('LOCAL_REPO_PATH') + if local_path_field: + path_str = local_path_field.value.strip() + else: + path_str = self.config.get('LOCAL_REPO_PATH', '').strip() + + if not path_str: + path_str = str(Path.home() / "Downloads" / "github_repos") + + print(f"šŸ” Scanning for repos in: {path_str}") + base_path = Path(path_str) + + if not base_path.exists(): + if self.detected_repos_dropdown_ref.current: + self.detected_repos_dropdown_ref.current.value = 'No repos found (directory does not exist)' + self.detected_repos_dropdown_ref.current.options = [] + self.page.update() + return + + # Scan for git repositories + repos = [] + try: + # First, check if repos are directly in the base path (flat structure) + for item in base_path.iterdir(): + if not item.is_dir(): + continue + + git_dir = item / ".git" + if git_dir.exists(): + # This is a git repo directly in the base path + # Try to get the remote origin to get owner/repo format + try: + import subprocess + result = subprocess.run( + ['git', 'config', '--get', 'remote.origin.url'], + cwd=str(item), + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode == 0: + url = result.stdout.strip() + # Parse GitHub URL to get owner/repo + if 'github.com' in url: + # Handle both HTTPS and SSH URLs + if url.startswith('https://'): + # https://github.com/owner/repo.git + parts = url.replace('https://github.com/', '').replace('.git', '').split('/') + if len(parts) >= 2: + repo_name = f"{parts[0]}/{parts[1]}" + repos.append(repo_name) + continue + elif url.startswith('git@'): + # git@github.com:owner/repo.git + parts = url.replace('git@github.com:', '').replace('.git', '').split('/') + if len(parts) >= 2: + repo_name = f"{parts[0]}/{parts[1]}" + repos.append(repo_name) + continue + except: + pass + + # Fallback: use directory name + repos.append(f"local/{item.name}") + else: + # Check if this is an owner directory with repos inside (nested structure) + for repo_dir in item.iterdir(): + if not repo_dir.is_dir(): + continue + + git_dir = repo_dir / ".git" + if git_dir.exists(): + repo_name = f"{item.name}/{repo_dir.name}" + repos.append(repo_name) + + except Exception as e: + print(f"Error scanning repos: {e}") + import traceback + traceback.print_exc() + + # Update dropdown + if self.detected_repos_dropdown_ref.current: + if repos: + repos.sort() + print(f"āœ… Found {len(repos)} repo(s): {', '.join(repos)}") + self.detected_repos_dropdown_ref.current.options = [ + ft.dropdown.Option(repo) for repo in repos + ] + if len(repos) == 1: + self.detected_repos_dropdown_ref.current.value = repos[0] + else: + self.detected_repos_dropdown_ref.current.value = f'{len(repos)} repo(s) found - select one' + else: + print(f"āŒ No git repositories found in {path_str}") + self.detected_repos_dropdown_ref.current.value = 'No git repositories found' + self.detected_repos_dropdown_ref.current.options = [] + + self.page.update() + + except Exception as e: + print(f"Error in _scan_repos_async: {e}") + + async def _scan_ollama_models_async(self): + """Scan Ollama server for available models""" + ollama_url = self.entries.get('OLLAMA_URL').value.strip() if 'OLLAMA_URL' in self.entries else '' + + if not ollama_url: + self._show_alert("Ollama URL Required", "Please enter the Ollama Server URL first.") + return + + if not ollama_url.startswith('http'): + ollama_url = f"http://{ollama_url}" + + def scan_models(): + try: + import requests + + ollama_api_key = self.entries.get('OLLAMA_API_KEY').value.strip() if 'OLLAMA_API_KEY' in self.entries else '' + + headers = {} + if ollama_api_key: + headers['Authorization'] = f'Bearer {ollama_api_key}' + + response = requests.get(f"{ollama_url}/api/tags", headers=headers, timeout=10) + response.raise_for_status() + + data = response.json() + models = data.get('models', []) + model_names = [model.get('name', '') for model in models if model.get('name')] + + # Cache the models + if self.cache_manager and model_names: + # Convert to cache format (list of dicts) + cache_data = [{'name': name} for name in model_names] + self.cache_manager.save_to_cache('ollama_models', ollama_url, cache_data) + print(f"Cached {len(model_names)} Ollama models") + + # Update UI + if self.ollama_model_dropdown_ref.current: + if model_names: + self.ollama_model_dropdown_ref.current.options = [ + ft.dropdown.Option(name) for name in model_names + ] + + # Restore saved selection if it exists in the list + saved_model = self.config.get('OLLAMA_MODEL', '') + if saved_model and saved_model in model_names: + self.ollama_model_dropdown_ref.current.value = saved_model + elif model_names: + # Otherwise select first model + self.ollama_model_dropdown_ref.current.value = model_names[0] + + self.page.update() + + models_text = "\n".join(f"• {name}" for name in model_names[:10]) + if len(model_names) > 10: + models_text += f"\n\n...and {len(model_names) - 10} more" + + else: + self._show_alert("No Models Found", "No models found on the Ollama server.\n\nUse 'ollama pull ' to download models.") + + except requests.exceptions.ConnectionError: + self._show_alert("Connection Error", f"Could not connect to Ollama server at:\n{ollama_url}\n\nMake sure Ollama is running and the URL is correct.") + except Exception as e: + self._show_alert("Scan Error", f"An error occurred while scanning for models:\n{str(e)}") + + await asyncio.to_thread(scan_models) + + def _test_connection(self, e): + """Test connection to configured services""" + config_values = self._get_config_values() + + results = [] + + # Test GitHub + if config_values.get('GITHUB_PAT'): + try: + from .github_api import GitHubAPI + api = GitHubAPI(config_values.get('GITHUB_PAT')) + results.append("GitHub: āœ… Token configured") + + if config_values.get('GITHUB_REPO'): + results.append(f"GitHub Repository: āœ… {config_values.get('GITHUB_REPO')}") + else: + results.append("GitHub Repository: āš ļø Not configured") + except Exception as e: + results.append(f"GitHub: āŒ Error - {str(e)}") + else: + results.append("GitHub: āŒ No token configured") + + # Test AI Provider + ai_provider = config_values.get('AI_PROVIDER', 'none').lower() + if ai_provider and ai_provider != 'none': + try: + from .ai_manager import AIManager + ai_manager = AIManager() + available, missing = ai_manager.check_ai_module_availability(ai_provider) + + if available: + results.append(f"AI Provider ({ai_provider}): āœ… Available") + else: + results.append(f"AI Provider ({ai_provider}): āš ļø Missing packages: {', '.join(missing)}") + except Exception as e: + results.append(f"AI Provider ({ai_provider}): āš ļø Error - {str(e)}") + else: + results.append("AI Provider: ā„¹ļø Disabled (using standard method)") + + # Show results + if results: + self._show_alert( + "Connection Test Results", + "\n".join(results) + "\n\nšŸ’” Full validation requires running the application." + ) + + def _clear_cache(self, e): + """Clear all cached items""" + def do_clear(): + if self.cache_manager: + self.cache_manager.invalidate_cache() + self._show_alert( + "Cache Cleared", + "All cached items have been cleared.\nFresh data will be loaded on next app start." + ) + else: + self._show_alert("Error", "Cache manager not available") + + # Show confirmation dialog + self._show_confirmation( + "Clear Cache", + "Are you sure you want to clear all cached items?\n\nAll cached data will be removed.\nThe next time you open the app, it will auto-load fresh data.", + on_confirm=do_clear + ) + + def _get_config_values(self) -> Dict[str, Any]: + """Get configuration values from entries""" + config_values = {} + + for key, widget in self.entries.items(): + if isinstance(widget, ft.Checkbox): + config_values[key] = 'true' if widget.value else 'false' + elif isinstance(widget, (ft.TextField, ft.Dropdown)): + value = widget.value or '' + if isinstance(value, str): + value = value.strip() + config_values[key] = value + + return config_values + + def _save_clicked(self, e): + """Handle save button click""" + try: + config_values = self._get_config_values() + + # Validate required fields + if not config_values.get('GITHUB_PAT'): + self._show_alert( + "Missing Configuration", + "GitHub Personal Access Token is required for basic functionality." + ) + return + + # Check AI provider setup + ai_provider = config_values.get('AI_PROVIDER', '').strip().lower() + if ai_provider and ai_provider not in ['none', '']: + if ai_provider in ['chatgpt', 'claude', 'anthropic', 'github-copilot', 'copilot', 'github_copilot', 'ollama']: + available, missing = self._check_ai_packages(ai_provider) + if not available and missing: + # Offer to install missing packages + in_venv, env_name = self._detect_environment() + env_text = f"virtual environment '{env_name}'" if in_venv else "system-wide" + + def handle_install_and_save(e): + self.page.close(package_warning_dialog) + # Install packages and then save + self.page.run_task(lambda: self._install_and_save(missing, ai_provider, config_values)) + + def handle_save_anyway(e): + self.page.close(package_warning_dialog) + # Continue with save + self._do_save(config_values) + + def handle_cancel_save(e): + self.page.close(package_warning_dialog) + + package_warning_dialog = ft.AlertDialog( + modal=True, + title=ft.Text("Missing AI Packages"), + content=ft.Text( + f"AI provider '{ai_provider}' requires additional packages:\n\n" + f"{', '.join(missing)}\n\n" + f"Installation location: {env_text}\n\n" + f"Would you like to install them now?" + ), + actions=[ + ft.TextButton("Cancel", on_click=handle_cancel_save), + ft.TextButton("Save Without Installing", on_click=handle_save_anyway), + ft.FilledButton("Install & Save", on_click=handle_install_and_save), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + + self.page.open(package_warning_dialog) + return # Don't save yet, wait for user choice + + # Save configuration (packages are already installed or not needed) + self._do_save(config_values) + + except Exception as e: + self._show_alert("Save Error", f"Error saving settings:\n{str(e)}") + + def _save_to_env_file(self, config_values: Dict[str, Any]) -> bool: + """Fallback method to save configuration to .env file""" + try: + env_content = "# GitHub Pulse Configuration\n" + env_content += "# Generated by Settings Dialog\n\n" + + for key, value in config_values.items(): + if value: + env_content += f"{key}={value}\n" + else: + env_content += f"{key}=\n" + + env_path = os.path.join(os.getcwd(), '.env') + with open(env_path, 'w', encoding='utf-8') as f: + f.write(env_content) + + return True + + except Exception as e: + print(f"Error saving to .env file: {e}") + return False + + def _cancel_clicked(self, e): + """Handle cancel button click""" + self.result = None + self._close_dialog() + + def _close_dialog(self): + """Close the dialog""" + # Use Flet 0.28+ API: page.close() instead of page.dialog + if self.dialog_ref.current: + self.page.close(self.dialog_ref.current) + + if self.on_result: + self.on_result(self.result) + + def _show_alert(self, title: str, message: str): + """Show an alert dialog""" + def close_dlg(e): + self.page.close(alert_dialog) + + alert_dialog = ft.AlertDialog( + modal=True, + title=ft.Text(title), + content=ft.Text(message), + actions=[ft.TextButton("OK", on_click=close_dlg)], + actions_alignment=ft.MainAxisAlignment.END, + ) + + self.page.open(alert_dialog) + + def _show_confirmation(self, title: str, message: str, on_confirm=None, on_cancel=None): + """Show a confirmation dialog""" + def handle_yes(e): + self.page.close(confirm_dialog) + if on_confirm: + on_confirm() + + def handle_no(e): + self.page.close(confirm_dialog) + if on_cancel: + on_cancel() + + confirm_dialog = ft.AlertDialog( + modal=True, + title=ft.Text(title), + content=ft.Text(message), + actions=[ + ft.TextButton("No", on_click=handle_no), + ft.FilledButton("Yes", on_click=handle_yes), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + + self.page.open(confirm_dialog) diff --git a/src/app_components/settings_manager.py b/src/app_components/settings_manager.py new file mode 100644 index 0000000..c75c12e --- /dev/null +++ b/src/app_components/settings_manager.py @@ -0,0 +1,307 @@ +""" +Settings Manager +Handles application settings with live updates and secure storage. + +Non-secret settings are stored in config.json. +Secrets (API keys, tokens) are stored in the system keyring. +""" + +import json +import os +from pathlib import Path +from typing import Dict, Any, Optional, Callable +import keyring + + +class SettingsManager: + """ + Manages application settings with live updates. + + Features: + - Non-secret settings stored in JSON + - API keys stored securely in system keyring + - Live update notifications to registered listeners + - No app restart required for changes + """ + + # Keyring service name for this app + SERVICE_NAME = "GitHubPulse" + + # Keys that should be stored in keyring (secrets) + SECRET_KEYS = { + 'GITHUB_PAT', + 'ANTHROPIC_API_KEY', + 'OPENAI_API_KEY', + 'GITHUB_COPILOT_TOKEN', + 'CLAUDE_API_KEY', # Alternative name for Anthropic + 'GITHUB_TOKEN', # For GitHub Copilot + 'OLLAMA_API_KEY', # Optional Ollama API key + } + + # Default settings (non-secrets) + DEFAULT_SETTINGS = { + # GitHub Configuration + 'GITHUB_REPO': '', + 'FORKED_REPO': '', + 'LOCAL_REPO_PATH': '', + + # Application Settings + 'AI_PROVIDER': 'none', + 'DRY_RUN': 'false', + 'DEFAULT_BRANCH': 'main', + 'THEME_MODE': 'dark', + 'AUTO_REFRESH': 'true', + 'REFRESH_INTERVAL': '300', + + # Ollama Configuration + 'OLLAMA_URL': '', + 'OLLAMA_MODEL': '', + + # Custom AI Instructions + 'CUSTOM_INSTRUCTIONS': '', + } + + def __init__(self, config_dir: Optional[Path] = None): + """ + Initialize the settings manager. + + Args: + config_dir: Directory to store config.json. Defaults to app directory. + """ + # Determine config directory + if config_dir is None: + # Use app directory + config_dir = Path(__file__).parent.parent + self.config_dir = Path(config_dir) + self.config_file = self.config_dir / "config.json" + + # Settings storage + self._settings: Dict[str, Any] = {} + + # Registered change listeners + self._listeners: list[Callable[[str, Any], None]] = [] + + # Load settings + self.load() + + def load(self) -> Dict[str, Any]: + """ + Load settings from config.json and keyring. + + Returns: + Dictionary of all settings (secrets and non-secrets combined) + """ + # Start with defaults + self._settings = self.DEFAULT_SETTINGS.copy() + + # Load from JSON file + if self.config_file.exists(): + try: + with open(self.config_file, 'r') as f: + saved_settings = json.load(f) + # Only load non-secret settings from JSON + for key, value in saved_settings.items(): + if key not in self.SECRET_KEYS: + self._settings[key] = value + except Exception as e: + print(f"Error loading config.json: {e}") + + # Load secrets from keyring + for secret_key in self.SECRET_KEYS: + try: + value = keyring.get_password(self.SERVICE_NAME, secret_key) + if value: + self._settings[secret_key] = value + except Exception as e: + print(f"Error loading {secret_key} from keyring: {e}") + + return self._settings.copy() + + def save(self, settings: Optional[Dict[str, Any]] = None) -> bool: + """ + Save settings to config.json and keyring. + + Args: + settings: Settings to save. If None, saves current settings. + + Returns: + True if successful, False otherwise + """ + if settings is not None: + # Update internal settings + for key, value in settings.items(): + old_value = self._settings.get(key) + self._settings[key] = value + + # Notify listeners of changes + if old_value != value: + self._notify_change(key, value) + + try: + # Save non-secrets to JSON + json_settings = { + key: value for key, value in self._settings.items() + if key not in self.SECRET_KEYS + } + + with open(self.config_file, 'w') as f: + json.dump(json_settings, f, indent=2) + + # Save secrets to keyring + for secret_key in self.SECRET_KEYS: + if secret_key in self._settings: + value = self._settings[secret_key] + if value: # Only save non-empty values + try: + keyring.set_password(self.SERVICE_NAME, secret_key, str(value)) + except Exception as e: + print(f"Error saving {secret_key} to keyring: {e}") + + return True + + except Exception as e: + print(f"Error saving settings: {e}") + return False + + def get(self, key: str, default: Any = None) -> Any: + """ + Get a setting value. + + Args: + key: Setting key + default: Default value if key doesn't exist + + Returns: + Setting value or default + """ + return self._settings.get(key, default) + + def set(self, key: str, value: Any, save: bool = True) -> bool: + """ + Set a setting value with live update. + + Args: + key: Setting key + value: New value + save: Whether to persist immediately + + Returns: + True if successful + """ + old_value = self._settings.get(key) + self._settings[key] = value + + # Notify listeners + if old_value != value: + self._notify_change(key, value) + + # Save if requested + if save: + return self.save() + + return True + + def get_all(self) -> Dict[str, Any]: + """ + Get all settings. + + Returns: + Dictionary of all settings + """ + return self._settings.copy() + + def register_listener(self, callback: Callable[[str, Any], None]): + """ + Register a callback to be notified of setting changes. + + The callback will be called with (key, new_value) when a setting changes. + + Args: + callback: Function to call on settings change + """ + if callback not in self._listeners: + self._listeners.append(callback) + + def unregister_listener(self, callback: Callable[[str, Any], None]): + """ + Unregister a settings change callback. + + Args: + callback: Function to remove from listeners + """ + if callback in self._listeners: + self._listeners.remove(callback) + + def _notify_change(self, key: str, value: Any): + """ + Notify all registered listeners of a setting change. + + Args: + key: Setting key that changed + value: New value + """ + for listener in self._listeners: + try: + listener(key, value) + except Exception as e: + print(f"Error notifying listener of {key} change: {e}") + + def delete_secret(self, key: str) -> bool: + """ + Delete a secret from the keyring. + + Args: + key: Secret key to delete + + Returns: + True if successful + """ + if key not in self.SECRET_KEYS: + return False + + try: + keyring.delete_password(self.SERVICE_NAME, key) + if key in self._settings: + del self._settings[key] + self._notify_change(key, None) + return True + except Exception as e: + print(f"Error deleting {key} from keyring: {e}") + return False + + def migrate_from_env(self, env_file: Path) -> bool: + """ + Migrate settings from a .env file to the new system. + + Args: + env_file: Path to .env file + + Returns: + True if migration successful + """ + if not env_file.exists(): + print(f"Env file not found: {env_file}") + return False + + try: + # Read .env file + env_settings = {} + with open(env_file, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + if '=' in line: + key, value = line.split('=', 1) + key = key.strip() + value = value.strip().strip('"').strip("'") + env_settings[key] = value + + # Save to new system + self.save(env_settings) + + print(f"Successfully migrated {len(env_settings)} settings from .env") + return True + + except Exception as e: + print(f"Error migrating from .env: {e}") + return False diff --git a/src/app_components/utils.py b/src/app_components/utils.py new file mode 100644 index 0000000..63b8461 --- /dev/null +++ b/src/app_components/utils.py @@ -0,0 +1,662 @@ +""" +Utility functions and helpers +""" + +import json +import os +import re +import subprocess +import threading +import datetime +from pathlib import Path +from typing import Dict, Any, Optional, Tuple, List +from urllib.parse import urlparse + + +class Logger: + """Simple logger for GUI applications""" + + def __init__(self, text_widget=None): + self.text_widget = text_widget + self._lock = threading.Lock() + + def log(self, message: str) -> None: + """Log a message to the text widget and console""" + timestamp = __import__('datetime').datetime.now().strftime("%H:%M:%S") + formatted_message = f"[{timestamp}] {message}" + + try: + print(formatted_message) + except UnicodeEncodeError: + # Fallback: replace Unicode emojis with ASCII equivalents + safe_message = formatted_message.replace('āœ…', '[SUCCESS]').replace('āŒ', '[ERROR]').replace('āš ļø', '[WARNING]').replace('šŸ“‹', '[INFO]').replace('šŸ“„', '[FILE]').replace('šŸ“', '[LOCATION]').replace('šŸ“', '[EDIT]') + print(safe_message) + + if self.text_widget: + def update_widget(): + try: + with self._lock: + self.text_widget.config(state='normal') + self.text_widget.insert('end', formatted_message + '\n') + self.text_widget.see('end') + self.text_widget.config(state='disabled') + self.text_widget.update_idletasks() + except: + pass # Widget might be destroyed + + # Schedule update on main thread + if hasattr(self.text_widget, 'after'): + self.text_widget.after(0, update_widget) + else: + update_widget() + + +class PRNumberManager: + """Manages PR numbers for branch naming""" + + PR_COUNTER_FILE = '.pr_counter.json' + + @classmethod + def get_pr_counter_file(cls) -> str: + """Get the path to the PR counter file""" + script_dir = os.path.dirname(os.path.abspath(__file__)) + return os.path.join(script_dir, cls.PR_COUNTER_FILE) + + @classmethod + def load_pr_counter(cls) -> Dict[str, int]: + """Load the PR counter from file""" + counter_file = cls.get_pr_counter_file() + if os.path.exists(counter_file): + try: + with open(counter_file, 'r', encoding='utf-8') as f: + return json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + pass + return {} + + @classmethod + def save_pr_counter(cls, counter: Dict[str, int]) -> None: + """Save the PR counter to file""" + counter_file = cls.get_pr_counter_file() + try: + with open(counter_file, 'w', encoding='utf-8') as f: + json.dump(counter, f, indent=2) + except Exception as e: + print(f"Warning: Could not save PR counter: {e}") + + @classmethod + def get_next_pr_number(cls, provider_key: str) -> int: + """ + Get the next PR number for a given provider + + Args: + provider_key: Either the AI provider name ('chatgpt', 'claude') or 'gh_copilot' + + Returns: + Next available PR number for this provider + """ + try: + counter = cls.load_pr_counter() + current_number = counter.get(provider_key, 0) + next_number = current_number + 1 + counter[provider_key] = next_number + cls.save_pr_counter(counter) + return next_number + + except Exception as e: + print(f"Error managing PR counter: {e}") + # Fallback to a timestamp-based number + import time + return int(time.time()) % 10000 + + +class GitHubInfoExtractor: + """Extracts GitHub repository information from URLs""" + + @staticmethod + def extract_github_info(doc_url: str) -> Dict[str, Any]: + """Extract GitHub repository information from a document URL""" + try: + if not doc_url or 'github.com' not in doc_url: + return {'error': 'Not a GitHub URL'} + + parsed = urlparse(doc_url) + path_parts = parsed.path.strip('/').split('/') + + if len(path_parts) < 2: + return {'error': 'Invalid GitHub URL format'} + + owner = path_parts[0] + repo = path_parts[1] + + # Try to extract file path if it's a blob URL + file_path = None + if len(path_parts) > 3 and path_parts[2] == 'blob': + # Skip branch name and get file path + if len(path_parts) > 4: + file_path = '/'.join(path_parts[4:]) + + result = { + 'owner': owner, + 'repo': repo, + 'original_content_git_url': doc_url + } + + if file_path: + result['file_path'] = file_path + + # Try to find ms.author from the URL or repo name + ms_author = GitHubInfoExtractor._extract_ms_author(owner, repo, doc_url) + if ms_author: + result['ms_author'] = ms_author + + return result + + except Exception as e: + return {'error': f'Error parsing GitHub URL: {str(e)}'} + + @staticmethod + def _extract_ms_author(owner: str, repo: str, url: str) -> Optional[str]: + """Try to extract ms.author from various sources""" + try: + # Method 1: Check if owner looks like a Microsoft username + if owner.startswith('Microsoft') or 'microsoft' in owner.lower(): + # Try to extract from repo name or URL patterns + if '-' in repo: + parts = repo.split('-') + for part in parts: + if len(part) > 2 and part.islower(): + return part + + # Method 2: Look for patterns in the URL + url_lower = url.lower() + + # Common patterns for ms.author + patterns = [ + r'/([a-z][a-z0-9-]+[a-z0-9])/', # username-like patterns + r'author[=:]([a-z][a-z0-9-]+)', # author= or author: patterns + ] + + for pattern in patterns: + match = re.search(pattern, url_lower) + if match: + candidate = match.group(1) + # Validate it looks like a reasonable username + if 3 <= len(candidate) <= 20 and candidate.replace('-', '').isalnum(): + return candidate + + return None + + except Exception: + return None + + +class WorkItemFieldExtractor: + """Extracts and processes item fields (placeholder for future implementation)""" + + @staticmethod + def extract_work_item_fields(work_item: Dict[str, Any]) -> Dict[str, Any]: + """Extract and process fields from work item (placeholder)""" + fields = work_item.get('fields', {}) + + # Extract basic fields + item_id = work_item.get('id', 'Unknown') + title = fields.get('System.Title', 'No Title') + + # Extract custom fields with fallbacks + nature_of_request = ( + fields.get('Custom.Natureofrequest') or + fields.get('Custom.NatureOfRequest') or + fields.get('Microsoft.VSTS.Common.DescriptionHtml', '') + ) + + # Clean HTML if present + if nature_of_request and '<' in nature_of_request: + nature_of_request = WorkItemFieldExtractor._clean_html(nature_of_request) + + mydoc_url = ( + fields.get('Custom.MyDocURL') or + fields.get('Custom.DocumentURL') or + fields.get('Custom.URL', '') + ) + + text_to_change = ( + fields.get('Custom.TextToChange') or + fields.get('Custom.CurrentText', '') + ) + + new_text = ( + fields.get('Custom.NewText') or + fields.get('Custom.ProposedText') or + fields.get('Custom.ReplacementText', '') + ) + + # Extract GitHub info from the document URL + github_info = GitHubInfoExtractor.extract_github_info(mydoc_url) + + return { + 'id': item_id, + 'title': title, + 'nature_of_request': nature_of_request, + 'mydoc_url': mydoc_url, + 'text_to_change': text_to_change, + 'new_text': new_text, + 'github_info': github_info, + 'status': 'Ready', + 'source': 'Generic' + } + + @staticmethod + def extract_uuf_item_fields(uuf_item: Dict[str, Any]) -> Dict[str, Any]: + """Extract and process fields from custom item (placeholder)""" + # UUF items have different field structure + item_id = uuf_item.get('cr_uufitemid', 'Unknown') + title = uuf_item.get('cr_title', 'No Title') + + nature_of_request = uuf_item.get('cr_description', '') + mydoc_url = uuf_item.get('cr_documenturl', '') + text_to_change = uuf_item.get('cr_currenttext', '') + new_text = uuf_item.get('cr_newtext', '') + + # Extract GitHub info + github_info = GitHubInfoExtractor.extract_github_info(mydoc_url) + + return { + 'id': item_id, + 'title': title, + 'nature_of_request': nature_of_request, + 'mydoc_url': mydoc_url, + 'text_to_change': text_to_change, + 'new_text': new_text, + 'github_info': github_info, + 'status': 'Ready', + 'source': 'UUF' + } + + @staticmethod + def _clean_html(html_text: str) -> str: + """Remove HTML tags and decode entities""" + import html + + # Remove HTML tags + clean_text = re.sub(r'<[^>]+>', '', html_text) + + # Decode HTML entities + clean_text = html.unescape(clean_text) + + # Clean up whitespace + clean_text = re.sub(r'\s+', ' ', clean_text).strip() + + return clean_text + + +class ContentBuilders: + """Builds content for GitHub issues and PRs""" + + @staticmethod + def build_issue_title(item: Dict[str, Any]) -> str: + """Build GitHub issue title""" + item_id = item.get('id', '') + if item_id: + return f"[#{item_id}] {item['title']}" + return f"{item['title']}" + + @staticmethod + def build_issue_body(item: Dict[str, Any], github_info: Dict[str, Any]) -> str: + """Build GitHub issue body""" + body_parts = [] + + # Header + body_parts.append("## Item Details") + body_parts.append("") + + # Make ID a hyperlink if source URL is available + if item.get('source_url'): + body_parts.append(f"**ID:** [{item['id']}]({item['source_url']})") + else: + body_parts.append(f"**ID:** {item['id']}") + + body_parts.append(f"**Title:** {item['title']}") + body_parts.append("") + + # Nature of request + if item['nature_of_request']: + body_parts.append("**Nature of Request:**") + body_parts.append(item['nature_of_request']) + body_parts.append("") + + # Document information + if item['mydoc_url']: + body_parts.append("**Document URL:**") + body_parts.append(item['mydoc_url']) + body_parts.append("") + + # Change details + body_parts.append("## Change Details") + body_parts.append("") + + if item['text_to_change']: + body_parts.append("**Text to Change:**") + body_parts.append("```") + body_parts.append(item['text_to_change']) + body_parts.append("```") + body_parts.append("") + + if item['new_text']: + body_parts.append("**Proposed New Text:**") + body_parts.append("```") + body_parts.append(item['new_text']) + body_parts.append("```") + body_parts.append("") + + # Repository info + if github_info.get('owner') and github_info.get('repo'): + body_parts.append("## Repository Information") + body_parts.append("") + body_parts.append(f"**Repository:** {github_info['owner']}/{github_info['repo']}") + + if github_info.get('ms_author'): + body_parts.append(f"**Author:** @{github_info['ms_author']}") + + body_parts.append("") + + # Instructions for manual review + body_parts.append("## Instructions") + body_parts.append("") + body_parts.append("This issue requires manual review of the proposed documentation change.") + body_parts.append("") + body_parts.append("**Next Steps:**") + body_parts.append("1. Review the proposed change above") + body_parts.append("2. Navigate to the document URL") + body_parts.append("3. Locate the text that needs to be changed") + body_parts.append("4. Make the appropriate updates") + body_parts.append("5. Close this issue when complete") + body_parts.append("") + body_parts.append("---") + body_parts.append("*Created automatically by GitHub Pulse*") + + return "\n".join(body_parts) + + @staticmethod + def build_pr_title(item: Dict[str, Any]) -> str: + """Build GitHub PR title""" + item_id = item.get('id', '') + if item_id: + return f"[#{item_id}] {item['title']}" + return f"{item['title']}" + + @staticmethod + def build_pr_body(item: Dict[str, Any], github_info: Dict[str, Any]) -> str: + """Build GitHub PR body""" + body_parts = [] + + # Header + body_parts.append("## Documentation Update") + body_parts.append("") + + # Make ID a hyperlink if source URL is available + if item.get('source_url'): + body_parts.append(f"**ID:** [{item['id']}]({item['source_url']})") + else: + body_parts.append(f"**ID:** {item['id']}") + + body_parts.append(f"**Title:** {item['title']}") + body_parts.append("") + + # Nature of request + if item['nature_of_request']: + body_parts.append("**Description:**") + body_parts.append(item['nature_of_request']) + body_parts.append("") + + # Change summary + body_parts.append("## Changes Made") + body_parts.append("") + body_parts.append("This PR updates documentation as requested.") + body_parts.append("") + + if item['text_to_change'] and item['new_text']: + body_parts.append("**Change Summary:**") + body_parts.append("- Updated specific text content as requested") + body_parts.append("") + + body_parts.append("

") + body_parts.append("View Change Details") + body_parts.append("") + body_parts.append("**Original Text:**") + body_parts.append("```") + body_parts.append(item['text_to_change']) + body_parts.append("```") + body_parts.append("") + body_parts.append("**New Text:**") + body_parts.append("```") + body_parts.append(item['new_text']) + body_parts.append("```") + body_parts.append("
") + body_parts.append("") + + # Repository info + if github_info.get('ms_author'): + body_parts.append(f"**Author:** @{github_info['ms_author']}") + body_parts.append("") + + # Review instructions + body_parts.append("## Review Checklist") + body_parts.append("") + body_parts.append("- [ ] Changes match the requested update") + body_parts.append("- [ ] No unintended changes were made") + body_parts.append("- [ ] Grammar and formatting are correct") + body_parts.append("- [ ] Links and references are working") + body_parts.append("") + + body_parts.append("---") + body_parts.append("*Created automatically by GitHub Pulse*") + + return "\n".join(body_parts) + + +class LocalRepositoryScanner: + """Scans local repository path for Git repositories""" + + @staticmethod + def scan_local_repos(local_repo_path: str) -> List[str]: + """Scan local path for Git repositories""" + if not local_repo_path or not os.path.exists(local_repo_path): + return [] + + repos = [] + try: + for item in os.listdir(local_repo_path): + item_path = os.path.join(local_repo_path, item) + if os.path.isdir(item_path): + git_path = os.path.join(item_path, '.git') + if os.path.exists(git_path): + # Get remote origin URL to determine repo name + repo_info = LocalRepositoryScanner.get_repo_info(item_path) + if repo_info: + repos.append(repo_info) + else: + # Fallback to folder name + repos.append(f"local/{item}") + except PermissionError: + pass # Skip directories we can't access + except Exception as e: + print(f"Error scanning local repos: {e}") + + return sorted(repos) + + @staticmethod + def get_repo_info(repo_path: str) -> Optional[str]: + """Get repository information from local Git repo""" + try: + # Get remote origin URL + result = subprocess.run( + ['git', 'config', '--get', 'remote.origin.url'], + cwd=repo_path, + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode == 0: + url = result.stdout.strip() + return LocalRepositoryScanner.parse_git_url(url) + except Exception: + pass + + return None + + @staticmethod + def parse_git_url(url: str) -> Optional[str]: + """Parse Git URL to extract owner/repo format""" + try: + # Handle GitHub URLs + if 'github.com' in url: + # Handle both HTTPS and SSH URLs + if url.startswith('git@'): + # SSH: git@github.com:owner/repo.git + parts = url.split(':')[-1].replace('.git', '') + return parts + else: + # HTTPS: https://github.com/owner/repo.git + parsed = urlparse(url) + path = parsed.path.strip('/').replace('.git', '') + return path + except: + pass + + return None + + @staticmethod + def clone_repository(repo_url: str, local_path: str, repo_name: str) -> bool: + """Clone a repository to local path""" + try: + target_path = os.path.join(local_path, repo_name.split('/')[-1]) + + if os.path.exists(target_path): + print(f"Repository already exists at {target_path}") + return True + + os.makedirs(local_path, exist_ok=True) + + result = subprocess.run( + ['git', 'clone', repo_url, target_path], + capture_output=True, + text=True, + timeout=300 # 5 minutes timeout + ) + + if result.returncode == 0: + print(f"Successfully cloned {repo_url} to {target_path}") + return True + else: + print(f"Failed to clone repository: {result.stderr}") + return False + + except Exception as e: + print(f"Error cloning repository: {e}") + return False + + +class ConfigurationHelpers: + """Configuration and validation utilities""" + + @staticmethod + def validate_ai_provider_setup(config: Dict[str, Any], parent_window=None) -> bool: + """Validate AI provider setup and offer to install missing modules + + Args: + config: Configuration dictionary + parent_window: Parent tkinter window for dialogs + + Returns: + bool: True if setup is valid or user handled the issue + """ + ai_provider = config.get('AI_PROVIDER', '').lower() + + if not ai_provider or ai_provider == 'none': + return True # No AI provider selected, nothing to validate + + try: + # Try to import AI manager for validation + from .ai_manager import AIManager + ai_manager = AIManager() + + # Check if modules are available + available, missing = ai_manager.check_ai_module_availability(ai_provider) + + if available: + return True # All modules available + + print(f"āš ļø AI Provider '{ai_provider}' selected but missing required packages: {', '.join(missing)}") + + # Offer to install missing packages + success = ai_manager.install_ai_packages(missing, parent_window) + + if success: + # Re-check availability after installation + available, still_missing = ai_manager.check_ai_module_availability(ai_provider) + if available: + print(f"āœ… AI Provider '{ai_provider}' is now ready to use") + return True + else: + print(f"āš ļø Some packages may still be missing: {', '.join(still_missing)}") + print("Please restart the application after installation completes") + return False + + return False + + except ImportError: + # AI manager not available, skip validation + return True + + @staticmethod + def create_default_env_file() -> bool: + """Create a default .env file with all settings blank""" + try: + default_config = """# GitHub Pulse Configuration +# Generated automatically - fill in your values +# IMPORTANT: Do NOT commit this file to source control. Add it to .gitignore. + +# GitHub Configuration +GITHUB_PAT= +GITHUB_REPO= +FORKED_REPO= + +# Application Settings +DRY_RUN=false + +# AI Provider Configuration (for local PR creation with AI assistance) +AI_PROVIDER= +CLAUDE_API_KEY= +OPENAI_API_KEY= +GITHUB_TOKEN= +LOCAL_REPO_PATH= + +# Custom AI Instructions (optional) +CUSTOM_INSTRUCTIONS= +""" + with open('.env', 'w', encoding='utf-8') as f: + f.write(default_config) + + print("Created default .env file with blank values") + return True + + except Exception as e: + print(f"Error creating default .env file: {e}") + return False + +# Compatibility functions for direct function access +def get_next_pr_number(provider_key: str) -> int: + """Compatibility function for direct access to PR number generation""" + return PRNumberManager.get_next_pr_number(provider_key) + + +def validate_ai_provider_setup(config: Dict[str, Any], parent_window=None) -> bool: + """Compatibility function for direct access to AI provider validation""" + return ConfigurationHelpers.validate_ai_provider_setup(config, parent_window) + + +def create_default_env_file() -> bool: + """Compatibility function for direct access to .env file creation""" + return ConfigurationHelpers.create_default_env_file() \ No newline at end of file diff --git a/src/app_components/workflow.py b/src/app_components/workflow.py new file mode 100644 index 0000000..56eee04 --- /dev/null +++ b/src/app_components/workflow.py @@ -0,0 +1,653 @@ +""" +Workflow Manager +Manages GitHub workflow items (Issues and Pull Requests) from target and fork repositories +""" + +import requests +from typing import List, Dict, Any, Optional, Tuple + + +class WorkflowItem: + """Represents a GitHub workflow item (Issue or PR)""" + + def __init__(self, item_type: str, data: Dict[str, Any], repo_source: str): + """ + Initialize a workflow item + + Args: + item_type: 'issue' or 'pull_request' + data: Raw data from GitHub API + repo_source: 'target' or 'fork' + """ + self.item_type = item_type + self.repo_source = repo_source + self.data = data + + # Extract common fields + self.number = data.get('number') + self.title = data.get('title', 'No Title') + self.state = data.get('state', 'unknown') + self.created_at = data.get('created_at', '') + self.updated_at = data.get('updated_at', '') + self.body = data.get('body', '') + self.url = data.get('html_url', '') + self.api_url = data.get('url', '') + + # Author information + user = data.get('user', {}) + self.author = user.get('login', 'unknown') if user else 'unknown' + self.author_url = user.get('html_url', '') if user else '' + + # Labels + self.labels = [label.get('name', '') for label in data.get('labels', [])] + + # Assignees + assignees = data.get('assignees', []) + self.assignees = [a.get('login', '') for a in assignees if a] + + # PR-specific fields + if item_type == 'pull_request': + self.is_draft = data.get('draft', False) + self.mergeable_state = data.get('mergeable_state', 'unknown') + self.merged = data.get('merged', False) + self.base_ref = data.get('base', {}).get('ref', '') + self.head_ref = data.get('head', {}).get('ref', '') + else: + self.is_draft = False + self.mergeable_state = None + self.merged = False + self.base_ref = None + self.head_ref = None + + # Comments count + self.comments_count = data.get('comments', 0) + + def __repr__(self): + return f"" + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary for easy serialization""" + return { + 'item_type': self.item_type, + 'repo_source': self.repo_source, + 'data': self.data, # Include raw data for full reconstruction + 'number': self.number, + 'title': self.title, + 'state': self.state, + 'created_at': self.created_at, + 'updated_at': self.updated_at, + 'body': self.body, + 'url': self.url, + 'api_url': self.api_url, + 'author': self.author, + 'author_url': self.author_url, + 'labels': self.labels, + 'assignees': self.assignees, + 'is_draft': self.is_draft, + 'mergeable_state': self.mergeable_state, + 'merged': self.merged, + 'base_ref': self.base_ref, + 'head_ref': self.head_ref, + 'comments_count': self.comments_count + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'WorkflowItem': + """Create WorkflowItem from dictionary (for cache deserialization)""" + # Extract the raw GitHub API data if available, otherwise use the dict itself + raw_data = data.get('data', data) + item_type = data.get('item_type', 'issue') + repo_source = data.get('repo_source', 'target') + + return cls(item_type, raw_data, repo_source) + + +class GitHubRepoFetcher: + """Fetches repository information from GitHub""" + + def __init__(self, github_token: str, logger=None): + """ + Initialize the repo fetcher + + Args: + github_token: GitHub Personal Access Token + logger: Optional logger instance + """ + self.token = github_token + self.logger = logger + self.headers = { + "Authorization": f"Bearer {github_token}", + "Accept": "application/vnd.github+json", + "User-Agent": "github-automation-tool/1.0" + } + + def log(self, message: str): + """Log a message""" + if self.logger: + self.logger.log(message) + else: + print(message) + + def get_authenticated_user(self) -> Optional[Dict[str, Any]]: + """ + Get information about the authenticated user + + Returns: + Dictionary with user information or None if error + """ + try: + url = "https://api.github.com/user" + response = requests.get(url, headers=self.headers, timeout=30) + response.raise_for_status() + return response.json() + except Exception as e: + self.log(f"āŒ Error fetching authenticated user: {str(e)}") + return None + + def fetch_user_repos(self, repo_type: str = 'owner', per_page: int = 100) -> List[Dict[str, Any]]: + """ + Fetch repositories for the authenticated user + + Args: + repo_type: 'owner', 'member', or 'all' + per_page: Number of repos per page (max 100) + + Returns: + List of repository dictionaries + """ + try: + url = "https://api.github.com/user/repos" + params = { + 'type': repo_type, + 'per_page': min(per_page, 100), + 'sort': 'updated', + 'direction': 'desc' + } + + response = requests.get(url, headers=self.headers, params=params, timeout=30) + response.raise_for_status() + + repos = response.json() + self.log(f"āœ… Found {len(repos)} repositories ({repo_type})") + return repos + + except Exception as e: + self.log(f"āŒ Error fetching user repos: {str(e)}") + return [] + + def fetch_repos_with_permissions(self, min_permission: str = 'push') -> List[Dict[str, Any]]: + """ + Fetch repositories where user has specific permissions + + Args: + min_permission: Minimum permission level ('pull', 'push', 'admin') + + Returns: + List of repository dictionaries with sufficient permissions + """ + try: + # Fetch all repos user has access to + all_repos = self.fetch_user_repos(repo_type='all') + + # Filter by permission level + filtered_repos = [] + permission_levels = {'pull': 0, 'push': 1, 'admin': 2} + min_level = permission_levels.get(min_permission, 1) + + for repo in all_repos: + permissions = repo.get('permissions', {}) + + # Check permission level + if permissions.get('admin'): + level = 2 + elif permissions.get('push'): + level = 1 + elif permissions.get('pull'): + level = 0 + else: + level = -1 + + if level >= min_level: + filtered_repos.append(repo) + + self.log(f"āœ… Found {len(filtered_repos)} repos with '{min_permission}' permission or higher") + return filtered_repos + + except Exception as e: + self.log(f"āŒ Error fetching repos with permissions: {str(e)}") + return [] + + def search_repositories(self, query: str, per_page: int = 30) -> List[Dict[str, Any]]: + """ + Search for repositories on GitHub + + Args: + query: Search query string + per_page: Number of results per page (max 100) + + Returns: + List of repository dictionaries + """ + if not query or not query.strip(): + return [] + + try: + url = "https://api.github.com/search/repositories" + params = { + 'q': query.strip(), + 'per_page': min(per_page, 100), + 'sort': 'updated', + 'order': 'desc' + } + + response = requests.get(url, headers=self.headers, params=params, timeout=30) + response.raise_for_status() + + data = response.json() + repos = data.get('items', []) + total_count = data.get('total_count', 0) + + self.log(f"āœ… Search found {total_count} repositories (showing {len(repos)})") + return repos + + except Exception as e: + self.log(f"āŒ Error searching repositories: {str(e)}") + return [] + + def get_repo_names(self, repos: List[Dict[str, Any]]) -> List[str]: + """ + Extract repository names in 'owner/repo' format + + Args: + repos: List of repository dictionaries + + Returns: + List of repository name strings + """ + return [repo.get('full_name', '') for repo in repos if repo.get('full_name')] + + +class WorkflowManager: + """Manages workflow items from GitHub repositories""" + + def __init__(self, github_token: str, logger=None): + """ + Initialize the workflow manager + + Args: + github_token: GitHub Personal Access Token + logger: Optional logger instance + """ + self.token = github_token + self.logger = logger + self.headers = { + "Authorization": f"Bearer {github_token}", + "Accept": "application/vnd.github+json", + "User-Agent": "github-automation-tool/1.0" + } + # Initialize repo fetcher + self.repo_fetcher = GitHubRepoFetcher(github_token, logger) + + def log(self, message: str): + """Log a message""" + if self.logger: + self.logger.log(message) + else: + print(message) + + def _parse_repo(self, repo_str: str) -> Optional[Tuple[str, str]]: + """ + Parse a repository string into owner and name + + Args: + repo_str: Repository string in format "owner/repo" + + Returns: + Tuple of (owner, repo) or None if invalid + """ + if not repo_str or '/' not in repo_str: + return None + + parts = repo_str.strip().split('/') + if len(parts) != 2: + return None + + return parts[0], parts[1] + + def fetch_issues(self, repo_str: str, repo_source: str = 'target', + state: str = 'all', per_page: int = 100) -> List[WorkflowItem]: + """ + Fetch issues from a repository + + Args: + repo_str: Repository string in format "owner/repo" + repo_source: 'target' or 'fork' to identify source + state: 'open', 'closed', or 'all' + per_page: Number of items per page (max 100) + + Returns: + List of WorkflowItem objects + """ + parsed = self._parse_repo(repo_str) + if not parsed: + self.log(f"L Invalid repository format: {repo_str}") + return [] + + owner, repo = parsed + self.log(f"Fetching issues from {owner}/{repo} ({repo_source})...") + + try: + url = f"https://api.github.com/repos/{owner}/{repo}/issues" + params = { + 'state': state, + 'per_page': min(per_page, 100), + 'sort': 'updated', + 'direction': 'desc' + } + + response = requests.get(url, headers=self.headers, params=params, timeout=30) + response.raise_for_status() + + items_data = response.json() + + # Filter out pull requests (GitHub's issues endpoint includes PRs) + issues_data = [item for item in items_data if 'pull_request' not in item] + + issues = [WorkflowItem('issue', data, repo_source) for data in issues_data] + + self.log(f" Found {len(issues)} issues in {owner}/{repo}") + return issues + + except requests.HTTPError as e: + self.log(f"L HTTP Error fetching issues from {owner}/{repo}: {e}") + if e.response.status_code == 401: + self.log(" Check your GitHub Personal Access Token") + elif e.response.status_code == 404: + self.log(" Repository not found or no access") + return [] + except Exception as e: + self.log(f"L Error fetching issues from {owner}/{repo}: {str(e)}") + return [] + + def fetch_pull_requests(self, repo_str: str, repo_source: str = 'target', + state: str = 'all', per_page: int = 100) -> List[WorkflowItem]: + """ + Fetch pull requests from a repository + + Args: + repo_str: Repository string in format "owner/repo" + repo_source: 'target' or 'fork' to identify source + state: 'open', 'closed', or 'all' + per_page: Number of items per page (max 100) + + Returns: + List of WorkflowItem objects + """ + parsed = self._parse_repo(repo_str) + if not parsed: + self.log(f"L Invalid repository format: {repo_str}") + return [] + + owner, repo = parsed + self.log(f"Fetching pull requests from {owner}/{repo} ({repo_source})...") + + try: + url = f"https://api.github.com/repos/{owner}/{repo}/pulls" + params = { + 'state': state, + 'per_page': min(per_page, 100), + 'sort': 'updated', + 'direction': 'desc' + } + + response = requests.get(url, headers=self.headers, params=params, timeout=30) + response.raise_for_status() + + prs_data = response.json() + prs = [WorkflowItem('pull_request', data, repo_source) for data in prs_data] + + self.log(f" Found {len(prs)} pull requests in {owner}/{repo}") + return prs + + except requests.HTTPError as e: + self.log(f"L HTTP Error fetching PRs from {owner}/{repo}: {e}") + if e.response.status_code == 401: + self.log(" Check your GitHub Personal Access Token") + elif e.response.status_code == 404: + self.log(" Repository not found or no access") + return [] + except Exception as e: + self.log(f"L Error fetching PRs from {owner}/{repo}: {str(e)}") + return [] + + def fetch_all_workflow_items(self, target_repo: str, fork_repo: str = None, + include_issues: bool = True, + include_prs: bool = True, + state: str = 'all') -> Dict[str, List[WorkflowItem]]: + """ + Fetch all workflow items from both target and fork repositories + + Args: + target_repo: Target repository string "owner/repo" + fork_repo: Fork repository string "owner/repo" (optional) + include_issues: Whether to fetch issues + include_prs: Whether to fetch pull requests + state: 'open', 'closed', or 'all' + + Returns: + Dictionary with keys 'target_issues', 'target_prs', 'fork_issues', 'fork_prs' + """ + results = { + 'target_issues': [], + 'target_prs': [], + 'fork_issues': [], + 'fork_prs': [] + } + + # Fetch from target repository + if target_repo: + if include_issues: + results['target_issues'] = self.fetch_issues(target_repo, 'target', state) + if include_prs: + results['target_prs'] = self.fetch_pull_requests(target_repo, 'target', state) + + # Fetch from fork repository + if fork_repo: + if include_issues: + results['fork_issues'] = self.fetch_issues(fork_repo, 'fork', state) + if include_prs: + results['fork_prs'] = self.fetch_pull_requests(fork_repo, 'fork', state) + + # Log summary + total = sum(len(items) for items in results.values()) + self.log(f"\n=ļæ½ Summary: Fetched {total} total items") + self.log(f" Target Issues: {len(results['target_issues'])}") + self.log(f" Target PRs: {len(results['target_prs'])}") + if fork_repo: + self.log(f" Fork Issues: {len(results['fork_issues'])}") + self.log(f" Fork PRs: {len(results['fork_prs'])}") + + return results + + def get_combined_items(self, workflow_items: Dict[str, List[WorkflowItem]], + sort_by: str = 'updated') -> List[WorkflowItem]: + """ + Combine and sort all workflow items + + Args: + workflow_items: Dictionary from fetch_all_workflow_items() + sort_by: 'updated', 'created', or 'number' + + Returns: + Sorted list of all workflow items + """ + all_items = [] + for items_list in workflow_items.values(): + all_items.extend(items_list) + + # Sort items + if sort_by == 'updated': + all_items.sort(key=lambda x: x.updated_at, reverse=True) + elif sort_by == 'created': + all_items.sort(key=lambda x: x.created_at, reverse=True) + elif sort_by == 'number': + all_items.sort(key=lambda x: x.number, reverse=True) + + return all_items + + def filter_items(self, items: List[WorkflowItem], **filters) -> List[WorkflowItem]: + """ + Filter workflow items based on criteria + + Args: + items: List of WorkflowItem objects + **filters: Filter criteria (state, item_type, repo_source, author, labels) + + Returns: + Filtered list of items + """ + filtered = items + + if 'state' in filters and filters['state']: + filtered = [item for item in filtered if item.state == filters['state']] + + if 'item_type' in filters and filters['item_type']: + filtered = [item for item in filtered if item.item_type == filters['item_type']] + + if 'repo_source' in filters and filters['repo_source']: + filtered = [item for item in filtered if item.repo_source == filters['repo_source']] + + if 'author' in filters and filters['author']: + filtered = [item for item in filtered if item.author == filters['author']] + + if 'labels' in filters and filters['labels']: + label_filter = filters['labels'] + if isinstance(label_filter, str): + label_filter = [label_filter] + filtered = [item for item in filtered + if any(label in item.labels for label in label_filter)] + + return filtered + + def fetch_comments(self, repo_str: str, issue_number: int, is_pull_request: bool = False) -> List[Dict[str, Any]]: + """ + Fetch comments for an issue or pull request + + Args: + repo_str: Repository string in format "owner/repo" + issue_number: Issue or PR number + is_pull_request: Whether this is a pull request (for PR-specific comments) + + Returns: + List of comment dictionaries with keys: 'user', 'body', 'created_at', 'updated_at' + """ + try: + # Parse repository string + if '/' not in repo_str: + self.log(f"Invalid repository format: {repo_str}") + return [] + + owner, repo = repo_str.split('/', 1) + + # Fetch issue/PR comments (these are the same endpoint for both issues and PRs) + url = f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}/comments" + print(f"DEBUG: Fetching comments from URL: {url}", flush=True) + + response = requests.get(url, headers=self.headers) + print(f"DEBUG: Response status code: {response.status_code}", flush=True) + print(f"DEBUG: Response headers: {dict(response.headers)}", flush=True) + print(f"DEBUG: Response text length: {len(response.text)}", flush=True) + print(f"DEBUG: Response content (first 500): {response.text[:500]}", flush=True) + + response.raise_for_status() + + response_data = response.json() + print(f"DEBUG: Response data type: {type(response_data)}", flush=True) + print(f"DEBUG: Number of items: {len(response_data) if isinstance(response_data, list) else 'Not a list'}", flush=True) + + if isinstance(response_data, list) and len(response_data) > 0: + print(f"DEBUG: First item keys: {list(response_data[0].keys())}", flush=True) + + comments = [] + for comment_data in response_data: + comments.append({ + 'user': comment_data.get('user', {}).get('login', 'unknown'), + 'body': comment_data.get('body', ''), + 'created_at': comment_data.get('created_at', ''), + 'updated_at': comment_data.get('updated_at', ''), + 'url': comment_data.get('html_url', '') + }) + + self.log(f"Fetched {len(comments)} comments for {repo_str} #{issue_number}") + print(f"DEBUG: Successfully parsed {len(comments)} comments", flush=True) + return comments + + except requests.exceptions.RequestException as e: + self.log(f"Error fetching comments for {repo_str} #{issue_number}: {e}") + print(f"DEBUG: RequestException occurred: {e}", flush=True) + import traceback + traceback.print_exc() + return [] + except Exception as e: + self.log(f"Unexpected error fetching comments: {e}") + print(f"DEBUG: Exception occurred: {e}", flush=True) + import traceback + traceback.print_exc() + return [] + + def fetch_pr_files(self, repo_str: str, pr_number: int) -> List[Dict[str, Any]]: + """ + Fetch the list of files changed in a pull request + + Args: + repo_str: Repository string in format "owner/repo" + pr_number: Pull request number + + Returns: + List of file dictionaries with keys: 'filename', 'status', 'additions', 'deletions', 'changes', 'patch' + """ + try: + # Parse repository string + if '/' not in repo_str: + self.log(f"Invalid repository format: {repo_str}") + return [] + + owner, repo = repo_str.split('/', 1) + + # Fetch PR files + url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/files" + print(f"DEBUG: Fetching PR files from URL: {url}", flush=True) + + response = requests.get(url, headers=self.headers) + response.raise_for_status() + + files_data = response.json() + print(f"DEBUG: Found {len(files_data)} files in PR #{pr_number}", flush=True) + + files = [] + for file_data in files_data: + files.append({ + 'filename': file_data.get('filename', ''), + 'status': file_data.get('status', ''), # added, removed, modified, renamed + 'additions': file_data.get('additions', 0), + 'deletions': file_data.get('deletions', 0), + 'changes': file_data.get('changes', 0), + 'patch': file_data.get('patch', ''), # The actual diff patch + 'blob_url': file_data.get('blob_url', ''), + }) + + self.log(f"Fetched {len(files)} files for PR {repo_str} #{pr_number}") + return files + + except requests.exceptions.RequestException as e: + self.log(f"Error fetching PR files for {repo_str} #{pr_number}: {e}") + print(f"DEBUG: RequestException occurred: {e}", flush=True) + import traceback + traceback.print_exc() + return [] + except Exception as e: + self.log(f"Unexpected error fetching PR files: {e}") + print(f"DEBUG: Exception occurred: {e}", flush=True) + import traceback + traceback.print_exc() + return [] diff --git a/src/assets/icon.png b/src/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6159727486ebd2d374db0aab7f9e2fadfa0c9a8d GIT binary patch literal 25961 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 h{@)(|_i(s=2.8.0 # For ChatGPT integration +anthropic>=0.72.1 # For Claude integration + +# Note: Ollama and GitHub Copilot use REST APIs and don't require additional packages diff --git a/src/requirements/requirements-base.txt b/src/requirements/requirements-base.txt new file mode 100644 index 0000000..2b0d341 --- /dev/null +++ b/src/requirements/requirements-base.txt @@ -0,0 +1,14 @@ +# Base requirements for all platforms +# Core dependencies required for the application to run + +# UI Framework (Flet - Python wrapper for Flutter) +flet==0.28.3 + +# HTTP requests for API calls +requests>=2.32.5 + +# Secure credential storage (cross-platform) +keyring>=25.6.0 + +# Git operations for repository management +GitPython>=3.1.45 diff --git a/src/requirements/requirements-dev.txt b/src/requirements/requirements-dev.txt new file mode 100644 index 0000000..d9cde79 --- /dev/null +++ b/src/requirements/requirements-dev.txt @@ -0,0 +1,12 @@ +# Development requirements +# Includes all dependencies for development, testing, and building + +# Include AI requirements (which includes base) +-r requirements-ai.txt + +# Development tools +flet[all]==0.28.3 # Flet with all development extras + +# Build tools (optional) +# pyinstaller>=6.0.0 # Alternative bundler if needed +# cx-Freeze>=6.15.0 # Alternative bundler if needed diff --git a/src/requirements/requirements.txt b/src/requirements/requirements.txt new file mode 100644 index 0000000..5100b0d --- /dev/null +++ b/src/requirements/requirements.txt @@ -0,0 +1,23 @@ +# GitHub Pulse - Main Requirements File +# This file includes all dependencies needed for production use + +# Core dependencies +requests>=2.32.5 # HTTP client for GitHub API and AI providers +keyring>=25.6.0 # Secure credential storage (cross-platform) +GitPython>=3.1.45 # Git operations for repository management + +# UI Framework - Flet (Python wrapper for Flutter) +flet==0.28.3 # Pin to specific version for build compatibility + +# AI Providers (optional but included for full functionality) +openai>=2.8.0 # ChatGPT/OpenAI API integration +anthropic>=0.72.1 # Claude/Anthropic API integration + +# Platform-specific notes: +# - Linux: Requires libgtk-3-dev, clang, cmake, ninja-build +# - Windows: Requires Visual Studio 2016+ +# - Android: Requires Android Studio and SDK +# - iOS/macOS: Requires Xcode + +# For minimal installation (no AI), use: pip install -r requirements-base.txt +# For development: pip install -r requirements-dev.txt \ No newline at end of file