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:
rob
2026-01-14 04:38:35 -04:00
co-authored by Claude Opus 4.5
parent 604b473806
commit 4fe2d26244
30 changed files with 2726 additions and 5102 deletions
+211
View File
@@ -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()
)