"""Providers page - manage AI providers.""" from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QHeaderView, QPushButton, QLabel, QMessageBox ) from PySide6.QtCore import Qt from ...providers import Provider, load_providers, delete_provider class ProvidersPage(QWidget): """Provider management page.""" def __init__(self, main_window): super().__init__() self.main_window = main_window self._setup_ui() def _setup_ui(self): """Set up the UI.""" layout = QVBoxLayout(self) layout.setContentsMargins(24, 24, 24, 24) layout.setSpacing(16) # Header header = QWidget() header_layout = QHBoxLayout(header) header_layout.setContentsMargins(0, 0, 0, 0) title = QLabel("AI Providers") title.setObjectName("heading") header_layout.addWidget(title) header_layout.addStretch() self.btn_install = QPushButton("Install Provider...") self.btn_install.setToolTip("Install a new AI provider CLI tool") self.btn_install.clicked.connect(self._install_provider) header_layout.addWidget(self.btn_install) self.btn_add = QPushButton("Add Provider") self.btn_add.setToolTip("Manually add a provider configuration") self.btn_add.clicked.connect(self._add_provider) header_layout.addWidget(self.btn_add) layout.addWidget(header) # Description desc = QLabel( "Providers are external AI commands that CmdForge tools can use. " "Each provider wraps a CLI tool that accepts input on stdin and outputs to stdout." ) desc.setWordWrap(True) desc.setStyleSheet("color: #718096;") layout.addWidget(desc) # Providers table self.table = QTableWidget() self.table.setColumnCount(3) self.table.setHorizontalHeaderLabels(["Name", "Command", "Description"]) self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) self.table.setSelectionBehavior(QTableWidget.SelectRows) self.table.setSelectionMode(QTableWidget.SingleSelection) self.table.verticalHeader().setVisible(False) self.table.itemSelectionChanged.connect(self._on_selection_changed) layout.addWidget(self.table, 1) # Action buttons buttons = QWidget() btn_layout = QHBoxLayout(buttons) btn_layout.setContentsMargins(0, 0, 0, 0) btn_layout.setSpacing(12) self.btn_edit = QPushButton("Edit") self.btn_edit.setObjectName("secondary") self.btn_edit.clicked.connect(self._edit_provider) self.btn_edit.setEnabled(False) btn_layout.addWidget(self.btn_edit) self.btn_delete = QPushButton("Delete") self.btn_delete.setObjectName("danger") self.btn_delete.clicked.connect(self._delete_provider) self.btn_delete.setEnabled(False) btn_layout.addWidget(self.btn_delete) btn_layout.addStretch() self.btn_test = QPushButton("Test") self.btn_test.clicked.connect(self._test_provider) self.btn_test.setEnabled(False) btn_layout.addWidget(self.btn_test) layout.addWidget(buttons) def refresh(self): """Refresh the provider list.""" providers = load_providers() self.table.setRowCount(len(providers)) for row, provider in enumerate(sorted(providers, key=lambda p: p.name)): name_item = QTableWidgetItem(provider.name) name_item.setData(Qt.UserRole, provider) self.table.setItem(row, 0, name_item) self.table.setItem(row, 1, QTableWidgetItem(provider.command)) self.table.setItem(row, 2, QTableWidgetItem(provider.description or "")) self._on_selection_changed() def _on_selection_changed(self): """Handle selection change.""" has_selection = len(self.table.selectedItems()) > 0 self.btn_edit.setEnabled(has_selection) self.btn_delete.setEnabled(has_selection) self.btn_test.setEnabled(has_selection) def _get_selected_provider(self) -> tuple: """Get selected provider name and object.""" items = self.table.selectedItems() if not items: return None, None row = items[0].row() name_item = self.table.item(row, 0) return name_item.text(), name_item.data(Qt.UserRole) def _install_provider(self): """Open the provider installation wizard.""" from ..dialogs.provider_install_dialog import ProviderInstallDialog dialog = ProviderInstallDialog(self) if dialog.exec(): self.refresh() self.main_window.show_status("Providers updated") def _add_provider(self): """Add a new provider.""" from ..dialogs.provider_dialog import ProviderDialog dialog = ProviderDialog(self) if dialog.exec(): self.refresh() self.main_window.show_status("Provider added") def _edit_provider(self): """Edit selected provider.""" name, provider = self._get_selected_provider() if not name: return from ..dialogs.provider_dialog import ProviderDialog dialog = ProviderDialog(self, name, provider) if dialog.exec(): self.refresh() self.main_window.show_status("Provider updated") def _delete_provider(self): """Delete selected provider.""" name, _ = self._get_selected_provider() if not name: return reply = QMessageBox.question( self, "Delete Provider", f"Are you sure you want to delete provider '{name}'?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: try: delete_provider(name) self.refresh() self.main_window.show_status(f"Deleted provider '{name}'") except Exception as e: QMessageBox.critical(self, "Error", f"Failed to delete provider:\n{e}") def _test_provider(self): """Test selected provider.""" name, provider = self._get_selected_provider() if not name or not provider: return import subprocess import shutil # Check if command exists cmd_parts = provider.command.split() if not cmd_parts: QMessageBox.warning(self, "Invalid", "Provider has no command configured") return exe = cmd_parts[0] if not shutil.which(exe): QMessageBox.warning( self, "Not Found", f"Command '{exe}' not found in PATH.\n\n" "Make sure the provider is installed and accessible." ) return # Try running with --help or similar try: result = subprocess.run( cmd_parts + ["--help"], capture_output=True, text=True, timeout=5 ) QMessageBox.information( self, "Provider Test", f"Provider '{name}' is available.\n\n" f"Command: {provider.command}\n" f"Exit code: {result.returncode}" ) except subprocess.TimeoutExpired: QMessageBox.information( self, "Provider Test", f"Provider '{name}' command exists but timed out.\n" "This may be normal for interactive commands." ) except Exception as e: QMessageBox.warning( self, "Provider Test", f"Error testing provider:\n{e}" )