229 lines
7.9 KiB
Python
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}"
|
|
)
|