"""Tool builder page - create and edit tools.""" from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QLineEdit, QTextEdit, QComboBox, QPushButton, QGroupBox, QListWidget, QListWidgetItem, QLabel, QMessageBox, QSplitter, QFrame ) from PySide6.QtCore import Qt from ...tool import ( Tool, ToolArgument, PromptStep, CodeStep, load_tool, save_tool, validate_tool_name, DEFAULT_CATEGORIES ) class ToolBuilderPage(QWidget): """Tool builder/editor page.""" def __init__(self, main_window, tool_name: str = None): super().__init__() self.main_window = main_window self.editing = tool_name is not None self.original_name = tool_name self._tool = None self._setup_ui() if tool_name: self._load_tool(tool_name) 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("Edit Tool" if self.editing else "Create Tool") title.setObjectName("heading") header_layout.addWidget(title) header_layout.addStretch() self.btn_cancel = QPushButton("Cancel") self.btn_cancel.setObjectName("secondary") self.btn_cancel.clicked.connect(self._cancel) header_layout.addWidget(self.btn_cancel) self.btn_save = QPushButton("Save") self.btn_save.clicked.connect(self._save) header_layout.addWidget(self.btn_save) layout.addWidget(header) # Main form splitter splitter = QSplitter(Qt.Horizontal) # Left: Basic info left = QWidget() left_layout = QVBoxLayout(left) left_layout.setContentsMargins(0, 0, 0, 0) left_layout.setSpacing(16) # Basic info group info_box = QGroupBox("Basic Information") info_layout = QFormLayout(info_box) info_layout.setSpacing(12) self.name_input = QLineEdit() self.name_input.setPlaceholderText("my-tool") info_layout.addRow("Name:", self.name_input) self.desc_input = QLineEdit() self.desc_input.setPlaceholderText("A brief description of what this tool does") info_layout.addRow("Description:", self.desc_input) self.category_combo = QComboBox() self.category_combo.setEditable(True) for cat in DEFAULT_CATEGORIES: self.category_combo.addItem(cat) info_layout.addRow("Category:", self.category_combo) left_layout.addWidget(info_box) # Arguments group args_box = QGroupBox("Arguments") args_layout = QVBoxLayout(args_box) self.args_list = QListWidget() self.args_list.itemDoubleClicked.connect(self._edit_argument) args_layout.addWidget(self.args_list) args_btns = QHBoxLayout() self.btn_add_arg = QPushButton("Add") self.btn_add_arg.clicked.connect(self._add_argument) args_btns.addWidget(self.btn_add_arg) self.btn_edit_arg = QPushButton("Edit") self.btn_edit_arg.setObjectName("secondary") self.btn_edit_arg.clicked.connect(self._edit_argument) args_btns.addWidget(self.btn_edit_arg) self.btn_del_arg = QPushButton("Delete") self.btn_del_arg.setObjectName("danger") self.btn_del_arg.clicked.connect(self._delete_argument) args_btns.addWidget(self.btn_del_arg) args_btns.addStretch() args_layout.addLayout(args_btns) left_layout.addWidget(args_box) splitter.addWidget(left) # Right: Steps and output right = QWidget() right_layout = QVBoxLayout(right) right_layout.setContentsMargins(0, 0, 0, 0) right_layout.setSpacing(16) # Steps group steps_box = QGroupBox("Steps") steps_layout = QVBoxLayout(steps_box) self.steps_list = QListWidget() self.steps_list.itemDoubleClicked.connect(self._edit_step) steps_layout.addWidget(self.steps_list) steps_btns = QHBoxLayout() self.btn_add_prompt = QPushButton("Add Prompt") self.btn_add_prompt.clicked.connect(self._add_prompt_step) steps_btns.addWidget(self.btn_add_prompt) self.btn_add_code = QPushButton("Add Code") self.btn_add_code.clicked.connect(self._add_code_step) steps_btns.addWidget(self.btn_add_code) self.btn_edit_step = QPushButton("Edit") self.btn_edit_step.setObjectName("secondary") self.btn_edit_step.clicked.connect(self._edit_step) steps_btns.addWidget(self.btn_edit_step) self.btn_del_step = QPushButton("Delete") self.btn_del_step.setObjectName("danger") self.btn_del_step.clicked.connect(self._delete_step) steps_btns.addWidget(self.btn_del_step) steps_btns.addStretch() steps_layout.addLayout(steps_btns) right_layout.addWidget(steps_box, 1) # Steps box gets stretch priority # Output group output_box = QGroupBox("Output Template") output_layout = QVBoxLayout(output_box) self.output_input = QTextEdit() self.output_input.setPlaceholderText("Use {variable} to reference step outputs, e.g. {response}") self.output_input.setPlainText("{response}") # Default value self.output_input.setMinimumHeight(80) self.output_input.setMaximumHeight(120) output_layout.addWidget(self.output_input) right_layout.addWidget(output_box, 0) # Output box stays fixed size splitter.addWidget(right) splitter.setSizes([400, 600]) layout.addWidget(splitter, 1) def _load_tool(self, name: str): """Load an existing tool for editing.""" tool = load_tool(name) if not tool: QMessageBox.critical(self, "Error", f"Tool '{name}' not found") self._cancel() return self._tool = tool self.name_input.setText(tool.name) self.name_input.setEnabled(False) # Can't rename self.desc_input.setText(tool.description or "") # Set category if tool.category: idx = self.category_combo.findText(tool.category) if idx >= 0: self.category_combo.setCurrentIndex(idx) else: self.category_combo.setCurrentText(tool.category) # Load arguments self._refresh_arguments() # Load steps self._refresh_steps() # Set output self.output_input.setPlainText(tool.output or "{response}") def _refresh_arguments(self): """Refresh arguments list.""" self.args_list.clear() if self._tool and self._tool.arguments: for arg in self._tool.arguments: item = QListWidgetItem(f"{arg.flag} → ${arg.variable}") item.setData(Qt.UserRole, arg) self.args_list.addItem(item) def _refresh_steps(self): """Refresh steps list.""" self.steps_list.clear() if self._tool and self._tool.steps: for i, step in enumerate(self._tool.steps, 1): if isinstance(step, PromptStep): text = f"{i}. Prompt [{step.provider}] → ${step.output_var}" elif isinstance(step, CodeStep): text = f"{i}. Code [python] → ${step.output_var}" else: text = f"{i}. Unknown step" item = QListWidgetItem(text) item.setData(Qt.UserRole, step) self.steps_list.addItem(item) def _add_argument(self): """Add a new argument.""" from ..dialogs.argument_dialog import ArgumentDialog dialog = ArgumentDialog(self) if dialog.exec(): arg = dialog.get_argument() if not self._tool: self._tool = Tool(name="", description="", arguments=[], steps=[], output="{response}") self._tool.arguments.append(arg) self._refresh_arguments() def _edit_argument(self): """Edit selected argument.""" items = self.args_list.selectedItems() if not items: return arg = items[0].data(Qt.UserRole) idx = self.args_list.row(items[0]) from ..dialogs.argument_dialog import ArgumentDialog dialog = ArgumentDialog(self, arg) if dialog.exec(): self._tool.arguments[idx] = dialog.get_argument() self._refresh_arguments() def _delete_argument(self): """Delete selected argument.""" items = self.args_list.selectedItems() if not items: return idx = self.args_list.row(items[0]) del self._tool.arguments[idx] self._refresh_arguments() def _get_available_vars(self, up_to_step: int = -1) -> list: """Get available variables for a step. Args: up_to_step: Include variables from steps before this index. -1 means include all existing steps (for new steps). """ vars_list = ["input"] # Add argument variables if self._tool and self._tool.arguments: for arg in self._tool.arguments: if arg.variable and arg.variable not in vars_list: vars_list.append(arg.variable) # Add output variables from previous steps if self._tool and self._tool.steps: steps_to_check = self._tool.steps if up_to_step < 0 else self._tool.steps[:up_to_step] for step in steps_to_check: if hasattr(step, 'output_var') and step.output_var: # Handle comma-separated output vars for var in step.output_var.split(','): var = var.strip() if var and var not in vars_list: vars_list.append(var) return vars_list def _add_prompt_step(self): """Add a prompt step.""" from ..dialogs.step_dialog import PromptStepDialog dialog = PromptStepDialog(self) if dialog.exec(): step = dialog.get_step() if not self._tool: self._tool = Tool(name="", description="", arguments=[], steps=[], output="{response}") self._tool.steps.append(step) self._refresh_steps() def _add_code_step(self): """Add a code step.""" from ..dialogs.step_dialog import CodeStepDialog available_vars = self._get_available_vars() dialog = CodeStepDialog(self, available_vars=available_vars) if dialog.exec(): step = dialog.get_step() if not self._tool: self._tool = Tool(name="", description="", arguments=[], steps=[], output="{response}") self._tool.steps.append(step) self._refresh_steps() def _edit_step(self): """Edit selected step.""" items = self.steps_list.selectedItems() if not items: return step = items[0].data(Qt.UserRole) idx = self.steps_list.row(items[0]) if isinstance(step, PromptStep): from ..dialogs.step_dialog import PromptStepDialog dialog = PromptStepDialog(self, step) elif isinstance(step, CodeStep): from ..dialogs.step_dialog import CodeStepDialog available_vars = self._get_available_vars(up_to_step=idx) dialog = CodeStepDialog(self, step, available_vars=available_vars) else: return if dialog.exec(): self._tool.steps[idx] = dialog.get_step() self._refresh_steps() def _delete_step(self): """Delete selected step.""" items = self.steps_list.selectedItems() if not items: return idx = self.steps_list.row(items[0]) del self._tool.steps[idx] self._refresh_steps() def _save(self): """Save the tool.""" name = self.name_input.text().strip() if not name: QMessageBox.warning(self, "Validation", "Tool name is required") return # Validate name error = validate_tool_name(name) if error: QMessageBox.warning(self, "Validation", error) return description = self.desc_input.text().strip() category = self.category_combo.currentText() output = self.output_input.toPlainText().strip() or "{response}" # Build tool object tool = Tool( name=name, description=description, category=category, arguments=self._tool.arguments if self._tool else [], steps=self._tool.steps if self._tool else [], output=output ) # Preserve source if editing if self._tool and self._tool.source: tool.source = self._tool.source try: save_tool(tool) self.main_window.show_status(f"Saved tool '{name}'") self.main_window.close_tool_builder() except Exception as e: QMessageBox.critical(self, "Error", f"Failed to save tool:\n{e}") def _cancel(self): """Cancel and return to tools page.""" self.main_window.close_tool_builder() def save_tool(self): """Public method for keyboard shortcut to save the tool.""" self._save()