Skip to content

feat: add React Flow Mermaid renderer - #9

Merged
donovan-yohan merged 13 commits into
mainfrom
feat/reactflow-mermaid-renderer
Aug 8, 2026
Merged

donovan-yohan merged 13 commits into
mainfrom
feat/reactflow-mermaid-renderer

Conversation

@donovan-yohan

@donovan-yohan donovan-yohan commented Jun 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • add @xyflow/react and render flowchart nodes/edges through React Flow using Mermaid SVG hit-map layout as the initial layout source
  • keep Mermaid source and UI mutations in sync for add/edit/delete/shape/edge operations via the existing mutation queue
  • add a fixed add-node toolbar with label + shape controls and a reset-layout button for locally dragged React Flow nodes
  • extend the Playwright validation script for React Flow node alignment, drag/reset, fixed-toolbar add, and label edit sync

Verification

  • npx --yes pnpm@10.15.0 --filter @arielcharts/shared build
  • npx --yes pnpm@10.15.0 typecheck
  • npx --yes pnpm@10.15.0 test
  • npx --yes pnpm@10.15.0 build
  • npx --yes tsx e2e-validate.ts

Note: this devbox is on Node v22.22.3, so pnpm prints the repo's Node >=24 engine warning, but all checks above passed.

Summary by CodeRabbit

Release Notes

  • New Features

    • Interactive Mermaid flowcharts now support dragging, connecting, node creation, and layout reset.
    • Collaborative node positions are persisted and synchronized across sessions.
    • Edges can be selected, relabeled, deleted, and used to create connected nodes.
    • Empty Mermaid input can be treated as buildable.
  • Bug Fixes

    • Improved edge targeting during editing and deletion, including concurrent changes.
    • Mutation failures now appear in the workspace.
  • Style

    • Refreshed diagram canvas styling and Mermaid shape rendering.
  • Tests

    • Expanded coverage for collaboration, editing, positioning, and diagram interactions.

@vercel

vercel Bot commented Jun 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
arielcharts Ready Ready Preview Aug 8, 2026 5:32am

@coderabbitai

coderabbitai Bot commented Jun 9, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b11fb3c2-ad70-4dfe-8a03-78148b6a084a

📥 Commits

Reviewing files that changed from the base of the PR and between 515ed94 and 782fb84.

📒 Files selected for processing (11)
  • apps/web/src/components/diagram-canvas.tsx
  • apps/web/src/components/session-workspace.tsx
  • apps/web/src/lib/diagram-connect-state.test.ts
  • apps/web/src/lib/diagram-connect-state.ts
  • apps/web/src/lib/diagram-flow-identity.test.ts
  • apps/web/src/lib/diagram-flow-identity.ts
  • apps/web/src/lib/diagram-layout.test.ts
  • apps/web/src/lib/diagram-layout.ts
  • apps/web/src/lib/diagram-mutations.test.ts
  • apps/web/src/lib/diagram-mutations.ts
  • e2e-validate.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/lib/diagram-layout.test.ts
  • e2e-validate.ts

📝 Walkthrough

Walkthrough

This PR adds React Flow-based Mermaid diagram interaction, collaborative node-position persistence through Yjs, identity-based edge mutations, connection-state helpers, updated canvas and session wiring, and expanded browser validation.

Changes

React Flow Integration for Interactive Diagram Layout

