From e5047e75a18b627c68f37bbe3f2f5967d3935e63 Mon Sep 17 00:00:00 2001 From: b-tsammmons <233864410+b-tsammons@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:17:45 -1000 Subject: [PATCH] Refactor SettingsDialog: Remove target and forked repository dropdowns, update GitHub PAT section, and clean up async repo loading methods. Enhance WorkflowItem with data reconstruction and add fetch_comments method for GitHub issues/PRs. --- application/app_components/ai_manager.py | 27 +- application/app_components/cache_manager.py | 22 +- application/app_components/github_api.py | 8 +- application/app_components/main_gui.py | 887 ++++++++++++++++-- application/app_components/settings_dialog.py | 298 +----- application/app_components/utils.py | 5 - application/app_components/workflow.py | 78 ++ 7 files changed, 935 insertions(+), 390 deletions(-) diff --git a/application/app_components/ai_manager.py b/application/app_components/ai_manager.py index c7b0021..c898f7c 100644 --- a/application/app_components/ai_manager.py +++ b/application/app_components/ai_manager.py @@ -1479,6 +1479,9 @@ Current file content: 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""" @@ -1501,11 +1504,11 @@ class GitHubCopilotProvider(AIProvider): 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 = "https://models.inference.ai.azure.com/chat/completions" + + url = self.GITHUB_MODELS_API_URL headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", @@ -1865,11 +1868,11 @@ Return the complete updated file content now (NO explanatory text):""" 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 = "https://models.inference.ai.azure.com/chat/completions" + + url = self.GITHUB_MODELS_API_URL headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", @@ -1937,11 +1940,11 @@ Generate the new content now:""" 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 = "https://models.inference.ai.azure.com/chat/completions" + + url = self.GITHUB_MODELS_API_URL headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", @@ -2014,11 +2017,11 @@ Find the exact text to correct:""" 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 = "https://models.inference.ai.azure.com/chat/completions" + + url = self.GITHUB_MODELS_API_URL headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", diff --git a/application/app_components/cache_manager.py b/application/app_components/cache_manager.py index dbd5a6d..077412c 100644 --- a/application/app_components/cache_manager.py +++ b/application/app_components/cache_manager.py @@ -1,5 +1,5 @@ """ -Cache Manager for Work Items and UUF Items +Cache Manager for GitHub PRs and Issues Stores fetched items in temporary cache to avoid reloading on every app start """ @@ -13,7 +13,7 @@ from hashlib import md5 class CacheManager: - """Manages caching of work items and UUF items""" + """Manages caching of GitHub PRs and Issues""" def __init__(self, cache_duration_hours: int = 24): """ @@ -23,7 +23,7 @@ class CacheManager: cache_duration_hours: How long cache is valid (default 24 hours) """ self.cache_duration_seconds = cache_duration_hours * 3600 - self.cache_dir = Path(tempfile.gettempdir()) / "devops_to_github_cache" + self.cache_dir = Path(tempfile.gettempdir()) / "github_pulse_cache" self.cache_dir.mkdir(exist_ok=True) def _get_cache_key(self, source_type: str, identifier: str) -> str: @@ -50,14 +50,14 @@ class CacheManager: def load_from_cache(self, source_type: str, identifier: str) -> Optional[List[Dict[str, Any]]]: """ - Load work items from cache + Load GitHub items from cache Args: - source_type: 'azure_devops' or 'uuf' - identifier: query URL hash or config hash + source_type: 'github_prs', 'github_issues', 'target_prs', 'fork_prs', etc. + identifier: repository identifier or config hash Returns: - List of work items if cache is valid, None otherwise + List of items if cache is valid, None otherwise """ if not self.is_cache_valid(source_type, identifier): return None @@ -81,12 +81,12 @@ class CacheManager: def save_to_cache(self, source_type: str, identifier: str, items: List[Dict[str, Any]]) -> bool: """ - Save work items to cache + Save GitHub items to cache Args: - source_type: 'azure_devops' or 'uuf' - identifier: query URL hash or config hash - items: List of work items to cache + 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 diff --git a/application/app_components/github_api.py b/application/app_components/github_api.py index 457022a..a3ab660 100644 --- a/application/app_components/github_api.py +++ b/application/app_components/github_api.py @@ -229,9 +229,7 @@ class GitHubGQL: if self.dry_run: # Return sample data for dry run return [ - "username/fabric-docs", - "username/azure-docs", - "username/powerbi-docs" + "username/repo_name", ] try: @@ -324,8 +322,8 @@ class GitHubGQL: if self.dry_run: return { - "target_alternatives": ["microsoftdocs/fabric-docs-pr"], - "fork_alternatives": ["b-tsammons/azure-docs-pr"] + "target_alternatives": ["username/target_repo_name"], + "fork_alternatives": ["username/fork_repo_name"] } try: diff --git a/application/app_components/main_gui.py b/application/app_components/main_gui.py index b9219fb..788a337 100644 --- a/application/app_components/main_gui.py +++ b/application/app_components/main_gui.py @@ -67,8 +67,6 @@ class MainGUI: 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.prev_button_ref = ft.Ref[ft.IconButton]() - self.next_button_ref = ft.Ref[ft.IconButton]() self.go_button_ref = ft.Ref[ft.ElevatedButton]() # Mode and filter refs @@ -84,6 +82,10 @@ class MainGUI: # 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.item_detail_dialog_ref = ft.Ref[ft.AlertDialog]() + # Sidebar state self.sidebar_visible = True self.sidebar_ref = ft.Ref[ft.Container]() @@ -196,9 +198,10 @@ class MainGUI: async def _async_init(self): """Async initialization""" await asyncio.sleep(0.5) - await self._auto_load_cached_items() 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""" @@ -335,6 +338,23 @@ class MainGUI: expand=True, on_change=self._on_workflow_item_selected, ), + ft.Divider(height=10), + ft.Text("All Items", weight=ft.FontWeight.BOLD, size=14), + 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, ) @@ -431,20 +451,6 @@ class MainGUI: # Navigation buttons nav_buttons = ft.Row( [ - ft.IconButton( - ref=self.prev_button_ref, - icon=ft.icons.ARROW_BACK, - tooltip="Previous", - on_click=self._previous_item, - disabled=True, - ), - ft.IconButton( - ref=self.next_button_ref, - icon=ft.icons.ARROW_FORWARD, - tooltip="Next", - on_click=self._next_item, - disabled=True, - ), ft.Container(expand=True), ft.ElevatedButton( "Go", @@ -623,14 +629,17 @@ class MainGUI: 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("Nature")), - ft.DataColumn(ft.Text("GitHub Repo")), - ft.DataColumn(ft.Text("ms.author")), + 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( @@ -707,6 +716,9 @@ class MainGUI: 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: @@ -789,6 +801,369 @@ class MainGUI: # Implementation would populate fields with workflow item data pass + def _populate_all_items(self): + """Populate the all items list with all loaded PRs and Issues""" + 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) + + if not all_items: + 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 workflow item in the dropdown""" + if not self.workflow_item_dropdown_ref.current: + return + + # Update filters to match the selected item + # Set repo source (target/fork) + if self.repo_source_ref.current: + self.repo_source_ref.current.value = item.repo_source + + # Set item type (pull_request/issue) + if self.item_type_ref.current: + self.item_type_ref.current.value = item.item_type + + # Re-filter the workflow items with the new settings + self._filter_workflow_items() + + # Set the dropdown value to this item's title + self.workflow_item_dropdown_ref.current.value = item.title + + # 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 _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: @@ -796,13 +1171,6 @@ class MainGUI: self._display_current_item() self._update_navigation_buttons() - def _next_item(self, e): - """Navigate to next item""" - if self.current_item_index < len(self.current_work_items) - 1: - 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: @@ -875,14 +1243,172 @@ class MainGUI: # ===== Async Operations ===== async def _auto_load_cached_items(self): - """Auto-load cached items on startup""" - try: - # Try to load from cache - if self.cache_manager: - # Implementation would load cached items - pass - except Exception as e: - print(f"Error auto-loading cached items: {e}") + """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() + + # Populate all items table in the All Items tab + self._populate_all_items_table() + + 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() + + # Populate all items table in the All Items tab + self._populate_all_items_table() + + 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""" @@ -949,8 +1475,178 @@ class MainGUI: async def _search_target_repos_async(self): """Search for repositories on GitHub""" - # Implementation would search GitHub repos - pass + # 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""" @@ -1020,13 +1716,26 @@ class MainGUI: async def _load_workflow_items_async(self): """Load workflow items (PRs/Issues)""" - 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) + # 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: @@ -1068,10 +1777,39 @@ class MainGUI: if self.logger: self.logger.log(f"đŸ“Ĩ Loading PRs and issues from target repo: {target_repo}") - print(f"Calling workflow_manager.fetch_pull_requests('{target_repo}')...") - self.workflow_items['target_prs'] = workflow_manager.fetch_pull_requests(target_repo) - print(f"Calling workflow_manager.fetch_issues('{target_repo}')...") - self.workflow_items['target_issues'] = workflow_manager.fetch_issues(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', [])) @@ -1088,14 +1826,51 @@ class MainGUI: 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}") - self.workflow_items['fork_prs'] = workflow_manager.fetch_pull_requests(forked_repo) - self.workflow_items['fork_issues'] = workflow_manager.fetch_issues(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() + + # Populate all items table in the All Items tab + self._populate_all_items_table() + except Exception as e: if self.logger: self.logger.log(f"Error loading workflow items: {e}") @@ -1136,18 +1911,6 @@ class MainGUI: self.page.update() self._update_navigation_buttons() - def _update_navigation_buttons(self): - """Update navigation button states""" - if self.prev_button_ref.current: - self.prev_button_ref.current.disabled = (self.current_item_index == 0) - - if self.next_button_ref.current: - self.next_button_ref.current.disabled = ( - self.current_item_index >= len(self.current_work_items) - 1 - ) - - self.page.update() - def update_status(self, message: str): """Update status message""" if self.status_text_ref.current: diff --git a/application/app_components/settings_dialog.py b/application/app_components/settings_dialog.py index 7923dc5..495f0e5 100644 --- a/application/app_components/settings_dialog.py +++ b/application/app_components/settings_dialog.py @@ -7,10 +7,7 @@ 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 threading -import subprocess from typing import Dict, Any, Optional -import sys import os import asyncio @@ -27,13 +24,7 @@ class SettingsDialog: self.entries = {} self.dialog_ref = ft.Ref[ft.AlertDialog]() - # Repository data - self.target_repos = [] - self.forked_repos = [] - # Dropdown refs - self.target_repo_dropdown_ref = ft.Ref[ft.Dropdown]() - self.forked_repo_dropdown_ref = ft.Ref[ft.Dropdown]() self.detected_repos_dropdown_ref = ft.Ref[ft.Dropdown]() self.ollama_model_dropdown_ref = ft.Ref[ft.Dropdown]() @@ -71,8 +62,6 @@ class SettingsDialog: """Initialize async operations""" await asyncio.sleep(0.1) await self._scan_repos_async() - await self._load_target_repos_async() - await self._load_user_forks_async() def _create_dialog(self) -> ft.AlertDialog: """Create the settings dialog""" @@ -136,7 +125,7 @@ class SettingsDialog: controls = [] # GitHub Configuration Section - controls.append(self._create_section_header("🐙 GitHub Configuration")) + controls.append(self._create_section_header("🐙 GitHub Personal Access Token")) # GitHub PAT github_pat = ft.TextField( @@ -150,71 +139,6 @@ class SettingsDialog: self.entries['GITHUB_PAT'] = github_pat controls.append(github_pat) - # Target Repository - controls.append(ft.Text("Target Repository", weight=ft.FontWeight.BOLD, size=14)) - target_repo_row = ft.Row( - [ - ft.Dropdown( - ref=self.target_repo_dropdown_ref, - label="Target Repository", - value=self.config.get('GITHUB_REPO', ''), - options=[], - hint_text="Select or type repository", - expand=True, - on_change=lambda e: self._on_target_repo_search(e), - ), - 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, - ) - controls.append(target_repo_row) - controls.append(ft.Text( - "â„šī¸ Upstream repo where PRs will be created. Type to search all GitHub repos.", - size=12, - color="grey400", - )) - - # Forked Repository - controls.append(ft.Text("Forked Repository", weight=ft.FontWeight.BOLD, size=14)) - forked_repo_row = ft.Row( - [ - ft.Dropdown( - ref=self.forked_repo_dropdown_ref, - label="Forked Repository", - value=self.config.get('FORKED_REPO', ''), - options=[], - hint_text="Select your fork", - expand=True, - ), - 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, - ) - controls.append(forked_repo_row) - controls.append(ft.Text( - "â„šī¸ Your fork where changes will be made. Leave empty to auto-detect from document URL.", - size=12, - color="grey400", - )) - # General Options Section controls.append(self._create_section_header("âš™ī¸ General Options")) @@ -268,8 +192,7 @@ class SettingsDialog: "💡 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", + " â€ĸ Note: Target and Fork repositories are configured in the main GUI", size=12, color="grey400", ), @@ -283,9 +206,7 @@ class SettingsDialog: content=ft.Text( "💡 Getting Started:\n" "1. Create a GitHub Personal Access Token\n" - "2. Configure GitHub repositories:\n" - " â€ĸ Target Repository: Where PRs will be created\n" - " â€ĸ Forked Repository: Your fork where changes are made\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", @@ -517,214 +438,6 @@ class SettingsDialog: except Exception as e: print(f"Error in _scan_repos_async: {e}") - async def _load_target_repos_async(self): - """Load target repos (with push/admin access) asynchronously""" - def load_repos(): - try: - github_token = self.config.get('GITHUB_PAT', '') - if not github_token: - return - - from .workflow import GitHubRepoFetcher - repo_fetcher = GitHubRepoFetcher(github_token) - repos = repo_fetcher.fetch_repos_with_permissions(min_permission='push') - self.target_repos = repo_fetcher.get_repo_names(repos) - - # Update UI on main thread - if self.target_repo_dropdown_ref.current: - self.page.run_task(self._update_target_dropdown_async) - - except Exception as e: - print(f"Error loading target repos: {e}") - - await asyncio.to_thread(load_repos) - - async def _update_target_dropdown_async(self): - """Update the target repository dropdown""" - try: - 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 - self.page.update() - - except Exception as e: - print(f"Error updating target dropdown: {e}") - - 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""" - if not self.target_repo_dropdown_ref.current: - return - - query = self.target_repo_dropdown_ref.current.value.strip() - if not query: - return - - def search_repos(): - try: - github_token = self.config.get('GITHUB_PAT', '') - if not github_token: - return - - from .workflow import GitHubRepoFetcher - repo_fetcher = GitHubRepoFetcher(github_token) - repos = repo_fetcher.search_repositories(query, per_page=50) - search_results = repo_fetcher.get_repo_names(repos) - - # Update UI - if self.target_repo_dropdown_ref.current: - 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]) - - if search_results: - options.append(ft.dropdown.Option(f"--- Search Results for \"{query}\" ---", disabled=True)) - options.extend([ft.dropdown.Option(repo) for repo in search_results]) - - self.target_repo_dropdown_ref.current.options = options - self.page.update() - - except Exception as e: - print(f"Error searching repos: {e}") - - await asyncio.to_thread(search_repos) - - def _on_target_repo_search(self, e): - """Handle typing in target repo field for auto-search""" - # Debounce search - could be implemented with a timer - pass - - async 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 .workflow import GitHubRepoFetcher - repo_fetcher = GitHubRepoFetcher(github_token) - repos = repo_fetcher.fetch_user_repos(repo_type='owner') - self.forked_repos = 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: - print(f"Error loading user forks: {e}") - - await asyncio.to_thread(load_forks) - - async def _update_forked_dropdown_async(self): - """Update the forked repository dropdown with GitHub forks""" - try: - if not self.forked_repo_dropdown_ref.current: - return - - options = [] - - # Add 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) - if local_repos: - options.append(ft.dropdown.Option("--- Local Repositories ---", disabled=True)) - options.extend([ft.dropdown.Option(repo) for repo in local_repos]) - except Exception as e: - print(f"Error scanning local repos: {e}") - - # Add GitHub repos - if self.forked_repos: - options.append(ft.dropdown.Option("--- Your GitHub Repos ---", disabled=True)) - options.extend([ft.dropdown.Option(repo) for repo in self.forked_repos]) - - self.forked_repo_dropdown_ref.current.options = options - self.page.update() - - except Exception as e: - print(f"Error updating forked dropdown: {e}") - - async def _refresh_forked_repos_async(self): - """Refresh the forked repositories dropdown""" - await self._load_user_forks_async() - await self._update_forked_dropdown_async() - - def _clone_forked_repo(self, e): - """Clone the selected forked repository to the local repo path""" - if not self.forked_repo_dropdown_ref.current: - return - - selected_repo = self.forked_repo_dropdown_ref.current.value.strip() - - if not selected_repo or selected_repo.startswith('---'): - self._show_alert("Invalid Selection", "Please select a repository, not a section header.") - return - - local_repo_path = self.config.get('LOCAL_REPO_PATH', '').strip() - if not local_repo_path: - self._show_alert("Local Path Not Configured", "Please configure the Local Repository Path in settings first.") - return - - # Start clone in background - self.page.run_task(lambda: self._clone_repo_async(selected_repo, local_repo_path)) - - async def _clone_repo_async(self, repo_name: str, local_repo_path: str): - """Clone repository asynchronously""" - try: - os.makedirs(local_repo_path, exist_ok=True) - - if '/' not in repo_name: - self._show_alert("Invalid Repository", "Repository must be in 'owner/repo' format.") - return - - folder_name = repo_name.split('/')[-1] - target_path = os.path.join(local_repo_path, folder_name) - - if os.path.exists(target_path): - # Show confirmation dialog - self._show_alert( - "Directory Exists", - f"The directory '{folder_name}' already exists. Clone may fail if it's already a git repository." - ) - return - - clone_url = f"https://github.com/{repo_name}.git" - - # Show progress - self._show_alert("Cloning Repository", f"Cloning {repo_name}...\nThis may take a few moments.") - - # Run git clone - result = await asyncio.to_thread( - subprocess.run, - ['git', 'clone', clone_url, target_path], - capture_output=True, - text=True, - timeout=300 - ) - - if result.returncode == 0: - self._show_alert("Clone Successful", f"Successfully cloned {repo_name}!\n\nLocation: {folder_name}/") - await self._refresh_forked_repos_async() - else: - error_msg = result.stderr if result.stderr else result.stdout - self._show_alert("Clone Failed", f"Failed to clone {repo_name}.\n\nError:\n{error_msg}") - - except Exception as e: - self._show_alert("Clone Error", f"An error occurred while cloning:\n{str(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 '' @@ -767,7 +480,6 @@ class SettingsDialog: if len(model_names) > 10: models_text += f"\n\n...and {len(model_names) - 10} more" - self._show_alert("Models Found", f"Found {len(model_names)} model(s):\n\n{models_text}") else: self._show_alert("No Models Found", "No models found on the Ollama server.\n\nUse 'ollama pull ' to download models.") @@ -857,10 +569,6 @@ class SettingsDialog: config_values[key] = value # Handle dropdown values specially - if self.target_repo_dropdown_ref.current: - config_values['GITHUB_REPO'] = self.target_repo_dropdown_ref.current.value or '' - if self.forked_repo_dropdown_ref.current: - config_values['FORKED_REPO'] = self.forked_repo_dropdown_ref.current.value or '' if self.ollama_model_dropdown_ref.current: config_values['OLLAMA_MODEL'] = self.ollama_model_dropdown_ref.current.value or '' diff --git a/application/app_components/utils.py b/application/app_components/utils.py index 6baca23..63b8461 100644 --- a/application/app_components/utils.py +++ b/application/app_components/utils.py @@ -646,11 +646,6 @@ CUSTOM_INSTRUCTIONS= print(f"Error creating default .env file: {e}") return False - -# Removed EnhancedContentBuilders class - was specific to Azure DevOps -# Use ContentBuilders class for generic GitHub automation instead - - # Compatibility functions for direct function access def get_next_pr_number(provider_key: str) -> int: """Compatibility function for direct access to PR number generation""" diff --git a/application/app_components/workflow.py b/application/app_components/workflow.py index 9f1b453..32a20aa 100644 --- a/application/app_components/workflow.py +++ b/application/app_components/workflow.py @@ -70,6 +70,7 @@ class WorkflowItem: 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, @@ -77,6 +78,7 @@ class WorkflowItem: '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, @@ -89,6 +91,16 @@ class WorkflowItem: '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""" @@ -516,3 +528,69 @@ class WorkflowManager: 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 []