Skip to content

sync(ladder): handle branches with nested parallels#756

Merged
thiagoralves merged 1 commit into
developmentfrom
sync/handle-branches-on-ladder
May 15, 2026
Merged

sync(ladder): handle branches with nested parallels#756
thiagoralves merged 1 commit into
developmentfrom
sync/handle-branches-on-ladder

Conversation

@thiagoralves
Copy link
Copy Markdown
Contributor

@thiagoralves thiagoralves commented May 13, 2026

Summary

  • Editor-side mirror of openplc-web PR #395 (multi-branch-ladder).
  • Brings handle branches: contacts/coils/parallels can be wired to a function block's secondary boolean input/output handles (e.g. CTU R) via a compact branch with its own local rail.
  • Includes layout (placement, spacing, vertical clearance, parallel paths inside branches with nested-parallel support), routing (placeholders, drops, splices), drag-drop dispatch (classify-then-dispatch refactor), persistence (rung state index, Zod load paths), and XML serializer wiring for FB input handles.
  • 25 files changed across frontend/ and middleware/shared/ports/types.ts. Shared surface verified byte-identical against the web branch.

What's different vs editor PR #750

PR #750 was an earlier attempt that ran into too many layout bugs; this PR is the rewritten implementation that web has been iterating on. They are mutually exclusive — once this lands, #750 can be closed.

Test plan

  • Drop a contact onto a CTU's R boolean input — it appears as a branch with a local rail next to the FB.
  • Add multiple OR-paths inside a single in-branch parallel; serially chain elements inside each path.
  • Stack three nested in-branch parallels — sibling block (e.g. CTU1) stays below the deepest nested contact, no diagonal curve.
  • Place an FB at the start of a rung (predecessor = main rail) with an input branch — local rail and contact don't overlap the main rail.
  • Try to add to a handle branch on a block that's in parallel with other elements — refused with a toast.
  • Wire a res1 contact to CTU R via a branch and compile — when res1 is TRUE, the counter resets.
  • Save / reload the project — branches survive the round-trip.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added ability to attach branching circuit paths to block input/output handles in ladder diagrams.
    • Enabled clearing variables from blocks by submitting empty values.
  • Bug Fixes

    • Corrected edge matching logic for parallel node connections.
    • Fixed rung traversal to properly handle disconnected edge chains.
  • Improvements

    • Enhanced ladder diagram layout for blocks with branching paths.
    • Improved drag-and-drop element placement with better placeholder positioning.
    • Refined block replacement with automatic branch structure reconciliation.

Review Change Stack

Pairs with openplc-web PR #395 (multi-branch-ladder). Brings handle
branches: contacts/coils/parallels can be wired to a function block's
secondary boolean input/output handles (e.g. CTU `R`) via a compact
branch with its own local rail.

Includes layout (placement, spacing, vertical clearance, parallel paths
inside branches with nested-parallel support), routing (placeholders,
drops, splices), drag-drop dispatch (classify-then-dispatch), persistence
(rung state index, Zod load paths), and XML serializer wiring for FB
input handles.

Shared surface verified byte-identical against openplc-web at d79f2a1a
(plus the type-cast/lint/prettier follow-ups 3f1acfe1, cb923351).
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

Walkthrough

This PR introduces a comprehensive handle-branches feature for the ladder graphical editor, enabling elements (contacts, coils, parallels) to be attached to block input/output handles. The implementation includes branch positioning, layout expansion, drag-and-drop routing, XML code generation for branches, and full store persistence with rung duplication support.

Changes

Handle Branches Feature Implementation

