"""Tools page - main view for managing tools.""" from collections import defaultdict from pathlib import Path from typing import Optional, Tuple import yaml from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QSplitter, QTreeWidget, QTreeWidgetItem, QTextEdit, QLabel, QPushButton, QGroupBox, QMessageBox, QFrame ) from PySide6.QtCore import Qt from PySide6.QtGui import QFont, QColor, QBrush from ...tool import ( Tool, ToolArgument, PromptStep, CodeStep, ToolStep, list_tools, load_tool, delete_tool, DEFAULT_CATEGORIES, get_tools_dir ) from ...config import load_config from ...hash_utils import compute_config_hash def get_tool_publish_state(tool_name: str) -> Tuple[str, Optional[str]]: """ Get the publish state of a tool. Returns: Tuple of (state, registry_hash) where state is: - "published" - has registry_hash and current hash matches - "modified" - has registry_hash but current hash differs - "local" - no registry_hash (never published/downloaded) """ config_path = get_tools_dir() / tool_name / "config.yaml" if not config_path.exists(): return ("local", None) try: config = yaml.safe_load(config_path.read_text()) registry_hash = config.get("registry_hash") if not registry_hash: return ("local", None) # Compute current hash (excluding hash fields) current_hash = compute_config_hash(config) if current_hash == registry_hash: return ("published", registry_hash) else: return ("modified", registry_hash) except Exception: return ("local", None) class ToolsPage(QWidget): """Main tools management page.""" def __init__(self, main_window): super().__init__() self.main_window = main_window self._current_tool = None self._setup_ui() self.refresh() 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("My Tools") title.setObjectName("heading") header_layout.addWidget(title) header_layout.addStretch() # Connection status config = load_config() if config.registry.token: status = QLabel("Connected to Registry") status.setStyleSheet("color: #38a169; font-weight: 500;") else: status = QLabel("Not connected") status.setStyleSheet("color: #718096;") header_layout.addWidget(status) layout.addWidget(header) # Main content splitter splitter = QSplitter(Qt.Horizontal) # Left side: Tool list left = QWidget() left_layout = QVBoxLayout(left) left_layout.setContentsMargins(0, 0, 0, 0) left_layout.setSpacing(8) self.tool_tree = QTreeWidget() self.tool_tree.setHeaderHidden(True) self.tool_tree.setIndentation(16) self.tool_tree.setRootIsDecorated(True) self.tool_tree.itemSelectionChanged.connect(self._on_selection_changed) self.tool_tree.itemDoubleClicked.connect(self._on_double_click) left_layout.addWidget(self.tool_tree, 1) splitter.addWidget(left) # Right side: Info panel right = QWidget() right_layout = QVBoxLayout(right) right_layout.setContentsMargins(0, 0, 0, 0) right_layout.setSpacing(16) # Tool info info_box = QGroupBox("Tool Details") info_layout = QVBoxLayout(info_box) self.info_text = QTextEdit() self.info_text.setReadOnly(True) self.info_text.setPlaceholderText("Select a tool to view details") info_layout.addWidget(self.info_text) right_layout.addWidget(info_box, 1) splitter.addWidget(right) # Set splitter sizes splitter.setSizes([350, 650]) layout.addWidget(splitter, 1) # Action buttons buttons = QWidget() btn_layout = QHBoxLayout(buttons) btn_layout.setContentsMargins(0, 0, 0, 0) btn_layout.setSpacing(12) self.btn_create = QPushButton("Create") self.btn_create.clicked.connect(self._create_tool) btn_layout.addWidget(self.btn_create) self.btn_edit = QPushButton("Edit") self.btn_edit.setObjectName("secondary") self.btn_edit.clicked.connect(self._edit_tool) 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_tool) self.btn_delete.setEnabled(False) btn_layout.addWidget(self.btn_delete) btn_layout.addStretch() # Connect/Publish button config = load_config() if config.registry.token: self.btn_publish = QPushButton("Publish") self.btn_publish.clicked.connect(self._publish_tool) self.btn_publish.setEnabled(False) else: self.btn_publish = QPushButton("Connect") self.btn_publish.clicked.connect(self._connect_registry) btn_layout.addWidget(self.btn_publish) layout.addWidget(buttons) def refresh(self): """Refresh the tool list.""" self.tool_tree.clear() self._current_tool = None self.info_text.clear() tools = list_tools() # Group tools by category tools_by_category = defaultdict(list) for name in tools: tool = load_tool(name) if tool: category = tool.category if tool.category else "Other" tools_by_category[category].append((name, tool)) # Build tree with categories all_categories = list(DEFAULT_CATEGORIES) for cat in tools_by_category: if cat not in all_categories: all_categories.append(cat) for category in all_categories: if category in tools_by_category and tools_by_category[category]: # Category item cat_item = QTreeWidgetItem([category]) cat_item.setExpanded(True) font = cat_item.font(0) font.setBold(True) cat_item.setFont(0, font) cat_item.setFlags(cat_item.flags() & ~Qt.ItemIsSelectable) # Tools in category for name, tool in sorted(tools_by_category[category], key=lambda x: x[0]): # Get publish state state, registry_hash = get_tool_publish_state(name) # Show state indicator in display name if state == "published": display_name = f"{name} ✓" tooltip = "Published to registry - up to date" color = QColor(56, 161, 105) # Green elif state == "modified": display_name = f"{name} ●" tooltip = "Published to registry - local modifications" color = QColor(221, 107, 32) # Orange else: display_name = name tooltip = "Local tool - not published" color = None tool_item = QTreeWidgetItem([display_name]) tool_item.setData(0, Qt.UserRole, name) if color: tool_item.setForeground(0, QBrush(color)) # Build tooltip if tool.source and tool.source.type == "imported": tooltip = f"Imported from {tool.source.url or 'registry'}" tool_item.setToolTip(0, tooltip) cat_item.addChild(tool_item) self.tool_tree.addTopLevelItem(cat_item) # Update button states self._update_buttons() if not tools: self.info_text.setPlaceholderText( "No tools found.\n\nClick 'Create' to build your first tool, " "or browse the Registry to install community tools." ) def _on_selection_changed(self): """Handle tool selection change.""" items = self.tool_tree.selectedItems() if not items: self._current_tool = None self.info_text.clear() self._update_buttons() return item = items[0] tool_name = item.data(0, Qt.UserRole) if not tool_name: self._current_tool = None self.info_text.clear() self._update_buttons() return tool = load_tool(tool_name) if tool: self._current_tool = tool self._show_tool_info(tool) self._update_buttons() def _on_double_click(self, item, column): """Handle double-click on tool.""" tool_name = item.data(0, Qt.UserRole) if tool_name: self._edit_tool() def _show_tool_info(self, tool: Tool): """Display tool information.""" lines = [] # Name and description lines.append(f"

