From d1f0c2f89363a3f410e1b1e62de38b7922c89a62 Mon Sep 17 00:00:00 2001 From: rob Date: Thu, 15 Jan 2026 23:50:18 -0400 Subject: [PATCH] Add flow view reordering via connection breaking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a connection is broken in the flow view: 1. The node that lost its input moves to the end of the chain 2. The remaining nodes reconnect automatically 3. Tool model updates to reflect new order 4. Dependency validation warns about broken references Flow: Input → A → B → C → Output Break B's input: Input → A → C → B → Output Co-Authored-By: Claude Opus 4.5 --- src/cmdforge/gui/pages/tool_builder_page.py | 30 ++++++++++++ src/cmdforge/gui/widgets/flow_graph.py | 54 +++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/src/cmdforge/gui/pages/tool_builder_page.py b/src/cmdforge/gui/pages/tool_builder_page.py index 08535bd..da0c82f 100644 --- a/src/cmdforge/gui/pages/tool_builder_page.py +++ b/src/cmdforge/gui/pages/tool_builder_page.py @@ -241,6 +241,7 @@ class ToolBuilderPage(QWidget): self._flow_widget = FlowGraphWidget() self._flow_widget.node_double_clicked.connect(self._on_flow_node_double_clicked) self._flow_widget.steps_deleted.connect(self._on_flow_steps_deleted) + self._flow_widget.steps_reordered.connect(self._on_flow_steps_reordered) # Replace placeholder old_widget = self.steps_stack.widget(1) @@ -282,6 +283,35 @@ class ToolBuilderPage(QWidget): # Refresh the list view (flow view will be refreshed by set_tool) self._refresh_steps() + def _on_flow_steps_reordered(self, new_order: list): + """Handle step reordering from flow view. + + Args: + new_order: List of old indices in new order. + e.g., [0, 2, 1] means step 0 stays first, + step 2 moves to second, step 1 moves to third. + """ + if not self._tool or not self._tool.steps: + return + + # Reorder steps according to new_order + old_steps = self._tool.steps[:] + new_steps = [old_steps[i] for i in new_order if i < len(old_steps)] + + # Validate the new order + warnings = self._validate_step_order(new_steps) + + if warnings: + # Show warning but allow the reorder + warning_msg = "Variable dependency warnings:\n\n" + "\n".join(warnings) + warning_msg += "\n\nThe reorder has been applied. You may need to fix these issues." + QMessageBox.warning(self, "Dependency Warning", warning_msg) + + self._tool.steps = new_steps + + # Refresh both views + self._refresh_steps() + def _load_tool(self, name: str): """Load an existing tool for editing.""" tool = load_tool(name) diff --git a/src/cmdforge/gui/widgets/flow_graph.py b/src/cmdforge/gui/widgets/flow_graph.py index de2bf1d..bd733df 100644 --- a/src/cmdforge/gui/widgets/flow_graph.py +++ b/src/cmdforge/gui/widgets/flow_graph.py @@ -125,6 +125,9 @@ class FlowGraphWidget(QWidget): # Emitted when steps are deleted from flow view (list of step indices) steps_deleted = Signal(list) + # Emitted when steps are reordered (new order as list of step indices) + steps_reordered = Signal(list) + def __init__(self, parent=None): super().__init__(parent) self._tool: Optional[Tool] = None @@ -153,6 +156,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) # Add graph widget layout.addWidget(self._graph.widget, 1) @@ -427,6 +431,56 @@ 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. + + 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 + """ + if not self._tool or not self._tool.steps: + 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() + def refresh(self): """Refresh the graph from current tool data.""" self._rebuild_graph()