Add custom step names and fix F key shortcut
- Add name field to PromptStep, CodeStep, and ToolStep dataclasses - Update step dialogs to allow editing step names - Display custom step names in both list and flow views - Fix F key shortcut by installing event filter on graph widget - Steps without custom names show default "Prompt N" / "Code N" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3c39cc9cda
commit
88c9a8a1e7
|
|
@ -36,6 +36,11 @@ class PromptStepDialog(QDialog):
|
||||||
form = QFormLayout()
|
form = QFormLayout()
|
||||||
form.setSpacing(12)
|
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
|
# Provider selection
|
||||||
self.provider_combo = QComboBox()
|
self.provider_combo = QComboBox()
|
||||||
providers = load_providers()
|
providers = load_providers()
|
||||||
|
|
@ -95,6 +100,10 @@ class PromptStepDialog(QDialog):
|
||||||
|
|
||||||
def _load_step(self, step: PromptStep):
|
def _load_step(self, step: PromptStep):
|
||||||
"""Load step data into form."""
|
"""Load step data into form."""
|
||||||
|
# Load name
|
||||||
|
if step.name:
|
||||||
|
self.name_input.setText(step.name)
|
||||||
|
|
||||||
idx = self.provider_combo.findText(step.provider)
|
idx = self.provider_combo.findText(step.provider)
|
||||||
if idx >= 0:
|
if idx >= 0:
|
||||||
self.provider_combo.setCurrentIndex(idx)
|
self.provider_combo.setCurrentIndex(idx)
|
||||||
|
|
@ -130,11 +139,14 @@ class PromptStepDialog(QDialog):
|
||||||
# Don't store "None" profile
|
# Don't store "None" profile
|
||||||
if profile == "None":
|
if profile == "None":
|
||||||
profile = None
|
profile = None
|
||||||
|
# Get name, use None if empty
|
||||||
|
name = self.name_input.text().strip() or None
|
||||||
return PromptStep(
|
return PromptStep(
|
||||||
prompt=self.prompt_input.toPlainText(),
|
prompt=self.prompt_input.toPlainText(),
|
||||||
provider=self.provider_combo.currentText(),
|
provider=self.provider_combo.currentText(),
|
||||||
output_var=self.output_input.text().strip(),
|
output_var=self.output_input.text().strip(),
|
||||||
profile=profile
|
profile=profile,
|
||||||
|
name=name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -179,9 +191,15 @@ class CodeStepDialog(QDialog):
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.setSpacing(12)
|
layout.setSpacing(12)
|
||||||
|
|
||||||
# Top: Output variable
|
# Top: Step name and Output variable
|
||||||
form = QFormLayout()
|
form = QFormLayout()
|
||||||
form.setSpacing(8)
|
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 = QLineEdit()
|
||||||
self.output_input.setPlaceholderText("result")
|
self.output_input.setPlaceholderText("result")
|
||||||
self.output_input.setText("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):
|
def _load_step(self, step: CodeStep):
|
||||||
"""Load step data into form."""
|
"""Load step data into form."""
|
||||||
|
# Load name
|
||||||
|
if step.name:
|
||||||
|
self.name_input.setText(step.name)
|
||||||
self.output_input.setText(step.output_var)
|
self.output_input.setText(step.output_var)
|
||||||
self.code_input.setPlainText(step.code)
|
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:
|
def get_step(self) -> CodeStep:
|
||||||
"""Get the step from form data."""
|
"""Get the step from form data."""
|
||||||
|
# Get name, use None if empty
|
||||||
|
name = self.name_input.text().strip() or None
|
||||||
return CodeStep(
|
return CodeStep(
|
||||||
code=self.code_input.toPlainText(),
|
code=self.code_input.toPlainText(),
|
||||||
output_var=self.output_input.text().strip()
|
output_var=self.output_input.text().strip(),
|
||||||
|
name=name
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -309,9 +309,12 @@ class ToolBuilderPage(QWidget):
|
||||||
if self._tool and self._tool.steps:
|
if self._tool and self._tool.steps:
|
||||||
for i, step in enumerate(self._tool.steps, 1):
|
for i, step in enumerate(self._tool.steps, 1):
|
||||||
if isinstance(step, PromptStep):
|
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):
|
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:
|
else:
|
||||||
text = f"{i}. Unknown step"
|
text = f"{i}. Unknown step"
|
||||||
item = QListWidgetItem(text)
|
item = QListWidgetItem(text)
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
|
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QPushButton
|
||||||
from PySide6.QtCore import Signal, Qt, QTimer
|
from PySide6.QtCore import Signal, Qt, QTimer, QEvent
|
||||||
|
from PySide6.QtGui import QKeyEvent
|
||||||
|
|
||||||
from NodeGraphQt import NodeGraph, BaseNode
|
from NodeGraphQt import NodeGraph, BaseNode
|
||||||
|
|
||||||
|
|
@ -163,9 +164,23 @@ class FlowGraphWidget(QWidget):
|
||||||
help_layout.addStretch()
|
help_layout.addStretch()
|
||||||
|
|
||||||
# Fit view button
|
# Fit view button
|
||||||
fit_label = QLabel("Press F to fit all")
|
self.btn_fit = QPushButton("Fit All (F)")
|
||||||
fit_label.setStyleSheet("color: #667eea; font-weight: 500;")
|
self.btn_fit.setStyleSheet("""
|
||||||
help_layout.addWidget(fit_label)
|
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)
|
layout.addWidget(help_bar)
|
||||||
|
|
||||||
|
|
@ -184,6 +199,9 @@ class FlowGraphWidget(QWidget):
|
||||||
# Add graph widget
|
# Add graph widget
|
||||||
layout.addWidget(self._graph.widget, 1)
|
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]):
|
def set_tool(self, tool: Optional[Tool]):
|
||||||
"""Set the tool to visualize."""
|
"""Set the tool to visualize."""
|
||||||
self._tool = tool
|
self._tool = tool
|
||||||
|
|
@ -218,16 +236,20 @@ class FlowGraphWidget(QWidget):
|
||||||
|
|
||||||
for i, step in enumerate(self._tool.steps or []):
|
for i, step in enumerate(self._tool.steps or []):
|
||||||
if isinstance(step, PromptStep):
|
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(
|
node = self._graph.create_node(
|
||||||
'cmdforge.PromptNode',
|
'cmdforge.PromptNode',
|
||||||
name=f'Prompt {i+1}',
|
name=node_name,
|
||||||
pos=[x_pos, 0]
|
pos=[x_pos, 0]
|
||||||
)
|
)
|
||||||
node.set_step(step, i)
|
node.set_step(step, i)
|
||||||
elif isinstance(step, CodeStep):
|
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(
|
node = self._graph.create_node(
|
||||||
'cmdforge.CodeNode',
|
'cmdforge.CodeNode',
|
||||||
name=f'Code {i+1}',
|
name=node_name,
|
||||||
pos=[x_pos, 0]
|
pos=[x_pos, 0]
|
||||||
)
|
)
|
||||||
node.set_step(step, i)
|
node.set_step(step, i)
|
||||||
|
|
@ -272,6 +294,36 @@ class FlowGraphWidget(QWidget):
|
||||||
# Clear selection
|
# Clear selection
|
||||||
self._graph.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):
|
def _on_node_double_clicked(self, node):
|
||||||
"""Handle node double-click."""
|
"""Handle node double-click."""
|
||||||
if hasattr(node, '_step_index') and node._step_index >= 0:
|
if hasattr(node, '_step_index') and node._step_index >= 0:
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ class PromptStep:
|
||||||
output_var: str # Variable to store output
|
output_var: str # Variable to store output
|
||||||
prompt_file: Optional[str] = None # Optional filename for external prompt
|
prompt_file: Optional[str] = None # Optional filename for external prompt
|
||||||
profile: Optional[str] = None # Optional AI persona profile name
|
profile: Optional[str] = None # Optional AI persona profile name
|
||||||
|
name: Optional[str] = None # Optional display name for the step
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
d = {
|
d = {
|
||||||
|
|
@ -62,6 +63,8 @@ class PromptStep:
|
||||||
d["prompt_file"] = self.prompt_file
|
d["prompt_file"] = self.prompt_file
|
||||||
if self.profile:
|
if self.profile:
|
||||||
d["profile"] = self.profile
|
d["profile"] = self.profile
|
||||||
|
if self.name:
|
||||||
|
d["name"] = self.name
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -71,7 +74,8 @@ class PromptStep:
|
||||||
provider=data["provider"],
|
provider=data["provider"],
|
||||||
output_var=data["output_var"],
|
output_var=data["output_var"],
|
||||||
prompt_file=data.get("prompt_file"),
|
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)
|
code: str # Python code (inline or loaded from file)
|
||||||
output_var: str # Variable name(s) to capture (comma-separated for multiple)
|
output_var: str # Variable name(s) to capture (comma-separated for multiple)
|
||||||
code_file: Optional[str] = None # Optional filename for external code
|
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:
|
def to_dict(self) -> dict:
|
||||||
d = {
|
d = {
|
||||||
|
|
@ -90,6 +95,8 @@ class CodeStep:
|
||||||
}
|
}
|
||||||
if self.code_file:
|
if self.code_file:
|
||||||
d["code_file"] = self.code_file
|
d["code_file"] = self.code_file
|
||||||
|
if self.name:
|
||||||
|
d["name"] = self.name
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -97,7 +104,8 @@ class CodeStep:
|
||||||
return cls(
|
return cls(
|
||||||
code=data.get("code", ""),
|
code=data.get("code", ""),
|
||||||
output_var=data["output_var"],
|
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)
|
input_template: str = "{input}" # Input template (supports variable substitution)
|
||||||
args: dict = field(default_factory=dict) # Arguments to pass to the tool
|
args: dict = field(default_factory=dict) # Arguments to pass to the tool
|
||||||
provider: Optional[str] = None # Provider override for the called 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:
|
def to_dict(self) -> dict:
|
||||||
d = {
|
d = {
|
||||||
|
|
@ -122,6 +131,8 @@ class ToolStep:
|
||||||
d["args"] = self.args
|
d["args"] = self.args
|
||||||
if self.provider:
|
if self.provider:
|
||||||
d["provider"] = self.provider
|
d["provider"] = self.provider
|
||||||
|
if self.name:
|
||||||
|
d["name"] = self.name
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -131,7 +142,8 @@ class ToolStep:
|
||||||
output_var=data["output_var"],
|
output_var=data["output_var"],
|
||||||
input_template=data.get("input", "{input}"),
|
input_template=data.get("input", "{input}"),
|
||||||
args=data.get("args", {}),
|
args=data.get("args", {}),
|
||||||
provider=data.get("provider")
|
provider=data.get("provider"),
|
||||||
|
name=data.get("name")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue