Compare commits
No commits in common. "ad9a59283cfa0e34a9fe93ecd505d28635a6c550" and "a5e0948881be37e5266fbbdbe018a1db54d63407" have entirely different histories.
ad9a59283c
...
a5e0948881
|
|
@ -1,80 +0,0 @@
|
||||||
# PySide6 6.7+ Compatibility Issue: setSelectionArea API Change
|
|
||||||
|
|
||||||
## Issue
|
|
||||||
|
|
||||||
When using NodeGraphQt-QuiltiX-fork with PySide6 6.7+, dragging a rubber band selection in the node graph causes repeated TypeError exceptions:
|
|
||||||
|
|
||||||
```
|
|
||||||
TypeError: 'PySide6.QtWidgets.QGraphicsScene.setSelectionArea' called with wrong argument types:
|
|
||||||
PySide6.QtWidgets.QGraphicsScene.setSelectionArea(QPainterPath, ItemSelectionMode)
|
|
||||||
Supported signatures:
|
|
||||||
PySide6.QtWidgets.QGraphicsScene.setSelectionArea(PySide6.QtGui.QPainterPath, PySide6.QtGui.QTransform)
|
|
||||||
PySide6.QtWidgets.QGraphicsScene.setSelectionArea(PySide6.QtGui.QPainterPath, PySide6.QtCore.Qt.ItemSelectionOperation = Instance(Qt.ReplaceSelection), PySide6.QtCore.Qt.ItemSelectionMode = Instance(Qt.IntersectsItemShape), PySide6.QtGui.QTransform = Default(QTransform))
|
|
||||||
```
|
|
||||||
|
|
||||||
## Root Cause
|
|
||||||
|
|
||||||
The `QGraphicsScene.setSelectionArea()` method signature changed in PySide6 6.7. The old signature allowed:
|
|
||||||
|
|
||||||
```python
|
|
||||||
setSelectionArea(path, mode) # mode = Qt.ItemSelectionMode
|
|
||||||
```
|
|
||||||
|
|
||||||
The new signature requires:
|
|
||||||
|
|
||||||
```python
|
|
||||||
setSelectionArea(path, operation, mode) # operation = Qt.ItemSelectionOperation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Affected File
|
|
||||||
|
|
||||||
`NodeGraphQt/widgets/viewer.py` line 524-526
|
|
||||||
|
|
||||||
## Current Code
|
|
||||||
|
|
||||||
```python
|
|
||||||
self.scene().setSelectionArea(
|
|
||||||
path, QtCore.Qt.IntersectsItemShape
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Fix
|
|
||||||
|
|
||||||
```python
|
|
||||||
self.scene().setSelectionArea(
|
|
||||||
path, QtCore.Qt.ReplaceSelection,
|
|
||||||
QtCore.Qt.IntersectsItemShape
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment
|
|
||||||
|
|
||||||
- Python: 3.12
|
|
||||||
- PySide6: 6.6.3.1 (installed by NodeGraphQt-QuiltiX-fork, but issue affects 6.7+)
|
|
||||||
- NodeGraphQt-QuiltiX-fork: 0.7.0
|
|
||||||
|
|
||||||
## Workaround
|
|
||||||
|
|
||||||
Apply this patch after installation:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# In viewer.py, change line 524-526 from:
|
|
||||||
self.scene().setSelectionArea(
|
|
||||||
path, QtCore.Qt.IntersectsItemShape
|
|
||||||
)
|
|
||||||
|
|
||||||
# To:
|
|
||||||
self.scene().setSelectionArea(
|
|
||||||
path, QtCore.Qt.ReplaceSelection,
|
|
||||||
QtCore.Qt.IntersectsItemShape
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
This issue may also exist in:
|
|
||||||
- Original NodeGraphQt
|
|
||||||
- OdenGraphQt
|
|
||||||
- Other forks
|
|
||||||
|
|
||||||
The fix is backward compatible with older PySide6 versions that support the 3-argument signature.
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
--- a/NodeGraphQt/widgets/viewer.py
|
|
||||||
+++ b/NodeGraphQt/widgets/viewer.py
|
|
||||||
@@ -521,8 +521,10 @@ class NodeViewer(QtWidgets.QGraphicsView):
|
|
||||||
path = QtGui.QPainterPath()
|
|
||||||
path.addRect(map_rect)
|
|
||||||
self._rubber_band.setGeometry(rect)
|
|
||||||
+ # PySide6 6.7+ requires ItemSelectionOperation parameter
|
|
||||||
self.scene().setSelectionArea(
|
|
||||||
- path, QtCore.Qt.IntersectsItemShape
|
|
||||||
+ path, QtCore.Qt.ReplaceSelection,
|
|
||||||
+ QtCore.Qt.IntersectsItemShape
|
|
||||||
)
|
|
||||||
self.scene().update(map_rect)
|
|
||||||
|
|
@ -46,17 +46,11 @@ registry = [
|
||||||
"sentry-sdk[flask]>=1.0",
|
"sentry-sdk[flask]>=1.0",
|
||||||
"gunicorn>=21.0",
|
"gunicorn>=21.0",
|
||||||
]
|
]
|
||||||
flow = [
|
|
||||||
"NodeGraphQt-QuiltiX-fork[pyside6]>=0.7.0",
|
|
||||||
"setuptools", # Required for distutils compatibility
|
|
||||||
]
|
|
||||||
all = [
|
all = [
|
||||||
"Flask>=2.3",
|
"Flask>=2.3",
|
||||||
"argon2-cffi>=21.0",
|
"argon2-cffi>=21.0",
|
||||||
"sentry-sdk[flask]>=1.0",
|
"sentry-sdk[flask]>=1.0",
|
||||||
"gunicorn>=21.0",
|
"gunicorn>=21.0",
|
||||||
"NodeGraphQt-QuiltiX-fork[pyside6]>=0.7.0",
|
|
||||||
"setuptools",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Patch NodeGraphQt for PySide6 6.7+ compatibility.
|
|
||||||
|
|
||||||
The setSelectionArea() API changed in PySide6 6.7 to require an
|
|
||||||
ItemSelectionOperation parameter. This script patches the installed
|
|
||||||
NodeGraphQt package to work with newer PySide6 versions.
|
|
||||||
|
|
||||||
Issue: setSelectionArea(path, mode) -> setSelectionArea(path, operation, mode)
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python scripts/patch_nodegraphqt.py
|
|
||||||
|
|
||||||
This can be run after `pip install NodeGraphQt-QuiltiX-fork[pyside6]`
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import site
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def find_viewer_file():
|
|
||||||
"""Find the NodeGraphQt viewer.py file in site-packages."""
|
|
||||||
# Check common locations
|
|
||||||
for site_dir in site.getsitepackages() + [site.getusersitepackages()]:
|
|
||||||
viewer_path = Path(site_dir) / "NodeGraphQt" / "widgets" / "viewer.py"
|
|
||||||
if viewer_path.exists():
|
|
||||||
return viewer_path
|
|
||||||
|
|
||||||
# Check virtual environment
|
|
||||||
venv_path = Path(sys.prefix) / "lib"
|
|
||||||
for python_dir in venv_path.glob("python*"):
|
|
||||||
viewer_path = python_dir / "site-packages" / "NodeGraphQt" / "widgets" / "viewer.py"
|
|
||||||
if viewer_path.exists():
|
|
||||||
return viewer_path
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def patch_file(viewer_path: Path) -> bool:
|
|
||||||
"""Apply the PySide6 compatibility patch."""
|
|
||||||
content = viewer_path.read_text()
|
|
||||||
|
|
||||||
# Check if already patched
|
|
||||||
if "Qt.ReplaceSelection" in content:
|
|
||||||
print(f"Already patched: {viewer_path}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# The old code pattern
|
|
||||||
old_code = """self.scene().setSelectionArea(
|
|
||||||
path, QtCore.Qt.IntersectsItemShape
|
|
||||||
)"""
|
|
||||||
|
|
||||||
# The new code with ItemSelectionOperation
|
|
||||||
new_code = """self.scene().setSelectionArea(
|
|
||||||
path, QtCore.Qt.ReplaceSelection,
|
|
||||||
QtCore.Qt.IntersectsItemShape
|
|
||||||
)"""
|
|
||||||
|
|
||||||
if old_code not in content:
|
|
||||||
print(f"Warning: Could not find code to patch in {viewer_path}")
|
|
||||||
print("The file may have a different format or already be modified.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Apply patch
|
|
||||||
patched_content = content.replace(old_code, new_code)
|
|
||||||
|
|
||||||
# Backup original
|
|
||||||
backup_path = viewer_path.with_suffix(".py.bak")
|
|
||||||
if not backup_path.exists():
|
|
||||||
viewer_path.rename(backup_path)
|
|
||||||
print(f"Backup created: {backup_path}")
|
|
||||||
|
|
||||||
# Write patched file
|
|
||||||
viewer_path.write_text(patched_content)
|
|
||||||
print(f"Patched successfully: {viewer_path}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("NodeGraphQt PySide6 Compatibility Patch")
|
|
||||||
print("=" * 40)
|
|
||||||
|
|
||||||
viewer_path = find_viewer_file()
|
|
||||||
|
|
||||||
if not viewer_path:
|
|
||||||
print("Error: Could not find NodeGraphQt installation.")
|
|
||||||
print("Make sure NodeGraphQt-QuiltiX-fork is installed:")
|
|
||||||
print(" pip install 'NodeGraphQt-QuiltiX-fork[pyside6]'")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print(f"Found: {viewer_path}")
|
|
||||||
|
|
||||||
if patch_file(viewer_path):
|
|
||||||
print("\nPatch applied successfully!")
|
|
||||||
print("The rubber band selection should now work in PySide6 6.7+")
|
|
||||||
return 0
|
|
||||||
else:
|
|
||||||
print("\nPatch failed.")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Test script for NodeGraphQt integration."""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
|
|
||||||
from NodeGraphQt import NodeGraph, BaseNode
|
|
||||||
|
|
||||||
|
|
||||||
class InputNode(BaseNode):
|
|
||||||
"""Node representing tool input."""
|
|
||||||
|
|
||||||
__identifier__ = 'cmdforge.nodes'
|
|
||||||
NODE_NAME = 'Input'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.set_color(100, 100, 180) # Blue-ish
|
|
||||||
self.add_output('input', color=(180, 180, 250))
|
|
||||||
self.add_output('file', color=(180, 180, 250))
|
|
||||||
|
|
||||||
|
|
||||||
class PromptNode(BaseNode):
|
|
||||||
"""Node representing a prompt step."""
|
|
||||||
|
|
||||||
__identifier__ = 'cmdforge.nodes'
|
|
||||||
NODE_NAME = 'Prompt'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.set_color(80, 120, 180) # Indigo-ish
|
|
||||||
self.add_input('in', color=(180, 180, 250))
|
|
||||||
self.add_output('response', color=(180, 250, 180))
|
|
||||||
|
|
||||||
# Add properties
|
|
||||||
self.add_text_input('provider', 'Provider', text='claude')
|
|
||||||
self.add_text_input('output_var', 'Output Var', text='response')
|
|
||||||
|
|
||||||
|
|
||||||
class CodeNode(BaseNode):
|
|
||||||
"""Node representing a code step."""
|
|
||||||
|
|
||||||
__identifier__ = 'cmdforge.nodes'
|
|
||||||
NODE_NAME = 'Code'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.set_color(80, 160, 100) # Green-ish
|
|
||||||
self.add_input('in', color=(180, 250, 180))
|
|
||||||
self.add_output('result', color=(250, 220, 180))
|
|
||||||
|
|
||||||
# Add properties
|
|
||||||
self.add_text_input('output_var', 'Output Var', text='result')
|
|
||||||
|
|
||||||
|
|
||||||
class OutputNode(BaseNode):
|
|
||||||
"""Node representing tool output."""
|
|
||||||
|
|
||||||
__identifier__ = 'cmdforge.nodes'
|
|
||||||
NODE_NAME = 'Output'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.set_color(180, 120, 80) # Orange-ish
|
|
||||||
self.add_input('in', color=(250, 220, 180))
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
app = QApplication(sys.argv)
|
|
||||||
|
|
||||||
# Create main window
|
|
||||||
window = QMainWindow()
|
|
||||||
window.setWindowTitle('CmdForge Flow Visualization - Prototype')
|
|
||||||
window.setGeometry(100, 100, 1000, 700)
|
|
||||||
|
|
||||||
# Create node graph
|
|
||||||
graph = NodeGraph()
|
|
||||||
|
|
||||||
# Register our custom nodes
|
|
||||||
graph.register_node(InputNode)
|
|
||||||
graph.register_node(PromptNode)
|
|
||||||
graph.register_node(CodeNode)
|
|
||||||
graph.register_node(OutputNode)
|
|
||||||
|
|
||||||
# Get the graph widget
|
|
||||||
graph_widget = graph.widget
|
|
||||||
|
|
||||||
# Set as central widget
|
|
||||||
window.setCentralWidget(graph_widget)
|
|
||||||
|
|
||||||
# Create a sample tool visualization
|
|
||||||
input_node = graph.create_node('cmdforge.nodes.InputNode', name='Input')
|
|
||||||
input_node.set_pos(-300, 0)
|
|
||||||
|
|
||||||
prompt_node = graph.create_node('cmdforge.nodes.PromptNode', name='Summarize')
|
|
||||||
prompt_node.set_pos(-50, 0)
|
|
||||||
prompt_node.set_property('provider', 'claude')
|
|
||||||
prompt_node.set_property('output_var', 'response')
|
|
||||||
|
|
||||||
code_node = graph.create_node('cmdforge.nodes.CodeNode', name='Format')
|
|
||||||
code_node.set_pos(200, 0)
|
|
||||||
code_node.set_property('output_var', 'formatted')
|
|
||||||
|
|
||||||
output_node = graph.create_node('cmdforge.nodes.OutputNode', name='Output')
|
|
||||||
output_node.set_pos(450, 0)
|
|
||||||
|
|
||||||
# Connect nodes
|
|
||||||
input_node.output(0).connect_to(prompt_node.input(0))
|
|
||||||
prompt_node.output(0).connect_to(code_node.input(0))
|
|
||||||
code_node.output(0).connect_to(output_node.input(0))
|
|
||||||
|
|
||||||
# Auto-layout
|
|
||||||
graph.auto_layout_nodes()
|
|
||||||
|
|
||||||
# Fit view
|
|
||||||
graph.fit_to_selection()
|
|
||||||
|
|
||||||
window.show()
|
|
||||||
|
|
||||||
print("Flow visualization prototype running...")
|
|
||||||
print("Nodes created: Input -> Prompt -> Code -> Output")
|
|
||||||
print("Close the window to exit.")
|
|
||||||
|
|
||||||
sys.exit(app.exec())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
|
|
@ -4,8 +4,7 @@ from PySide6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QFormLayout,
|
QWidget, QVBoxLayout, QHBoxLayout, QFormLayout,
|
||||||
QLineEdit, QTextEdit, QComboBox, QPushButton,
|
QLineEdit, QTextEdit, QComboBox, QPushButton,
|
||||||
QGroupBox, QListWidget, QListWidgetItem, QLabel,
|
QGroupBox, QListWidget, QListWidgetItem, QLabel,
|
||||||
QMessageBox, QSplitter, QFrame, QStackedWidget,
|
QMessageBox, QSplitter, QFrame
|
||||||
QButtonGroup
|
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
|
|
||||||
|
|
@ -24,7 +23,6 @@ class ToolBuilderPage(QWidget):
|
||||||
self.editing = tool_name is not None
|
self.editing = tool_name is not None
|
||||||
self.original_name = tool_name
|
self.original_name = tool_name
|
||||||
self._tool = None
|
self._tool = None
|
||||||
self._flow_widget = None # Lazy-loaded
|
|
||||||
|
|
||||||
self._setup_ui()
|
self._setup_ui()
|
||||||
|
|
||||||
|
|
@ -125,57 +123,14 @@ class ToolBuilderPage(QWidget):
|
||||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
right_layout.setSpacing(16)
|
right_layout.setSpacing(16)
|
||||||
|
|
||||||
# Steps group with view toggle
|
# Steps group
|
||||||
steps_box = QGroupBox("Steps")
|
steps_box = QGroupBox("Steps")
|
||||||
steps_layout = QVBoxLayout(steps_box)
|
steps_layout = QVBoxLayout(steps_box)
|
||||||
|
|
||||||
# View toggle header
|
|
||||||
view_header = QHBoxLayout()
|
|
||||||
|
|
||||||
self.btn_list_view = QPushButton("List")
|
|
||||||
self.btn_list_view.setCheckable(True)
|
|
||||||
self.btn_list_view.setChecked(True)
|
|
||||||
self.btn_list_view.setObjectName("viewToggle")
|
|
||||||
self.btn_list_view.clicked.connect(lambda: self._set_view_mode(0))
|
|
||||||
|
|
||||||
self.btn_flow_view = QPushButton("Flow")
|
|
||||||
self.btn_flow_view.setCheckable(True)
|
|
||||||
self.btn_flow_view.setObjectName("viewToggle")
|
|
||||||
self.btn_flow_view.clicked.connect(lambda: self._set_view_mode(1))
|
|
||||||
|
|
||||||
# Button group for mutual exclusivity
|
|
||||||
self.view_group = QButtonGroup(self)
|
|
||||||
self.view_group.addButton(self.btn_list_view, 0)
|
|
||||||
self.view_group.addButton(self.btn_flow_view, 1)
|
|
||||||
|
|
||||||
view_header.addWidget(self.btn_list_view)
|
|
||||||
view_header.addWidget(self.btn_flow_view)
|
|
||||||
view_header.addStretch()
|
|
||||||
steps_layout.addLayout(view_header)
|
|
||||||
|
|
||||||
# Stacked widget for list/flow views
|
|
||||||
self.steps_stack = QStackedWidget()
|
|
||||||
|
|
||||||
# List view (index 0)
|
|
||||||
list_container = QWidget()
|
|
||||||
list_layout = QVBoxLayout(list_container)
|
|
||||||
list_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
|
|
||||||
self.steps_list = QListWidget()
|
self.steps_list = QListWidget()
|
||||||
self.steps_list.itemDoubleClicked.connect(self._edit_step)
|
self.steps_list.itemDoubleClicked.connect(self._edit_step)
|
||||||
list_layout.addWidget(self.steps_list)
|
steps_layout.addWidget(self.steps_list)
|
||||||
|
|
||||||
self.steps_stack.addWidget(list_container)
|
|
||||||
|
|
||||||
# Flow view placeholder (index 1) - lazy loaded
|
|
||||||
flow_placeholder = QLabel("Loading flow visualization...")
|
|
||||||
flow_placeholder.setAlignment(Qt.AlignCenter)
|
|
||||||
flow_placeholder.setStyleSheet("color: #718096; padding: 40px;")
|
|
||||||
self.steps_stack.addWidget(flow_placeholder)
|
|
||||||
|
|
||||||
steps_layout.addWidget(self.steps_stack)
|
|
||||||
|
|
||||||
# Step action buttons
|
|
||||||
steps_btns = QHBoxLayout()
|
steps_btns = QHBoxLayout()
|
||||||
self.btn_add_prompt = QPushButton("Add Prompt")
|
self.btn_add_prompt = QPushButton("Add Prompt")
|
||||||
self.btn_add_prompt.clicked.connect(self._add_prompt_step)
|
self.btn_add_prompt.clicked.connect(self._add_prompt_step)
|
||||||
|
|
@ -218,52 +173,6 @@ class ToolBuilderPage(QWidget):
|
||||||
|
|
||||||
layout.addWidget(splitter, 1)
|
layout.addWidget(splitter, 1)
|
||||||
|
|
||||||
def _set_view_mode(self, mode: int):
|
|
||||||
"""Switch between list (0) and flow (1) views."""
|
|
||||||
if mode == 1 and self._flow_widget is None:
|
|
||||||
# Lazy-load flow widget
|
|
||||||
self._init_flow_widget()
|
|
||||||
|
|
||||||
self.steps_stack.setCurrentIndex(mode)
|
|
||||||
|
|
||||||
# Update flow widget if visible
|
|
||||||
if mode == 1 and self._flow_widget:
|
|
||||||
self._flow_widget.set_tool(self._tool)
|
|
||||||
|
|
||||||
def _init_flow_widget(self):
|
|
||||||
"""Initialize the flow visualization widget."""
|
|
||||||
try:
|
|
||||||
from ..widgets.flow_graph import FlowGraphWidget
|
|
||||||
self._flow_widget = FlowGraphWidget()
|
|
||||||
self._flow_widget.node_double_clicked.connect(self._on_flow_node_double_clicked)
|
|
||||||
|
|
||||||
# Replace placeholder
|
|
||||||
old_widget = self.steps_stack.widget(1)
|
|
||||||
self.steps_stack.removeWidget(old_widget)
|
|
||||||
old_widget.deleteLater()
|
|
||||||
self.steps_stack.insertWidget(1, self._flow_widget)
|
|
||||||
|
|
||||||
# Set current tool
|
|
||||||
if self._tool:
|
|
||||||
self._flow_widget.set_tool(self._tool)
|
|
||||||
except ImportError as e:
|
|
||||||
# NodeGraphQt not available
|
|
||||||
error_label = QLabel(f"Flow visualization unavailable:\n{e}")
|
|
||||||
error_label.setAlignment(Qt.AlignCenter)
|
|
||||||
error_label.setStyleSheet("color: #e53e3e; padding: 40px;")
|
|
||||||
old_widget = self.steps_stack.widget(1)
|
|
||||||
self.steps_stack.removeWidget(old_widget)
|
|
||||||
old_widget.deleteLater()
|
|
||||||
self.steps_stack.insertWidget(1, error_label)
|
|
||||||
|
|
||||||
def _on_flow_node_double_clicked(self, step_index: int, step_type: str):
|
|
||||||
"""Handle double-click on a flow node."""
|
|
||||||
if not self._tool or step_index < 0 or step_index >= len(self._tool.steps):
|
|
||||||
return
|
|
||||||
|
|
||||||
step = self._tool.steps[step_index]
|
|
||||||
self._edit_step_at_index(step_index, step)
|
|
||||||
|
|
||||||
def _load_tool(self, name: str):
|
def _load_tool(self, name: str):
|
||||||
"""Load an existing tool for editing."""
|
"""Load an existing tool for editing."""
|
||||||
tool = load_tool(name)
|
tool = load_tool(name)
|
||||||
|
|
@ -304,7 +213,7 @@ class ToolBuilderPage(QWidget):
|
||||||
self.args_list.addItem(item)
|
self.args_list.addItem(item)
|
||||||
|
|
||||||
def _refresh_steps(self):
|
def _refresh_steps(self):
|
||||||
"""Refresh steps list and flow view."""
|
"""Refresh steps list."""
|
||||||
self.steps_list.clear()
|
self.steps_list.clear()
|
||||||
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):
|
||||||
|
|
@ -318,10 +227,6 @@ class ToolBuilderPage(QWidget):
|
||||||
item.setData(Qt.UserRole, step)
|
item.setData(Qt.UserRole, step)
|
||||||
self.steps_list.addItem(item)
|
self.steps_list.addItem(item)
|
||||||
|
|
||||||
# Update flow widget if initialized
|
|
||||||
if self._flow_widget:
|
|
||||||
self._flow_widget.set_tool(self._tool)
|
|
||||||
|
|
||||||
def _add_argument(self):
|
def _add_argument(self):
|
||||||
"""Add a new argument."""
|
"""Add a new argument."""
|
||||||
from ..dialogs.argument_dialog import ArgumentDialog
|
from ..dialogs.argument_dialog import ArgumentDialog
|
||||||
|
|
@ -410,17 +315,14 @@ class ToolBuilderPage(QWidget):
|
||||||
self._refresh_steps()
|
self._refresh_steps()
|
||||||
|
|
||||||
def _edit_step(self):
|
def _edit_step(self):
|
||||||
"""Edit selected step from list view."""
|
"""Edit selected step."""
|
||||||
items = self.steps_list.selectedItems()
|
items = self.steps_list.selectedItems()
|
||||||
if not items:
|
if not items:
|
||||||
return
|
return
|
||||||
|
|
||||||
step = items[0].data(Qt.UserRole)
|
step = items[0].data(Qt.UserRole)
|
||||||
idx = self.steps_list.row(items[0])
|
idx = self.steps_list.row(items[0])
|
||||||
self._edit_step_at_index(idx, step)
|
|
||||||
|
|
||||||
def _edit_step_at_index(self, idx: int, step):
|
|
||||||
"""Edit a step at a specific index."""
|
|
||||||
if isinstance(step, PromptStep):
|
if isinstance(step, PromptStep):
|
||||||
from ..dialogs.step_dialog import PromptStepDialog
|
from ..dialogs.step_dialog import PromptStepDialog
|
||||||
dialog = PromptStepDialog(self, step)
|
dialog = PromptStepDialog(self, step)
|
||||||
|
|
|
||||||
|
|
@ -76,25 +76,6 @@ QPushButton#danger:hover {
|
||||||
background-color: #c53030;
|
background-color: #c53030;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* View toggle buttons */
|
|
||||||
QPushButton#viewToggle {
|
|
||||||
background-color: #e2e8f0;
|
|
||||||
color: #4a5568;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
min-height: 24px;
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#viewToggle:checked {
|
|
||||||
background-color: #667eea;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#viewToggle:hover:!checked {
|
|
||||||
background-color: #cbd5e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Input fields */
|
/* Input fields */
|
||||||
QLineEdit, QTextEdit, QPlainTextEdit {
|
QLineEdit, QTextEdit, QPlainTextEdit {
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1 @@
|
||||||
"""Custom widgets for CmdForge GUI."""
|
"""Custom widgets for CmdForge GUI."""
|
||||||
|
|
||||||
from .flow_graph import FlowGraphWidget
|
|
||||||
|
|
||||||
__all__ = ['FlowGraphWidget']
|
|
||||||
|
|
|
||||||
|
|
@ -1,240 +0,0 @@
|
||||||
"""Flow visualization widget using NodeGraphQt."""
|
|
||||||
|
|
||||||
from typing import Optional, List
|
|
||||||
|
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout
|
|
||||||
from PySide6.QtCore import Signal
|
|
||||||
|
|
||||||
from NodeGraphQt import NodeGraph, BaseNode
|
|
||||||
|
|
||||||
from ...tool import Tool, PromptStep, CodeStep
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Custom Node Types
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
class CmdForgeBaseNode(BaseNode):
|
|
||||||
"""Base class for CmdForge nodes with common styling."""
|
|
||||||
|
|
||||||
__identifier__ = 'cmdforge'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
# Store reference to the step data
|
|
||||||
self._step_data = None
|
|
||||||
self._step_index = -1
|
|
||||||
|
|
||||||
|
|
||||||
class InputNode(CmdForgeBaseNode):
|
|
||||||
"""Node representing tool input (stdin and arguments)."""
|
|
||||||
|
|
||||||
NODE_NAME = 'Input'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.set_color(90, 90, 160) # Indigo
|
|
||||||
# Default output for stdin
|
|
||||||
self.add_output('input', color=(180, 180, 250))
|
|
||||||
|
|
||||||
def set_arguments(self, arguments: list):
|
|
||||||
"""Add output ports for each argument variable."""
|
|
||||||
for arg in arguments:
|
|
||||||
if arg.variable:
|
|
||||||
# Check if port already exists
|
|
||||||
existing = [p.name() for p in self.output_ports()]
|
|
||||||
if arg.variable not in existing:
|
|
||||||
self.add_output(arg.variable, color=(180, 180, 250))
|
|
||||||
|
|
||||||
|
|
||||||
class PromptNode(CmdForgeBaseNode):
|
|
||||||
"""Node representing a prompt step."""
|
|
||||||
|
|
||||||
NODE_NAME = 'Prompt'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.set_color(102, 126, 234) # Indigo (matching CmdForge theme)
|
|
||||||
self.add_input('in', color=(180, 180, 250), multi_input=True)
|
|
||||||
self.add_output('out', color=(180, 250, 180))
|
|
||||||
|
|
||||||
# Add properties for display
|
|
||||||
self.add_text_input('provider', 'Provider', text='claude')
|
|
||||||
self.add_text_input('output_var', 'Output', text='response')
|
|
||||||
|
|
||||||
def set_step(self, step: PromptStep, index: int):
|
|
||||||
"""Configure node from a PromptStep."""
|
|
||||||
self._step_data = step
|
|
||||||
self._step_index = index
|
|
||||||
self.set_property('provider', step.provider or 'claude')
|
|
||||||
self.set_property('output_var', step.output_var or 'response')
|
|
||||||
# Update output port name
|
|
||||||
if self.output_ports():
|
|
||||||
self.output_ports()[0]._name = step.output_var or 'response'
|
|
||||||
|
|
||||||
|
|
||||||
class CodeNode(CmdForgeBaseNode):
|
|
||||||
"""Node representing a code step."""
|
|
||||||
|
|
||||||
NODE_NAME = 'Code'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.set_color(72, 187, 120) # Green
|
|
||||||
self.add_input('in', color=(180, 250, 180), multi_input=True)
|
|
||||||
self.add_output('out', color=(250, 220, 180))
|
|
||||||
|
|
||||||
# Add properties
|
|
||||||
self.add_text_input('output_var', 'Output', text='result')
|
|
||||||
|
|
||||||
def set_step(self, step: CodeStep, index: int):
|
|
||||||
"""Configure node from a CodeStep."""
|
|
||||||
self._step_data = step
|
|
||||||
self._step_index = index
|
|
||||||
self.set_property('output_var', step.output_var or 'result')
|
|
||||||
# Update output port name
|
|
||||||
if self.output_ports():
|
|
||||||
self.output_ports()[0]._name = step.output_var or 'result'
|
|
||||||
|
|
||||||
|
|
||||||
class OutputNode(CmdForgeBaseNode):
|
|
||||||
"""Node representing tool output."""
|
|
||||||
|
|
||||||
NODE_NAME = 'Output'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.set_color(237, 137, 54) # Orange
|
|
||||||
self.add_input('in', color=(250, 220, 180), multi_input=True)
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Flow Graph Widget
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
class FlowGraphWidget(QWidget):
|
|
||||||
"""Widget for visualizing tool flow as a node graph."""
|
|
||||||
|
|
||||||
# Emitted when a node is double-clicked (step_index, step_type)
|
|
||||||
node_double_clicked = Signal(int, str)
|
|
||||||
|
|
||||||
# Emitted when the flow structure changes
|
|
||||||
flow_changed = Signal()
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self._tool: Optional[Tool] = None
|
|
||||||
self._graph: Optional[NodeGraph] = None
|
|
||||||
self._input_node = None
|
|
||||||
self._output_node = None
|
|
||||||
self._step_nodes: List[CmdForgeBaseNode] = []
|
|
||||||
|
|
||||||
self._setup_ui()
|
|
||||||
|
|
||||||
def _setup_ui(self):
|
|
||||||
"""Set up the UI."""
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
|
|
||||||
# Create node graph
|
|
||||||
self._graph = NodeGraph()
|
|
||||||
|
|
||||||
# Register custom node types
|
|
||||||
self._graph.register_node(InputNode)
|
|
||||||
self._graph.register_node(PromptNode)
|
|
||||||
self._graph.register_node(CodeNode)
|
|
||||||
self._graph.register_node(OutputNode)
|
|
||||||
|
|
||||||
# Connect signals
|
|
||||||
self._graph.node_double_clicked.connect(self._on_node_double_clicked)
|
|
||||||
|
|
||||||
# Add graph widget
|
|
||||||
layout.addWidget(self._graph.widget)
|
|
||||||
|
|
||||||
def set_tool(self, tool: Optional[Tool]):
|
|
||||||
"""Set the tool to visualize."""
|
|
||||||
self._tool = tool
|
|
||||||
self._rebuild_graph()
|
|
||||||
|
|
||||||
def _rebuild_graph(self):
|
|
||||||
"""Rebuild the graph from the tool data."""
|
|
||||||
if not self._graph:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Clear existing nodes
|
|
||||||
self._graph.clear_session()
|
|
||||||
self._input_node = None
|
|
||||||
self._output_node = None
|
|
||||||
self._step_nodes = []
|
|
||||||
|
|
||||||
if not self._tool:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Create input node
|
|
||||||
self._input_node = self._graph.create_node(
|
|
||||||
'cmdforge.InputNode',
|
|
||||||
name='Input',
|
|
||||||
pos=[-300, 0]
|
|
||||||
)
|
|
||||||
if self._tool.arguments:
|
|
||||||
self._input_node.set_arguments(self._tool.arguments)
|
|
||||||
|
|
||||||
# Create step nodes
|
|
||||||
x_pos = 0
|
|
||||||
prev_node = self._input_node
|
|
||||||
|
|
||||||
for i, step in enumerate(self._tool.steps or []):
|
|
||||||
if isinstance(step, PromptStep):
|
|
||||||
node = self._graph.create_node(
|
|
||||||
'cmdforge.PromptNode',
|
|
||||||
name=f'Prompt {i+1}',
|
|
||||||
pos=[x_pos, 0]
|
|
||||||
)
|
|
||||||
node.set_step(step, i)
|
|
||||||
elif isinstance(step, CodeStep):
|
|
||||||
node = self._graph.create_node(
|
|
||||||
'cmdforge.CodeNode',
|
|
||||||
name=f'Code {i+1}',
|
|
||||||
pos=[x_pos, 0]
|
|
||||||
)
|
|
||||||
node.set_step(step, i)
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._step_nodes.append(node)
|
|
||||||
|
|
||||||
# Connect to previous node
|
|
||||||
if prev_node and prev_node.output_ports():
|
|
||||||
prev_node.output_ports()[0].connect_to(node.input_ports()[0])
|
|
||||||
|
|
||||||
prev_node = node
|
|
||||||
x_pos += 250
|
|
||||||
|
|
||||||
# Create output node
|
|
||||||
self._output_node = self._graph.create_node(
|
|
||||||
'cmdforge.OutputNode',
|
|
||||||
name='Output',
|
|
||||||
pos=[x_pos, 0]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Connect last step to output
|
|
||||||
if prev_node and prev_node.output_ports():
|
|
||||||
prev_node.output_ports()[0].connect_to(self._output_node.input_ports()[0])
|
|
||||||
|
|
||||||
# Auto-layout and fit view
|
|
||||||
self._graph.auto_layout_nodes()
|
|
||||||
self._graph.fit_to_selection()
|
|
||||||
|
|
||||||
def _on_node_double_clicked(self, node):
|
|
||||||
"""Handle node double-click."""
|
|
||||||
if hasattr(node, '_step_index') and node._step_index >= 0:
|
|
||||||
step_type = 'prompt' if isinstance(node, PromptNode) else 'code'
|
|
||||||
self.node_double_clicked.emit(node._step_index, step_type)
|
|
||||||
|
|
||||||
def refresh(self):
|
|
||||||
"""Refresh the graph from current tool data."""
|
|
||||||
self._rebuild_graph()
|
|
||||||
|
|
||||||
def get_graph(self) -> Optional[NodeGraph]:
|
|
||||||
"""Get the underlying NodeGraph instance."""
|
|
||||||
return self._graph
|
|
||||||
Loading…
Reference in New Issue