Layer / File(s) Summary
Type definitions and schema extensions
src/middleware/shared/ports/types.ts, src/frontend/components/_atoms/graphical-editor/ladder/utils/types.ts
Adds HandleBranch type capturing block/handle IDs, direction, and ordered node IDs; extends BasicNodeData with branchContext (branch ownership) and handleBranchTarget (drop target metadata).
Edge splicing and parallel wiring utilities
src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/core/index.ts, src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/edges.ts
Low-level graph manipulation: spliceEdgeAndInsertNode, spliceNodeIntoEdgeChain, wireParallelAroundElement for flexible edge routing; fixes parallel close-node source-handle matching.
Core branch manipulation and positioning
src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/handle-branch/index.ts
Comprehensive branch utilities: querying branch presence, calculating multi-phase positions with depth-aware parallels, managing rail handles, inserting/removing/replacing elements, and reconciling branches during block updates.
Utility and traversal updates
src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/utils/index.ts
Updates rung traversal to exclude branch-context nodes, filter edges by source presence, and terminate chain walks on missing nodes to prevent stalls.
Diagram layout and branch positioning system
src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/diagram/index.ts
Multi-phase layout system for branches: compute branch element positions with depth-aware parallel regions, expand block dimensions for overlapping content, manage rail handle positioning and synchronization.
Branch-aware placeholder generation
src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/placeholder/index.ts
Generates depth-aware placeholders for main-rail and branch-context nodes; adds handle placeholders for block inputs/outputs when branches are compatible; uses uuidv4 for unique placeholder IDs.
Scenario-based drag-and-drop handlers
src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/drag-n-drop/handlers.ts, src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/drag-n-drop/index.ts
Refactored drop logic with classification system: specialized handlers for restore, branch/main parallels, branch/main serials, branch edge chains, and branch creation with index adjustment and edge reconciliation.
Element add/remove operations with branch support
src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/index.ts, src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/variable-block/index.ts
Extends add/remove element flows: insert into branches, create new branches, replace variables with branches, propagate branch context metadata, filter variable handles when branches exist.
Parallel connection refactoring
src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/parallel/index.ts
Refactors startParallelConnection to preserve existing above nodes and delegate edge wiring to wireParallelAroundElement with nested parallel handle overrides.
Atomic component updates
src/frontend/components/_atoms/graphical-editor/ladder/power-rail.tsx, src/frontend/components/_atoms/graphical-editor/ladder/variable.tsx, src/frontend/components/_atoms/highlighted-textarea/index.tsx, src/frontend/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx
PowerRail: dynamic height from handle positions; Variable: improved sync and clearing support; HighlightedTextArea: pass textarea value to submit handler; Autocomplete: clear variable on empty name.
Block component with branch reconciliation
src/frontend/components/_atoms/graphical-editor/ladder/block.tsx
BlockNodeElement and Block wrapper reconcile branches on block name/type changes; compute per-handle Y offsets; persist reconciled branch state via setHandleBranches.
Block feature component integration
src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/block/index.tsx
BlockElement calls reconcileBranchesIfNeeded during block submission; persists reconciled branches to store; passes branch data into diagram updates.
Rung body integration with branch state
src/frontend/components/_molecules/graphical-editor/ladder/rung/body.tsx
RungBody captures and propagates handleBranches from element mutations through store actions, keeping branch state synchronized across add/remove/drag operations.
Store integration and state persistence
src/frontend/store/slices/ladder/slice.ts, src/frontend/store/slices/ladder/types.ts, src/frontend/store/slices/ladder/utils/index.ts
Redux/Zustand ladder slice manages handleBranches: removeNodes captures and stores branches, new setHandleBranches action updates per-rung state, rung duplication remaps branch identifiers and edge handles.
XML code generation with handle-scoped connections
src/frontend/utils/PLC/xml-generator/codesys/language/ladder-xml.ts, src/frontend/utils/PLC/xml-generator/old-editor/language/ladder-xml.ts
Both CodesysIDE and old-editor XML generators now support handle-scoped connection tracing: findConnections accepts optional targetHandle, derives @formalParameter from source handles, traces branch connections when no variable node exists.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • JoaoGSP

Poem

🐰 Branches bloom where handles dwell,
Each input point a tale to tell,
Parallels nest in layers deep,
Where rabbit code makes layout leap!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The PR description includes a comprehensive summary of changes, references to parallel work, implementation scope, and a detailed test plan. However, the provided description_template requires specific sections (References, DOD checklist) that are not present in the actual PR description. Consider adding explicit References section and completing the DOD (Definition of Done) checklist to match the repository template standards.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'sync(ladder): handle branches with nested parallels' is specific and directly describes the main change: adding support for handle branches (contacts/coils wired to secondary block I/O handles) with nested parallel functionality.
Docstring Coverage ✅ Passed Docstring coverage is 97.50% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sync/handle-branches-on-ladder

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/placeholder/index.ts (1)

285-368: 💤 Low value

Extract a shared helper for input/output handle placeholder generation.

The input (296-331) and output (335-367) loops differ only in direction, the posX sign/offset, and the 'left'/'right' position string. Any future change to handle placeholder gating, geometry, or handleBranchTarget shape must be applied in two places, which is an easy source of drift (e.g., the recent BOOL gating + hasBranchOnHandle skip already needs to stay in lockstep).

