This repository has been archived on 2026-05-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
github_pulse/application/app_components/main_gui.py
T

1288 lines
46 KiB
Python

"""
Main GUI Interface (Flet version)
The primary user interface for the application
"""
import flet as ft
# Compatibility fix for Flet 0.28+ (Icons vs icons, Colors vs colors)
ft.icons = ft.Icons
ft.colors = ft.Colors
import os
import threading
import webbrowser
import asyncio
from typing import List, Dict, Any, Optional
from pathlib import Path
from .utils import Logger
from .settings_dialog import SettingsDialog
class DryRunVar:
"""Compatibility class for dry run variable"""
def __init__(self, app):
self.app = app
def get(self):
return self.app.dry_run_enabled
def set(self, value):
self.app.dry_run_enabled = bool(value)
class MainGUI:
"""Main GUI interface for the application"""
def __init__(self, page: ft.Page, config_manager, ai_manager, app):
self.page = page
self.config_manager = config_manager
self.ai_manager = ai_manager
self.app = app
# Application state
self.current_work_items = []
self.current_item_index = 0
self.current_organization = None
self.edit_mode = False
self.workflow_items = {}
self.current_workflow_items = []
# Repository data
self.target_repos = []
self.forked_repos = {'local': [], 'github': []}
# Create dry run compatibility wrapper
self.dry_run_var = DryRunVar(app)
# UI References
self.status_text_ref = ft.Ref[ft.Text]()
self.progress_bar_ref = ft.Ref[ft.ProgressBar]()
self.work_item_id_ref = ft.Ref[ft.Text]()
self.nature_text_ref = ft.Ref[ft.TextField]()
self.live_doc_url_ref = ft.Ref[ft.TextField]()
self.text_to_change_ref = ft.Ref[ft.TextField]()
self.proposed_new_text_ref = ft.Ref[ft.TextField]()
self.custom_instructions_ref = ft.Ref[ft.TextField]()
self.diff_text_ref = ft.Ref[ft.TextField]()
self.log_text_ref = ft.Ref[ft.TextField]()
self.edit_button_ref = ft.Ref[ft.IconButton]()
self.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
self.tools_mode_ref = ft.Ref[ft.RadioGroup]()
self.repo_source_ref = ft.Ref[ft.RadioGroup]()
self.item_type_ref = ft.Ref[ft.RadioGroup]()
self.create_type_ref = ft.Ref[ft.RadioGroup]()
self.target_repo_dropdown_ref = ft.Ref[ft.Dropdown]()
self.forked_repo_dropdown_ref = ft.Ref[ft.Dropdown]()
self.workflow_item_dropdown_ref = ft.Ref[ft.Dropdown]()
self.item_counter_ref = ft.Ref[ft.Text]()
# DataTable ref for all items
self.items_table_ref = ft.Ref[ft.DataTable]()
# Sidebar state
self.sidebar_visible = True
self.sidebar_ref = ft.Ref[ft.Container]()
self.tools_content_ref = ft.Ref[ft.Column]()
# Initialize cache manager
from .cache_manager import CacheManager
self.cache_manager = CacheManager(cache_duration_hours=24)
# Initialize logger
self.logger = None # Will be set after UI is created
# Register settings change listener for live updates
self.config_manager.register_listener(self._on_settings_changed)
def build(self) -> ft.Container:
"""Build and return the main UI with VS Code-style layout"""
# Top navigation bar with branding and buttons
top_nav = ft.Container(
content=ft.Row(
[
ft.IconButton(
icon=ft.icons.MENU,
tooltip="Toggle GitHub Tools",
on_click=self._toggle_sidebar,
),
ft.Icon(ft.icons.BOLT, color="blue", size=24),
ft.Text(
"GitHub Pulse",
size=20,
weight=ft.FontWeight.BOLD,
color="blue",
),
ft.Container(expand=True),
ft.IconButton(
icon=ft.icons.PSYCHOLOGY,
tooltip="Check AI Modules",
on_click=self._check_ai_modules_manual,
),
ft.IconButton(
icon=ft.icons.SETTINGS,
tooltip="Settings",
on_click=self._open_settings,
),
],
alignment=ft.MainAxisAlignment.START,
),
padding=15,
bgcolor=ft.colors.BLUE_GREY_900,
)
# Create sidebar (GitHub Tools) - collapsible
sidebar = ft.Container(
ref=self.sidebar_ref,
content=self._create_sidebar_content(),
width=350,
bgcolor=ft.colors.BLUE_GREY_900,
padding=15,
)
# Create main content area (tabs + status)
main_content = ft.Column(
[
self._create_status_section(),
self._create_tabs_section(),
],
spacing=10,
expand=True,
)
# Bottom section: Sidebar on left, content on right
bottom_section = ft.Row(
[
sidebar,
ft.VerticalDivider(width=1),
ft.Container(
content=main_content,
expand=True,
padding=20,
),
],
spacing=0,
expand=True,
vertical_alignment=ft.CrossAxisAlignment.STRETCH,
)
# Overall layout: Top nav + bottom section
app_layout = ft.Column(
[
top_nav,
ft.Divider(height=1),
bottom_section,
],
spacing=0,
expand=True,
)
# Initialize logger after UI is created
if self.log_text_ref.current:
self.logger = Logger(self.log_text_ref.current)
# Start async initialization
self.page.run_task(self._async_init)
return ft.Container(
content=app_layout,
expand=True,
)
async def _async_init(self):
"""Async initialization"""
await asyncio.sleep(0.5)
await self._auto_load_cached_items()
await self._load_custom_instructions()
await self._init_load_repos()
def _toggle_sidebar(self, e):
"""Toggle sidebar visibility"""
self.sidebar_visible = not self.sidebar_visible
if self.sidebar_ref.current:
if self.sidebar_visible:
self.sidebar_ref.current.width = 350
self.sidebar_ref.current.visible = True
else:
self.sidebar_ref.current.width = 0
self.sidebar_ref.current.visible = False
self.page.update()
def _create_title_section(self) -> ft.Container:
"""Create the title section with buttons"""
return ft.Container(
content=ft.Row(
[
ft.Container(expand=True),
ft.IconButton(
icon=ft.icons.PSYCHOLOGY,
tooltip="Check AI Modules",
on_click=self._check_ai_modules_manual,
),
ft.IconButton(
icon=ft.icons.SETTINGS,
tooltip="Settings",
on_click=self._open_settings,
),
],
alignment=ft.MainAxisAlignment.END,
),
padding=ft.padding.only(bottom=10),
)
def _create_sidebar_content(self) -> ft.Column:
"""Create the controls section"""
# Mode selection
mode_controls = ft.RadioGroup(
ref=self.tools_mode_ref,
content=ft.Row([
ft.Radio(value="create", label="Create PR/Issue"),
ft.Radio(value="action", label="Action Existing PR/Issue"),
]),
value="action",
on_change=self._on_mode_changed,
)
# Target Repository
target_repo_row = ft.Row(
[
ft.Dropdown(
ref=self.target_repo_dropdown_ref,
label="Target Repository",
hint_text="Select target repository",
options=[],
expand=True,
on_change=self._on_repo_selection_changed,
),
ft.IconButton(
icon=ft.icons.REFRESH,
tooltip="Refresh",
on_click=lambda e: self.page.run_task(self._refresh_target_repos_async),
),
ft.IconButton(
icon=ft.icons.SEARCH,
tooltip="Search",
on_click=lambda e: self.page.run_task(self._search_target_repos_async),
),
],
spacing=5,
)
# Forked Repository
forked_repo_row = ft.Row(
[
ft.Dropdown(
ref=self.forked_repo_dropdown_ref,
label="Forked Repository",
hint_text="Select forked repository",
options=[],
expand=True,
on_change=self._on_repo_selection_changed,
),
ft.IconButton(
icon=ft.icons.REFRESH,
tooltip="Refresh",
on_click=lambda e: self.page.run_task(self._refresh_forked_repos_async),
),
ft.IconButton(
icon=ft.icons.DOWNLOAD,
tooltip="Clone",
on_click=self._clone_forked_repo,
),
],
spacing=5,
)
# Action controls (for action mode)
action_controls = ft.Column(
[
ft.Text("View", weight=ft.FontWeight.BOLD),
ft.RadioGroup(
ref=self.repo_source_ref,
content=ft.Row([
ft.Radio(value="target", label="Target"),
ft.Radio(value="fork", label="Fork"),
]),
value="target",
on_change=lambda e: self._filter_workflow_items(),
),
ft.Text("Item Type", weight=ft.FontWeight.BOLD),
ft.RadioGroup(
ref=self.item_type_ref,
content=ft.Row([
ft.Radio(value="pull_request", label="PRs"),
ft.Radio(value="issue", label="Issues"),
]),
value="pull_request",
on_change=lambda e: self._filter_workflow_items(),
),
ft.Row([
ft.ElevatedButton(
"📥 Load Items",
on_click=lambda e: self.page.run_task(self._load_workflow_items_async),
),
ft.Text(ref=self.item_counter_ref, value="No items loaded"),
]),
ft.Dropdown(
ref=self.workflow_item_dropdown_ref,
label="Select Workflow Item",
hint_text="Select an item",
options=[],
expand=True,
on_change=self._on_workflow_item_selected,
),
],
spacing=10,
)
# Create controls (for create mode)
create_controls = ft.Column(
[
ft.Text("Create Type", weight=ft.FontWeight.BOLD),
ft.RadioGroup(
ref=self.create_type_ref,
content=ft.Row([
ft.Radio(value="pull_request", label="Pull Request"),
ft.Radio(value="issue", label="Issue"),
]),
value="pull_request",
),
ft.ElevatedButton(
"✏️ Create New",
on_click=self._create_new_item,
),
],
spacing=10,
visible=False,
)
# GitHub Tools content
return ft.Column(
[
ft.Row([
ft.Icon(ft.icons.SOURCE, size=20),
ft.Text("GitHub Tools", size=18, weight=ft.FontWeight.BOLD),
]),
ft.Divider(height=20),
mode_controls,
ft.Divider(height=10),
target_repo_row,
forked_repo_row,
ft.Divider(height=10),
action_controls,
create_controls,
],
spacing=10,
scroll=ft.ScrollMode.AUTO,
expand=True, # Make column expand to fill available space
)
def _create_status_section(self) -> ft.Container:
"""Create the status section"""
return ft.Container(
content=ft.Column([
ft.ProgressBar(ref=self.progress_bar_ref, visible=False),
ft.Text(ref=self.status_text_ref, value="Ready", size=14),
]),
padding=ft.padding.symmetric(vertical=10),
)
def _create_tabs_section(self) -> ft.Container:
"""Create the tabbed interface"""
tabs = ft.Tabs(
selected_index=0,
animation_duration=300,
tabs=[
ft.Tab(
text="Current Item",
icon=ft.icons.DESCRIPTION,
content=self._create_current_item_tab()
),
ft.Tab(
text="View Diff",
icon=ft.icons.DIFFERENCE,
content=self._create_diff_tab()
),
ft.Tab(
text="Processing Log",
icon=ft.icons.LIST_ALT,
content=self._create_log_tab()
),
ft.Tab(
text="All Items",
icon=ft.icons.VIEW_LIST,
content=self._create_all_items_tab()
),
],
expand=True,
)
return ft.Container(
content=tabs,
expand=True,
)
def _create_current_item_tab(self) -> ft.Container:
"""Create the current item tab"""
# 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",
ref=self.go_button_ref,
icon=ft.icons.PLAY_ARROW,
on_click=self._create_github_resource,
disabled=True,
),
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
)
# Work Item ID (clickable)
work_item_id = ft.Text(
ref=self.work_item_id_ref,
value="No item selected",
size=16,
weight=ft.FontWeight.BOLD,
color="blue",
)
# Fields
nature_text = ft.TextField(
ref=self.nature_text_ref,
label="Nature of Request",
multiline=True,
min_lines=2,
max_lines=4,
read_only=True,
expand=True,
)
live_doc_url = ft.TextField(
ref=self.live_doc_url_ref,
label="Live Doc URL",
read_only=True,
expand=True,
)
text_to_change = ft.TextField(
ref=self.text_to_change_ref,
label="Text to Change",
multiline=True,
min_lines=5,
max_lines=10,
read_only=True,
expand=True,
)
# Proposed New Text with Edit button
proposed_header = ft.Row(
[
ft.Text("Proposed New Text", weight=ft.FontWeight.BOLD),
ft.Container(expand=True),
ft.IconButton(
ref=self.edit_button_ref,
icon=ft.icons.EDIT,
tooltip="Edit",
on_click=self._toggle_edit_mode,
disabled=True,
),
],
)
proposed_new_text = ft.TextField(
ref=self.proposed_new_text_ref,
multiline=True,
min_lines=5,
max_lines=10,
read_only=True,
expand=True,
)
# Custom Instructions
custom_instructions_header = ft.Row(
[
ft.Text("Custom AI Instructions", weight=ft.FontWeight.BOLD),
ft.Container(expand=True),
ft.IconButton(
icon=ft.icons.SAVE,
tooltip="Save Instructions",
on_click=self.save_custom_instructions,
),
ft.IconButton(
icon=ft.icons.DELETE,
tooltip="Clear Instructions",
on_click=self.clear_custom_instructions,
),
],
)
custom_instructions = ft.TextField(
ref=self.custom_instructions_ref,
hint_text="Enter custom instructions for AI processing...",
multiline=True,
min_lines=3,
max_lines=6,
expand=True,
)
return ft.Container(
content=ft.ListView(
controls=[
nav_buttons,
work_item_id,
ft.Divider(),
nature_text,
live_doc_url,
text_to_change,
proposed_header,
proposed_new_text,
ft.Divider(),
custom_instructions_header,
custom_instructions,
],
spacing=15,
padding=20,
),
expand=True,
)
def _create_diff_tab(self) -> ft.Container:
"""Create the diff view tab"""
diff_buttons = ft.Row(
[
ft.ElevatedButton(
"Find .diff Files",
icon=ft.icons.SEARCH,
on_click=self.find_and_load_diff_files,
),
ft.ElevatedButton(
"Clear Diff",
icon=ft.icons.CLEAR,
on_click=self.clear_diff_display,
),
],
spacing=10,
)
diff_text = ft.TextField(
ref=self.diff_text_ref,
multiline=True,
read_only=True,
expand=True,
text_style=ft.TextStyle(font_family="Courier New"),
)
return ft.Container(
content=ft.Column([
diff_buttons,
diff_text,
], spacing=10, expand=True),
padding=20,
expand=True,
)
def _create_log_tab(self) -> ft.Container:
"""Create the processing log tab"""
log_text = ft.TextField(
ref=self.log_text_ref,
multiline=True,
read_only=True,
expand=True,
text_style=ft.TextStyle(font_family="Courier New"),
)
return ft.Container(
content=log_text,
padding=20,
expand=True,
)
def _create_all_items_tab(self) -> ft.Container:
"""Create the all items tab"""
# DataTable for items
items_table = ft.DataTable(
ref=self.items_table_ref,
columns=[
ft.DataColumn(ft.Text("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("Status")),
],
rows=[],
)
set_current_button = ft.ElevatedButton(
"Set as Current Item",
icon=ft.icons.CHECK_CIRCLE,
on_click=self._select_current_item,
)
return ft.Container(
content=ft.Column([
set_current_button,
ft.ListView(
controls=[items_table],
expand=True,
),
], spacing=10, expand=True),
padding=20,
expand=True,
)
# ===== Event Handlers =====
def _on_settings_changed(self, key: str, value: any):
"""
Handle settings changes from settings dialog (live updates).
Args:
key: Setting key that changed
value: New value
"""
# Update repository dropdowns when repos change in settings
if key == 'GITHUB_REPO':
if self.target_repo_dropdown_ref.current:
self.target_repo_dropdown_ref.current.value = value
self.page.update()
print(f"✓ Main GUI: Target repo updated to {value}")
elif key == 'FORKED_REPO':
if self.forked_repo_dropdown_ref.current:
self.forked_repo_dropdown_ref.current.value = value
self.page.update()
print(f"✓ Main GUI: Forked repo updated to {value}")
def _on_mode_changed(self, e):
"""Handle mode change between create and action"""
# This would toggle visibility of create vs action controls
# Implementation depends on UI structure
pass
def _on_repo_selection_changed(self, e):
"""Handle repository selection change"""
# Save selected repos to settings
config = self.config_manager.get_config()
if self.target_repo_dropdown_ref.current and self.target_repo_dropdown_ref.current.value:
target_value = self.target_repo_dropdown_ref.current.value
# Don't save separator headers
if not target_value.startswith('---'):
config['GITHUB_REPO'] = target_value
if self.forked_repo_dropdown_ref.current and self.forked_repo_dropdown_ref.current.value:
forked_value = self.forked_repo_dropdown_ref.current.value
# Don't save separator headers
if not forked_value.startswith('---'):
config['FORKED_REPO'] = forked_value
# Save to config
self.config_manager.save_configuration(config)
# Clear workflow items when repos change
self.workflow_items = {}
self.current_workflow_items = []
if self.workflow_item_dropdown_ref.current:
self.workflow_item_dropdown_ref.current.options = []
self.page.update()
def _on_workflow_item_selected(self, e):
"""Handle workflow item selection"""
if not self.workflow_item_dropdown_ref.current:
return
selected = self.workflow_item_dropdown_ref.current.value
if selected:
# Find the item and display it
for item in self.current_workflow_items:
if hasattr(item, 'title') and item.title == selected:
self._display_workflow_item(item)
break
def _filter_workflow_items(self):
"""Filter workflow items based on current selections"""
print("=" * 60)
print("FILTER METHOD CALLED")
print("=" * 60)
if not self.repo_source_ref.current or not self.item_type_ref.current:
print("ERROR: repo_source or item_type ref not available")
if self.logger:
self.logger.log("Cannot filter: repo source or item type not selected")
return
source = self.repo_source_ref.current.value
item_type = self.item_type_ref.current.value
print(f"DEBUG: source='{source}', item_type='{item_type}'")
# Map item_type to the correct key suffix
# "pull_request" → "prs", "issue" → "issues"
if item_type == "pull_request":
type_suffix = "prs"
elif item_type == "issue":
type_suffix = "issues"
else:
type_suffix = f"{item_type}s"
key = f"{source}_{type_suffix}"
print(f"DEBUG: Mapped item_type '{item_type}' to suffix '{type_suffix}'")
print(f"DEBUG: Looking for key '{key}'")
print(f"DEBUG: Available keys in workflow_items: {list(self.workflow_items.keys())}")
self.current_workflow_items = self.workflow_items.get(key, [])
print(f"DEBUG: Found {len(self.current_workflow_items)} items for key '{key}'")
if self.logger:
self.logger.log(f"Filtering workflow items: source={source}, type={item_type}, key={key}")
self.logger.log(f"Available workflow item keys: {list(self.workflow_items.keys())}")
self.logger.log(f"Found {len(self.current_workflow_items)} items for key '{key}'")
# Update dropdown
if self.workflow_item_dropdown_ref.current:
options = []
for item in self.current_workflow_items:
if hasattr(item, 'title'):
options.append(ft.dropdown.Option(item.title))
print(f" - Added item: {item.title}")
else:
print(f" - WARNING: Item has no title attribute: {item}")
print(f"DEBUG: Created {len(options)} dropdown options")
self.workflow_item_dropdown_ref.current.options = options
if self.item_counter_ref.current:
count_text = f"{len(options)} item(s) loaded"
if len(options) == 0:
count_text = f"No {item_type}s found in {source} repo"
self.item_counter_ref.current.value = count_text
print(f"DEBUG: Counter text set to: {count_text}")
print("DEBUG: Calling page.update()...")
self.page.update()
print("DEBUG: page.update() completed")
else:
print("ERROR: workflow_item_dropdown_ref.current is None!")
def _display_workflow_item(self, item):
"""Display a workflow item"""
# Implementation would populate fields with workflow item data
pass
def _previous_item(self, e):
"""Navigate to previous item"""
if self.current_item_index > 0:
self.current_item_index -= 1
self._display_current_item()
self._update_navigation_buttons()
def _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:
return
self.edit_mode = not self.edit_mode
if self.edit_mode:
self.proposed_new_text_ref.current.read_only = False
self.edit_button_ref.current.icon = ft.icons.SAVE
self.edit_button_ref.current.tooltip = "Save"
else:
# Save the changes
if self.current_work_items and self.current_item_index < len(self.current_work_items):
self.current_work_items[self.current_item_index]['new_text'] = \
self.proposed_new_text_ref.current.value
self.proposed_new_text_ref.current.read_only = True
self.edit_button_ref.current.icon = ft.icons.EDIT
self.edit_button_ref.current.tooltip = "Edit"
self.page.update()
def save_custom_instructions(self, e):
"""Save custom AI instructions"""
if not self.custom_instructions_ref.current:
return
instructions = self.custom_instructions_ref.current.value
config_values = {'CUSTOM_INSTRUCTIONS': instructions}
success = self.config_manager.save_configuration(config_values)
if success:
self._show_snackbar("Custom instructions saved successfully!")
else:
self._show_snackbar("Failed to save custom instructions", error=True)
def clear_custom_instructions(self, e):
"""Clear custom instructions"""
if self.custom_instructions_ref.current:
self.custom_instructions_ref.current.value = ""
self.page.update()
def _create_github_resource(self, e):
"""Create GitHub resource (PR or Issue)"""
# Implementation would handle GitHub resource creation
self._show_snackbar("Creating GitHub resource...")
def _create_new_item(self, e):
"""Create new PR/Issue"""
# Implementation for creating new items
pass
def _select_current_item(self, e):
"""Set selected item as current from table"""
# Implementation to set current item from table selection
pass
def find_and_load_diff_files(self, e):
"""Find and load .diff files"""
# Implementation to find and load diff files
pass
def clear_diff_display(self, e):
"""Clear the diff display"""
if self.diff_text_ref.current:
self.diff_text_ref.current.value = ""
self.page.update()
# ===== Async Operations =====
async def _auto_load_cached_items(self):
"""Auto-load cached items on startup"""
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}")
async def _load_custom_instructions(self):
"""Load custom instructions from config"""
try:
config = self.config_manager.get_config()
instructions = config.get('CUSTOM_INSTRUCTIONS', '')
if self.custom_instructions_ref.current:
self.custom_instructions_ref.current.value = instructions
self.page.update()
except Exception as e:
print(f"Error loading custom instructions: {e}")
async def _init_load_repos(self):
"""Initialize repository loading"""
await self._load_target_repos_async()
await self._load_forked_repos_async()
async def _load_target_repos_async(self):
"""Load target repositories"""
def load_repos():
try:
github_token = self.config_manager.get_config().get('GITHUB_PAT', '')
if not github_token:
return
from .workflow import GitHubRepoFetcher
repo_fetcher = GitHubRepoFetcher(github_token, self.logger)
repos = repo_fetcher.fetch_repos_with_permissions(min_permission='push')
self.target_repos = repo_fetcher.get_repo_names(repos)
# Update UI
if self.target_repo_dropdown_ref.current:
self.page.run_task(self._update_target_dropdown_async)
except Exception as e:
if self.logger:
self.logger.log(f"Error loading target repos: {e}")
await asyncio.to_thread(load_repos)
async def _update_target_dropdown_async(self):
"""Update target repository dropdown"""
if not self.target_repo_dropdown_ref.current:
return
options = []
if self.target_repos:
options.append(ft.dropdown.Option("--- Your Repos (with edit access) ---", disabled=True))
options.extend([ft.dropdown.Option(repo) for repo in self.target_repos])
self.target_repo_dropdown_ref.current.options = options
# Set value from saved settings
saved_repo = self.config_manager.get_config().get('GITHUB_REPO', '')
if saved_repo:
self.target_repo_dropdown_ref.current.value = saved_repo
self.page.update()
async def _refresh_target_repos_async(self):
"""Refresh target repositories"""
await self._load_target_repos_async()
async def _search_target_repos_async(self):
"""Search for repositories on GitHub"""
# Implementation would search GitHub repos
pass
async def _load_forked_repos_async(self):
"""Load forked repositories"""
def load_forks():
try:
# Load local repos
local_repo_path = self.config_manager.get_config().get('LOCAL_REPO_PATH', '')
if local_repo_path:
try:
from .utils import LocalRepositoryScanner
self.forked_repos['local'] = LocalRepositoryScanner.scan_local_repos(local_repo_path)
except Exception as e:
print(f"Error scanning local repos: {e}")
# Load GitHub repos
github_token = self.config_manager.get_config().get('GITHUB_PAT', '')
if github_token:
from .workflow import GitHubRepoFetcher
repo_fetcher = GitHubRepoFetcher(github_token, self.logger)
repos = repo_fetcher.fetch_user_repos(repo_type='owner')
self.forked_repos['github'] = repo_fetcher.get_repo_names(repos)
# Update UI
if self.forked_repo_dropdown_ref.current:
self.page.run_task(self._update_forked_dropdown_async)
except Exception as e:
if self.logger:
self.logger.log(f"Error loading forked repos: {e}")
await asyncio.to_thread(load_forks)
async def _update_forked_dropdown_async(self):
"""Update forked repository dropdown"""
if not self.forked_repo_dropdown_ref.current:
return
options = []
# Add local repos
if self.forked_repos.get('local'):
options.append(ft.dropdown.Option("--- Local Repositories ---", disabled=True))
options.extend([ft.dropdown.Option(repo) for repo in self.forked_repos['local']])
# Add GitHub repos
if self.forked_repos.get('github'):
options.append(ft.dropdown.Option("--- Your GitHub Repos ---", disabled=True))
options.extend([ft.dropdown.Option(repo) for repo in self.forked_repos['github']])
self.forked_repo_dropdown_ref.current.options = options
# Set value from saved settings
saved_repo = self.config_manager.get_config().get('FORKED_REPO', '')
if saved_repo:
self.forked_repo_dropdown_ref.current.value = saved_repo
self.page.update()
async def _refresh_forked_repos_async(self):
"""Refresh forked repositories"""
await self._load_forked_repos_async()
def _clone_forked_repo(self, e):
"""Clone forked repository"""
# Implementation would clone the selected repo
pass
async def _load_workflow_items_async(self):
"""Load workflow items (PRs/Issues)"""
print("=" * 60)
print("🔄 Load Items button clicked!")
print("=" * 60)
if self.logger:
self.logger.log("=" * 60)
self.logger.log("🔄 Load Items button clicked - starting workflow item load")
self.logger.log("=" * 60)
def load_items():
try:
print(f"DEBUG: target_repo_dropdown exists: {self.target_repo_dropdown_ref.current is not None}")
print(f"DEBUG: forked_repo_dropdown exists: {self.forked_repo_dropdown_ref.current is not None}")
if self.target_repo_dropdown_ref.current:
print(f"DEBUG: target_repo value = '{self.target_repo_dropdown_ref.current.value}'")
if self.forked_repo_dropdown_ref.current:
print(f"DEBUG: forked_repo value = '{self.forked_repo_dropdown_ref.current.value}'")
if not self.target_repo_dropdown_ref.current and not self.forked_repo_dropdown_ref.current:
if self.logger:
self.logger.log("❌ No repositories dropdown controls found")
print("ERROR: No repo dropdowns found!")
return
github_token = self.config_manager.get_config().get('GITHUB_PAT', '')
if not github_token:
if self.logger:
self.logger.log("❌ No GitHub token configured")
print("ERROR: No GitHub token!")
return
from .workflow import WorkflowManager
workflow_manager = WorkflowManager(github_token, self.logger)
# Load from target repo
target_repo = self.target_repo_dropdown_ref.current.value if self.target_repo_dropdown_ref.current else None
print(f"DEBUG: target_repo extracted = '{target_repo}'")
print(f"DEBUG: Validation checks:")
print(f" - target_repo is not None: {target_repo is not None}")
print(f" - not starts with '---': {not target_repo.startswith('---') if target_repo else 'N/A'}")
print(f" - contains '/': {'/' in target_repo if target_repo else 'N/A'}")
# Filter out separator headers and None values
if target_repo and not target_repo.startswith('---') and '/' in target_repo:
print(f"✓ Validation PASSED for target repo: {target_repo}")
if self.logger:
self.logger.log(f"📥 Loading PRs and issues from target repo: {target_repo}")
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)
pr_count = len(self.workflow_items.get('target_prs', []))
issue_count = len(self.workflow_items.get('target_issues', []))
print(f"✓ Loaded {pr_count} PRs and {issue_count} issues from target repo")
if self.logger:
self.logger.log(f"✅ Loaded {pr_count} PRs and {issue_count} issues from target repo")
else:
print(f"✗ Validation FAILED for target repo: {target_repo}")
# Load from forked repo
forked_repo = self.forked_repo_dropdown_ref.current.value if self.forked_repo_dropdown_ref.current else None
# Filter out separator headers and None values
if forked_repo and not forked_repo.startswith('---') and '/' in forked_repo:
if self.logger:
self.logger.log(f"Loading PRs and issues from forked repo: {forked_repo}")
self.workflow_items['fork_prs'] = workflow_manager.fetch_pull_requests(forked_repo)
self.workflow_items['fork_issues'] = workflow_manager.fetch_issues(forked_repo)
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)
except Exception as e:
if self.logger:
self.logger.log(f"Error loading workflow items: {e}")
import traceback
self.logger.log(traceback.format_exc())
await asyncio.to_thread(load_items)
async def _filter_workflow_items_async(self):
"""Filter workflow items async"""
self._filter_workflow_items()
# ===== Helper Methods =====
def _display_current_item(self):
"""Display the current work item"""
if not self.current_work_items or self.current_item_index >= len(self.current_work_items):
return
item = self.current_work_items[self.current_item_index]
# Update UI fields
if self.work_item_id_ref.current:
self.work_item_id_ref.current.value = f"Work Item {item.get('id', 'N/A')}"
if self.nature_text_ref.current:
self.nature_text_ref.current.value = item.get('nature', '')
if self.live_doc_url_ref.current:
self.live_doc_url_ref.current.value = item.get('live_doc_url', '')
if self.text_to_change_ref.current:
self.text_to_change_ref.current.value = item.get('old_text', '')
if self.proposed_new_text_ref.current:
self.proposed_new_text_ref.current.value = item.get('new_text', '')
self.page.update()
self._update_navigation_buttons()
def _update_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:
self.status_text_ref.current.value = message
self.page.update()
def _show_progress(self):
"""Show progress bar"""
if self.progress_bar_ref.current:
self.progress_bar_ref.current.visible = True
self.page.update()
def _hide_progress(self):
"""Hide progress bar"""
if self.progress_bar_ref.current:
self.progress_bar_ref.current.visible = False
self.page.update()
def _show_snackbar(self, message: str, error: bool = False):
"""Show snackbar notification"""
self.page.snack_bar = ft.SnackBar(
content=ft.Text(message),
bgcolor="error" if error else "green",
)
self.page.snack_bar.open = True
self.page.update()
def _open_settings(self, e):
"""Open settings dialog"""
try:
print("Settings button clicked!")
# Use Flet 0.28+ API: page.open() instead of page.dialog
config = self.config_manager.get_config()
print(f"Got config: {config.keys() if config else 'None'}")
settings_dialog = SettingsDialog(
self.page,
config,
self.config_manager,
self.cache_manager
)
print("SettingsDialog created")
def on_settings_result(result):
if result:
# Reload configuration
self.config_manager.load_configuration()
self._show_snackbar("Settings saved successfully!")
print("Calling settings_dialog.show()...")
settings_dialog.show(on_result=on_settings_result)
print("settings_dialog.show() completed")
except Exception as ex:
print(f"Error in _open_settings: {ex}")
import traceback
traceback.print_exc()
self._show_snackbar(f"Error opening settings: {ex}", error=True)
def _show_real_settings(self):
"""Show the real settings dialog"""
try:
config = self.config_manager.get_config()
print(f"Got config: {config.keys() if config else 'None'}")
settings_dialog = SettingsDialog(
self.page,
config,
self.config_manager,
self.cache_manager
)
print("SettingsDialog created")
def on_settings_result(result):
if result:
# Reload configuration
self.config_manager.load_configuration()
self._show_snackbar("Settings saved successfully!")
print("Calling settings_dialog.show()...")
settings_dialog.show(on_result=on_settings_result)
print("settings_dialog.show() completed")
except Exception as ex:
print(f"Error in _show_real_settings: {ex}")
import traceback
traceback.print_exc()
self._show_snackbar(f"Error showing settings: {ex}", error=True)
def _check_ai_modules_manual(self, e):
"""Manually check AI modules"""
config = self.config_manager.get_config()
ai_provider = config.get('AI_PROVIDER', 'none').lower()
if ai_provider and ai_provider != 'none':
self.page.run_task(lambda: self._check_ai_provider_async(ai_provider))
else:
self._show_snackbar("No AI provider configured")
async def _check_ai_provider_async(self, ai_provider: str):
"""Check AI provider setup"""
try:
available, missing = self.ai_manager.check_ai_module_availability(ai_provider)
if available:
self._show_snackbar(f"AI Provider ({ai_provider}): All modules available!")
else:
self._show_snackbar(
f"AI Provider ({ai_provider}): Missing packages: {', '.join(missing)}",
error=True
)
except Exception as e:
self._show_snackbar(f"Error checking AI provider: {e}", error=True)
def update_diff_display(self, diff_content: str):
"""Update diff display"""
if self.diff_text_ref.current:
self.diff_text_ref.current.value = diff_content
self.page.update()
class Logger:
"""Logger class for Flet"""
def __init__(self, text_field: ft.TextField):
self.text_field = text_field
def log(self, message: str):
"""Log a message"""
import datetime
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_message = f"[{timestamp}] {message}\n"
if self.text_field:
current = self.text_field.value or ""
self.text_field.value = current + log_message
# Auto-scroll is handled by Flet