CmdForge/src/cmdforge/gui/pages/providers_page.py

229 lines
7.9 KiB
Python

"""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}"
)