""" Settings Dialog GUI for configuring application settings """ import tkinter as tk import threading from tkinter import ttk, messagebox, scrolledtext from typing import Dict, Any, Optional import sys import os class SettingsDialog: """Settings configuration dialog""" def __init__(self, parent, config: Dict[str, Any], config_manager=None, cache_manager=None): self.parent = parent self.config = config.copy() self.config_manager = config_manager self.cache_manager = cache_manager self.result = None self.entries = {} # Create dialog self.dialog = tk.Toplevel(parent) self.dialog.title("āš™ļø Settings") self.dialog.geometry("900x1000") self.dialog.resizable(True, True) self.dialog.transient(parent) self.dialog.grab_set() self._create_widgets() self._bind_events() def _create_widgets(self): """Create dialog widgets""" # Main frame with scrollbar main_frame = ttk.Frame(self.dialog, padding="20") main_frame.pack(fill=tk.BOTH, expand=True) # Create notebook for tabbed settings notebook = ttk.Notebook(main_frame) notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 20)) # Create tabs self._create_general_tab(notebook) self._create_ai_tab(notebook) self._create_dataverse_tab(notebook) # Buttons frame buttons_frame = ttk.Frame(main_frame) buttons_frame.pack(fill=tk.X, pady=(10, 0)) # Buttons ttk.Button(buttons_frame, text="šŸ’¾ Save Settings", command=self._save_clicked).pack(side=tk.RIGHT, padx=(5, 0)) ttk.Button(buttons_frame, text="āŒ Cancel", command=self._cancel_clicked).pack(side=tk.RIGHT) ttk.Button(buttons_frame, text="šŸ—‘ļø Clear Cache", command=self._clear_cache).pack(side=tk.LEFT, padx=(5, 0)) ttk.Button(buttons_frame, text="Test Connection", command=self._test_connection).pack(side=tk.LEFT) # Center dialog after everything is created self._center_dialog() def _create_general_tab(self, notebook): """Create general settings tab""" general_frame = ttk.Frame(notebook) notebook.add(general_frame, text="General") # Scrollable frame canvas = tk.Canvas(general_frame) scrollbar = ttk.Scrollbar(general_frame, orient="vertical", command=canvas.yview) scrollable_frame = ttk.Frame(canvas) scrollable_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) # Configure column weights for proper expansion scrollable_frame.columnconfigure(1, weight=1) current_row = 0 # Azure DevOps section self._create_section_header(scrollable_frame, current_row, "šŸ”· Azure DevOps Configuration") current_row += 1 self._create_label_entry(scrollable_frame, current_row, "Query URL:", 'AZURE_DEVOPS_QUERY', width=60, multiline=True) current_row += 1 self._create_label_entry(scrollable_frame, current_row, "Personal Access Token:", 'AZURE_DEVOPS_PAT', password=True, width=60) current_row += 1 # GitHub section self._create_section_header(scrollable_frame, current_row, "šŸ™ GitHub Configuration") current_row += 1 self._create_label_entry(scrollable_frame, current_row, "Personal Access Token:", 'GITHUB_PAT', password=True, width=60) current_row += 1 self._create_label_entry(scrollable_frame, current_row, "Target Repository (owner/repo):", 'GITHUB_REPO', width=60) current_row += 1 self._create_forked_repo_dropdown(scrollable_frame, current_row) current_row += 1 # General options section self._create_section_header(scrollable_frame, current_row, "āš™ļø General Options") current_row += 1 self._create_dry_run_checkbox(scrollable_frame, current_row) current_row += 1 self._create_label_entry(scrollable_frame, current_row, "Local Repo Path:", 'LOCAL_REPO_PATH', width=60) current_row += 1 # Detected repos dropdown ttk.Label(scrollable_frame, text="Detected Repos:", font=('Arial', 10, 'bold')).grid( row=current_row, column=0, sticky=tk.W, pady=5, padx=10) # Frame for dropdown and refresh button detected_frame = ttk.Frame(scrollable_frame) detected_frame.grid(row=current_row, column=1, sticky=(tk.W, tk.E), pady=5, padx=10) self.detected_repos_var = tk.StringVar(value='Scanning...') self.detected_repos_dropdown = ttk.Combobox(detected_frame, textvariable=self.detected_repos_var, state='readonly', width=45) self.detected_repos_dropdown.pack(side=tk.LEFT, fill=tk.X, expand=True) self.detected_repos_dropdown.bind('<>', self._on_repo_selected) refresh_button = ttk.Button(detected_frame, text="šŸ”„ Scan", command=self._scan_repos, width=8) refresh_button.pack(side=tk.LEFT, padx=(5, 0)) current_row += 1 # Help text for local repo path repo_help = ttk.Label(scrollable_frame, 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" " • Target Repository: Upstream repo for PRs (e.g., microsoft/repo)\n" " • Fork Workflow: Work on your fork locally, create PRs to upstream", font=('Arial', 9), foreground='gray', justify=tk.LEFT, wraplength=850) repo_help.grid(row=current_row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 20), padx=10) current_row += 1 # Help text help_text = ttk.Label(scrollable_frame, text="šŸ’” Getting Started:\n" "1. Set your Azure DevOps Query URL (copy from browser)\n" "2. Create Personal Access Tokens for both services\n" "3. Configure GitHub repositories:\n" " • Target Repository: Where PRs will be created\n" " • Forked Repository: Your fork where changes are made\n" "4. Set Local Repo Path for automatic repository detection\n" "5. Configure AI provider in the AI tab (optional)\n" "6. Test your connection before processing items", font=('Arial', 9), foreground='blue', justify=tk.LEFT, wraplength=850) help_text.grid(row=current_row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(20, 30), padx=10) # Scan for repos after creating the UI self.dialog.after(100, self._scan_repos) # Pack canvas and scrollbar canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") def _create_ai_tab(self, notebook): """Create AI settings tab""" ai_frame = ttk.Frame(notebook) notebook.add(ai_frame, text="AI Providers") # Scrollable frame canvas = tk.Canvas(ai_frame) scrollbar = ttk.Scrollbar(ai_frame, orient="vertical", command=canvas.yview) scrollable_frame = ttk.Frame(canvas) scrollable_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) # AI Provider section self._create_section_header(scrollable_frame, 0, "šŸ¤– AI Provider Configuration") # Provider dropdown ttk.Label(scrollable_frame, text="AI Provider:", font=('Arial', 10, 'bold')).grid( row=1, column=0, sticky=tk.W, pady=5, padx=10) self.ai_provider_var = tk.StringVar(value=self.config.get('AI_PROVIDER', 'none')) provider_dropdown = ttk.Combobox(scrollable_frame, textvariable=self.ai_provider_var, values=['none', 'claude', 'chatgpt', 'github-copilot'], state='readonly', width=47) provider_dropdown.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5, padx=10) self.entries['AI_PROVIDER'] = self.ai_provider_var # API Keys self._create_label_entry(scrollable_frame, 2, "Claude API Key:", 'CLAUDE_API_KEY', password=True) self._create_label_entry(scrollable_frame, 3, "ChatGPT API Key:", 'OPENAI_API_KEY', password=True) self._create_label_entry(scrollable_frame, 4, "GitHub Token (for Copilot) [defaults to GitHub PAT]:", 'GITHUB_TOKEN', password=True) # Help text help_text = ttk.Label(scrollable_frame, text="\nšŸ’” 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" "• Cost: ~$0.01-0.05 per PR with AI, free with 'none'\n" "• AI providers clone repos locally to make changes before pushing", font=('Arial', 9), foreground='blue', justify=tk.LEFT, wraplength=800) help_text.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=20, padx=10) # Pack canvas and scrollbar canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") def _create_dataverse_tab(self, notebook): """Create Dataverse/PowerApp settings tab""" dataverse_frame = ttk.Frame(notebook) notebook.add(dataverse_frame, text="UUF/Dataverse") # Scrollable frame canvas = tk.Canvas(dataverse_frame) scrollbar = ttk.Scrollbar(dataverse_frame, orient="vertical", command=canvas.yview) scrollable_frame = ttk.Frame(canvas) scrollable_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) # Dataverse section self._create_section_header(scrollable_frame, 0, "šŸ“Š PowerApp/Dataverse Configuration") self._create_label_entry(scrollable_frame, 1, "Environment URL:", 'DATAVERSE_ENVIRONMENT_URL', width=60, multiline=True) self._create_label_entry(scrollable_frame, 2, "Table Name:", 'DATAVERSE_TABLE_NAME') # Azure AD section self._create_section_header(scrollable_frame, 3, "šŸ” Azure AD Configuration") self._create_label_entry(scrollable_frame, 4, "Client ID:", 'AZURE_AD_CLIENT_ID', width=60) self._create_label_entry(scrollable_frame, 5, "Client Secret:", 'AZURE_AD_CLIENT_SECRET', password=True, width=60) self._create_label_entry(scrollable_frame, 6, "Tenant ID:", 'AZURE_AD_TENANT_ID', width=60) # Help text help_text = ttk.Label(scrollable_frame, text="\nšŸ’” UUF Integration:\n" "• This section is only needed if you want to fetch UUF items\n" "• UUF items are processed differently than Azure DevOps work items\n" "• Environment URL: Your Dataverse environment\n" "• Azure AD app must have appropriate permissions\n" "• Contact your PowerApp administrator for these values\n" "• Leave blank if not using UUF integration", font=('Arial', 9), foreground='blue', justify=tk.LEFT, wraplength=800) help_text.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=20, padx=10) # Pack canvas and scrollbar canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") def _create_section_header(self, parent, row: int, text: str): """Create a section header""" header_frame = ttk.Frame(parent) header_frame.grid(row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(20, 10), padx=10) header_frame.columnconfigure(1, weight=1) ttk.Label(header_frame, text=text, font=('Arial', 12, 'bold')).grid(row=0, column=0, sticky=tk.W) ttk.Separator(header_frame, orient='horizontal').grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(10, 0)) def _create_label_entry(self, parent, row: int, label_text: str, config_key: str, password: bool = False, width: int = 50, multiline: bool = False): """Create a label and entry pair""" ttk.Label(parent, text=label_text, font=('Arial', 10, 'bold')).grid( row=row, column=0, sticky=tk.W, pady=5, padx=10) if multiline: entry = scrolledtext.ScrolledText(parent, height=3, width=width) entry.grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=10) entry.insert('1.0', self.config.get(config_key, '') or '') elif password: entry = ttk.Entry(parent, show="*", width=width) entry.grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=10) # Special handling for GITHUB_TOKEN - show placeholder if using default if config_key == 'GITHUB_TOKEN': github_token = self.config.get('GITHUB_TOKEN', '').strip() github_pat = self.config.get('GITHUB_PAT', '').strip() if not github_token and github_pat: # Show placeholder for defaulted value, but don't actually set it entry.config(foreground='gray') entry.insert(0, '(using GitHub PAT)') # Add event handlers to clear placeholder on focus def on_focus_in(event): if entry.get() == '(using GitHub PAT)': entry.delete(0, tk.END) entry.config(foreground='black') def on_focus_out(event): if not entry.get(): entry.config(foreground='gray') entry.insert(0, '(using GitHub PAT)') entry.bind('', on_focus_in) entry.bind('', on_focus_out) else: entry.insert(0, github_token) else: entry.insert(0, self.config.get(config_key, '') or '') else: entry = ttk.Entry(parent, width=width) entry.grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=10) entry.insert(0, self.config.get(config_key, '') or '') self.entries[config_key] = entry parent.columnconfigure(1, weight=1) def _create_forked_repo_dropdown(self, parent, row: int): """Create forked repository dropdown with local repo detection""" ttk.Label(parent, text="Forked Repository:", font=('Arial', 10, 'bold')).grid( row=row, column=0, sticky=tk.W, pady=5, padx=10) # Frame for dropdown and refresh button dropdown_frame = ttk.Frame(parent) dropdown_frame.grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5, padx=10) dropdown_frame.columnconfigure(0, weight=1) # Initial options repo_options = [''] # Empty option # Add local repositories local_repo_path = self.config.get('LOCAL_REPO_PATH', '') if local_repo_path: try: from .utils import LocalRepositoryScanner local_repos = LocalRepositoryScanner.scan_local_repos(local_repo_path) if local_repos: repo_options.append('--- Local Repositories ---') repo_options.extend(local_repos) except Exception as e: print(f"Error scanning local repos: {e}") # Placeholder for user's forks (will be populated asynchronously) self.forked_repos = [] self.forked_repo_var = tk.StringVar(value=self.config.get('FORKED_REPO', '')) self.forked_repo_dropdown = ttk.Combobox(dropdown_frame, textvariable=self.forked_repo_var, values=repo_options, width=50) self.forked_repo_dropdown.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5)) self.entries['FORKED_REPO'] = self.forked_repo_var # Refresh button refresh_btn = ttk.Button(dropdown_frame, text="šŸ”„", width=3, command=self._refresh_forked_repos) refresh_btn.grid(row=0, column=1) # Help text for forked repo help_label = ttk.Label(parent, text=" ā„¹ļø Your fork where changes will be made. Leave empty to auto-detect from document URL.", font=('Arial', 9), foreground='gray') help_label.grid(row=row+1, column=0, columnspan=3, sticky=tk.W, padx=10) # Start async loading of user's forks self.dialog.after(100, self._load_user_forks_async) def _refresh_forked_repos(self): """Refresh the forked repositories dropdown""" self._load_user_forks_async() # Also refresh local repos local_repo_path = self.config.get('LOCAL_REPO_PATH', '') if local_repo_path: try: from .utils import LocalRepositoryScanner local_repos = LocalRepositoryScanner.scan_local_repos(local_repo_path) # Update dropdown with current values plus refreshed local repos current_values = list(self.forked_repo_dropdown['values']) # Remove old local repos section if '--- Local Repositories ---' in current_values: start_idx = current_values.index('--- Local Repositories ---') # Find where GitHub repos start or end of list end_idx = len(current_values) for i in range(start_idx + 1, len(current_values)): if current_values[i].startswith('--- ') and 'GitHub' in current_values[i]: end_idx = i break # Remove local repos section current_values = current_values[:start_idx] + current_values[end_idx:] # Add refreshed local repos if local_repos: current_values.insert(1, '--- Local Repositories ---') for i, repo in enumerate(local_repos): current_values.insert(2 + i, repo) self.forked_repo_dropdown['values'] = current_values except Exception as e: print(f"Error refreshing local repos: {e}") def _load_user_forks_async(self): """Load user's GitHub forks asynchronously""" def load_forks(): try: github_token = self.config.get('GITHUB_PAT', '') if not github_token: return from .github_api import GitHubGQL github_api = GitHubGQL(github_token, dry_run=False) self.forked_repos = github_api.get_user_forks() # Update dropdown on main thread if hasattr(self, 'dialog') and self.dialog.winfo_exists(): self.dialog.after(0, self._update_forked_dropdown) except Exception as e: print(f"Error loading user forks: {e}") threading.Thread(target=load_forks, daemon=True).start() def _update_forked_dropdown(self): """Update the forked repository dropdown with GitHub forks""" try: # Check if dialog and dropdown still exist if not hasattr(self, 'dialog') or not self.dialog.winfo_exists(): return if not hasattr(self, 'forked_repo_dropdown') or not self.forked_repo_dropdown.winfo_exists(): return current_values = list(self.forked_repo_dropdown['values']) # Remove old GitHub forks section if exists if '--- Your GitHub Forks ---' in current_values: start_idx = current_values.index('--- Your GitHub Forks ---') current_values = current_values[:start_idx] # Add GitHub forks section if self.forked_repos: current_values.append('--- Your GitHub Forks ---') current_values.extend(self.forked_repos) self.forked_repo_dropdown['values'] = current_values except Exception as e: print(f"Error updating forked dropdown: {e}") def _create_dry_run_checkbox(self, parent, row: int): """Create dry run checkbox""" self.dry_run_var = tk.BooleanVar() dry_run_value = self.config.get('DRY_RUN', 'false') self.dry_run_var.set(str(dry_run_value).lower() in ('true', '1', 'yes', 'on')) dry_run_frame = ttk.Frame(parent) dry_run_frame.grid(row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10, padx=10) dry_run_checkbox = ttk.Checkbutton( dry_run_frame, text="🧪 Dry Run Mode (Test without making changes)", variable=self.dry_run_var ) dry_run_checkbox.pack(side=tk.LEFT) help_label = ttk.Label(dry_run_frame, text=" ā„¹ļø Simulates operations without creating actual GitHub issues/PRs", font=('Arial', 9), foreground='gray') help_label.pack(side=tk.LEFT) self.entries['DRY_RUN'] = self.dry_run_var def _scan_repos(self): """Scan work items to detect commonly used repositories""" try: # This is a placeholder - could be enhanced to actually scan work items # and suggest repositories based on document URLs found pass except Exception as e: print(f"Could not scan repositories: {e}") def _bind_events(self): """Bind keyboard events""" self.dialog.bind('', lambda e: self._save_clicked()) self.dialog.bind('', lambda e: self._cancel_clicked()) # Set focus to first entry if available if self.entries: first_entry = next(iter(self.entries.values())) if hasattr(first_entry, 'focus_set'): first_entry.focus_set() def _test_connection(self): """Test connection to configured services""" # Get current values config_values = self._get_config_values() results = [] # Test Azure DevOps if config_values.get('AZURE_DEVOPS_QUERY') and config_values.get('AZURE_DEVOPS_PAT'): try: # Try to import and test Azure DevOps API from .azure_devops_api import AzureDevOpsAPI api = AzureDevOpsAPI(config_values.get('AZURE_DEVOPS_PAT')) # Basic connection test (this would need actual implementation) results.append("Azure DevOps: āœ… Configuration looks valid") except ImportError: results.append("Azure DevOps: āš ļø Configuration set (API module not available)") except Exception as e: results.append(f"Azure DevOps: āŒ Error - {str(e)}") elif config_values.get('AZURE_DEVOPS_QUERY') or config_values.get('AZURE_DEVOPS_PAT'): results.append("Azure DevOps: āš ļø Incomplete configuration") # Test GitHub if config_values.get('GITHUB_PAT'): try: # Try to import and test GitHub API from .github_api import GitHubAPI api = GitHubAPI(config_values.get('GITHUB_PAT')) # Basic connection test 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 ImportError: results.append("GitHub: āš ļø Token set (API module not available)") 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 ImportError: results.append(f"AI Provider ({ai_provider}): āš ļø Configuration set (AI manager not available)") else: results.append("AI Provider: ā„¹ļø Disabled (using standard method)") # Show results if results: messagebox.showinfo("Connection Test Results", "\n".join(results) + "\n\nšŸ’” Full validation requires running the application.", parent=self.dialog) else: messagebox.showwarning("Connection Test", "No configuration to test.", parent=self.dialog) def _center_dialog(self): """Center the dialog over the parent window""" self.dialog.update_idletasks() # Get parent window position and size self.parent.update_idletasks() x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.dialog.winfo_width() // 2) y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.dialog.winfo_height() // 2) self.dialog.geometry(f"+{x}+{y}") 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, tk.BooleanVar): config_values[key] = 'true' if widget.get() else 'false' elif isinstance(widget, tk.StringVar): config_values[key] = widget.get().strip() elif isinstance(widget, scrolledtext.ScrolledText): config_values[key] = widget.get('1.0', tk.END).strip() elif isinstance(widget, ttk.Combobox): config_values[key] = widget.get().strip() else: # Entry widget value = widget.get().strip() # Special handling for GITHUB_TOKEN placeholder if key == 'GITHUB_TOKEN' and value == '(using GitHub PAT)': value = '' # Save empty string when using placeholder config_values[key] = value return config_values def _save_clicked(self): """Handle save button click""" try: # Get configuration values config_values = self._get_config_values() # Validate required fields required_for_basic = ['AZURE_DEVOPS_QUERY', 'AZURE_DEVOPS_PAT', 'GITHUB_PAT'] missing_basic = [field for field in required_for_basic if not config_values.get(field)] if missing_basic: messagebox.showwarning( "Missing Configuration", f"The following required fields are missing:\n\n" f"• {', '.join(missing_basic)}\n\n" f"These are required for basic functionality." ) return # Check AI provider setup before saving 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']: try: # Import here to avoid circular imports from .ai_manager import AIManager ai_manager = AIManager() available, missing = ai_manager.check_ai_module_availability(ai_provider) if not available: # Offer to install missing packages install_success = ai_manager.install_ai_packages(missing, self.dialog) if not install_success: # Installation failed or was cancelled, but still save settings messagebox.showwarning("AI Modules Not Installed", f"Settings saved, but AI provider '{ai_provider}' " f"requires additional packages: {', '.join(missing)}\n\n" f"You can install them later with:\n" f"pip install {' '.join(missing)}", parent=self.dialog) except ImportError: # AIManager not available, skip AI validation pass # Save configuration using the provided config manager if self.config_manager: success = self.config_manager.save_configuration(config_values) else: # Fallback: create new config manager or save directly to file try: from .config_manager import ConfigManager config_manager = ConfigManager() success = config_manager.save_configuration(config_values) except ImportError: # Fallback to basic file saving if ConfigManager not available success = self._save_to_env_file(config_values) if success: self.result = config_values # Ask user if they want to restart the application restart = messagebox.askyesno( "Settings Saved", "Settings have been saved to .env file!\n\n" "Would you like to restart the application now to apply changes?", parent=self.dialog ) self.dialog.destroy() if restart: self._restart_application() else: messagebox.showerror("Save Error", "Failed to save settings to .env file.", parent=self.dialog) except Exception as e: messagebox.showerror("Save Error", f"Error saving settings:\n{str(e)}", parent=self.dialog) def _save_to_env_file(self, config_values: Dict[str, Any]) -> bool: """Fallback method to save configuration to .env file""" try: import os # Create .env content env_content = "# Azure DevOps to GitHub Tool Configuration\n" env_content += "# Generated by Settings Dialog\n\n" # Add all configuration values for key, value in config_values.items(): if value: # Only add non-empty values env_content += f"{key}={value}\n" else: env_content += f"{key}=\n" # Write to .env file 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 _on_repo_selected(self, event=None): """Handle repo selection from dropdown - informational only for fork workflow""" # The detected repo dropdown shows which FORK the AI will work on locally # The GITHUB_REPO field is the UPSTREAM repo where PRs are created # This supports the fork workflow: work on fork, PR to upstream pass def _scan_repos(self): """Scan for git repositories in the local repo path""" try: from pathlib import Path # Get the local repo path from the entry field local_path = self.entries.get('LOCAL_REPO_PATH') if local_path and hasattr(local_path, 'get'): path_str = local_path.get().strip() else: path_str = self.config.get('LOCAL_REPO_PATH', '').strip() # If no path configured, use default if not path_str: path_str = str(Path.home() / "Downloads" / "github_repos") base_path = Path(path_str) # Check if path exists if not base_path.exists(): self.detected_repos_var.set('No repos found (directory does not exist)') self.detected_repos_dropdown['values'] = [] return # Scan for git repositories repos = [] try: # Look for owner/repo structure: base_path/owner/repo/.git for owner_dir in base_path.iterdir(): if not owner_dir.is_dir(): continue for repo_dir in owner_dir.iterdir(): if not repo_dir.is_dir(): continue # Check if it's a git repo git_dir = repo_dir / ".git" if git_dir.exists(): repo_name = f"{owner_dir.name}/{repo_dir.name}" repos.append(repo_name) except PermissionError: self.detected_repos_var.set('Permission denied accessing directory') self.detected_repos_dropdown['values'] = [] return except Exception as e: self.detected_repos_var.set(f'Error scanning: {str(e)[:50]}') self.detected_repos_dropdown['values'] = [] return # Update dropdown if repos: repos.sort() self.detected_repos_dropdown['values'] = repos # Auto-select if only one repo found if len(repos) == 1: self.detected_repos_var.set(repos[0]) # Trigger the selection handler to offer auto-populating GITHUB_REPO self.dialog.after(200, self._on_repo_selected) else: self.detected_repos_var.set(f'{len(repos)} repo(s) found - select one') else: self.detected_repos_var.set('No git repositories found') self.detected_repos_dropdown['values'] = [] except Exception as e: self.detected_repos_var.set(f'Error: {str(e)[:50]}') self.detected_repos_dropdown['values'] = [] def _restart_application(self): """Restart the application""" try: # Get the parent root window (main application) root = self.parent while root.master: root = root.master # Close the main window root.quit() # Restart the application using the same Python executable and script python = sys.executable script = sys.argv[0] # If running as a module (python -m), preserve that if script.endswith('__main__.py'): # Running as module, restart with module syntax os.execl(python, python, '-m', 'app') else: # Running as script, restart directly os.execl(python, python, script, *sys.argv[1:]) except Exception as e: messagebox.showerror( "Restart Failed", f"Could not restart application automatically:\n{str(e)}\n\n" "Please restart the application manually.", parent=self.parent ) def _cancel_clicked(self): """Handle cancel button click""" self.result = None self.dialog.destroy() def _clear_cache(self): """Clear all cached work items""" result = messagebox.askyesno( "Clear Cache", "Are you sure you want to clear all cached items?\n\n" "Cached work items and UUF items will be removed.\n" "The next time you open the app, it will auto-load fresh data." ) if result: try: # Use cache manager passed to dialog if self.cache_manager: self.cache_manager.invalidate_cache() messagebox.showinfo( "Cache Cleared", "All cached items have been cleared.\n" "Fresh data will be loaded on next app start." ) else: messagebox.showerror("Error", "Cache manager not available") except Exception as e: messagebox.showerror("Error", f"Failed to clear cache: {str(e)}") def show(self) -> Optional[Dict[str, Any]]: """Show dialog and return result""" self.dialog.wait_window() return self.result