Add step type icons and inline name editing

- Add icons module with programmatically drawn icons:
  - Speech bubble for prompt steps
  - Code brackets </> for code steps
  - Arrow icons for input/output nodes
- Display icons in list view next to step names
- Display icons in flow view node headers
- Add inline name editing in flow view via property_changed signal
- Sync name changes from flow view to tool model and list view

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
rob
2026-01-16 01:21:20 -04:00
co-authored by Claude Opus 4.5
parent bc970fb9f7
commit 8e6b03cade
3 changed files with 378 additions and 53 deletions
+29 -50
View File
@@ -9,6 +9,10 @@ from PySide6.QtGui import QKeyEvent, QAction
from NodeGraphQt import NodeGraph, BaseNode
from ...tool import Tool, PromptStep, CodeStep
from .icons import (
get_prompt_icon_path, get_code_icon_path,
get_input_icon_path, get_output_icon_path
)
# =============================================================================
@@ -35,6 +39,7 @@ class InputNode(CmdForgeBaseNode):
def __init__(self):
super().__init__()
self.set_color(90, 90, 160) # Indigo
self.set_icon(get_input_icon_path(16))
# Default output for stdin
self.add_output('input', color=(180, 180, 250))
@@ -56,7 +61,8 @@ class PromptNode(CmdForgeBaseNode):
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.set_icon(get_prompt_icon_path(16))
self.add_input('in', color=(180, 180, 250))
self.add_output('out', color=(180, 250, 180))
# Add properties for display
@@ -82,7 +88,8 @@ class CodeNode(CmdForgeBaseNode):
def __init__(self):
super().__init__()
self.set_color(72, 187, 120) # Green
self.add_input('in', color=(180, 250, 180), multi_input=True)
self.set_icon(get_code_icon_path(16))
self.add_input('in', color=(180, 250, 180))
self.add_output('out', color=(250, 220, 180))
# Add properties
@@ -106,7 +113,8 @@ class OutputNode(CmdForgeBaseNode):
def __init__(self):
super().__init__()
self.set_color(237, 137, 54) # Orange
self.add_input('in', color=(250, 220, 180), multi_input=True)
self.set_icon(get_output_icon_path(16))
self.add_input('in', color=(250, 220, 180))
# =============================================================================
@@ -114,7 +122,12 @@ class OutputNode(CmdForgeBaseNode):
# =============================================================================
class FlowGraphWidget(QWidget):
"""Widget for visualizing tool flow as a node graph."""
"""Widget for visualizing tool flow as a node graph.
Note: Currently the flow view is read-only for connections. Reordering
steps by manipulating connections is a future enhancement (TODO).
Use the list view for reordering steps via drag-drop.
"""
# Emitted when a node is double-clicked (step_index, step_type)
node_double_clicked = Signal(int, str)
@@ -128,6 +141,9 @@ class FlowGraphWidget(QWidget):
# Emitted when steps are reordered (new order as list of step indices)
steps_reordered = Signal(list)
# Emitted when a step name is changed (step_index, new_name)
step_name_changed = Signal(int, str)
def __init__(self, parent=None):
super().__init__(parent)
self._tool: Optional[Tool] = None
@@ -157,7 +173,7 @@ class FlowGraphWidget(QWidget):
# Connect signals
self._graph.node_double_clicked.connect(self._on_node_double_clicked)
self._graph.nodes_deleted.connect(self._on_nodes_deleted)
self._graph.port_disconnected.connect(self._on_port_disconnected)
self._graph.property_changed.connect(self._on_property_changed)
# Add graph widget
layout.addWidget(self._graph.widget, 1)
@@ -172,7 +188,6 @@ class FlowGraphWidget(QWidget):
"Zoom: Scroll | "
"Select: Click/drag | "
"Edit: Double-click | "
"Cut links: Alt+Shift+drag | "
"Select All: A | "
"Fit: F"
)
@@ -449,59 +464,23 @@ class FlowGraphWidget(QWidget):
self.steps_deleted.emit(deleted_indices)
self.flow_changed.emit()
def _on_port_disconnected(self, input_port, output_port):
"""Handle port disconnection - reorder steps.
def _on_property_changed(self, node, prop_name, value):
"""Handle node property change.
When a connection is broken:
1. The node that lost its input moves to the end
2. The chain reconnects (previous node → next node)
3. Disconnected node attaches at the end before Output
Filters for 'name' property to detect step name changes.
"""
# Ignore during rebuild
if self._rebuilding:
return
if not self._tool or not self._tool.steps:
# Only interested in name changes
if prop_name != 'name':
return
# Get the nodes involved
# input_port belongs to the node that lost its input
# output_port belongs to the node that was providing the input
disconnected_node = input_port.node()
source_node = output_port.node()
# Only handle step nodes (not Input/Output)
if not hasattr(disconnected_node, '_step_index') or disconnected_node._step_index < 0:
return
disconnected_idx = disconnected_node._step_index
# Find the node that the disconnected node was connected to (its output target)
next_node = None
if disconnected_node.output_ports():
out_port = disconnected_node.output_ports()[0]
connected_ports = out_port.connected_ports()
if connected_ports:
next_node = connected_ports[0].node()
# Build new order: move disconnected step to end
old_order = list(range(len(self._tool.steps)))
old_order.remove(disconnected_idx)
old_order.append(disconnected_idx)
# Reconnect the graph visually
# Connect source to next (if next is a step node)
if next_node and hasattr(next_node, '_step_index') and next_node._step_index >= 0:
if source_node.output_ports() and next_node.input_ports():
source_node.output_ports()[0].connect_to(next_node.input_ports()[0])
# Connect the last step (before disconnected) to disconnected node
# and disconnected node to output
# This will be handled by the rebuild after reorder
# Emit the new order
self.steps_reordered.emit(old_order)
self.flow_changed.emit()
if hasattr(node, '_step_index') and node._step_index >= 0:
self.step_name_changed.emit(node._step_index, value)
self.flow_changed.emit()
def refresh(self):
"""Refresh the graph from current tool data."""