Add registry sort/pagination features and improve code step dialog
Registry:
- Add owner and rating sort options to both server endpoints
- Add letter-based prefix filtering (A-Z, #) with server-side validation
- LEFT JOIN tool_stats for average_rating/rating_count in responses
- Add interactive numbered page buttons with sliding window and ellipsis
- Add letter bar UI (shown when sorting by name) with highlight state
- Auto-select asc/desc order based on sort field
- Disable cell editing on results table
- Client: add order and prefix params to list_tools/search_tools
Code step dialog:
- Split AI prompt into user instruction input and collapsible wrapper
- User types plain instruction, wrapper is hidden by default
- Injection of user text into wrapper via {user_instruction} placeholder
- Increase dialog minimum height to 750px
Runner:
- Support variable substitution in prompt step provider name
Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -177,7 +177,7 @@ class CodeStepDialog(QDialog):
|
||||
def __init__(self, parent, step: CodeStep = None, available_vars: list = None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Edit Code Step" if step else "Add Code Step")
|
||||
self.setMinimumSize(900, 600)
|
||||
self.setMinimumSize(900, 750)
|
||||
self._step = step
|
||||
self._available_vars = available_vars or ["input"]
|
||||
self._worker = None
|
||||
@@ -254,16 +254,30 @@ class CodeStepDialog(QDialog):
|
||||
provider_layout.addStretch()
|
||||
ai_layout.addLayout(provider_layout)
|
||||
|
||||
# Prompt editor
|
||||
prompt_label = QLabel("Describe what you want the code to do:")
|
||||
ai_layout.addWidget(prompt_label)
|
||||
# User instruction input (always visible)
|
||||
instruction_label = QLabel("Describe what you want the code to do:")
|
||||
ai_layout.addWidget(instruction_label)
|
||||
|
||||
self.ai_prompt_input = QPlainTextEdit()
|
||||
self.ai_prompt_input.setPlaceholderText(
|
||||
self.user_instruction_input = QPlainTextEdit()
|
||||
self.user_instruction_input.setPlaceholderText(
|
||||
"Example: Parse the input as JSON and extract the 'name' field"
|
||||
)
|
||||
# Set default prompt template
|
||||
self.user_instruction_input.setMaximumHeight(100)
|
||||
ai_layout.addWidget(self.user_instruction_input, 1)
|
||||
|
||||
# Collapsible prompt wrapper section
|
||||
self.wrapper_toggle = QPushButton("Edit prompt wrapper ▶")
|
||||
self.wrapper_toggle.setFlat(True)
|
||||
self.wrapper_toggle.setStyleSheet(
|
||||
"QPushButton { color: #4a5568; font-size: 12px; text-align: left; "
|
||||
"padding: 2px 0; } QPushButton:hover { color: #2d3748; }"
|
||||
)
|
||||
self.wrapper_toggle.clicked.connect(self._toggle_wrapper)
|
||||
ai_layout.addWidget(self.wrapper_toggle)
|
||||
|
||||
self.ai_prompt_input = QPlainTextEdit()
|
||||
self._set_default_prompt()
|
||||
self.ai_prompt_input.hide()
|
||||
ai_layout.addWidget(self.ai_prompt_input, 2)
|
||||
|
||||
# Generate button
|
||||
@@ -306,6 +320,15 @@ class CodeStepDialog(QDialog):
|
||||
|
||||
layout.addLayout(buttons)
|
||||
|
||||
def _toggle_wrapper(self):
|
||||
"""Toggle visibility of the prompt wrapper editor."""
|
||||
if self.ai_prompt_input.isVisible():
|
||||
self.ai_prompt_input.hide()
|
||||
self.wrapper_toggle.setText("Edit prompt wrapper ▶")
|
||||
else:
|
||||
self.ai_prompt_input.show()
|
||||
self.wrapper_toggle.setText("Edit prompt wrapper ▼")
|
||||
|
||||
def _set_default_prompt(self):
|
||||
"""Set the default AI prompt template."""
|
||||
vars_formatted = ', '.join(f'"{{{v}}}"' for v in self._available_vars)
|
||||
@@ -317,7 +340,7 @@ Example:
|
||||
my_var = \"\"\"{{input}}\"\"\"
|
||||
result = my_var.upper()
|
||||
|
||||
INSTRUCTION: [Describe what you want the code to do]
|
||||
INSTRUCTION: {{user_instruction}}
|
||||
|
||||
CURRENT CODE:
|
||||
{{code}}
|
||||
@@ -329,14 +352,20 @@ IMPORTANT: Return ONLY executable inline Python code. No function definitions, n
|
||||
|
||||
def _generate_code(self):
|
||||
"""Generate code using AI."""
|
||||
prompt_template = self.ai_prompt_input.toPlainText().strip()
|
||||
if not prompt_template:
|
||||
self.ai_output.setHtml("<span style='color: #e53e3e;'>Please enter a prompt</span>")
|
||||
user_instruction = self.user_instruction_input.toPlainText().strip()
|
||||
if not user_instruction:
|
||||
self.ai_output.setHtml("<span style='color: #e53e3e;'>Please describe what you want the code to do</span>")
|
||||
return
|
||||
|
||||
# Replace {code} placeholder with current code
|
||||
prompt_template = self.ai_prompt_input.toPlainText().strip()
|
||||
if not prompt_template:
|
||||
self.ai_output.setHtml("<span style='color: #e53e3e;'>Prompt wrapper is empty</span>")
|
||||
return
|
||||
|
||||
# Inject user instruction and current code into the wrapper
|
||||
current_code = self.code_input.toPlainText().strip() or "# No code yet"
|
||||
prompt = prompt_template.replace("{code}", current_code)
|
||||
prompt = prompt_template.replace("{user_instruction}", user_instruction)
|
||||
prompt = prompt.replace("{code}", current_code)
|
||||
|
||||
provider = self.ai_provider_combo.currentText()
|
||||
|
||||
|
||||
@@ -14,17 +14,24 @@ from ...config import load_config
|
||||
from ...tool import list_tools, load_tool, get_all_categories
|
||||
|
||||
|
||||
# Sort fields that should default to ascending order
|
||||
ASC_SORTS = {"name", "owner"}
|
||||
|
||||
|
||||
class SearchWorker(QThread):
|
||||
"""Background worker for registry search."""
|
||||
finished = Signal(object) # PaginatedResponse
|
||||
error = Signal(str)
|
||||
|
||||
def __init__(self, query: str = "", category: str = None, sort: str = "downloads",
|
||||
order: str = "desc", prefix: str = None,
|
||||
tags: list = None, page: int = 1, per_page: int = 20):
|
||||
super().__init__()
|
||||
self.query = query
|
||||
self.category = category
|
||||
self.sort = sort
|
||||
self.order = order
|
||||
self.prefix = prefix
|
||||
self.tags = tags
|
||||
self.page = page
|
||||
self.per_page = per_page
|
||||
@@ -40,7 +47,9 @@ class SearchWorker(QThread):
|
||||
category=category,
|
||||
page=self.page,
|
||||
per_page=self.per_page,
|
||||
sort=self.sort
|
||||
sort=self.sort,
|
||||
order=self.order,
|
||||
prefix=self.prefix
|
||||
)
|
||||
else:
|
||||
result = client.search_tools(
|
||||
@@ -50,6 +59,8 @@ class SearchWorker(QThread):
|
||||
page=self.page,
|
||||
per_page=self.per_page,
|
||||
sort=self.sort,
|
||||
order=self.order,
|
||||
prefix=self.prefix,
|
||||
include_facets=True
|
||||
)
|
||||
self.finished.emit(result)
|
||||
@@ -111,6 +122,7 @@ class RegistryPage(QWidget):
|
||||
self._current_page = 1
|
||||
self._total_pages = 1
|
||||
self._current_tags = []
|
||||
self._current_prefix = None # Active letter filter (None = no filter)
|
||||
self._installed_tools = {} # name -> version
|
||||
self._setup_ui()
|
||||
|
||||
@@ -152,8 +164,11 @@ class RegistryPage(QWidget):
|
||||
cat_label = QLabel("Category:")
|
||||
filters_layout.addWidget(cat_label)
|
||||
self.category_combo = QComboBox()
|
||||
self.category_combo.setEditable(True)
|
||||
self.category_combo.lineEdit().setReadOnly(True)
|
||||
self.category_combo.addItems(["All"] + get_all_categories())
|
||||
self.category_combo.setMinimumWidth(100)
|
||||
self.category_combo.setMinimumHeight(28)
|
||||
self.category_combo.setToolTip("Filter by tool category")
|
||||
self.category_combo.currentTextChanged.connect(self._on_filter_changed)
|
||||
filters_layout.addWidget(self.category_combo)
|
||||
@@ -162,12 +177,17 @@ class RegistryPage(QWidget):
|
||||
sort_label = QLabel("Sort:")
|
||||
filters_layout.addWidget(sort_label)
|
||||
self.sort_combo = QComboBox()
|
||||
self.sort_combo.setEditable(True)
|
||||
self.sort_combo.lineEdit().setReadOnly(True)
|
||||
self.sort_combo.addItem("Most Popular", "downloads")
|
||||
self.sort_combo.addItem("Newest", "published_at")
|
||||
self.sort_combo.addItem("Name (A-Z)", "name")
|
||||
self.sort_combo.addItem("Owner", "owner")
|
||||
self.sort_combo.addItem("Rating", "average_rating")
|
||||
self.sort_combo.setMinimumWidth(120)
|
||||
self.sort_combo.setMinimumHeight(28)
|
||||
self.sort_combo.setToolTip("Change sort order of results")
|
||||
self.sort_combo.currentIndexChanged.connect(self._on_filter_changed)
|
||||
self.sort_combo.currentIndexChanged.connect(self._on_sort_changed)
|
||||
filters_layout.addWidget(self.sort_combo)
|
||||
|
||||
# Search button
|
||||
@@ -190,6 +210,42 @@ class RegistryPage(QWidget):
|
||||
self.tags_widget.hide()
|
||||
layout.addWidget(self.tags_widget)
|
||||
|
||||
# Letter bar (shown only when sorting by name)
|
||||
self.letter_bar_widget = QWidget()
|
||||
self.letter_bar_widget.setMinimumHeight(40)
|
||||
letter_bar_layout = QHBoxLayout(self.letter_bar_widget)
|
||||
letter_bar_layout.setContentsMargins(0, 4, 0, 4)
|
||||
letter_bar_layout.setSpacing(2)
|
||||
self._letter_buttons = {}
|
||||
letters = ["#"] + [chr(c) for c in range(ord("A"), ord("Z") + 1)]
|
||||
for letter in letters:
|
||||
btn = QPushButton(letter)
|
||||
btn.setMinimumSize(26, 26)
|
||||
btn.setMaximumSize(30, 30)
|
||||
btn.setStyleSheet(
|
||||
"QPushButton { border: 1px solid #a0aec0; border-radius: 4px; "
|
||||
"font-size: 12px; padding: 2px 0; background: #edf2f7; color: #1a202c; }"
|
||||
"QPushButton:hover { background: #cbd5e0; }"
|
||||
)
|
||||
btn.clicked.connect(lambda checked, l=letter: self._on_letter_clicked(l))
|
||||
letter_bar_layout.addWidget(btn)
|
||||
self._letter_buttons[letter] = btn
|
||||
letter_bar_layout.addStretch()
|
||||
# "All" button to clear letter filter
|
||||
btn_all = QPushButton("All")
|
||||
btn_all.setMinimumHeight(26)
|
||||
btn_all.setMaximumHeight(30)
|
||||
btn_all.setStyleSheet(
|
||||
"QPushButton { border: 1px solid #a0aec0; border-radius: 4px; "
|
||||
"font-size: 12px; padding: 2px 8px; background: #edf2f7; color: #1a202c; }"
|
||||
"QPushButton:hover { background: #cbd5e0; }"
|
||||
)
|
||||
btn_all.clicked.connect(self._clear_letter_filter)
|
||||
letter_bar_layout.addWidget(btn_all)
|
||||
self._letter_buttons["All"] = btn_all
|
||||
self.letter_bar_widget.hide()
|
||||
layout.addWidget(self.letter_bar_widget)
|
||||
|
||||
# Content splitter
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
|
||||
@@ -209,6 +265,7 @@ class RegistryPage(QWidget):
|
||||
self.results_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents)
|
||||
self.results_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeToContents)
|
||||
self.results_table.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeToContents)
|
||||
self.results_table.setEditTriggers(QTableWidget.NoEditTriggers)
|
||||
self.results_table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||
self.results_table.setSelectionMode(QTableWidget.SingleSelection)
|
||||
self.results_table.verticalHeader().setVisible(False)
|
||||
@@ -217,10 +274,11 @@ class RegistryPage(QWidget):
|
||||
|
||||
# Pagination controls
|
||||
pagination = QWidget()
|
||||
pagination.setMinimumHeight(44)
|
||||
pag_layout = QHBoxLayout(pagination)
|
||||
pag_layout.setContentsMargins(0, 8, 0, 0)
|
||||
pag_layout.setContentsMargins(0, 6, 0, 6)
|
||||
|
||||
self.btn_prev = QPushButton("← Previous")
|
||||
self.btn_prev = QPushButton("← Prev")
|
||||
self.btn_prev.setToolTip("Go to previous page of results")
|
||||
self.btn_prev.clicked.connect(self._prev_page)
|
||||
self.btn_prev.setEnabled(False)
|
||||
@@ -228,9 +286,13 @@ class RegistryPage(QWidget):
|
||||
|
||||
pag_layout.addStretch()
|
||||
|
||||
self.page_label = QLabel("Page 1 of 1")
|
||||
self.page_label.setStyleSheet("color: #718096;")
|
||||
pag_layout.addWidget(self.page_label)
|
||||
# Page buttons container (replaces static label)
|
||||
self.page_buttons_widget = QWidget()
|
||||
self.page_buttons_widget.setMinimumHeight(32)
|
||||
self.page_buttons_layout = QHBoxLayout(self.page_buttons_widget)
|
||||
self.page_buttons_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.page_buttons_layout.setSpacing(4)
|
||||
pag_layout.addWidget(self.page_buttons_widget)
|
||||
|
||||
pag_layout.addStretch()
|
||||
|
||||
@@ -323,18 +385,79 @@ class RegistryPage(QWidget):
|
||||
"""Browse all tools (no search query)."""
|
||||
self.search_input.clear()
|
||||
self._current_page = 1
|
||||
self._current_prefix = None
|
||||
self._do_search()
|
||||
|
||||
def _get_sort_order(self, sort_field: str) -> str:
|
||||
"""Get the appropriate sort order for a sort field."""
|
||||
if sort_field in ASC_SORTS:
|
||||
return "asc"
|
||||
return "desc"
|
||||
|
||||
def _on_filter_changed(self):
|
||||
"""Handle filter/sort change."""
|
||||
"""Handle category filter change."""
|
||||
self._current_page = 1
|
||||
self._current_prefix = None
|
||||
self._do_search()
|
||||
|
||||
def _on_sort_changed(self):
|
||||
"""Handle sort change — show/hide letter bar, reset prefix."""
|
||||
sort = self.sort_combo.itemData(self.sort_combo.currentIndex())
|
||||
if sort == "name":
|
||||
self.letter_bar_widget.show()
|
||||
else:
|
||||
self.letter_bar_widget.hide()
|
||||
self._current_prefix = None
|
||||
self._update_letter_bar_highlight()
|
||||
self._current_page = 1
|
||||
self._do_search()
|
||||
|
||||
def _on_letter_clicked(self, letter: str):
|
||||
"""Handle letter bar click."""
|
||||
self._current_prefix = letter
|
||||
self._current_page = 1
|
||||
self._update_letter_bar_highlight()
|
||||
self._do_search()
|
||||
|
||||
def _clear_letter_filter(self):
|
||||
"""Clear letter filter — show all."""
|
||||
self._current_prefix = None
|
||||
self._current_page = 1
|
||||
self._update_letter_bar_highlight()
|
||||
self._do_search()
|
||||
|
||||
def _update_letter_bar_highlight(self):
|
||||
"""Highlight the active letter button."""
|
||||
active_style = (
|
||||
"QPushButton { border: 1px solid #4299e1; border-radius: 4px; "
|
||||
"font-size: 12px; padding: 2px 0; background: #4299e1; color: white; font-weight: bold; }"
|
||||
)
|
||||
normal_style = (
|
||||
"QPushButton { border: 1px solid #a0aec0; border-radius: 4px; "
|
||||
"font-size: 12px; padding: 2px 0; background: #edf2f7; color: #1a202c; }"
|
||||
"QPushButton:hover { background: #cbd5e0; }"
|
||||
)
|
||||
normal_all_style = (
|
||||
"QPushButton { border: 1px solid #a0aec0; border-radius: 4px; "
|
||||
"font-size: 12px; padding: 2px 8px; background: #edf2f7; color: #1a202c; }"
|
||||
"QPushButton:hover { background: #cbd5e0; }"
|
||||
)
|
||||
active_all_style = (
|
||||
"QPushButton { border: 1px solid #4299e1; border-radius: 4px; "
|
||||
"font-size: 12px; padding: 2px 8px; background: #4299e1; color: white; font-weight: bold; }"
|
||||
)
|
||||
for letter, btn in self._letter_buttons.items():
|
||||
if letter == "All":
|
||||
btn.setStyleSheet(active_all_style if self._current_prefix is None else normal_all_style)
|
||||
else:
|
||||
btn.setStyleSheet(active_style if letter == self._current_prefix else normal_style)
|
||||
|
||||
def _do_search(self):
|
||||
"""Perform search with current filters."""
|
||||
query = self.search_input.text().strip()
|
||||
category = self.category_combo.currentText()
|
||||
sort = self.sort_combo.currentData()
|
||||
sort = self.sort_combo.itemData(self.sort_combo.currentIndex())
|
||||
order = self._get_sort_order(sort)
|
||||
|
||||
self.btn_search.setEnabled(False)
|
||||
self.btn_browse.setEnabled(False)
|
||||
@@ -345,6 +468,8 @@ class RegistryPage(QWidget):
|
||||
query=query,
|
||||
category=category,
|
||||
sort=sort,
|
||||
order=order,
|
||||
prefix=self._current_prefix,
|
||||
tags=self._current_tags if self._current_tags else None,
|
||||
page=self._current_page,
|
||||
per_page=20
|
||||
@@ -363,11 +488,11 @@ class RegistryPage(QWidget):
|
||||
total = result.total or len(tools)
|
||||
|
||||
self.status_label.setText(f"Found {total} tools")
|
||||
self.page_label.setText(f"Page {self._current_page} of {self._total_pages}")
|
||||
|
||||
# Update pagination buttons
|
||||
self.btn_prev.setEnabled(self._current_page > 1)
|
||||
self.btn_next.setEnabled(self._current_page < self._total_pages)
|
||||
self._update_pagination()
|
||||
|
||||
self.results_table.setRowCount(len(tools))
|
||||
for row, tool in enumerate(tools):
|
||||
@@ -413,6 +538,83 @@ class RegistryPage(QWidget):
|
||||
# Version
|
||||
self.results_table.setItem(row, 5, QTableWidgetItem(tool.get("version", "1.0.0")))
|
||||
|
||||
def _update_pagination(self):
|
||||
"""Rebuild interactive page number buttons."""
|
||||
# Clear existing buttons
|
||||
while self.page_buttons_layout.count():
|
||||
item = self.page_buttons_layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
if self._total_pages <= 1:
|
||||
lbl = QLabel("Page 1 of 1")
|
||||
lbl.setStyleSheet("color: #718096;")
|
||||
self.page_buttons_layout.addWidget(lbl)
|
||||
return
|
||||
|
||||
current = self._current_page
|
||||
total = self._total_pages
|
||||
|
||||
# Determine which page numbers to show
|
||||
if total <= 7:
|
||||
pages = list(range(1, total + 1))
|
||||
else:
|
||||
# Sliding window of 5 centered on current page
|
||||
half = 2
|
||||
start = max(1, current - half)
|
||||
end = min(total, current + half)
|
||||
# Adjust if near edges
|
||||
if current - half < 1:
|
||||
end = min(total, end + (1 - (current - half)))
|
||||
if current + half > total:
|
||||
start = max(1, start - (current + half - total))
|
||||
pages = list(range(start, end + 1))
|
||||
|
||||
# Leading ellipsis
|
||||
if pages and pages[0] > 1:
|
||||
self._add_page_button(1)
|
||||
if pages[0] > 2:
|
||||
lbl = QLabel("...")
|
||||
lbl.setStyleSheet("color: #718096; padding: 0 2px;")
|
||||
self.page_buttons_layout.addWidget(lbl)
|
||||
|
||||
for p in pages:
|
||||
self._add_page_button(p)
|
||||
|
||||
# Trailing ellipsis
|
||||
if pages and pages[-1] < total:
|
||||
if pages[-1] < total - 1:
|
||||
lbl = QLabel("...")
|
||||
lbl.setStyleSheet("color: #718096; padding: 0 2px;")
|
||||
self.page_buttons_layout.addWidget(lbl)
|
||||
self._add_page_button(total)
|
||||
|
||||
def _add_page_button(self, page_num: int):
|
||||
"""Add a single page number button."""
|
||||
btn = QPushButton(str(page_num))
|
||||
btn.setMinimumSize(32, 28)
|
||||
btn.setMaximumHeight(30)
|
||||
if page_num == self._current_page:
|
||||
btn.setStyleSheet(
|
||||
"QPushButton { background: #4299e1; color: white; border: none; "
|
||||
"border-radius: 4px; font-weight: bold; font-size: 12px; "
|
||||
"padding: 2px 6px; }"
|
||||
)
|
||||
else:
|
||||
btn.setStyleSheet(
|
||||
"QPushButton { background: #edf2f7; border: 1px solid #a0aec0; "
|
||||
"border-radius: 4px; color: #1a202c; font-size: 12px; "
|
||||
"padding: 2px 6px; }"
|
||||
"QPushButton:hover { background: #cbd5e0; }"
|
||||
)
|
||||
btn.clicked.connect(lambda checked, p=page_num: self._go_to_page(p))
|
||||
self.page_buttons_layout.addWidget(btn)
|
||||
|
||||
def _go_to_page(self, page: int):
|
||||
"""Navigate to a specific page."""
|
||||
self._current_page = page
|
||||
self._do_search()
|
||||
|
||||
def _rating_to_stars(self, rating: float) -> str:
|
||||
"""Convert rating to star display."""
|
||||
if rating <= 0:
|
||||
|
||||
Reference in New Issue
Block a user