♻️ Suggested extraction
const makeHandlePlaceholder = (
  blockNode: Node,
  handle: CustomHandleProps,
  blockData: BasicNodeData & { variant: BlockVariant },
  direction: 'input' | 'output',
) => {
  const handleId = handle.id as string
  const variableType = blockData.variant?.variables?.find((v) => v.name === handleId)
  if (!variableType) return null
  if (!canPlaceElementOnHandle(variableType)) return null
  if (hasBranchOnHandle(rung, blockNode.id, handleId)) return null

  const blockWidth = defaultCustomNodesStyles.block?.width ?? 80
  const offset = pStyle.gap + pStyle.width / 2
  const x =
    direction === 'input'
      ? blockNode.position.x - offset
      : blockNode.position.x + blockWidth + offset

  const placeholder = nodesBuilder.placeholder({
    id: `placeholder_handle_${blockNode.id}_${handleId}`,
    type: 'default',
    relatedNode: blockNode,
    position: direction === 'input' ? 'left' : 'right',
    posX: x,
    posY: handle.glbPosition.y - pStyle.handle.y,
    handleX: x,
    handleY: handle.glbPosition.y,
  })
  placeholder.data = {
    ...placeholder.data,
    handleBranchTarget: {
      blockId: blockNode.id,
      handleId,
      direction,
      handlePosition: { x: handle.glbPosition.x, y: handle.glbPosition.y },
    },
  }
  return placeholder
}

Then iterate inputHandles.slice(1) and outputHandles.slice(1) calling this helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/placeholder/index.ts`
around lines 285 - 368, The input/output placeholder generation is duplicated in
the nodes.forEach loop; extract a helper (e.g., makeHandlePlaceholder) that
accepts (blockNode, handle, blockData, direction) and encapsulates the shared
checks (variable lookup, canPlaceElementOnHandle, hasBranchOnHandle),
position/offset calculation (use pStyle and defaultCustomNodesStyles.block
width), nodesBuilder.placeholder creation, and attaching handleBranchTarget,
then replace both inputHandles and outputHandles loops to call this helper for
each handle (slice starting at index 1) and push non-null results into
placeholderNodes; keep references to hasBranchOnHandle, canPlaceElementOnHandle,
nodesBuilder.placeholder, and placeholder.data shape unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/frontend/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx`:
- Around line 165-187: Summary: Clearing a variable node leaves the parent
block's connectedVariables metadata pointing at a now-empty handle, so the
handle remains logically occupied. Fix: when handling the blockType ===
'variable' clear path, after you update the variable node via updateNode (the
existing call that updates variableNode.data.variable), also locate the parent
block node (the local block Node<BasicNodeData> in this scope) and update its
data.connectedVariables to remove/clear the entry for the handle (use the same
updateNode API to persist the change). Use getLadderPouVariablesRungNodeAndEdges
to get rung/variableNode as before and then call updateNode for the block node
to set block.data.connectedVariables[handleId] = undefined or delete that key so
the handle is no longer considered occupied.

In
`@src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/handle-branch/index.ts`:
- Around line 788-809: In insertIntoBranch, abort the insertion when oldEdge is
not found: instead of only logging a warning, return the unchanged rung state
(no new nodes/edges/handleBranches) and do not include newElement or modified
nodeIds; specifically, if oldEdge is falsy (the variable checked at the top),
skip calling spliceEdgeAndInsertNode and avoid pushing newElement into
rung.nodes or updating handleBranches — return an object signaling failure or
the original nodes/edges/handleBranches (consistent with the function's return
shape) so callers won't get disconnected branch nodes or stale metadata.

In `@src/frontend/utils/PLC/xml-generator/codesys/language/ladder-xml.ts`:
- Around line 85-95: The serialization anchors currently always use
node.data.inputConnector?.glbPosition even when a targetHandle is provided;
update findConnections to select the connector by handle when targetHandle is
set (e.g., resolve connector = targetHandle ? node.data.inputConnectors?.find(c
=> c.handle === targetHandle) : node.data.inputConnector) and use
connector.glbPosition for destination coordinates; apply the same change to the
other connection-serialization sites that reference node.data.inputConnector
(the blocks handling explicit handle-scoped connections) so they also pick the
per-handle connector position when targetHandle is present.

---