Layer / File(s) Summary
React Flow library setup and styling
apps/web/package.json, apps/web/src/app/globals.css
Adds the React Flow dependency and stylesheet. Defines canvas, edge, handle, and Mermaid node-shape styling.
Diagram identity and mutation contracts
apps/web/src/lib/diagram-flow-identity.ts, apps/web/src/lib/diagram-mutations.ts, apps/web/src/lib/diagram-connect-state.ts, apps/web/src/lib/*test.ts
Adds canonical edge identity resolution, identity-based edge editing and deletion, connected-node creation, mutation failure reporting, and connection activation logic with tests.
Collaborative node-position storage
apps/web/src/lib/diagram-layout.ts, apps/web/src/lib/diagram-layout.test.ts
Adds validated node-position types and Yjs map helpers with merge, replace, and remove behavior.
Canvas rendering and interaction model
apps/web/src/components/diagram-canvas.tsx
Adds React Flow rendering, synchronized node positions, connection handling, node and edge editing, selection, keyboard actions, toolbars, layout reset, viewport fitting, and legacy SVG fallback.
Session collaboration wiring
apps/web/src/components/session-workspace.tsx
Adds shared Yjs node positions, local synchronization, layout persistence, mutation error reporting, and canvas callbacks.
End-to-end validation updates
e2e-validate.ts
Adds React Flow checks for rendering, dragging, layout reset, node creation, connector creation, edge editing, overlay alignment, and failure exit status.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant DiagramCanvas
  participant ReactFlow
  participant SessionWorkspace
  participant Yjs
  User->>DiagramCanvas: Drag node or edit edge
  DiagramCanvas->>ReactFlow: Render interactive graph
  ReactFlow->>DiagramCanvas: Emit interaction event
  DiagramCanvas->>SessionWorkspace: Report layout or mutation change
  SessionWorkspace->>Yjs: Persist collaborative state
  Yjs->>SessionWorkspace: Notify synchronized positions
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: adding a React Flow renderer for Mermaid diagrams.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/reactflow-mermaid-renderer

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request integrates @xyflow/react (React Flow) into the diagram canvas component to enable interactive flowchart rendering, node dragging, and layout resets, alongside a new toolbar for adding nodes. The E2E tests have been updated to validate these interactive features. The review feedback highlights a potential crash in getShapeClassName if the shape is undefined, potential React Flow console errors from rendering edges with missing nodes, and UX improvements to keep the new node label input empty by default instead of pre-filling it.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +1474 to +1476
function getShapeClassName(shape: DiagramNodeShape): string {
return shape.replace(/[^a-z0-9_-]/gi, '').toLowerCase() || 'rect';
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The getShapeClassName function assumes that shape is always defined. However, node.shape is optional and can be undefined (as seen in other parts of the codebase using node?.shape ?? 'node'). If shape is undefined, calling shape.replace will throw a TypeError and crash the renderer. We should make the parameter optional and safely default to 'rect'.

Suggested change
function getShapeClassName(shape: DiagramNodeShape): string {
return shape.replace(/[^a-z0-9_-]/gi, '').toLowerCase() || 'rect';
}
function getShapeClassName(shape?: DiagramNodeShape): string {
return shape ? shape.replace(/[^a-z0-9_-]/gi, '').toLowerCase() || 'rect' : 'rect';
}

Comment on lines +279 to +293
const flowEdges = useMemo<MermaidFlowEdge[]>(() => {
if (!graph) {
return [];
}

return graph.links.map((link, index) => ({
id: `${link.source}-${link.target}-${index}`,
label: getLinkText(link),
markerEnd: { color: '#38bdf8', type: MarkerType.ArrowClosed },
source: link.source,
style: { stroke: '#38bdf8', strokeWidth: 1.6 },
target: link.target,
type: 'smoothstep',
}));
}, [graph]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The flowEdges memoization maps all links from the graph directly. However, if a node is filtered out or not yet present in interactiveNodeBounds (e.g., during initial load or transitions), React Flow will log console errors about missing source/target nodes. We should filter the edges to ensure both the source and target nodes exist in interactiveNodeBounds.

const flowEdges = useMemo<MermaidFlowEdge[]>(() => {
    if (!graph || !interactiveNodeBounds) {
      return [];
    }

    const edges: MermaidFlowEdge[] = [];
    graph.links.forEach((link, index) => {
      if (interactiveNodeBounds.has(link.source) && interactiveNodeBounds.has(link.target)) {
        edges.push({
          id: `${link.source}-${link.target}-${index}`,
          label: getLinkText(link),
          markerEnd: { color: '#38bdf8', type: MarkerType.ArrowClosed },
          source: link.source,
          style: { stroke: '#38bdf8', strokeWidth: 1.6 },
          target: link.target,
          type: 'smoothstep',
        });
      }
    });
    return edges;
  }, [graph, interactiveNodeBounds]);

const [pendingEdgeLabel, setPendingEdgeLabel] = useState('');
const [groupPromptValue, setGroupPromptValue] = useState('');
const [showGroupPrompt, setShowGroupPrompt] = useState(false);
const [newNodeLabel, setNewNodeLabel] = useState(DEFAULT_NEW_NODE_LABEL);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Initializing newNodeLabel with DEFAULT_NEW_NODE_LABEL ('New Node') pre-fills the input field in the toolbar, which hides the placeholder and forces the user to manually clear the text before typing. Initializing it to an empty string '' provides a better user experience, as the placeholder 'label' will be visible.

Suggested change
const [newNodeLabel, setNewNodeLabel] = useState(DEFAULT_NEW_NODE_LABEL);
const [newNodeLabel, setNewNodeLabel] = useState('');

Comment on lines +805 to +808
const addNodeFromToolbar = useCallback(() => {
onAddNode?.(newNodeLabel.trim() || DEFAULT_NEW_NODE_LABEL, newNodeShape);
setNewNodeLabel(DEFAULT_NEW_NODE_LABEL);
}, [newNodeLabel, newNodeShape, onAddNode]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Resetting newNodeLabel to DEFAULT_NEW_NODE_LABEL after adding a node pre-fills the input field again. We should reset it to an empty string '' to keep the input clear for the next entry, while still falling back to the default label on submission.

Suggested change
const addNodeFromToolbar = useCallback(() => {
onAddNode?.(newNodeLabel.trim() || DEFAULT_NEW_NODE_LABEL, newNodeShape);
setNewNodeLabel(DEFAULT_NEW_NODE_LABEL);
}, [newNodeLabel, newNodeShape, onAddNode]);
const addNodeFromToolbar = useCallback(() => {
onAddNode?.(newNodeLabel.trim() || DEFAULT_NEW_NODE_LABEL, newNodeShape);
setNewNodeLabel('');
}, [newNodeLabel, newNodeShape, onAddNode]);

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/src/components/diagram-canvas.tsx (1)

232-244: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fit-to-diagram ignores dragged node positions.

graphBounds is still computed from hitMap.nodes, so after React Flow drag updates, fitToDiagram() can zoom/pan to stale extents and miss moved nodes.

Suggested fix
   const graphBounds = useMemo(() => {
     if (!hitMap) {
       return null;
     }

+    const nodeBounds = interactiveNodeBounds
+      ? [...interactiveNodeBounds.values()]
+      : [...hitMap.nodes.values()];
+
     const allBounds = [
-      ...hitMap.nodes.values(),
+      ...nodeBounds,
       ...hitMap.subgraphs.values(),
       ...[...hitMap.edges.values()].map((edge) => edge.bounds),
     ];

     return getBoundsUnion(allBounds);
-  }, [hitMap]);
+  }, [hitMap, interactiveNodeBounds]);
🤖 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 `@apps/web/src/components/diagram-canvas.tsx` around lines 232 - 244,
graphBounds is computed only from hitMap.nodes so dragged node positions
(updated by React Flow) are ignored; update the useMemo that defines graphBounds
to incorporate the current node positions when available by merging hitMap data
with the latest nodes from React Flow (e.g., use the nodes array or a
getNodes()/nodesRef accessor), replacing bounds for node ids present in the
current nodes list before calling getBoundsUnion; reference graphBounds,
hitMap.nodes, fitToDiagram and getBoundsUnion so the memo uses up-to-date node
positions when computing extents.
e2e-validate.ts (1)

35-57: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid index-based node/overlay pairing in alignment checks.

Line 38 and Line 173 assume SVG node order and overlay DOM order are identical. That ordering can diverge after interaction, causing false alignment failures.

Proposed fix (nearest-neighbor matching instead of index matching)
-    const matches: Array<{ nodeId: string; svgCenter: string; overlayCenter: string; offsetPx: number }> = [];
-    nodes.forEach((n, i) => {
-      const g = n as SVGGElement;
-      const overlay = overlays[i] as HTMLElement | undefined;
-      if (!overlay) return;
-
-      const svgRect = g.getBoundingClientRect();
-      const overlayRect = overlay.getBoundingClientRect();
-
-      const svgCx = Math.round(svgRect.x + svgRect.width / 2);
-      const svgCy = Math.round(svgRect.y + svgRect.height / 2);
-      const overlayCx = Math.round(overlayRect.x + overlayRect.width / 2);
-      const overlayCy = Math.round(overlayRect.y + overlayRect.height / 2);
-
-      matches.push({
-        nodeId: g.id,
-        svgCenter: `${svgCx},${svgCy}`,
-        overlayCenter: `${overlayCx},${overlayCy}`,
-        offsetPx: Math.round(Math.sqrt((svgCx - overlayCx) ** 2 + (svgCy - overlayCy) ** 2)),
-      });
-    });
+    const nodeCenters = Array.from(nodes).map((n) => {
+      const r = (n as SVGGElement).getBoundingClientRect();
+      return { nodeId: (n as SVGGElement).id, x: r.x + r.width / 2, y: r.y + r.height / 2 };
+    });
+    const overlayCenters = Array.from(overlays).map((o) => {
+      const r = (o as HTMLElement).getBoundingClientRect();
+      return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
+    });
+
+    const matches = overlayCenters.map((overlay, i) => {
+      const nearest = Math.min(
+        ...nodeCenters.map((node) => Math.hypot(node.x - overlay.x, node.y - overlay.y)),
+      );
+      return { overlayIndex: i, offsetPx: Math.round(nearest) };
+    });

Apply the same pattern to both alignment sections.

Also applies to: 170-188

🤖 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 `@e2e-validate.ts` around lines 35 - 57, The current code pairs SVG nodes and
overlays by array index (using overlays[i] for nodes.forEach), which can be
incorrect after DOM reordering; change to nearest-neighbor matching instead:
compute center points for all SVG nodes (using svgRect/x/y/width/height and
g.id) and for all overlays (using overlayRect), then for each overlay find the
closest SVG node by Euclidean distance, pair them, mark that SVG node as used to
avoid duplicates, and push the pair into matches with svgCenter, overlayCenter
and offsetPx; apply the exact same nearest-neighbor pairing logic to the other
alignment block referenced around lines 170-188 so both sections use identical
matching instead of index-based matching (update references to overlays, nodes,
matches, g.id, svgRect and overlayRect accordingly).
🤖 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 `@apps/web/src/app/globals.css`:
- Around line 637-677: Add a CSS rule for .mermaid-flow-node--cylinder in
globals.css to match the cylinder option used in diagram-canvas.tsx (the
"cylinder" node type referenced around the diagram-canvas component), so it no
longer falls back to a rectangle: give the selector a large horizontal
border-radius (e.g., 999px) and appropriate padding/height, and implement a
::before (and optional ::after) pseudo-element(s) positioned absolutely to
render the rounded top (and bottom) ellipses (matching the visual style of other
mermaid flow-node rules such as .mermaid-flow-node--round and
.mermaid-flow-node--subroutine) so the cylinder shape appears correctly in React
Flow mode.

In `@apps/web/src/components/diagram-canvas.tsx`:
- Around line 284-292: The edges mapping currently forces MarkerType.ArrowClosed
for every edge; instead detect each graph.links[].type (e.g., 'arrow_open',
'arrow_circle', 'arrow_cross') when building flowEdges and set
markerStart/markerEnd to reference custom SVG marker IDs (e.g., markerOpen,
markerCircle, markerCross) rather than a MarkerType enum. Add a <defs> block in
the DiagramCanvas component that defines those three custom <marker> elements
(SVG paths matching the visual styles) and ensure flowEdges uses markerEnd: {
type: 'url(`#markerOpen`)' } or similar URL refs per link.type (and fall back to
ArrowClosed for default cases); update the mapping in the function that returns
edges (the code around graph.links.map) to use link.type to pick the proper
marker id.

In `@e2e-validate.ts`:
- Around line 105-110: Wrap the Reset button click and subsequent boundingBox
check in a try/catch so a missing or timeout on
page.locator('button[aria-label="Reset dragged node layout to Mermaid"]') does
not throw and abort the script; on catch set resetPass = false, push the result
into results (using the existing results.push call), and log the failure message
(console.log) so the test continues to later validations; continue to compute
bounding box only if the click succeeds and beforeDrag/dragTarget are present,
but never rethrow the error.

---

Outside diff comments:
In `@apps/web/src/components/diagram-canvas.tsx`:
- Around line 232-244: graphBounds is computed only from hitMap.nodes so dragged
node positions (updated by React Flow) are ignored; update the useMemo that
defines graphBounds to incorporate the current node positions when available by
merging hitMap data with the latest nodes from React Flow (e.g., use the nodes
array or a getNodes()/nodesRef accessor), replacing bounds for node ids present
in the current nodes list before calling getBoundsUnion; reference graphBounds,
hitMap.nodes, fitToDiagram and getBoundsUnion so the memo uses up-to-date node
positions when computing extents.

In `@e2e-validate.ts`:
- Around line 35-57: The current code pairs SVG nodes and overlays by array
index (using overlays[i] for nodes.forEach), which can be incorrect after DOM
reordering; change to nearest-neighbor matching instead: compute center points
for all SVG nodes (using svgRect/x/y/width/height and g.id) and for all overlays
(using overlayRect), then for each overlay find the closest SVG node by
Euclidean distance, pair them, mark that SVG node as used to avoid duplicates,
and push the pair into matches with svgCenter, overlayCenter and offsetPx; apply
the exact same nearest-neighbor pairing logic to the other alignment block
referenced around lines 170-188 so both sections use identical matching instead
of index-based matching (update references to overlays, nodes, matches, g.id,
svgRect and overlayRect accordingly).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8a9b8f93-d634-43c9-b2c6-b96c8dd65ac7

📥 Commits

Reviewing files that changed from the base of the PR and between 40364ec and f1fc03f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • apps/web/package.json
  • apps/web/src/app/globals.css
  • apps/web/src/components/diagram-canvas.tsx
  • e2e-validate.ts

Comment thread apps/web/src/app/globals.css
Comment thread apps/web/src/components/diagram-canvas.tsx Outdated
Comment thread e2e-validate.ts Outdated
@donovan-yohan

Copy link
Copy Markdown
Owner Author

Fugu review — changes requested

Verdict: Block.

  1. apps/web/src/components/diagram-canvas.tsx:279-292 / apps/web/src/app/globals.css:581-584 — React Flow rendering drops Mermaid edge semantics. The PR hides the original Mermaid edge paths/labels, then rebuilds every edge as smoothstep with MarkerType.ArrowClosed. That ignores link.type (arrow_open, arrow_circle, arrow_cross) and link.stroke, even though the toolbar still exposes those connection types. Result: the Mermaid text can say circle/cross/open/dotted/thick while the rendered diagram lies and shows a normal closed arrow. Fix by mapping Mermaid link type/stroke to React Flow edge/marker styling, or keep the Mermaid SVG edges visible until React Flow can faithfully render them.

  2. apps/web/src/components/diagram-canvas.tsx:95-105,1444-1475 / apps/web/src/app/globals.css:612-677 — node shape rendering is incomplete while the original Mermaid node SVGs are hidden. The toolbar offers cylinder, and DiagramNodeShape also includes shapes like lean_left, lean_right, inv_trapezoid, odd, etc., but the React Flow replacement only styles a subset. Unsupported shapes silently fall back to a rectangle, so existing Mermaid diagrams and toolbar mutations no longer render what the source says. Either implement all supported Mermaid shapes, restrict the UI/parser handoff to shapes React Flow can display, or do not hide the Mermaid nodes for shapes the replacement cannot faithfully render.

Concern, not blocking by itself: apps/web/src/components/diagram-canvas.tsx:874-908 disables the old accessible overlay path, while the old keyboard/aria node handlers live only in the fallback button branch at 912-1016. Please verify keyboard selection/editing still works through React Flow or wire equivalent aria labels/key handlers/onSelectionChange. Right now this looks like an accessibility regression, but the visual semantic drift above is already enough to block.

Bot feedback + learning:

  • Bot surfaces checked: reviews, inline PR comments, issue comments.
  • Gemini getShapeClassName(undefined) concern: false positive for current mermaid-ast@0.8.2 types (FlowchartNode.shape is required), though defensive defaulting would be harmless.
  • Gemini edge filtering concern: follow-up robustness, not the main blocker.
  • Gemini new-node input default concern: UX follow-up, not blocking.
  • CodeRabbit/Vercel comments were status/noise only.

Evidence reviewed: exact head f1fc03f977be11a0bda0f0febcc89aee30bbc5af, changed-file diff, diagram-canvas.tsx, globals.css, e2e-validate.ts, dependency/lockfile changes, GitHub bot surfaces, and CI status. CI verify passed on the expected head. Local pnpm validation could not be run because pnpm is not installed and npx pnpm is blocked by the local security approval gate, but CI covered the repo-required build/typecheck/test path.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/web/src/components/diagram-canvas.tsx (1)

542-558: 💤 Low value

Effect dependency on manualNodePositions causes unnecessary re-runs during drag.

Including manualNodePositions in the dependency array triggers this effect every time positions change (e.g., during node drag), not just when graph changes. The trim logic uses setNodePositions((current) => ...) which already receives the latest value through the callback parameter.

Consider removing manualNodePositions from deps and handling the null-graph case differently:

♻️ Proposed refactor
 useEffect(() => {
   if (!graph) {
-    if (Object.keys(manualNodePositions).length > 0) {
-      setNodePositions(() => ({}));
-    }
+    setNodePositions((current) => Object.keys(current).length > 0 ? {} : current);
     return;
   }

   const currentNodeIds = new Set(graph.nodes.map((node) => node.id));
   setNodePositions((current) => {
     const next = Object.fromEntries(
       Object.entries(current).filter(([nodeId]) => currentNodeIds.has(nodeId)),
     );

     return Object.keys(next).length === Object.keys(current).length ? current : next;
   });
-}, [graph, manualNodePositions, setNodePositions]);
+}, [graph, setNodePositions]);
🤖 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 `@apps/web/src/components/diagram-canvas.tsx` around lines 542 - 558, The
useEffect hook in the diagram-canvas.tsx file is including manualNodePositions
in its dependency array, which causes the effect to re-run every time node
positions change during drag operations rather than only when the graph
structure changes. Remove manualNodePositions from the dependency array of this
useEffect, keeping only graph and setNodePositions. The callback pattern in
setNodePositions((current) => ...) already provides access to the current
positions, so an explicit dependency on manualNodePositions is unnecessary and
causes excessive re-runs during interactive drag operations.
🤖 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 `@apps/web/src/components/session-workspace.tsx`:
- Around line 181-184: The positions dictionary is created using empty object
literal syntax `{}` which is vulnerable to prototype pollution attacks since Yjs
keys are collaborator-controlled and malicious keys like `__proto__` could
poison the object's prototype. Replace the object initialization with
`Object.create(null)` when creating the `positions` variable to create a
dictionary without a prototype chain, preventing prototype-key injection and
ensuring membership checks with the `in` operator remain reliable.

---

Nitpick comments:
In `@apps/web/src/components/diagram-canvas.tsx`:
- Around line 542-558: The useEffect hook in the diagram-canvas.tsx file is
including manualNodePositions in its dependency array, which causes the effect
to re-run every time node positions change during drag operations rather than
only when the graph structure changes. Remove manualNodePositions from the
dependency array of this useEffect, keeping only graph and setNodePositions. The
callback pattern in setNodePositions((current) => ...) already provides access
to the current positions, so an explicit dependency on manualNodePositions is
unnecessary and causes excessive re-runs during interactive drag operations.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 36bd3ea3-b73a-4b39-b4d7-17ab79a50aa6

📥 Commits

Reviewing files that changed from the base of the PR and between f1fc03f and 7652269.

📒 Files selected for processing (4)
  • apps/web/src/app/globals.css
  • apps/web/src/components/diagram-canvas.tsx
  • apps/web/src/components/session-workspace.tsx
  • e2e-validate.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/app/globals.css

Comment thread apps/web/src/components/session-workspace.tsx Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new React Flow–based renderer for Mermaid flowcharts in the web app, using the existing Mermaid SVG hit-map as the initial layout source, and extends the E2E validation script to cover React Flow interactions.

Changes:

  • Add @xyflow/react and render flowchart nodes/edges via React Flow while keeping Mermaid as the canonical source of truth.
  • Persist and sync manual node layout positions through Yjs (nodePositions) and expose layout reset + fixed add-node toolbar controls.
  • Extend e2e-validate.ts to validate empty-canvas building, React Flow semantics, drag/reset, toolbar add, and label-edit sync.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pnpm-lock.yaml Locks new React Flow dependency tree (@xyflow/react + transitive deps).
apps/web/package.json Adds @xyflow/react dependency to the web app.
apps/web/src/app/globals.css Imports React Flow base styles and adds styling for React Flow nodes/edges and Mermaid-like node shapes.
apps/web/src/components/diagram-canvas.tsx Implements React Flow rendering layer, node/edge mapping, drag/reset behavior, keyboard interaction, and add-node UI.
apps/web/src/components/session-workspace.tsx Adds Yjs-backed persistence for node positions and wires position/connected-node events to the canvas.
e2e-validate.ts Updates Playwright validation coverage for React Flow mode and adds failure exit semantics.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Comments suppressed due to low confidence (1)

e2e-validate.ts:55

  • The E2E script uses Control+A to select all editor content, which won’t work on macOS (the standard shortcut is Meta/Command+A). Playwright supports a cross-platform shortcut via ControlOrMeta+A, which keeps the script portable across developer machines and CI runners.
  console.log('2b. Typing flowchart...');
  await editor.click();
  await page.keyboard.press('Control+A');
  await page.keyboard.type(`flowchart TD
    A[Start] --> B{Bug?}
    B -->|Yes| C[Fix]
    B -->|No| D[Ship]`, { delay: 10 });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread e2e-validate.ts Outdated
Comment on lines +97 to +106
// --- Test: canonical Mermaid edge semantics + React Flow shape fidelity ---
console.log('\n3b. Checking Mermaid semantics in React Flow mode...');
await editor.click();
await page.keyboard.press('Control+A');
await page.keyboard.type(`flowchart TD
A[(Database)] --> B{Decision}
B -.-> C[Retry]
B ==>|thick| D[Done]
A --o E((Circle))
E --x F[/Trap/]`, { delay: 10 });
Comment on lines +701 to +717
const handleAddConnectedNode = useCallback((source: string, label: string, shape: DiagramNodeShape, position: NodePosition, type: DiagramLinkType) => {
const queue = mutationQueueRef.current;
if (!queue || !flowchartSnapshot) {
return;
}

const nodeId = ensureUniqueId(flowchartSnapshot.nodeIds, createNodeId(label));
void queue.addNode(label, { id: nodeId, shape })
.then(() => queue.addEdge(source, nodeId, { type }))
.then(() => {
handleNodePositionsChange({
...nodePositions,
[nodeId]: position,
});
setSelectedNodeIds([nodeId]);
});
}, [flowchartSnapshot, handleNodePositionsChange, nodePositions]);
Comment on lines +1639 to +1641
role="button"
tabIndex={focused ? 0 : -1}
>
@donovan-yohan

Copy link
Copy Markdown
Owner Author

Pushed f89b598 addressing the current PR feedback:

  • Preserved Mermaid edge semantics in React Flow edges: arrow/open/circle/cross markers plus dotted/thick strokes now map onto visible React Flow edges.
  • Filtered React Flow edges to nodes present in the interactive bounds map to avoid missing-node render errors during transitions.
  • Made getShapeClassName safe for missing shapes.
  • Cleared the add-node toolbar input by default and after submit.
  • Hardened collaborative node-position dictionaries against prototype-key issues (Object.create(null), Object.hasOwn).
  • Softened the reset-layout e2e step so the run reports the failure instead of aborting.

Verified locally:

  • npx --yes pnpm@10.15.0 --filter @arielcharts/web typecheck ✅
  • npx --yes tsx e2e-validate.ts ✅ all tests passed, including React Flow marker/stroke semantics and drag-out connected node creation
  • full gate ✅: shared build, repo typecheck, tests, build (/tmp/arielcharts-pr9-review-fixes-validate.log)

@donovan-yohan

Copy link
Copy Markdown
Owner Author

Pushed 9ba080c for the FigJam-style canvas issues:

  • Added source handles on all four node sides, so users can drag out from left/right/top/bottom. This is not a Mermaid limitation; Mermaid stores the graph edge, while the visual/manual layout layer decides where the node lives and which side the React Flow edge pins to.
  • Stopped drag-induced viewport zoom. Auto-fit now only runs for the first render; manual node drag keeps the current zoom and only allows pan updates from React Flow auto-pan.
  • Kept drag layout local/canonical: moving nodes still does not rewrite Mermaid text, only visual layout positions.
  • Added edge selection, edge delete, and edge label editing from the canvas.
  • Changed new React Flow edges to pick handles from relative node positions, so side placement pins edges to the nearest sensible side instead of global TD/LR-only handles.
  • Extended e2e to cover left/right handles, drag-no-zoom, edge label edit/delete, and drag-out node creation.

Verification:

  • npx --yes pnpm@10.15.0 --filter @arielcharts/web typecheck ✅
  • npx --yes tsx e2e-validate.ts ✅ all tests passed
  • npx --yes pnpm@10.15.0 --filter @arielcharts/shared test ✅
  • npx --yes pnpm@10.15.0 --filter @arielcharts/web test ✅
  • npx --yes pnpm@10.15.0 --filter @arielcharts/server exec vitest run --testTimeout 20000 --hookTimeout 20000 ✅
  • npx --yes pnpm@10.15.0 build ✅

Note: exact pnpm test on this host hit Vitest's default 5s server-test timeout under the current load, but the same server suite passes with a 20s timeout. Log: /tmp/arielcharts-figjam-ux-validate.log.

@donovan-yohan

Copy link
Copy Markdown
Owner Author

Fugu review gate — blocked as stale

Verdict: Block / no approval.

This review gate was scoped to exact PR head 951823cf3b65dd84c6cf1348fe4e18c3b5fdf537, but the live PR head is now 9ba080c19baa128c55d69eb0439e37d4ef1853d2 on feat/reactflow-mermaid-renderer.

I am not reviewing or approving a different head under a stale gate. Please create fresh exact-head QA/review/release gates for the current PR head before merge.

Bot feedback + learning:

  • Bot surfaces checked for PR feat: add React Flow Mermaid renderer #9: review summaries, inline PR comments, and issue comments.
  • Current substantive bot comments include Copilot findings against later commits; those belong to a fresh-head review of 9ba080c19baa128c55d69eb0439e37d4ef1853d2, not this stale 951823c gate.
  • CodeRabbit rate-limit/status and Vercel deployment comments are noise for this gate.

@donovan-yohan

Copy link
Copy Markdown
Owner Author

Fugu exact-head review — scoped approval

Verdict: Approve, scoped only to exact commit 951823cf3b65dd84c6cf1348fe4e18c3b5fdf537.

This is not approval for the current live PR head. At review time, PR #9 live head is 9ba080c19baa128c55d69eb0439e37d4ef1853d2, so release must still fail/stop unless it is intentionally targeting the reviewed 951823c artifact.

Findings:

  • No blocking findings against the 951823c remediation.
  • Renderer semantics: Mermaid edge paths/labels stay visible in React Flow mode; React Flow no longer redraws canonical Mermaid edges as generic closed arrows.
  • CSS fidelity: covered circle/doublecircle/ellipse, diamond, hexagon, trapezoid/inverted trapezoid, lean left/right, odd, cylinder, subroutine; local e2e confirms cylinder styling.
  • Accessibility/focus: React Flow node keyboard selection works in local e2e; first node gets a tabbable/focusable entry point through the focused-node state.

Non-blocking concerns:

  • e2e-validate.ts still uses Control+A; that passed on this Linux host, but ControlOrMeta+A would be more portable for macOS.
  • The reset-layout e2e step hard-fails if the reset button is missing; acceptable as validation strictness, but it can hide later test results.
  • The add-node input is prefilled/reset to New Node; harmless, just mildly annoying UX. bro, naturally.

Bot feedback + learning:

  • Bot surfaces checked: review summaries, inline PR comments, issue comments.
  • Gemini/CodeRabbit edge and cylinder findings are fixed in this exact head.
  • Gemini undefined-shape issue is not reproduced by typecheck/e2e, but defensive defaulting remains harmless.
  • CodeRabbit reset-button and Copilot Control+A comments are test robustness/portability follow-ups, not blockers for this remediation.
  • Later Copilot comments target commits after 951823c and are outside this exact-head approval.

Evidence reviewed:

  • Local checkout HEAD: 951823cf3b65dd84c6cf1348fe4e18c3b5fdf537
  • Changed files inspected: apps/web/src/components/diagram-canvas.tsx, apps/web/src/app/globals.css, e2e-validate.ts, apps/web/package.json, pnpm-lock.yaml
  • Commands: pnpm install --frozen-lockfile, pnpm typecheck, pnpm test, pnpm build, npx tsx e2e-validate.ts
  • Result: typecheck/test/build pass; e2e reports all 12 checks passed.

@donovan-yohan

Copy link
Copy Markdown
Owner Author

Fugu exact-head review — request changes

Verdict: Block / changes requested for live head 9ba080c19baa128c55d69eb0439e37d4ef1853d2.

I confirmed PR #9 live head is exactly 9ba080c19baa128c55d69eb0439e37d4ef1853d2 (feat/reactflow-mermaid-renderer -> main). CI is green, local typecheck/test/build/e2e are green, but the current head still has a collaborative layout data-loss path. Do not merge this head until that is fixed.

Blocking findings:

  1. apps/web/src/components/session-workspace.tsx:701-717 — drag-out connected-node creation can delete other collaborators' saved node layout positions.

    • Evidence: handleAddConnectedNode captures nodePositions from React state, waits for two async Mermaid mutations, then calls handleNodePositionsChange({ ...nodePositions, [nodeId]: position }).
    • handleNodePositionsChange rewrites the entire Yjs nodePositionsMap and deletes every existing key absent from that object at apps/web/src/components/session-workspace.tsx:681-699.
    • Impact: if another collaborator moves a node while the local add-node mutation is queued/in flight, their newer layout key may be missing from the captured nodePositions snapshot and gets deleted from the shared Yjs map. That is collaborative state loss. Copilot's nodePositions/Yjs sync comment is still valid.
    • Unblock: update the new node position incrementally instead of replacing the whole position map from a stale snapshot. Keep the full-replace/delete behavior only for explicit reset/trim paths that intentionally own the whole map.
  2. apps/web/src/components/diagram-canvas.tsx:1247-1265, apps/web/src/components/session-workspace.tsx:867-868, apps/web/src/lib/diagram-mutations.ts:177-185 — edge edit/delete is index-addressed across an async collaborative document.

    • Evidence: the React Flow edge id is edge-${index} and selection stores only selectedEdgeIndex; later removeEdgeAt(index) / editEdgeLabel(index, ...) mutates whatever link occupies that index at mutation time.
    • Impact: a concurrent collaborator adding/removing/reordering links between edge selection and edit/delete can make the action affect the wrong edge. Same class of collaborative data-loss bug, just on Mermaid links instead of nodePositions.
    • Unblock: carry a stable edge identity or validate the selected edge's source/target/type/label against the current snapshot immediately before mutating; if it changed, cancel/reselect instead of mutating by stale index.

Non-blocking concerns:

  • e2e-validate.ts:56 and e2e-validate.ts:105 still use Control+A; Copilot's keyboard-portability comment is still valid for macOS. Use Playwright ControlOrMeta+A when touching the script. This is not a merge blocker by itself.
  • Copilot's tabbable node-entry concern is no longer blocking: apps/web/src/components/diagram-canvas.tsx:663-672 ensures a first focused node id once nodes exist, so tabIndex={focused ? 0 : -1} at apps/web/src/components/diagram-canvas.tsx:1871 has a keyboard entry point after render.
  • Gemini/CodeRabbit older findings for undefined shape, missing edge filtering, cylinder styling, reset robustness, and fit bounds are fixed or non-blocking in this head.
  • CodeRabbit rate-limit/status and Vercel deployment comments are noise.

Evidence reviewed:

  • Files inspected: apps/web/src/components/diagram-canvas.tsx, apps/web/src/components/session-workspace.tsx, apps/web/src/lib/diagram-mutations.ts, apps/web/src/app/globals.css, e2e-validate.ts, apps/web/package.json, pnpm-lock.yaml.
  • Bot surfaces checked: PR review summaries, inline PR comments, issue comments.
  • GitHub checks: CodeRabbit, Vercel, Vercel Preview Comments, and verify are passing.
  • Local commands:
    • npx --yes pnpm@10.15.0 install --frozen-lockfile passed, with Node engine warning (node >=24 wanted, host is v22.22.3) and ignored build-script warning.
    • npx --yes pnpm@10.15.0 --filter @arielcharts/web typecheck passed.
    • npx --yes pnpm@10.15.0 --filter @arielcharts/web test passed: 1 file, 3 tests.
    • npx --yes pnpm@10.15.0 --filter @arielcharts/web build passed.
    • E2E passed against this workspace on port 3004 using a temporary copy of e2e-validate.ts with only the hardcoded port changed because port 3003 was occupied by a stale server from another workspace: all 21 checks passed.

Bot feedback + learning:

  • Substantive bot findings triaged: 1 blocker still valid (Copilot nodePositions/Yjs sync), 1 follow-up still valid (Copilot Control+A portability), 1 already mitigated (tabbable entry), older Gemini/CodeRabbit items already fixed or non-blocking, rate-limit/deployment boilerplate noise.
  • Learning retained: pending — collaborative canvas review should treat whole-map rewrites and index-addressed mutations as data-loss risks when Yjs/remote edits are involved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/web/src/lib/diagram-layout.ts (1)

29-45: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Write side skips the validation the read side enforces.

readNodePositions filters entries through isNodePosition (Lines 22-24), but writeNodePositions writes positions entries directly without the same check (Line 43). Invalid coordinates (e.g. NaN from a drag miscalculation) would be silently written into the shared Yjs map and then silently dropped again on the next readNodePositions call — leaving inert junk entries in the collaborative document instead of being rejected at the source.

♻️ Proposed fix to validate before writing
   for (const [nodeId, position] of Object.entries(positions)) {
-    positionMap.set(nodeId, { x: position.x, y: position.y });
+    if (!isNodePosition(position)) {
+      continue;
+    }
+    positionMap.set(nodeId, { x: position.x, y: position.y });
   }
🤖 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 `@apps/web/src/lib/diagram-layout.ts` around lines 29 - 45, writeNodePositions
currently writes every entry directly into the Y.Map without the same validation
that readNodePositions applies, so invalid coordinates can be persisted as junk.
Update writeNodePositions to validate each candidate position with the same
isNodePosition check used by readNodePositions before calling positionMap.set,
and skip or reject any invalid node positions (including NaN values) so only
valid coordinates are stored.
🤖 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 `@apps/web/src/components/diagram-canvas.tsx`:
- Around line 218-234: The default full-replace broadcast in setNodePositions is
clearing or overwriting collaborators’ shared layout state. Update the
graph-trim effect and the “Clean layout to Mermaid” action so they do not call
setNodePositions with an unconditional replace of the full map; instead scope
the sync to only the affected node keys or local drag-owned positions, following
the merge-style pattern used by handleFlowNodeDragStop. Keep the local
persistedNodePositionsRef update, but ensure onNodePositionsChange only receives
the minimal delta rather than syncPositions ?? next for these call sites.
- Around line 363-383: The edge index in flowEdges is being taken from the
filtered map position, but onEdgeClick/onEdgeDoubleClick and selectedEdgeIndex
expect the original graph.links index. Update the flowEdges construction in
diagram-canvas.tsx to preserve each link’s original index before filtering, and
use that original index for id, data.index, and selected comparison so
getFlowEdgeIndex/getDiagramEdgeIdentity resolve the correct edge. Keep the
filtering by interactiveNodeBounds, but do not let the filtered array position
drive edge identity or selection.

---

Nitpick comments:
In `@apps/web/src/lib/diagram-layout.ts`:
- Around line 29-45: writeNodePositions currently writes every entry directly
into the Y.Map without the same validation that readNodePositions applies, so
invalid coordinates can be persisted as junk. Update writeNodePositions to
validate each candidate position with the same isNodePosition check used by
readNodePositions before calling positionMap.set, and skip or reject any invalid
node positions (including NaN values) so only valid coordinates are stored.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 83989a59-dbad-48a6-b06b-d18a4b610463

📥 Commits

Reviewing files that changed from the base of the PR and between 7652269 and 515ed94.

📒 Files selected for processing (8)
  • apps/web/src/app/globals.css
  • apps/web/src/components/diagram-canvas.tsx
  • apps/web/src/components/session-workspace.tsx
  • apps/web/src/lib/diagram-layout.test.ts
  • apps/web/src/lib/diagram-layout.ts
  • apps/web/src/lib/diagram-mutations.test.ts
  • apps/web/src/lib/diagram-mutations.ts
  • e2e-validate.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/src/components/session-workspace.tsx
  • e2e-validate.ts
  • apps/web/src/app/globals.css

Comment on lines +218 to +234
const setNodePositions = useCallback((
update: (current: DiagramNodePositions) => DiagramNodePositions,
mode: NodePositionsSyncMode = 'replace',
syncPositions?: DiagramNodePositions,
) => {
const currentPositions = persistedNodePositionsRef.current;
const next = update(currentPositions);
if (next === currentPositions) {
return;
}

persistedNodePositionsRef.current = next;
if (nodePositions === undefined) {
setUncontrolledNodePositions(next);
}
onNodePositionsChange?.(syncPositions ?? next, mode);
}, [nodePositions, onNodePositionsChange]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

setNodePositions default full-replace broadcast can wipe collaborators' saved layout.

setNodePositions defaults mode to 'replace' and broadcasts syncPositions ?? next via onNodePositionsChange. Two call sites invoke it without a scoped syncPositions, so they broadcast the entire locally-known positions map as an unconditional replace of the shared state:

  • The graph-change trim effect (lines 638-656) calls setNodePositions((current) => {...trim...}) with no syncPositions, so any local node removal triggers a full 'replace' of the shared node-positions map with whatever this client currently has computed — a race window if another collaborator's concurrently-added position hasn't yet been merged locally.
  • The "Clean layout to Mermaid" button (line 1621) calls setNodePositions(() => ({})), broadcasting {} with 'replace' — this clears all persisted positions for every collaborator, not just this user's local drags.

This directly contradicts the PR's own stated scope ("reset-layout button for locally dragged React Flow nodes") and matches the collaboration-safety class of bug called out in review history (capturing a local snapshot and rewriting the whole shared map). Contrast with handleFlowNodeDragStop (982-992), which correctly scopes the broadcast to the single moved node via syncPositions + 'merge' — that pattern should be applied to these two call sites as well (e.g., only sync the specific removed keys, or track and clear only positions this client itself set).

Also applies to: 638-657, 1620-1624

🤖 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 `@apps/web/src/components/diagram-canvas.tsx` around lines 218 - 234, The
default full-replace broadcast in setNodePositions is clearing or overwriting
collaborators’ shared layout state. Update the graph-trim effect and the “Clean
layout to Mermaid” action so they do not call setNodePositions with an
unconditional replace of the full map; instead scope the sync to only the
affected node keys or local drag-owned positions, following the merge-style
pattern used by handleFlowNodeDragStop. Keep the local persistedNodePositionsRef
update, but ensure onNodePositionsChange only receives the minimal delta rather
than syncPositions ?? next for these call sites.

Comment thread apps/web/src/components/diagram-canvas.tsx
@donovan-yohan

Copy link
Copy Markdown
Owner Author

Fugu exact-head review — changes requested

Verdict: Block / changes requested for exact head 515ed9414faaf3db7b28a7e23d21ed6538f5344c.

The Yjs node-position fix is directionally correct: drag/add writes now call merge paths (diagram-canvas.tsx:982-986, session-workspace.tsx:666-682), and writeNodePositions(..., 'replace') is reserved for explicit trim/reset-style paths (diagram-layout.ts:29-44). The new layout tests cover merge vs replace semantics.

Blocking finding:

  1. apps/web/src/components/diagram-canvas.tsx:368-381, apps/web/src/components/diagram-canvas.tsx:1270-1282, apps/web/src/components/diagram-canvas.tsx:2049-2055 — React Flow edge ids still become stale/wrong when flowEdges filters links before assigning edge-${index}.
    • The code does graph.links.filter(...).map((link, index) => ...), so index is the filtered-list index, not the original graph.links index.
    • Edge click/double-click later decodes that filtered id with getFlowEdgeIndex(edge.id) and reads graph.links[edgeIndex].
    • Impact: if any earlier link is filtered because either endpoint is temporarily missing from interactiveNodeBounds, clicking/editing/deleting a later visible edge can build an identity for a different canonical Mermaid link. The mutation queue will then faithfully validate and mutate that wrong link, so the new identity validation does not save this path. This is the same class of index-addressed edge corruption the patch was supposed to close.
    • Unblock: preserve the canonical link index before filtering, e.g. graph.links.map((link, graphIndex) => ({ link, graphIndex })).filter(...).map(({ link, graphIndex }) => ({ id: getFlowEdgeId(graphIndex), data: { index: graphIndex }, selected: selectedEdgeIndex === graphIndex, ... })), or stop deriving identity from the React Flow id and carry DiagramEdgeIdentity directly in edge data.

Non-blocking notes:

  • apps/web/src/lib/diagram-mutations.ts:93-132 and :229-248 now validate source/target/type/stroke/length/label and reject ambiguous or stale identities. Good.
  • apps/web/src/lib/diagram-mutations.test.ts:15-46 covers concurrent insertion before the selected edge and stale replacement rejection. It does not cover the filtered React Flow index shift above.
  • apps/web/src/lib/diagram-layout.test.ts:5-33 meaningfully covers merge-vs-replace Yjs layout semantics.

Evidence reviewed:

  • Exact PR head: 515ed9414faaf3db7b28a7e23d21ed6538f5344c (feat/reactflow-mermaid-renderer).
  • Files inspected: diagram-canvas.tsx, session-workspace.tsx, diagram-mutations.ts, diagram-layout.ts, diagram-layout.test.ts, diagram-mutations.test.ts, globals.css, package manifests.
  • Local checks: bun x pnpm@10.15.0 install --frozen-lockfile passed with Node engine warning (node >=24 wanted, host v22.22.3) and ignored-build-script warning; bun x pnpm@10.15.0 --filter @arielcharts/web test passed (3 files, 7 tests); bun x pnpm@10.15.0 --filter @arielcharts/web typecheck passed.
  • GitHub checks at review: verify success, Vercel success, Vercel Preview Comments success, CodeRabbit pending, merge state UNSTABLE.

Bot feedback + learning:

  • Bot surfaces checked: review summaries, inline PR comments, issue comments.
  • Latest substantive bot findings triaged: Gemini new-node-label reset is already fixed at diagram-canvas.tsx:1047-1050; CodeRabbit cylinder styling is already fixed in globals.css:752-760; Copilot initial roving-tabindex concern is mitigated by the orderedNodeIds focus initialization at diagram-canvas.tsx:685-694. Older whole-map Yjs and stale index mutation findings are addressed except for the filtered-edge-index path above.
  • Substantive bot findings: 0 new blocker / 0 follow-up / 3 already fixed or false positive / status comments noise.
  • Learning retained: yes — filtered/reordered UI collections must preserve canonical identity/index before edit/delete mutation handoff.

@donovan-yohan

Copy link
Copy Markdown
Owner Author

Fugu exact-head review — approved

Verdict: Approve, scoped only to exact head dd9fa921b275bebfc1d2de3129853d251997292c.

Findings:

  • No blocking findings in the filtered React Flow edge identity fix.
  • apps/web/src/components/diagram-canvas.tsx:369-377 now maps graph.links to { graphIndex, link } before endpoint filtering, so React Flow edge id/data.index/selected state stay tied to the canonical Mermaid link index instead of the filtered array position.
  • apps/web/src/components/diagram-canvas.tsx:1270-1288 now resolves click/double-click selection through getDiagramEdgeIdentityForFlowEdge(graph.links, edge.id), so edit/delete receives the canonical DiagramEdgeIdentity for the Mermaid link represented by the visible edge.
  • apps/web/src/lib/diagram-flow-identity.test.ts:18-63 covers a three-link graph with the first edge filtered by missing bounds, then selects/edits/deletes the later visible B -> C edge by canonical identity. That is the regression that previously mutated the wrong link.

Verification run on local Node v22.22.3 (repo warns it wants >=24):

  • bun x pnpm@10.15.0 install --frozen-lockfile — pass, with expected engine warning and ignored-build-scripts warning.
  • bun x pnpm@10.15.0 --filter @arielcharts/web test -- apps/web/src/lib/diagram-flow-identity.test.ts — pass, 4 files / 9 tests.
  • bun x pnpm@10.15.0 --filter @arielcharts/web typecheck — pass.
  • git diff --check 515ed9414faaf3db7b28a7e23d21ed6538f5344c..HEAD -- apps/web/src/components/diagram-canvas.tsx apps/web/src/lib/diagram-flow-identity.ts apps/web/src/lib/diagram-flow-identity.test.ts — pass.
  • GitHub status at review: head dd9fa921b275bebfc1d2de3129853d251997292c, mergeStateStatus CLEAN; verify, CodeRabbit, Vercel, and Vercel Preview Comments successful.

Bot feedback + learning:

  • Bot surfaces checked: review summaries, inline PR comments, issue comments.
  • Substantive bot findings relevant to this fresh head: CodeRabbit's critical filtered-edge-index finding is fixed by the new canonical graphIndex flow. Older Gemini/Copilot/CodeRabbit comments are either already fixed in prior heads or unrelated to this focused fresh-head gate. Vercel/rate-limit/status boilerplate is noise.
  • Learning retained: no — no new durable review class beyond the existing edge identity/index-drift checklist.

This branch was successfully deployed

1 active deployment
Preview — 782fb846 Deployed Aug 8, 2026 by vercel[bot]
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.

2 participants