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.

This commit is contained in:
b-tsammmons
2025-11-12 14:17:45 -10:00
parent feafbc15af
commit e5047e75a1
7 changed files with 935 additions and 390 deletions
+15 -12
View File
@@ -1479,6 +1479,9 @@ Current file content:
class GitHubCopilotProvider(AIProvider): class GitHubCopilotProvider(AIProvider):
"""GitHub Copilot provider using GitHub Models API""" """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]: 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""" """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]: 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""" """Generate updated document content using GitHub Copilot"""
try: try:
import requests import requests
url = "https://models.inference.ai.azure.com/chat/completions" url = self.GITHUB_MODELS_API_URL
headers = { headers = {
"Authorization": f"Bearer {self.api_key}", "Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json", "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]: 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""" """Handle additive changes using GitHub Copilot"""
self.logger.log("🔨 GitHub Copilot handling additive change - generating new content...") self.logger.log("🔨 GitHub Copilot handling additive change - generating new content...")
try: try:
import requests import requests
url = "https://models.inference.ai.azure.com/chat/completions" url = self.GITHUB_MODELS_API_URL
headers = { headers = {
"Authorization": f"Bearer {self.api_key}", "Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json", "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]: 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""" """Handle corrective changes using GitHub Copilot"""
self.logger.log("🔍 GitHub Copilot handling corrective change - finding specific issues...") self.logger.log("🔍 GitHub Copilot handling corrective change - finding specific issues...")
try: try:
import requests import requests
url = "https://models.inference.ai.azure.com/chat/completions" url = self.GITHUB_MODELS_API_URL
headers = { headers = {
"Authorization": f"Bearer {self.api_key}", "Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json", "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]: 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""" """Handle general changes using GitHub Copilot with enhanced targeting"""
self.logger.log("🎯 GitHub Copilot handling general change with enhanced targeting...") self.logger.log("🎯 GitHub Copilot handling general change with enhanced targeting...")
try: try:
import requests import requests
url = "https://models.inference.ai.azure.com/chat/completions" url = self.GITHUB_MODELS_API_URL
headers = { headers = {
"Authorization": f"Bearer {self.api_key}", "Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json", "Content-Type": "application/json",
+11 -11
View File
@@ -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 Stores fetched items in temporary cache to avoid reloading on every app start
""" """
@@ -13,7 +13,7 @@ from hashlib import md5
class CacheManager: 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): 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) cache_duration_hours: How long cache is valid (default 24 hours)
""" """
self.cache_duration_seconds = cache_duration_hours * 3600 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) self.cache_dir.mkdir(exist_ok=True)
def _get_cache_key(self, source_type: str, identifier: str) -> str: 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]]]: 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: Args:
source_type: 'azure_devops' or 'uuf' source_type: 'github_prs', 'github_issues', 'target_prs', 'fork_prs', etc.
identifier: query URL hash or config hash identifier: repository identifier or config hash
Returns: 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): if not self.is_cache_valid(source_type, identifier):
return None return None
@@ -81,12 +81,12 @@ class CacheManager:
def save_to_cache(self, source_type: str, identifier: str, items: List[Dict[str, Any]]) -> bool: 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: Args:
source_type: 'azure_devops' or 'uuf' source_type: 'github_prs', 'github_issues', 'target_prs', 'fork_prs', etc.
identifier: query URL hash or config hash identifier: repository identifier or config hash
items: List of work items to cache items: List of items to cache (PRs or Issues)
Returns: Returns:
True if successful, False otherwise True if successful, False otherwise
+3 -5
View File
@@ -229,9 +229,7 @@ class GitHubGQL:
if self.dry_run: if self.dry_run:
# Return sample data for dry run # Return sample data for dry run
return [ return [
"username/fabric-docs", "username/repo_name",
"username/azure-docs",
"username/powerbi-docs"
] ]
try: try:
@@ -324,8 +322,8 @@ class GitHubGQL:
if self.dry_run: if self.dry_run:
return { return {
"target_alternatives": ["microsoftdocs/fabric-docs-pr"], "target_alternatives": ["username/target_repo_name"],
"fork_alternatives": ["b-tsammons/azure-docs-pr"] "fork_alternatives": ["username/fork_repo_name"]
} }
try: try:
+825 -62
View File
@@ -67,8 +67,6 @@ class MainGUI:
self.diff_text_ref = ft.Ref[ft.TextField]() self.diff_text_ref = ft.Ref[ft.TextField]()
self.log_text_ref = ft.Ref[ft.TextField]() self.log_text_ref = ft.Ref[ft.TextField]()
self.edit_button_ref = ft.Ref[ft.IconButton]() 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]() self.go_button_ref = ft.Ref[ft.ElevatedButton]()
# Mode and filter refs # Mode and filter refs
@@ -84,6 +82,10 @@ class MainGUI:
# DataTable ref for all items # DataTable ref for all items
self.items_table_ref = ft.Ref[ft.DataTable]() 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 # Sidebar state
self.sidebar_visible = True self.sidebar_visible = True
self.sidebar_ref = ft.Ref[ft.Container]() self.sidebar_ref = ft.Ref[ft.Container]()
@@ -196,9 +198,10 @@ class MainGUI:
async def _async_init(self): async def _async_init(self):
"""Async initialization""" """Async initialization"""
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
await self._auto_load_cached_items()
await self._load_custom_instructions() await self._load_custom_instructions()
await self._init_load_repos() await self._init_load_repos()
# Auto-load cached items after repos are loaded
await self._auto_load_cached_items()
def _toggle_sidebar(self, e): def _toggle_sidebar(self, e):
"""Toggle sidebar visibility""" """Toggle sidebar visibility"""
@@ -335,6 +338,23 @@ class MainGUI:
expand=True, expand=True,
on_change=self._on_workflow_item_selected, 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, spacing=10,
) )
@@ -431,20 +451,6 @@ class MainGUI:
# Navigation buttons # Navigation buttons
nav_buttons = ft.Row( 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.Container(expand=True),
ft.ElevatedButton( ft.ElevatedButton(
"Go", "Go",
@@ -623,14 +629,17 @@ class MainGUI:
items_table = ft.DataTable( items_table = ft.DataTable(
ref=self.items_table_ref, ref=self.items_table_ref,
columns=[ columns=[
ft.DataColumn(ft.Text("Repo")),
ft.DataColumn(ft.Text("Type")),
ft.DataColumn(ft.Text("ID")), ft.DataColumn(ft.Text("ID")),
ft.DataColumn(ft.Text("Title")), ft.DataColumn(ft.Text("Title")),
ft.DataColumn(ft.Text("Nature")), ft.DataColumn(ft.Text("Author")),
ft.DataColumn(ft.Text("GitHub Repo")),
ft.DataColumn(ft.Text("ms.author")),
ft.DataColumn(ft.Text("Status")), ft.DataColumn(ft.Text("Status")),
], ],
rows=[], 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_current_button = ft.ElevatedButton(
@@ -707,6 +716,9 @@ class MainGUI:
self.workflow_item_dropdown_ref.current.options = [] self.workflow_item_dropdown_ref.current.options = []
self.page.update() 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): def _on_workflow_item_selected(self, e):
"""Handle workflow item selection""" """Handle workflow item selection"""
if not self.workflow_item_dropdown_ref.current: if not self.workflow_item_dropdown_ref.current:
@@ -789,6 +801,369 @@ class MainGUI:
# Implementation would populate fields with workflow item data # Implementation would populate fields with workflow item data
pass 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): def _previous_item(self, e):
"""Navigate to previous item""" """Navigate to previous item"""
if self.current_item_index > 0: if self.current_item_index > 0:
@@ -796,13 +1171,6 @@ class MainGUI:
self._display_current_item() self._display_current_item()
self._update_navigation_buttons() 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): def _toggle_edit_mode(self, e):
"""Toggle edit mode for proposed new text""" """Toggle edit mode for proposed new text"""
if not self.proposed_new_text_ref.current or not self.edit_button_ref.current: if not self.proposed_new_text_ref.current or not self.edit_button_ref.current:
@@ -875,14 +1243,172 @@ class MainGUI:
# ===== Async Operations ===== # ===== Async Operations =====
async def _auto_load_cached_items(self): async def _auto_load_cached_items(self):
"""Auto-load cached items on startup""" """Auto-load cached items on startup if available"""
try: print("=" * 60)
# Try to load from cache print("🔄 Auto-loading cached items on startup...")
if self.cache_manager: print("=" * 60)
# Implementation would load cached items
pass def load_cached():
except Exception as e: try:
print(f"Error auto-loading cached items: {e}") # 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): async def _load_custom_instructions(self):
"""Load custom instructions from config""" """Load custom instructions from config"""
@@ -949,8 +1475,178 @@ class MainGUI:
async def _search_target_repos_async(self): async def _search_target_repos_async(self):
"""Search for repositories on GitHub""" """Search for repositories on GitHub"""
# Implementation would search GitHub repos # Create search dialog
pass 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): async def _load_forked_repos_async(self):
"""Load forked repositories""" """Load forked repositories"""
@@ -1020,13 +1716,26 @@ class MainGUI:
async def _load_workflow_items_async(self): async def _load_workflow_items_async(self):
"""Load workflow items (PRs/Issues)""" """Load workflow items (PRs/Issues)"""
print("=" * 60) # Check if items are already loaded to determine if this is a refresh
print("🔄 Load Items button clicked!") items_already_loaded = any(len(items) > 0 for items in self.workflow_items.values())
print("=" * 60) force_refresh = items_already_loaded
if self.logger:
self.logger.log("=" * 60) if force_refresh:
self.logger.log("🔄 Load Items button clicked - starting workflow item load") print("=" * 60)
self.logger.log("=" * 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(): def load_items():
try: try:
@@ -1068,10 +1777,39 @@ class MainGUI:
if self.logger: if self.logger:
self.logger.log(f"📥 Loading PRs and issues from target repo: {target_repo}") self.logger.log(f"📥 Loading PRs and issues from target repo: {target_repo}")
print(f"Calling workflow_manager.fetch_pull_requests('{target_repo}')...") # Try to load from cache first (unless forcing refresh)
self.workflow_items['target_prs'] = workflow_manager.fetch_pull_requests(target_repo) cached_prs = None if force_refresh else (self.cache_manager.load_from_cache('target_prs', target_repo) if self.cache_manager else None)
print(f"Calling workflow_manager.fetch_issues('{target_repo}')...") cached_issues = None if force_refresh else (self.cache_manager.load_from_cache('target_issues', target_repo) if self.cache_manager else None)
self.workflow_items['target_issues'] = workflow_manager.fetch_issues(target_repo)
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', [])) pr_count = len(self.workflow_items.get('target_prs', []))
issue_count = len(self.workflow_items.get('target_issues', [])) 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 forked_repo and not forked_repo.startswith('---') and '/' in forked_repo:
if self.logger: if self.logger:
self.logger.log(f"Loading PRs and issues from forked repo: {forked_repo}") 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: 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") 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 # Filter and update UI
self.page.run_task(self._filter_workflow_items_async) 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: except Exception as e:
if self.logger: if self.logger:
self.logger.log(f"Error loading workflow items: {e}") self.logger.log(f"Error loading workflow items: {e}")
@@ -1136,18 +1911,6 @@ class MainGUI:
self.page.update() self.page.update()
self._update_navigation_buttons() 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): def update_status(self, message: str):
"""Update status message""" """Update status message"""
if self.status_text_ref.current: if self.status_text_ref.current:
+3 -295
View File
@@ -7,10 +7,7 @@ import flet as ft
# Compatibility fix for Flet 0.28+ (Icons vs icons, Colors vs colors) # Compatibility fix for Flet 0.28+ (Icons vs icons, Colors vs colors)
ft.icons = ft.Icons ft.icons = ft.Icons
ft.colors = ft.Colors ft.colors = ft.Colors
import threading
import subprocess
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
import sys
import os import os
import asyncio import asyncio
@@ -27,13 +24,7 @@ class SettingsDialog:
self.entries = {} self.entries = {}
self.dialog_ref = ft.Ref[ft.AlertDialog]() self.dialog_ref = ft.Ref[ft.AlertDialog]()
# Repository data
self.target_repos = []
self.forked_repos = []
# Dropdown refs # 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.detected_repos_dropdown_ref = ft.Ref[ft.Dropdown]()
self.ollama_model_dropdown_ref = ft.Ref[ft.Dropdown]() self.ollama_model_dropdown_ref = ft.Ref[ft.Dropdown]()
@@ -71,8 +62,6 @@ class SettingsDialog:
"""Initialize async operations""" """Initialize async operations"""
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
await self._scan_repos_async() await self._scan_repos_async()
await self._load_target_repos_async()
await self._load_user_forks_async()
def _create_dialog(self) -> ft.AlertDialog: def _create_dialog(self) -> ft.AlertDialog:
"""Create the settings dialog""" """Create the settings dialog"""
@@ -136,7 +125,7 @@ class SettingsDialog:
controls = [] controls = []
# GitHub Configuration Section # 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
github_pat = ft.TextField( github_pat = ft.TextField(
@@ -150,71 +139,6 @@ class SettingsDialog:
self.entries['GITHUB_PAT'] = github_pat self.entries['GITHUB_PAT'] = github_pat
controls.append(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 # General Options Section
controls.append(self._create_section_header("⚙️ General Options")) controls.append(self._create_section_header("⚙️ General Options"))
@@ -268,8 +192,7 @@ class SettingsDialog:
"💡 Repository Setup Guide:\n" "💡 Repository Setup Guide:\n"
" • Local Repo Path: Where your fork repos are cloned (e.g., C:\\git\\repos)\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" " • Detected Repos: Shows your local fork (e.g., yourname/repo)\n"
" • Target Repository: Upstream repo for PRs (e.g., microsoft/repo)\n" " Note: Target and Fork repositories are configured in the main GUI",
" • Fork Workflow: Work on your fork locally, create PRs to upstream",
size=12, size=12,
color="grey400", color="grey400",
), ),
@@ -283,9 +206,7 @@ class SettingsDialog:
content=ft.Text( content=ft.Text(
"💡 Getting Started:\n" "💡 Getting Started:\n"
"1. Create a GitHub Personal Access Token\n" "1. Create a GitHub Personal Access Token\n"
"2. Configure GitHub repositories:\n" "2. Configure GitHub repositories in the main GUI\n"
" • Target Repository: Where PRs will be created\n"
" • Forked Repository: Your fork where changes are made\n"
"3. Set Local Repo Path for automatic repository detection\n" "3. Set Local Repo Path for automatic repository detection\n"
"4. Configure AI provider in the AI tab (optional)\n" "4. Configure AI provider in the AI tab (optional)\n"
"5. Test your connection before processing items", "5. Test your connection before processing items",
@@ -517,214 +438,6 @@ class SettingsDialog:
except Exception as e: except Exception as e:
print(f"Error in _scan_repos_async: {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): async def _scan_ollama_models_async(self):
"""Scan Ollama server for available models""" """Scan Ollama server for available models"""
ollama_url = self.entries.get('OLLAMA_URL').value.strip() if 'OLLAMA_URL' in self.entries else '' 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: if len(model_names) > 10:
models_text += f"\n\n...and {len(model_names) - 10} more" 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: else:
self._show_alert("No Models Found", "No models found on the Ollama server.\n\nUse 'ollama pull <model>' to download models.") self._show_alert("No Models Found", "No models found on the Ollama server.\n\nUse 'ollama pull <model>' to download models.")
@@ -857,10 +569,6 @@ class SettingsDialog:
config_values[key] = value config_values[key] = value
# Handle dropdown values specially # 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: if self.ollama_model_dropdown_ref.current:
config_values['OLLAMA_MODEL'] = self.ollama_model_dropdown_ref.current.value or '' config_values['OLLAMA_MODEL'] = self.ollama_model_dropdown_ref.current.value or ''
-5
View File
@@ -646,11 +646,6 @@ CUSTOM_INSTRUCTIONS=
print(f"Error creating default .env file: {e}") print(f"Error creating default .env file: {e}")
return False return False
# Removed EnhancedContentBuilders class - was specific to Azure DevOps
# Use ContentBuilders class for generic GitHub automation instead
# Compatibility functions for direct function access # Compatibility functions for direct function access
def get_next_pr_number(provider_key: str) -> int: def get_next_pr_number(provider_key: str) -> int:
"""Compatibility function for direct access to PR number generation""" """Compatibility function for direct access to PR number generation"""
+78
View File
@@ -70,6 +70,7 @@ class WorkflowItem:
return { return {
'item_type': self.item_type, 'item_type': self.item_type,
'repo_source': self.repo_source, 'repo_source': self.repo_source,
'data': self.data, # Include raw data for full reconstruction
'number': self.number, 'number': self.number,
'title': self.title, 'title': self.title,
'state': self.state, 'state': self.state,
@@ -77,6 +78,7 @@ class WorkflowItem:
'updated_at': self.updated_at, 'updated_at': self.updated_at,
'body': self.body, 'body': self.body,
'url': self.url, 'url': self.url,
'api_url': self.api_url,
'author': self.author, 'author': self.author,
'author_url': self.author_url, 'author_url': self.author_url,
'labels': self.labels, 'labels': self.labels,
@@ -89,6 +91,16 @@ class WorkflowItem:
'comments_count': self.comments_count '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: class GitHubRepoFetcher:
"""Fetches repository information from GitHub""" """Fetches repository information from GitHub"""
@@ -516,3 +528,69 @@ class WorkflowManager:
if any(label in item.labels for label in label_filter)] if any(label in item.labels for label in label_filter)]
return filtered 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 []