Nitpick comments:
In
`@src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/placeholder/index.ts`:
- Around line 285-368: The input/output placeholder generation is duplicated in
the nodes.forEach loop; extract a helper (e.g., makeHandlePlaceholder) that
accepts (blockNode, handle, blockData, direction) and encapsulates the shared
checks (variable lookup, canPlaceElementOnHandle, hasBranchOnHandle),
position/offset calculation (use pStyle and defaultCustomNodesStyles.block
width), nodesBuilder.placeholder creation, and attaching handleBranchTarget,
then replace both inputHandles and outputHandles loops to call this helper for
each handle (slice starting at index 1) and push non-null results into
placeholderNodes; keep references to hasBranchOnHandle, canPlaceElementOnHandle,
nodesBuilder.placeholder, and placeholder.data shape unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1390de75-2026-4e74-bf01-290c8417be18

📥 Commits

Reviewing files that changed from the base of the PR and between f06a6cd and d249df4.

📒 Files selected for processing (25)
  • src/frontend/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx
  • src/frontend/components/_atoms/graphical-editor/ladder/block.tsx
  • src/frontend/components/_atoms/graphical-editor/ladder/power-rail.tsx
  • src/frontend/components/_atoms/graphical-editor/ladder/utils/types.ts
  • src/frontend/components/_atoms/graphical-editor/ladder/variable.tsx
  • src/frontend/components/_atoms/highlighted-textarea/index.tsx
  • src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/block/index.tsx
  • src/frontend/components/_molecules/graphical-editor/ladder/rung/body.tsx
  • src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/edges.ts
  • src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/core/index.ts
  • src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/diagram/index.ts
  • src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/drag-n-drop/handlers.ts
  • src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/drag-n-drop/index.ts
  • src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/handle-branch/index.ts
  • src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/index.ts
  • src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/parallel/index.ts
  • src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/placeholder/index.ts
  • src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/utils/index.ts
  • src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/variable-block/index.ts
  • src/frontend/store/slices/ladder/slice.ts
  • src/frontend/store/slices/ladder/types.ts
  • src/frontend/store/slices/ladder/utils/index.ts
  • src/frontend/utils/PLC/xml-generator/codesys/language/ladder-xml.ts
  • src/frontend/utils/PLC/xml-generator/old-editor/language/ladder-xml.ts
  • src/middleware/shared/ports/types.ts

