diff --git a/src/cmdforge/gui/dialogs/step_dialog.py b/src/cmdforge/gui/dialogs/step_dialog.py index 3c57874..c5d02ee 100644 --- a/src/cmdforge/gui/dialogs/step_dialog.py +++ b/src/cmdforge/gui/dialogs/step_dialog.py @@ -36,6 +36,11 @@ class PromptStepDialog(QDialog): form = QFormLayout() form.setSpacing(12) + # Step name (optional) + self.name_input = QLineEdit() + self.name_input.setPlaceholderText("Optional display name") + form.addRow("Step name:", self.name_input) + # Provider selection self.provider_combo = QComboBox() providers = load_providers() @@ -95,6 +100,10 @@ class PromptStepDialog(QDialog): def _load_step(self, step: PromptStep): """Load step data into form.""" + # Load name + if step.name: + self.name_input.setText(step.name) + idx = self.provider_combo.findText(step.provider) if idx >= 0: self.provider_combo.setCurrentIndex(idx) @@ -130,11 +139,14 @@ class PromptStepDialog(QDialog): # Don't store "None" profile if profile == "None": profile = None + # Get name, use None if empty + name = self.name_input.text().strip() or None return PromptStep( prompt=self.prompt_input.toPlainText(), provider=self.provider_combo.currentText(), output_var=self.output_input.text().strip(), - profile=profile + profile=profile, + name=name ) @@ -179,9 +191,15 @@ class CodeStepDialog(QDialog): layout = QVBoxLayout(self) layout.setSpacing(12) - # Top: Output variable + # Top: Step name and Output variable form = QFormLayout() form.setSpacing(8) + + # Step name (optional) + self.name_input = QLineEdit() + self.name_input.setPlaceholderText("Optional display name") + form.addRow("Step name:", self.name_input) + self.output_input = QLineEdit() self.output_input.setPlaceholderText("result") self.output_input.setText("result") @@ -365,6 +383,9 @@ IMPORTANT: Return ONLY executable inline Python code. No function definitions, n def _load_step(self, step: CodeStep): """Load step data into form.""" + # Load name + if step.name: + self.name_input.setText(step.name) self.output_input.setText(step.output_var) self.code_input.setPlainText(step.code) @@ -396,7 +417,10 @@ IMPORTANT: Return ONLY executable inline Python code. No function definitions, n def get_step(self) -> CodeStep: """Get the step from form data.""" + # Get name, use None if empty + name = self.name_input.text().strip() or None return CodeStep( code=self.code_input.toPlainText(), - output_var=self.output_input.text().strip() + output_var=self.output_input.text().strip(), + name=name ) diff --git a/src/cmdforge/gui/pages/tool_builder_page.py b/src/cmdforge/gui/pages/tool_builder_page.py index 370a191..104699d 100644 --- a/src/cmdforge/gui/pages/tool_builder_page.py +++ b/src/cmdforge/gui/pages/tool_builder_page.py @@ -309,9 +309,12 @@ class ToolBuilderPage(QWidget): 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}" + # Use custom name if set, otherwise default format + step_name = step.name if step.name else f"Prompt {i}" + text = f"{i}. {step_name} [{step.provider}] → ${step.output_var}" elif isinstance(step, CodeStep): - text = f"{i}. Code [python] → ${step.output_var}" + step_name = step.name if step.name else f"Code {i}" + text = f"{i}. {step_name} [python] → ${step.output_var}" else: text = f"{i}. Unknown step" item = QListWidgetItem(text) diff --git a/src/cmdforge/gui/widgets/flow_graph.py b/src/cmdforge/gui/widgets/flow_graph.py index 71e0f55..957116a 100644 --- a/src/cmdforge/gui/widgets/flow_graph.py +++ b/src/cmdforge/gui/widgets/flow_graph.py @@ -2,8 +2,9 @@ from typing import Optional, List -from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout -from PySide6.QtCore import Signal, Qt, QTimer +from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QPushButton +from PySide6.QtCore import Signal, Qt, QTimer, QEvent +from PySide6.QtGui import QKeyEvent from NodeGraphQt import NodeGraph, BaseNode @@ -163,9 +164,23 @@ class FlowGraphWidget(QWidget): help_layout.addStretch() # Fit view button - fit_label = QLabel("Press F to fit all") - fit_label.setStyleSheet("color: #667eea; font-weight: 500;") - help_layout.addWidget(fit_label) + self.btn_fit = QPushButton("Fit All (F)") + self.btn_fit.setStyleSheet(""" + QPushButton { + background-color: #667eea; + color: white; + border: none; + border-radius: 4px; + padding: 4px 12px; + font-size: 11px; + font-weight: 500; + } + QPushButton:hover { + background-color: #5a67d8; + } + """) + self.btn_fit.clicked.connect(self.fit_all_nodes) + help_layout.addWidget(self.btn_fit) layout.addWidget(help_bar) @@ -184,6 +199,9 @@ class FlowGraphWidget(QWidget): # Add graph widget layout.addWidget(self._graph.widget, 1) + # Install event filter to catch F key on the graph widget + self._graph.widget.installEventFilter(self) + def set_tool(self, tool: Optional[Tool]): """Set the tool to visualize.""" self._tool = tool @@ -218,16 +236,20 @@ class FlowGraphWidget(QWidget): for i, step in enumerate(self._tool.steps or []): if isinstance(step, PromptStep): + # Use custom name if set, otherwise default to "Prompt N" + node_name = step.name if step.name else f'Prompt {i+1}' node = self._graph.create_node( 'cmdforge.PromptNode', - name=f'Prompt {i+1}', + name=node_name, pos=[x_pos, 0] ) node.set_step(step, i) elif isinstance(step, CodeStep): + # Use custom name if set, otherwise default to "Code N" + node_name = step.name if step.name else f'Code {i+1}' node = self._graph.create_node( 'cmdforge.CodeNode', - name=f'Code {i+1}', + name=node_name, pos=[x_pos, 0] ) node.set_step(step, i) @@ -272,6 +294,36 @@ class FlowGraphWidget(QWidget): # Clear selection self._graph.clear_selection() + def fit_all_nodes(self): + """Fit view to show all nodes.""" + if not self._graph: + return + + all_nodes = self._graph.all_nodes() + if not all_nodes: + return + + # Select all, fit, then clear selection + for node in all_nodes: + node.set_selected(True) + self._graph.fit_to_selection() + self._graph.clear_selection() + + def keyPressEvent(self, event: QKeyEvent): + """Handle keyboard shortcuts.""" + if event.key() == Qt.Key_F: + self.fit_all_nodes() + event.accept() + else: + super().keyPressEvent(event) + + def eventFilter(self, obj, event): + """Filter events from the graph widget to catch F key.""" + if event.type() == QEvent.KeyPress and event.key() == Qt.Key_F: + self.fit_all_nodes() + return True # Event handled + return super().eventFilter(obj, event) + def _on_node_double_clicked(self, node): """Handle node double-click.""" if hasattr(node, '_step_index') and node._step_index >= 0: diff --git a/src/cmdforge/tool.py b/src/cmdforge/tool.py index 2d92c1d..7c41d0e 100644 --- a/src/cmdforge/tool.py +++ b/src/cmdforge/tool.py @@ -50,6 +50,7 @@ class PromptStep: output_var: str # Variable to store output prompt_file: Optional[str] = None # Optional filename for external prompt profile: Optional[str] = None # Optional AI persona profile name + name: Optional[str] = None # Optional display name for the step def to_dict(self) -> dict: d = { @@ -62,6 +63,8 @@ class PromptStep: d["prompt_file"] = self.prompt_file if self.profile: d["profile"] = self.profile + if self.name: + d["name"] = self.name return d @classmethod @@ -71,7 +74,8 @@ class PromptStep: provider=data["provider"], output_var=data["output_var"], prompt_file=data.get("prompt_file"), - profile=data.get("profile") + profile=data.get("profile"), + name=data.get("name") ) @@ -81,6 +85,7 @@ class CodeStep: code: str # Python code (inline or loaded from file) output_var: str # Variable name(s) to capture (comma-separated for multiple) code_file: Optional[str] = None # Optional filename for external code + name: Optional[str] = None # Optional display name for the step def to_dict(self) -> dict: d = { @@ -90,6 +95,8 @@ class CodeStep: } if self.code_file: d["code_file"] = self.code_file + if self.name: + d["name"] = self.name return d @classmethod @@ -97,7 +104,8 @@ class CodeStep: return cls( code=data.get("code", ""), output_var=data["output_var"], - code_file=data.get("code_file") + code_file=data.get("code_file"), + name=data.get("name") ) @@ -109,6 +117,7 @@ class ToolStep: input_template: str = "{input}" # Input template (supports variable substitution) args: dict = field(default_factory=dict) # Arguments to pass to the tool provider: Optional[str] = None # Provider override for the called tool + name: Optional[str] = None # Optional display name for the step def to_dict(self) -> dict: d = { @@ -122,6 +131,8 @@ class ToolStep: d["args"] = self.args if self.provider: d["provider"] = self.provider + if self.name: + d["name"] = self.name return d @classmethod @@ -131,7 +142,8 @@ class ToolStep: output_var=data["output_var"], input_template=data.get("input", "{input}"), args=data.get("args", {}), - provider=data.get("provider") + provider=data.get("provider"), + name=data.get("name") )