{tool.name}

") if tool.description: lines.append(f"

{tool.description}

") # Publish state state, registry_hash = get_tool_publish_state(tool.name) if state == "published": lines.append( "

" "✓ Published to registry - up to date

" ) elif state == "modified": lines.append( "

" "● Modified since last publish - republish to update registry

" ) # Source info if tool.source: source_type = tool.source.type if source_type == "imported": source_url = tool.source.url or "registry" lines.append(f"

Imported from {source_url}

") elif source_type == "forked": lines.append(f"

Forked from {tool.source.original_tool}

") # Arguments if tool.arguments: lines.append("

Arguments

") lines.append("") # Steps if tool.steps: lines.append("

Steps

") lines.append("
    ") for i, step in enumerate(tool.steps, 1): if isinstance(step, PromptStep): lines.append(f"
  1. Prompt using {step.provider}${step.output_var}
  2. ") elif isinstance(step, CodeStep): lines.append(f"
  3. Code (python) → ${step.output_var}
  4. ") elif isinstance(step, ToolStep): lines.append(f"
  5. Tool: {step.tool}${step.output_var}
  6. ") lines.append("
") # Output if tool.output: lines.append("

Output Template

") output_escaped = tool.output.replace("<", "<").replace(">", ">") lines.append(f"
{output_escaped}
") # Category if tool.category: lines.append(f"

Category: {tool.category}

") self.info_text.setHtml("\n".join(lines)) def _update_buttons(self): """Update button enabled states.""" has_selection = self._current_tool is not None self.btn_edit.setEnabled(has_selection) self.btn_delete.setEnabled(has_selection) config = load_config() if config.registry.token: self.btn_publish.setEnabled(has_selection) def _create_tool(self): """Create a new tool.""" self.main_window.open_tool_builder() def _edit_tool(self): """Edit the selected tool.""" if self._current_tool: self.main_window.open_tool_builder(self._current_tool.name) def _delete_tool(self): """Delete the selected tool.""" if not self._current_tool: return reply = QMessageBox.question( self, "Delete Tool", f"Are you sure you want to delete '{self._current_tool.name}'?\n\n" "This will remove the tool configuration and wrapper script.", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: try: delete_tool(self._current_tool.name) self.main_window.show_status(f"Deleted tool '{self._current_tool.name}'") self.refresh() except Exception as e: QMessageBox.critical(self, "Error", f"Failed to delete tool:\n{e}") def _connect_registry(self): """Open connect dialog.""" from ..dialogs.connect_dialog import ConnectDialog dialog = ConnectDialog(self) if dialog.exec(): self.refresh() self.main_window.show_status("Connected to registry") # Recreate publish button self.btn_publish.setText("Publish") self.btn_publish.clicked.disconnect() self.btn_publish.clicked.connect(self._publish_tool) def _publish_tool(self): """Publish the selected tool.""" if not self._current_tool: return from ..dialogs.publish_dialog import PublishDialog dialog = PublishDialog(self, self._current_tool) if dialog.exec(): self.main_window.show_status(f"Published '{self._current_tool.name}'")