Comment on lines 165 to +187
if (!variableName.trim()) {
// For variable nodes on block handles, clearing the name resets the variable
// so that a branch (contacts/coils) can be placed on the handle instead.
if (blockType === 'variable') {
const { rung, node: variableNode } = getLadderPouVariablesRungNodeAndEdges(editor, pous, ladderFlows, {
nodeId: (block as Node<BasicNodeData>).id,
})
if (rung && variableNode) {
updateNode({
editorName: editor.meta.name,
rungId: rung.id,
nodeId: variableNode.id,
node: {
...variableNode,
data: {
...variableNode.data,
variable: { id: '', name: '' },
},
},
})
}
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear path leaves stale block-pin linkage metadata.

On Line 173-184, clearing a variable updates only the variable node. The connected block’s connectedVariables entry for that handle is not cleared, so handle state can remain logically “occupied” after clear.

💡 Suggested fix
         if (blockType === 'variable') {
           const { rung, node: variableNode } = getLadderPouVariablesRungNodeAndEdges(editor, pous, ladderFlows, {
             nodeId: (block as Node<BasicNodeData>).id,
           })
           if (rung && variableNode) {
             updateNode({
               editorName: editor.meta.name,
               rungId: rung.id,
               nodeId: variableNode.id,
               node: {
                 ...variableNode,
                 data: {
                   ...variableNode.data,
                   variable: { id: '', name: '' },
                 },
               },
             })
+
+            const relatedBlock = rung.nodes.find((n) => n.id === (variableNode as VariableNode).data.block?.id)
+            if (relatedBlock) {
+              const existing = Array.isArray((relatedBlock.data as BlockNodeData<BlockVariant>).connectedVariables)
+                ? (relatedBlock.data as BlockNodeData<BlockVariant>).connectedVariables
+                : []
+              const nextConnected = existing.filter(
+                (v) =>
+                  v.type !== (variableNode as VariableNode).data.variant ||
+                  v.handleId !== (variableNode as VariableNode).data.block.handleId,
+              )
+              updateNode({
+                editorName: editor.meta.name,
+                rungId: rung.id,
+                nodeId: relatedBlock.id,
+                node: {
+                  ...relatedBlock,
+                  data: {
+                    ...relatedBlock.data,
+                    connectedVariables: nextConnected,
+                  },
+                },
+              })
+            }
           }
           return
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx`
around lines 165 - 187, Summary: Clearing a variable node leaves the parent
block's connectedVariables metadata pointing at a now-empty handle, so the
handle remains logically occupied. Fix: when handling the blockType ===
'variable' clear path, after you update the variable node via updateNode (the
existing call that updates variableNode.data.variable), also locate the parent
block node (the local block Node<BasicNodeData> in this scope) and update its
data.connectedVariables to remove/clear the entry for the handle (use the same
updateNode API to persist the change). Use getLadderPouVariablesRungNodeAndEdges
to get rung/variableNode as before and then call updateNode for the block node
to set block.data.connectedVariables[handleId] = undefined or delete that key so
the handle is no longer considered occupied.

Comment on lines +788 to +809
if (!oldEdge) {
console.warn(
`insertIntoBranch: expected edge ${sourceId}[${sourceHandle}] → ${targetId}[${targetHandle}] not found — branch metadata may be stale`,
)
}
const newEdges = oldEdge
? spliceEdgeAndInsertNode(rung.edges, oldEdge, newElement.id, 'input', 'output')
: [...rung.edges]

// Step 4: Splice new element ID into branch's nodeIds
const newNodeIds = [...nodeIds]
newNodeIds.splice(idx, 0, newElement.id)

const handleBranches = (rung.handleBranches ?? []).map((b) =>
b.blockId === target.blockId && b.handleId === target.handleId ? { ...b, nodeIds: newNodeIds } : b,
)

// Step 5: Add element to nodes array
const newNodes = [...rung.nodes, newElement]

return { nodes: newNodes, edges: newEdges, handleBranches, newNode: newElement }
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Abort insertion when the split edge is missing.

Line 788 only warns, but Lines 798-807 still persist nodeIds and newElement. That can create disconnected branch nodes and stale branch metadata.

Suggested fix
-  if (!oldEdge) {
-    console.warn(
-      `insertIntoBranch: expected edge ${sourceId}[${sourceHandle}] → ${targetId}[${targetHandle}] not found — branch metadata may be stale`,
-    )
-  }
-  const newEdges = oldEdge
-    ? spliceEdgeAndInsertNode(rung.edges, oldEdge, newElement.id, 'input', 'output')
-    : [...rung.edges]
+  if (!oldEdge) {
+    throw new Error(
+      `insertIntoBranch: expected edge ${sourceId}[${sourceHandle}] → ${targetId}[${targetHandle}] not found`,
+    )
+  }
+  const newEdges = spliceEdgeAndInsertNode(rung.edges, oldEdge, newElement.id, 'input', 'output')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!oldEdge) {
console.warn(
`insertIntoBranch: expected edge ${sourceId}[${sourceHandle}] → ${targetId}[${targetHandle}] not found — branch metadata may be stale`,
)
}
const newEdges = oldEdge
? spliceEdgeAndInsertNode(rung.edges, oldEdge, newElement.id, 'input', 'output')
: [...rung.edges]
// Step 4: Splice new element ID into branch's nodeIds
const newNodeIds = [...nodeIds]
newNodeIds.splice(idx, 0, newElement.id)
const handleBranches = (rung.handleBranches ?? []).map((b) =>
b.blockId === target.blockId && b.handleId === target.handleId ? { ...b, nodeIds: newNodeIds } : b,
)
// Step 5: Add element to nodes array
const newNodes = [...rung.nodes, newElement]
return { nodes: newNodes, edges: newEdges, handleBranches, newNode: newElement }
}
if (!oldEdge) {
throw new Error(
`insertIntoBranch: expected edge ${sourceId}[${sourceHandle}] → ${targetId}[${targetHandle}] not found`,
)
}
const newEdges = spliceEdgeAndInsertNode(rung.edges, oldEdge, newElement.id, 'input', 'output')
// Step 4: Splice new element ID into branch's nodeIds
const newNodeIds = [...nodeIds]
newNodeIds.splice(idx, 0, newElement.id)
const handleBranches = (rung.handleBranches ?? []).map((b) =>
b.blockId === target.blockId && b.handleId === target.handleId ? { ...b, nodeIds: newNodeIds } : b,
)
// Step 5: Add element to nodes array
const newNodes = [...rung.nodes, newElement]
return { nodes: newNodes, edges: newEdges, handleBranches, newNode: newElement }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/handle-branch/index.ts`
around lines 788 - 809, In insertIntoBranch, abort the insertion when oldEdge is
not found: instead of only logging a warning, return the unchanged rung state
(no new nodes/edges/handleBranches) and do not include newElement or modified
nodeIds; specifically, if oldEdge is falsy (the variable checked at the top),
skip calling spliceEdgeAndInsertNode and avoid pushing newElement into
rung.nodes or updating handleBranches — return an object signaling failure or
the original nodes/edges/handleBranches (consistent with the function's return
shape) so callers won't get disconnected branch nodes or stale metadata.

Comment on lines +85 to +95
const findConnections = (
node: Node<BasicNodeData>,
rung: RungLadderState,
offsetY: number = 0,
targetHandle?: string,
) => {
const { nodes: rungNodes, edges: rungEdges } = rung

const connectedEdges = rungEdges.filter((edge) => edge.target === node.id)
const connectedEdges = rungEdges.filter(
(edge) => edge.target === node.id && (targetHandle === undefined || edge.targetHandle === targetHandle),
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle-scoped serialization still anchors coordinates to the main input connector.

When targetHandle is set, Line 112-114 still uses node.data.inputConnector?.glbPosition, so connections on secondary inputs (e.g., R) are serialized with wrong destination coordinates.

💡 Suggested fix
 const findConnections = (
   node: Node<BasicNodeData>,
   rung: RungLadderState,
   offsetY: number = 0,
   targetHandle?: string,
 ) => {
   const { nodes: rungNodes, edges: rungEdges } = rung
+  const targetConnector =
+    targetHandle && node.type === 'block'
+      ? (node as BlockNode<BlockVariant>).data.inputHandles.find((h) => h.id === targetHandle)
+      : node.data.inputConnector
@@
       return {
         '@refLocalId': sourceNode.data.numericId,
         '@formalParameter': sourceHandle === 'OUT' ? '' : sourceHandle,
         position: [
           // Final edge destination
           {
-            '@x': node.data.inputConnector?.glbPosition.x ?? 0,
-            '@y': (node.data.inputConnector?.glbPosition.y ?? 0) + offsetY,
+            '@x': targetConnector?.glbPosition.x ?? 0,
+            '@y': (targetConnector?.glbPosition.y ?? 0) + offsetY,
           },

Also applies to: 112-114, 445-510

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/utils/PLC/xml-generator/codesys/language/ladder-xml.ts` around
lines 85 - 95, The serialization anchors currently always use
node.data.inputConnector?.glbPosition even when a targetHandle is provided;
update findConnections to select the connector by handle when targetHandle is
set (e.g., resolve connector = targetHandle ? node.data.inputConnectors?.find(c
=> c.handle === targetHandle) : node.data.inputConnector) and use
connector.glbPosition for destination coordinates; apply the same change to the
other connection-serialization sites that reference node.data.inputConnector
(the blocks handling explicit handle-scoped connections) so they also pick the
per-handle connector position when targetHandle is present.

@thiagoralves thiagoralves merged commit d9604cf into development May 15, 2026
16 checks passed
@thiagoralves thiagoralves deleted the sync/handle-branches-on-ladder branch May 15, 2026 18:29
thiagoralves added a commit that referenced this pull request May 15, 2026
Brings the ~33 development commits accumulated since v4.2.0-rc1 was
cut back onto the release branch: EtherCAT scan + stats pipeline,
AI chat-history persistence, ladder branches with nested parallels
(#756), and the server-tree visibility fix.

Substantive conflict resolutions:

* `store/slices/device/{types,slice}.ts` + tests
    Both branches added different actions to the device slice
    (rc1: vendor-screen persistence; dev: ethercat status + polling
    toggle + temporary DHCP IP). Kept both sets verbatim.

* `_features/.../device/configuration/board.tsx`
* `_features/.../device/orchestrators/orchestrators-list.tsx`
    Both branches reworked the connected-runtime stats panel. Kept
    rc1's `<ScanCycleStats>` / `<EtherCATStats>` / `<PluginStatsPanel>`
    molecule layout (renders immediately on connect, no scan-count
    gate) and adopted dev's `setIncludeEthercatStatsInPolling`
    polling toggle so the global `useRuntimePolling` hook fetches
    EtherCAT status only while a stats screen is mounted.

* `_molecules/ethercat-stats/index.tsx`
    Rewrote so the molecule consumes
    `runtimeConnection.ethercatStatus` from the device slice instead
    of self-fetching on its own 2 s timer. Extended the column set
    with the metrics dev's `EthercatStatsSection` introduced (Master
    State, Slave Count, Max Exchange, Recovery Attempts, WKC
    consecutive-error badge) so no new health info is lost moving
    from cards back to the table.

* `_features/.../device/ethercat/components/ethercat-stats-section.tsx`
    Deleted — its render is now handled by the unified
    `<EtherCATStats>` molecule.

* `_organisms/explorer/project.tsx`
    Combined both gating intents: kept rc1's
    `projectCaps.hasServers` library gate AND dropped the
    `capabilities.hasLocalSerialPorts` clause per dev's d257e2a
    ("show Servers branch on platforms without local serial ports").

* `_features/.../device/ethercat/index.tsx`
    Kept rc1's `collectUsedIecAddresses` import path
    (`backend/shared/utils/iec-address`) and the extra
    `vendorScreenData` argument it now takes; adopted dev's `Modal`
    imports and `unmatched`-devices tracking in
    `handleAddSelectedFromScan`.

Test-fixture updates to match the now-richer `EtherCATCycleMetrics`
shape (min_*, period_*, latency_* fields are mandatory) and the
expanded `RuntimeConnection` state (`ethercatStatus` +
`includeEthercatStatsInPolling`).

`npx tsc --noEmit`, `npm run build:main`, and the device / shared /
ethercat / ladder test suites (1061 tests) all pass locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thiagoralves added a commit that referenced this pull request May 15, 2026
Brings the ~33 development commits accumulated since v4.2.0-rc1 was
cut back onto the release branch: EtherCAT scan + stats pipeline,
AI chat-history persistence, ladder branches with nested parallels
(#756), and the server-tree visibility fix.

Substantive conflict resolutions:

* `store/slices/device/{types,slice}.ts` + tests
    Both branches added different actions to the device slice
    (rc1: vendor-screen persistence; dev: ethercat status + polling
    toggle + temporary DHCP IP). Kept both sets verbatim.

* `_features/.../device/configuration/board.tsx`
* `_features/.../device/orchestrators/orchestrators-list.tsx`
    Both branches reworked the connected-runtime stats panel. Kept
    rc1's `<ScanCycleStats>` / `<EtherCATStats>` / `<PluginStatsPanel>`
    molecule layout (renders immediately on connect, no scan-count
    gate) and adopted dev's `setIncludeEthercatStatsInPolling`
    polling toggle so the global `useRuntimePolling` hook fetches
    EtherCAT status only while a stats screen is mounted.

* `_molecules/ethercat-stats/index.tsx`
    Rewrote so the molecule consumes
    `runtimeConnection.ethercatStatus` from the device slice instead
    of self-fetching on its own 2 s timer. Extended the column set
    with the metrics dev's `EthercatStatsSection` introduced (Master
    State, Slave Count, Max Exchange, Recovery Attempts, WKC
    consecutive-error badge) so no new health info is lost moving
    from cards back to the table.

* `_features/.../device/ethercat/components/ethercat-stats-section.tsx`
    Deleted — its render is now handled by the unified
    `<EtherCATStats>` molecule.

* `_organisms/explorer/project.tsx`
    Combined both gating intents: kept rc1's
    `projectCaps.hasServers` library gate AND dropped the
    `capabilities.hasLocalSerialPorts` clause per dev's d257e2a
    ("show Servers branch on platforms without local serial ports").

* `_features/.../device/ethercat/index.tsx`
    Kept rc1's `collectUsedIecAddresses` import path
    (`backend/shared/utils/iec-address`) and the extra
    `vendorScreenData` argument it now takes; adopted dev's `Modal`
    imports and `unmatched`-devices tracking in
    `handleAddSelectedFromScan`.

Test-fixture updates to match the now-richer `EtherCATCycleMetrics`
shape (min_*, period_*, latency_* fields are mandatory) and the
expanded `RuntimeConnection` state (`ethercatStatus` +
`includeEthercatStatsInPolling`).

`npx tsc --noEmit`, `npm run build:main`, and the device / shared /
ethercat / ladder test suites (1061 tests) all pass locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant