Convert TUI to PySide6 desktop GUI
Major UI overhaul replacing the urwid-based Terminal UI with a modern PySide6 desktop application. New GUI features: - Sidebar navigation (My Tools, Registry, Providers) - Tool Builder with visual form for creating/editing tools - Registry browser with search and one-click install - Provider management page - Connect dialog for account pairing - Publish dialog for sharing tools - Keyboard shortcuts (Ctrl+N, Ctrl+S, Ctrl+R, Ctrl+1/2/3, Escape, Ctrl+Q) - Window geometry persistence (remembers size/position) - Modern clean stylesheet Removed: - ui.py, ui_snack.py, ui_registry.py, ui_urwid.py - ui_urwid/ directory (urwid TUI implementation) Updated: - pyproject.toml: PySide6 now required, removed urwid - CLI entry points to launch GUI - All documentation (README, CLAUDE.md, AGENTS.md, wiki) Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""GUI dialogs."""
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Argument editor dialog."""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QFormLayout, QLineEdit,
|
||||
QCheckBox, QPushButton, QHBoxLayout, QLabel
|
||||
)
|
||||
|
||||
from ...tool import ToolArgument
|
||||
|
||||
|
||||
class ArgumentDialog(QDialog):
|
||||
"""Dialog for editing tool arguments."""
|
||||
|
||||
def __init__(self, parent, argument: ToolArgument = None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Edit Argument" if argument else "Add Argument")
|
||||
self.setMinimumWidth(400)
|
||||
self._argument = argument
|
||||
self._setup_ui()
|
||||
|
||||
if argument:
|
||||
self._load_argument(argument)
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the UI."""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# Form
|
||||
form = QFormLayout()
|
||||
form.setSpacing(12)
|
||||
|
||||
self.flag_input = QLineEdit()
|
||||
self.flag_input.setPlaceholderText("--input, -i")
|
||||
form.addRow("Flag:", self.flag_input)
|
||||
|
||||
self.var_input = QLineEdit()
|
||||
self.var_input.setPlaceholderText("input_text")
|
||||
form.addRow("Variable:", self.var_input)
|
||||
|
||||
self.default_input = QLineEdit()
|
||||
self.default_input.setPlaceholderText("(optional)")
|
||||
form.addRow("Default:", self.default_input)
|
||||
|
||||
self.desc_input = QLineEdit()
|
||||
self.desc_input.setPlaceholderText("(optional)")
|
||||
form.addRow("Description:", self.desc_input)
|
||||
|
||||
layout.addLayout(form)
|
||||
|
||||
# Help text
|
||||
help_text = QLabel(
|
||||
"The flag is how users specify this argument (e.g. --input).\n"
|
||||
"The variable name is used in prompts and templates as ${variable}."
|
||||
)
|
||||
help_text.setStyleSheet("color: #718096; font-size: 11px;")
|
||||
help_text.setWordWrap(True)
|
||||
layout.addWidget(help_text)
|
||||
|
||||
# Buttons
|
||||
buttons = QHBoxLayout()
|
||||
buttons.addStretch()
|
||||
|
||||
self.btn_cancel = QPushButton("Cancel")
|
||||
self.btn_cancel.setObjectName("secondary")
|
||||
self.btn_cancel.clicked.connect(self.reject)
|
||||
buttons.addWidget(self.btn_cancel)
|
||||
|
||||
self.btn_ok = QPushButton("OK")
|
||||
self.btn_ok.clicked.connect(self._validate_and_accept)
|
||||
buttons.addWidget(self.btn_ok)
|
||||
|
||||
layout.addLayout(buttons)
|
||||
|
||||
def _load_argument(self, arg: ToolArgument):
|
||||
"""Load argument data into form."""
|
||||
self.flag_input.setText(arg.flag)
|
||||
self.var_input.setText(arg.variable)
|
||||
self.default_input.setText(arg.default or "")
|
||||
self.desc_input.setText(arg.description or "")
|
||||
|
||||
def _validate_and_accept(self):
|
||||
"""Validate input and accept."""
|
||||
flag = self.flag_input.text().strip()
|
||||
variable = self.var_input.text().strip()
|
||||
|
||||
if not flag:
|
||||
self.flag_input.setFocus()
|
||||
return
|
||||
|
||||
if not variable:
|
||||
self.var_input.setFocus()
|
||||
return
|
||||
|
||||
self.accept()
|
||||
|
||||
def get_argument(self) -> ToolArgument:
|
||||
"""Get the argument from form data."""
|
||||
return ToolArgument(
|
||||
flag=self.flag_input.text().strip(),
|
||||
variable=self.var_input.text().strip(),
|
||||
default=self.default_input.text().strip() or None,
|
||||
description=self.desc_input.text().strip() or ""
|
||||
)
|
||||
@@ -0,0 +1,234 @@
|
||||
"""Connect to registry dialog."""
|
||||
|
||||
import socket
|
||||
import time
|
||||
import requests
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QLabel, QPushButton,
|
||||
QHBoxLayout, QLineEdit, QProgressBar, QTextEdit
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Signal, QTimer
|
||||
|
||||
from ...config import load_config, save_config, get_registry_url, set_registry_token
|
||||
|
||||
|
||||
class PairingWorker(QThread):
|
||||
"""Background worker for pairing flow."""
|
||||
success = Signal(str) # token
|
||||
error = Signal(str)
|
||||
status = Signal(str)
|
||||
|
||||
def __init__(self, username: str):
|
||||
super().__init__()
|
||||
self.username = username
|
||||
self.hostname = socket.gethostname()
|
||||
self._stop = False
|
||||
|
||||
def stop(self):
|
||||
self._stop = True
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
registry_url = get_registry_url()
|
||||
|
||||
# Remove trailing /api/v1 if present to get base URL
|
||||
base_url = registry_url.rstrip("/")
|
||||
if base_url.endswith("/api/v1"):
|
||||
base_url = base_url[:-7]
|
||||
|
||||
pairing_url = f"{base_url}/api/v1/pairing/check/{self.username}"
|
||||
|
||||
self.status.emit(f"Connecting to {base_url}...")
|
||||
|
||||
# Poll for pairing status
|
||||
max_attempts = 150 # 5 minutes at 2-second intervals
|
||||
attempt = 0
|
||||
|
||||
while attempt < max_attempts:
|
||||
if self._stop:
|
||||
return
|
||||
|
||||
attempt += 1
|
||||
try:
|
||||
response = requests.get(
|
||||
pairing_url,
|
||||
params={"hostname": self.hostname},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
status = data.get("data", {}).get("status")
|
||||
|
||||
if status == "connected":
|
||||
token = data.get("data", {}).get("token")
|
||||
if token:
|
||||
self.success.emit(token)
|
||||
return
|
||||
else:
|
||||
self.error.emit("Pairing completed but no token received")
|
||||
return
|
||||
elif status == "expired":
|
||||
self.error.emit("Pairing request expired. Please try again.")
|
||||
return
|
||||
elif status == "pending":
|
||||
self.status.emit(f"Pairing pending, approve in web UI... ({attempt})")
|
||||
elif status == "not_found":
|
||||
self.status.emit(f"Waiting for web UI pairing... ({attempt})")
|
||||
else:
|
||||
self.status.emit(f"Status: {status} ({attempt})")
|
||||
elif response.status_code == 404:
|
||||
self.status.emit(f"Waiting for web approval... ({attempt})")
|
||||
else:
|
||||
self.status.emit(f"Server returned {response.status_code}")
|
||||
|
||||
except requests.exceptions.RequestException:
|
||||
self.status.emit(f"Network error, retrying... ({attempt})")
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
self.error.emit("Timed out waiting for approval")
|
||||
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
class ConnectDialog(QDialog):
|
||||
"""Dialog for connecting to registry."""
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Connect to Registry")
|
||||
self.setMinimumWidth(450)
|
||||
self._worker = None
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the UI."""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# Title
|
||||
title = QLabel("Connect Your Account")
|
||||
title.setStyleSheet("font-size: 16px; font-weight: 600;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# Instructions
|
||||
self.instructions = QLabel(
|
||||
"To publish tools, connect your CmdForge account.\n\n"
|
||||
"1. Enter your CmdForge username below\n"
|
||||
"2. Click Connect\n"
|
||||
"3. Go to cmdforge.brrd.tech/dashboard/connected-apps\n"
|
||||
"4. Click 'Connect New App' and approve this device"
|
||||
)
|
||||
self.instructions.setWordWrap(True)
|
||||
layout.addWidget(self.instructions)
|
||||
|
||||
# Username input
|
||||
username_layout = QHBoxLayout()
|
||||
username_label = QLabel("Username:")
|
||||
self.username_input = QLineEdit()
|
||||
self.username_input.setPlaceholderText("your-username")
|
||||
self.username_input.returnPressed.connect(self._start_pairing)
|
||||
username_layout.addWidget(username_label)
|
||||
username_layout.addWidget(self.username_input, 1)
|
||||
layout.addLayout(username_layout)
|
||||
|
||||
# Device info
|
||||
hostname = socket.gethostname()
|
||||
device_label = QLabel(f"Device: {hostname}")
|
||||
device_label.setStyleSheet("color: #718096; font-size: 12px;")
|
||||
layout.addWidget(device_label)
|
||||
|
||||
# Status
|
||||
self.status_label = QLabel("")
|
||||
self.status_label.setStyleSheet("color: #718096;")
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
# Log area for debugging
|
||||
self.log_area = QTextEdit()
|
||||
self.log_area.setReadOnly(True)
|
||||
self.log_area.setMaximumHeight(120)
|
||||
self.log_area.setStyleSheet("font-family: monospace; font-size: 11px; background: #f7fafc;")
|
||||
self.log_area.hide()
|
||||
layout.addWidget(self.log_area)
|
||||
|
||||
# Progress
|
||||
self.progress = QProgressBar()
|
||||
self.progress.setRange(0, 0) # Indeterminate
|
||||
self.progress.hide()
|
||||
layout.addWidget(self.progress)
|
||||
|
||||
# Buttons
|
||||
buttons = QHBoxLayout()
|
||||
buttons.addStretch()
|
||||
|
||||
self.btn_cancel = QPushButton("Cancel")
|
||||
self.btn_cancel.setObjectName("secondary")
|
||||
self.btn_cancel.clicked.connect(self._cancel)
|
||||
buttons.addWidget(self.btn_cancel)
|
||||
|
||||
self.btn_connect = QPushButton("Connect")
|
||||
self.btn_connect.clicked.connect(self._start_pairing)
|
||||
buttons.addWidget(self.btn_connect)
|
||||
|
||||
layout.addLayout(buttons)
|
||||
|
||||
def _start_pairing(self):
|
||||
"""Start the pairing flow."""
|
||||
username = self.username_input.text().strip()
|
||||
if not username:
|
||||
self.status_label.setText("Please enter your username")
|
||||
self.status_label.setStyleSheet("color: #e53e3e;")
|
||||
return
|
||||
|
||||
self.btn_connect.setEnabled(False)
|
||||
self.username_input.setEnabled(False)
|
||||
self.progress.show()
|
||||
self.log_area.clear()
|
||||
self.log_area.show()
|
||||
self.status_label.setStyleSheet("color: #718096;")
|
||||
|
||||
self._worker = PairingWorker(username)
|
||||
self._worker.success.connect(self._on_success)
|
||||
self._worker.error.connect(self._on_error)
|
||||
self._worker.status.connect(self._on_status)
|
||||
self._worker.start()
|
||||
|
||||
def _on_success(self, token: str):
|
||||
"""Handle successful pairing."""
|
||||
set_registry_token(token)
|
||||
|
||||
self.progress.hide()
|
||||
self.status_label.setText("Connected successfully!")
|
||||
self.status_label.setStyleSheet("color: #38a169; font-weight: 600;")
|
||||
|
||||
QTimer.singleShot(1000, self.accept)
|
||||
|
||||
def _on_error(self, error: str):
|
||||
"""Handle error."""
|
||||
self.progress.hide()
|
||||
self.btn_connect.setEnabled(True)
|
||||
self.username_input.setEnabled(True)
|
||||
self.status_label.setText(f"Error: {error}")
|
||||
self.status_label.setStyleSheet("color: #e53e3e;")
|
||||
|
||||
def _on_status(self, status: str):
|
||||
"""Handle status update."""
|
||||
self.status_label.setText(status)
|
||||
# Also append to log for debugging
|
||||
self.log_area.append(status)
|
||||
|
||||
def _cancel(self):
|
||||
"""Cancel pairing."""
|
||||
if self._worker:
|
||||
self._worker.stop()
|
||||
self._worker.wait(1000)
|
||||
self.reject()
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle close."""
|
||||
if self._worker:
|
||||
self._worker.stop()
|
||||
self._worker.wait(1000)
|
||||
super().closeEvent(event)
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Provider editor dialog."""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QFormLayout, QLineEdit,
|
||||
QPushButton, QHBoxLayout, QLabel
|
||||
)
|
||||
|
||||
from ...providers import Provider, add_provider
|
||||
|
||||
|
||||
class ProviderDialog(QDialog):
|
||||
"""Dialog for adding/editing providers."""
|
||||
|
||||
def __init__(self, parent, name: str = None, provider: Provider = None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Edit Provider" if provider else "Add Provider")
|
||||
self.setMinimumWidth(450)
|
||||
self._editing = provider is not None
|
||||
self._original_name = name
|
||||
self._setup_ui()
|
||||
|
||||
if provider:
|
||||
self._load_provider(name, provider)
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the UI."""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# Form
|
||||
form = QFormLayout()
|
||||
form.setSpacing(12)
|
||||
|
||||
self.name_input = QLineEdit()
|
||||
self.name_input.setPlaceholderText("claude")
|
||||
form.addRow("Name:", self.name_input)
|
||||
|
||||
self.cmd_input = QLineEdit()
|
||||
self.cmd_input.setPlaceholderText("claude-cli")
|
||||
form.addRow("Command:", self.cmd_input)
|
||||
|
||||
self.desc_input = QLineEdit()
|
||||
self.desc_input.setPlaceholderText("Claude AI via claude-cli")
|
||||
form.addRow("Description:", self.desc_input)
|
||||
|
||||
layout.addLayout(form)
|
||||
|
||||
# Help text
|
||||
help_text = QLabel(
|
||||
"The command should accept input on stdin and output to stdout.\n"
|
||||
"Example commands: claude-cli, sgpt, llm, mods"
|
||||
)
|
||||
help_text.setStyleSheet("color: #718096; font-size: 11px;")
|
||||
help_text.setWordWrap(True)
|
||||
layout.addWidget(help_text)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
# Buttons
|
||||
buttons = QHBoxLayout()
|
||||
buttons.addStretch()
|
||||
|
||||
self.btn_cancel = QPushButton("Cancel")
|
||||
self.btn_cancel.setObjectName("secondary")
|
||||
self.btn_cancel.clicked.connect(self.reject)
|
||||
buttons.addWidget(self.btn_cancel)
|
||||
|
||||
self.btn_ok = QPushButton("Save")
|
||||
self.btn_ok.clicked.connect(self._save)
|
||||
buttons.addWidget(self.btn_ok)
|
||||
|
||||
layout.addLayout(buttons)
|
||||
|
||||
def _load_provider(self, name: str, provider: Provider):
|
||||
"""Load provider data into form."""
|
||||
self.name_input.setText(name)
|
||||
self.name_input.setEnabled(False) # Can't rename
|
||||
self.cmd_input.setText(provider.command)
|
||||
self.desc_input.setText(provider.description or "")
|
||||
|
||||
def _save(self):
|
||||
"""Save the provider."""
|
||||
name = self.name_input.text().strip()
|
||||
command = self.cmd_input.text().strip()
|
||||
|
||||
if not name:
|
||||
self.name_input.setFocus()
|
||||
return
|
||||
|
||||
if not command:
|
||||
self.cmd_input.setFocus()
|
||||
return
|
||||
|
||||
description = self.desc_input.text().strip() or None
|
||||
|
||||
try:
|
||||
add_provider(name, command, description)
|
||||
self.accept()
|
||||
except Exception as e:
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
QMessageBox.critical(self, "Error", f"Failed to save provider:\n{e}")
|
||||
@@ -0,0 +1,182 @@
|
||||
"""Publish tool dialog."""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QLabel, QPushButton,
|
||||
QHBoxLayout, QLineEdit, QTextEdit, QFormLayout,
|
||||
QProgressBar, QMessageBox, QComboBox
|
||||
)
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
|
||||
from ...tool import Tool
|
||||
from ...registry_client import RegistryClient, RegistryError
|
||||
from ...config import load_config
|
||||
|
||||
|
||||
class PublishWorker(QThread):
|
||||
"""Background worker for publishing."""
|
||||
success = Signal(dict)
|
||||
error = Signal(str)
|
||||
|
||||
def __init__(self, tool_data: dict):
|
||||
super().__init__()
|
||||
self.tool_data = tool_data
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
config = load_config()
|
||||
client = RegistryClient()
|
||||
client.token = config.registry.token
|
||||
|
||||
result = client.publish_tool(self.tool_data)
|
||||
self.success.emit(result)
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
class PublishDialog(QDialog):
|
||||
"""Dialog for publishing a tool."""
|
||||
|
||||
def __init__(self, parent, tool: Tool):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Publish Tool")
|
||||
self.setMinimumSize(500, 400)
|
||||
self._tool = tool
|
||||
self._worker = None
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the UI."""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# Title
|
||||
title = QLabel(f"Publish '{self._tool.name}'")
|
||||
title.setStyleSheet("font-size: 16px; font-weight: 600;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# Form
|
||||
form = QFormLayout()
|
||||
form.setSpacing(12)
|
||||
|
||||
# Version
|
||||
self.version_input = QLineEdit()
|
||||
self.version_input.setText("1.0.0")
|
||||
self.version_input.setPlaceholderText("1.0.0")
|
||||
form.addRow("Version:", self.version_input)
|
||||
|
||||
# Category
|
||||
self.category_combo = QComboBox()
|
||||
self.category_combo.setEditable(True)
|
||||
categories = [
|
||||
"Text Processing", "Code", "Data", "System",
|
||||
"Writing", "Analysis", "Productivity", "Other"
|
||||
]
|
||||
self.category_combo.addItems(categories)
|
||||
if self._tool.category:
|
||||
idx = self.category_combo.findText(self._tool.category)
|
||||
if idx >= 0:
|
||||
self.category_combo.setCurrentIndex(idx)
|
||||
else:
|
||||
self.category_combo.setCurrentText(self._tool.category)
|
||||
form.addRow("Category:", self.category_combo)
|
||||
|
||||
# Tags
|
||||
self.tags_input = QLineEdit()
|
||||
self.tags_input.setPlaceholderText("ai, text, productivity (comma separated)")
|
||||
form.addRow("Tags:", self.tags_input)
|
||||
|
||||
layout.addLayout(form)
|
||||
|
||||
# Description
|
||||
desc_label = QLabel("Description:")
|
||||
layout.addWidget(desc_label)
|
||||
|
||||
self.desc_input = QTextEdit()
|
||||
self.desc_input.setPlaceholderText("Describe what your tool does...")
|
||||
self.desc_input.setPlainText(self._tool.description or "")
|
||||
self.desc_input.setMaximumHeight(100)
|
||||
layout.addWidget(self.desc_input)
|
||||
|
||||
# Status
|
||||
self.status_label = QLabel("")
|
||||
self.status_label.setStyleSheet("color: #718096;")
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
# Progress
|
||||
self.progress = QProgressBar()
|
||||
self.progress.setRange(0, 0)
|
||||
self.progress.hide()
|
||||
layout.addWidget(self.progress)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
# Buttons
|
||||
buttons = QHBoxLayout()
|
||||
buttons.addStretch()
|
||||
|
||||
self.btn_cancel = QPushButton("Cancel")
|
||||
self.btn_cancel.setObjectName("secondary")
|
||||
self.btn_cancel.clicked.connect(self.reject)
|
||||
buttons.addWidget(self.btn_cancel)
|
||||
|
||||
self.btn_publish = QPushButton("Publish")
|
||||
self.btn_publish.clicked.connect(self._publish)
|
||||
buttons.addWidget(self.btn_publish)
|
||||
|
||||
layout.addLayout(buttons)
|
||||
|
||||
def _publish(self):
|
||||
"""Publish the tool."""
|
||||
version = self.version_input.text().strip()
|
||||
if not version:
|
||||
QMessageBox.warning(self, "Validation", "Version is required")
|
||||
return
|
||||
|
||||
description = self.desc_input.toPlainText().strip()
|
||||
if not description:
|
||||
QMessageBox.warning(self, "Validation", "Description is required")
|
||||
return
|
||||
|
||||
category = self.category_combo.currentText()
|
||||
tags = [t.strip() for t in self.tags_input.text().split(",") if t.strip()]
|
||||
|
||||
# Build tool data for publishing
|
||||
tool_data = {
|
||||
"name": self._tool.name,
|
||||
"version": version,
|
||||
"description": description,
|
||||
"category": category,
|
||||
"tags": tags,
|
||||
"definition": self._tool.to_dict() if hasattr(self._tool, 'to_dict') else {}
|
||||
}
|
||||
|
||||
self.btn_publish.setEnabled(False)
|
||||
self.btn_cancel.setEnabled(False)
|
||||
self.progress.show()
|
||||
self.status_label.setText("Publishing...")
|
||||
|
||||
self._worker = PublishWorker(tool_data)
|
||||
self._worker.success.connect(self._on_success)
|
||||
self._worker.error.connect(self._on_error)
|
||||
self._worker.start()
|
||||
|
||||
def _on_success(self, result: dict):
|
||||
"""Handle publish success."""
|
||||
self.progress.hide()
|
||||
self.status_label.setText("Published successfully!")
|
||||
self.status_label.setStyleSheet("color: #38a169; font-weight: 600;")
|
||||
|
||||
QMessageBox.information(
|
||||
self, "Success",
|
||||
f"Tool '{self._tool.name}' has been published.\n\n"
|
||||
f"It will be available after review."
|
||||
)
|
||||
self.accept()
|
||||
|
||||
def _on_error(self, error: str):
|
||||
"""Handle publish error."""
|
||||
self.progress.hide()
|
||||
self.btn_publish.setEnabled(True)
|
||||
self.btn_cancel.setEnabled(True)
|
||||
self.status_label.setText(f"Error: {error}")
|
||||
self.status_label.setStyleSheet("color: #e53e3e;")
|
||||
@@ -0,0 +1,211 @@
|
||||
"""Step editor dialogs."""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QFormLayout, QLineEdit,
|
||||
QComboBox, QPushButton, QHBoxLayout, QLabel,
|
||||
QPlainTextEdit
|
||||
)
|
||||
|
||||
from ...tool import PromptStep, CodeStep
|
||||
from ...providers import load_providers
|
||||
|
||||
|
||||
class PromptStepDialog(QDialog):
|
||||
"""Dialog for editing prompt steps."""
|
||||
|
||||
def __init__(self, parent, step: PromptStep = None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Edit Prompt Step" if step else "Add Prompt Step")
|
||||
self.setMinimumSize(500, 400)
|
||||
self._step = step
|
||||
self._setup_ui()
|
||||
|
||||
if step:
|
||||
self._load_step(step)
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the UI."""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# Form
|
||||
form = QFormLayout()
|
||||
form.setSpacing(12)
|
||||
|
||||
# Provider selection
|
||||
self.provider_combo = QComboBox()
|
||||
providers = load_providers()
|
||||
for provider in sorted(providers, key=lambda p: p.name):
|
||||
self.provider_combo.addItem(provider.name)
|
||||
# Add common defaults if not present
|
||||
for default in ["claude", "gpt", "mock"]:
|
||||
if self.provider_combo.findText(default) < 0:
|
||||
self.provider_combo.addItem(default)
|
||||
form.addRow("Provider:", self.provider_combo)
|
||||
|
||||
# Output variable
|
||||
self.output_input = QLineEdit()
|
||||
self.output_input.setPlaceholderText("response")
|
||||
self.output_input.setText("response")
|
||||
form.addRow("Output variable:", self.output_input)
|
||||
|
||||
layout.addLayout(form)
|
||||
|
||||
# Prompt text
|
||||
prompt_label = QLabel("Prompt template:")
|
||||
layout.addWidget(prompt_label)
|
||||
|
||||
self.prompt_input = QPlainTextEdit()
|
||||
self.prompt_input.setPlaceholderText(
|
||||
"Enter your prompt here...\n\n"
|
||||
"Use {input} for stdin input.\n"
|
||||
"Use {variable} for argument values.\n"
|
||||
"Use {prev_output} for previous step output."
|
||||
)
|
||||
layout.addWidget(self.prompt_input, 1)
|
||||
|
||||
# Buttons
|
||||
buttons = QHBoxLayout()
|
||||
buttons.addStretch()
|
||||
|
||||
self.btn_cancel = QPushButton("Cancel")
|
||||
self.btn_cancel.setObjectName("secondary")
|
||||
self.btn_cancel.clicked.connect(self.reject)
|
||||
buttons.addWidget(self.btn_cancel)
|
||||
|
||||
self.btn_ok = QPushButton("OK")
|
||||
self.btn_ok.clicked.connect(self._validate_and_accept)
|
||||
buttons.addWidget(self.btn_ok)
|
||||
|
||||
layout.addLayout(buttons)
|
||||
|
||||
def _load_step(self, step: PromptStep):
|
||||
"""Load step data into form."""
|
||||
idx = self.provider_combo.findText(step.provider)
|
||||
if idx >= 0:
|
||||
self.provider_combo.setCurrentIndex(idx)
|
||||
else:
|
||||
self.provider_combo.setCurrentText(step.provider)
|
||||
|
||||
self.output_input.setText(step.output_var)
|
||||
self.prompt_input.setPlainText(step.prompt)
|
||||
|
||||
def _validate_and_accept(self):
|
||||
"""Validate and accept."""
|
||||
prompt = self.prompt_input.toPlainText().strip()
|
||||
if not prompt:
|
||||
self.prompt_input.setFocus()
|
||||
return
|
||||
|
||||
output = self.output_input.text().strip()
|
||||
if not output:
|
||||
self.output_input.setFocus()
|
||||
return
|
||||
|
||||
self.accept()
|
||||
|
||||
def get_step(self) -> PromptStep:
|
||||
"""Get the step from form data."""
|
||||
return PromptStep(
|
||||
prompt=self.prompt_input.toPlainText(),
|
||||
provider=self.provider_combo.currentText(),
|
||||
output_var=self.output_input.text().strip()
|
||||
)
|
||||
|
||||
|
||||
class CodeStepDialog(QDialog):
|
||||
"""Dialog for editing code steps."""
|
||||
|
||||
def __init__(self, parent, step: CodeStep = None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Edit Code Step" if step else "Add Code Step")
|
||||
self.setMinimumSize(600, 500)
|
||||
self._step = step
|
||||
self._setup_ui()
|
||||
|
||||
if step:
|
||||
self._load_step(step)
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the UI."""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(16)
|
||||
|
||||
# Form
|
||||
form = QFormLayout()
|
||||
form.setSpacing(12)
|
||||
|
||||
# Output variable
|
||||
self.output_input = QLineEdit()
|
||||
self.output_input.setPlaceholderText("result")
|
||||
self.output_input.setText("result")
|
||||
form.addRow("Output variable:", self.output_input)
|
||||
|
||||
layout.addLayout(form)
|
||||
|
||||
# Code editor
|
||||
code_label = QLabel("Code:")
|
||||
layout.addWidget(code_label)
|
||||
|
||||
self.code_input = QPlainTextEdit()
|
||||
self.code_input.setPlaceholderText(
|
||||
"# Python code here\n"
|
||||
"# Access input with: input_text\n"
|
||||
"# Access args with their variable names\n"
|
||||
"# Access previous step outputs with their variable names\n\n"
|
||||
"result = input_text.upper()"
|
||||
)
|
||||
font = self.code_input.font()
|
||||
font.setFamily("Courier New, Consolas, monospace")
|
||||
self.code_input.setFont(font)
|
||||
layout.addWidget(self.code_input, 1)
|
||||
|
||||
# Help text
|
||||
help_text = QLabel(
|
||||
"The code will be executed with variables in scope. "
|
||||
"The last expression or the output variable will be captured."
|
||||
)
|
||||
help_text.setStyleSheet("color: #718096; font-size: 11px;")
|
||||
help_text.setWordWrap(True)
|
||||
layout.addWidget(help_text)
|
||||
|
||||
# Buttons
|
||||
buttons = QHBoxLayout()
|
||||
buttons.addStretch()
|
||||
|
||||
self.btn_cancel = QPushButton("Cancel")
|
||||
self.btn_cancel.setObjectName("secondary")
|
||||
self.btn_cancel.clicked.connect(self.reject)
|
||||
buttons.addWidget(self.btn_cancel)
|
||||
|
||||
self.btn_ok = QPushButton("OK")
|
||||
self.btn_ok.clicked.connect(self._validate_and_accept)
|
||||
buttons.addWidget(self.btn_ok)
|
||||
|
||||
layout.addLayout(buttons)
|
||||
|
||||
def _load_step(self, step: CodeStep):
|
||||
"""Load step data into form."""
|
||||
self.output_input.setText(step.output_var)
|
||||
self.code_input.setPlainText(step.code)
|
||||
|
||||
def _validate_and_accept(self):
|
||||
"""Validate and accept."""
|
||||
code = self.code_input.toPlainText().strip()
|
||||
if not code:
|
||||
self.code_input.setFocus()
|
||||
return
|
||||
|
||||
output = self.output_input.text().strip()
|
||||
if not output:
|
||||
self.output_input.setFocus()
|
||||
return
|
||||
|
||||
self.accept()
|
||||
|
||||
def get_step(self) -> CodeStep:
|
||||
"""Get the step from form data."""
|
||||
return CodeStep(
|
||||
code=self.code_input.toPlainText(),
|
||||
output_var=self.output_input.text().strip()
|
||||
)
|
||||
Reference in New Issue
Block a user