@tldiagram/core-ui 1.95.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/api/client.d.ts +184 -3
  2. package/dist/components/ConnectorPanel.d.ts +5 -1
  3. package/dist/components/CrossBranchControls.d.ts +4 -3
  4. package/dist/components/ElementNode.d.ts +5 -0
  5. package/dist/components/ElementPanel.d.ts +6 -1
  6. package/dist/components/LayoutSection.d.ts +2 -1
  7. package/dist/components/MergeDialog.d.ts +16 -0
  8. package/dist/components/NodeContainer.d.ts +2 -0
  9. package/dist/components/ProxyConnectorPanel.d.ts +4 -1
  10. package/dist/components/ViewExplorer/index.d.ts +1 -1
  11. package/dist/components/ViewFloatingMenu-vscode.d.ts +5 -0
  12. package/dist/components/ViewFloatingMenu.d.ts +8 -1
  13. package/dist/components/ViewGridNode.d.ts +3 -0
  14. package/dist/components/ViewPanel.d.ts +2 -1
  15. package/dist/components/WorkspacePanel.d.ts +2 -0
  16. package/dist/components/ZUI/ZUICanvas.d.ts +4 -0
  17. package/dist/components/ZUI/focus.d.ts +32 -0
  18. package/dist/components/ZUI/focus.test.d.ts +1 -0
  19. package/dist/components/ZUI/layout.d.ts +2 -2
  20. package/dist/components/ZUI/proxy.d.ts +20 -4
  21. package/dist/components/ZUI/renderer.d.ts +35 -1
  22. package/dist/components/ZUI/types.d.ts +6 -0
  23. package/dist/components/ZUI/useZUIInteraction.d.ts +1 -0
  24. package/dist/context/WorkspaceVersionContext.d.ts +49 -0
  25. package/dist/crossBranch/resolve.d.ts +39 -2
  26. package/dist/crossBranch/resolve.test.d.ts +1 -0
  27. package/dist/crossBranch/settings.d.ts +6 -1
  28. package/dist/crossBranch/types.d.ts +8 -0
  29. package/dist/hooks/useElementSearch.d.ts +8 -0
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +16529 -14030
  32. package/dist/pages/InfiniteZoom.d.ts +1 -0
  33. package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +6 -1
  34. package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +2 -0
  35. package/dist/pages/ViewEditor/hooks/useViewData.d.ts +4 -2
  36. package/dist/pages/ViewEditor/hooks/useViewEditHistory.d.ts +13 -0
  37. package/dist/pages/viewsJumpSearch.d.ts +22 -0
  38. package/dist/pages/viewsJumpSearch.test.d.ts +1 -0
  39. package/dist/store/useStore.d.ts +3 -0
  40. package/dist/types/index.d.ts +9 -0
  41. package/dist/utils/elementIcon.d.ts +2 -0
  42. package/dist/utils/elementIcon.test.d.ts +1 -0
  43. package/dist/utils/sourceEditor.d.ts +7 -0
  44. package/dist/utils/watchDiffSummary.d.ts +34 -0
  45. package/package.json +2 -2
  46. package/src/App.tsx +12 -8
  47. package/src/api/client.ts +488 -26
  48. package/src/components/CodePreviewPanel.tsx +90 -16
  49. package/src/components/ConnectorPanel.tsx +34 -3
  50. package/src/components/ContextNeighborElement.tsx +2 -5
  51. package/src/components/CrossBranchControls.tsx +46 -17
  52. package/src/components/ElementNode.tsx +98 -47
  53. package/src/components/ElementPanel.tsx +62 -25
  54. package/src/components/InlineElementAdder.tsx +8 -3
  55. package/src/components/LayoutSection.tsx +4 -1
  56. package/src/components/MergeDialog.tsx +269 -0
  57. package/src/components/NodeContainer.tsx +55 -17
  58. package/src/components/ProxyConnectorPanel.tsx +58 -16
  59. package/src/components/ViewBezierConnector.tsx +116 -21
  60. package/src/components/ViewExplorer/index.tsx +1 -1
  61. package/src/components/ViewFloatingMenu-vscode.tsx +5 -0
  62. package/src/components/ViewFloatingMenu.tsx +110 -1
  63. package/src/components/ViewGridNode.tsx +59 -8
  64. package/src/components/ViewPanel.tsx +3 -2
  65. package/src/components/WorkspacePanel.tsx +938 -0
  66. package/src/components/ZUI/ZUICanvas.tsx +216 -122
  67. package/src/components/ZUI/focus.test.ts +534 -0
  68. package/src/components/ZUI/focus.ts +293 -0
  69. package/src/components/ZUI/layout.ts +7 -11
  70. package/src/components/ZUI/proxy.ts +470 -114
  71. package/src/components/ZUI/renderer.ts +510 -134
  72. package/src/components/ZUI/types.ts +6 -0
  73. package/src/components/ZUI/useZUIInteraction.ts +66 -29
  74. package/src/context/WorkspaceVersionContext.tsx +126 -0
  75. package/src/crossBranch/resolve.test.ts +342 -0
  76. package/src/crossBranch/resolve.ts +368 -68
  77. package/src/crossBranch/settings.ts +49 -3
  78. package/src/crossBranch/types.ts +9 -0
  79. package/src/hooks/useElementSearch.ts +45 -0
  80. package/src/index.css +11 -0
  81. package/src/index.ts +7 -0
  82. package/src/pages/AppearanceSettings.tsx +24 -1
  83. package/src/pages/Dependencies.tsx +231 -65
  84. package/src/pages/InfiniteZoom.tsx +41 -19
  85. package/src/pages/Settings.tsx +1 -1
  86. package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +103 -24
  87. package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +102 -6
  88. package/src/pages/ViewEditor/hooks/useViewData.ts +42 -26
  89. package/src/pages/ViewEditor/hooks/useViewEditHistory.ts +62 -0
  90. package/src/pages/ViewEditor/index.tsx +549 -59
  91. package/src/pages/Views.tsx +112 -41
  92. package/src/pages/ViewsGrid.tsx +332 -113
  93. package/src/pages/viewsJumpSearch.test.ts +193 -0
  94. package/src/pages/viewsJumpSearch.ts +111 -0
  95. package/src/store/useStore.ts +58 -0
  96. package/src/types/index.ts +10 -0
  97. package/src/utils/elementIcon.test.ts +28 -0
  98. package/src/utils/elementIcon.ts +20 -0
  99. package/src/utils/sourceEditor.ts +46 -0
  100. package/src/utils/watchDiffSummary.ts +159 -0
@@ -90,6 +90,18 @@ function findNearestHandleTargetInCache(targets: HandleTarget[], clientX: number
90
90
  }
91
91
  }
92
92
 
93
+ function flattenViewTree(nodes: ViewTreeNode[]): ViewTreeNode[] {
94
+ const out: ViewTreeNode[] = []
95
+ const walk = (items: ViewTreeNode[]) => {
96
+ items.forEach((item) => {
97
+ out.push(item)
98
+ walk(item.children ?? [])
99
+ })
100
+ }
101
+ walk(nodes)
102
+ return out
103
+ }
104
+
93
105
  export function applyNodeChangesWithStructuralSharing(changes: NodeChange[], nodes: RFNode[]) {
94
106
  if (changes.length === 0) return nodes
95
107
 
@@ -181,6 +193,11 @@ interface CanvasInteractionOptions {
181
193
  handleElementDeleted: (id: number) => void
182
194
  handleElementPermanentlyDeleted: (id: number) => void
183
195
  handleConnectorDeleted: (id: number) => void
196
+ onPlacementMoved?: (before: PlacedElement, after: PlacedElement) => void
197
+ onPlacementRemoved?: (placement: PlacedElement) => void
198
+ onConnectorUpdated?: (before: Connector, after: Connector) => void
199
+ onConnectorDeleted?: (connector: Connector) => void
200
+ onUnsupportedMutation?: () => void
184
201
  handleUpdateTags: (elementId: number, tags: string[]) => Promise<void>
185
202
  drawingCanvasRef: React.MutableRefObject<DrawingCanvasHandle | null>
186
203
  snapToGrid?: boolean
@@ -259,6 +276,11 @@ export function useCanvasInteractions({
259
276
  handleElementDeleted,
260
277
  handleElementPermanentlyDeleted,
261
278
  handleConnectorDeleted,
279
+ onPlacementMoved,
280
+ onPlacementRemoved,
281
+ onConnectorUpdated,
282
+ onConnectorDeleted,
283
+ onUnsupportedMutation,
262
284
  handleUpdateTags,
263
285
  drawingCanvasRef,
264
286
  snapToGrid,
@@ -327,6 +349,13 @@ export function useCanvasInteractions({
327
349
  handleReconnectListenersRef.current = null
328
350
  }, [])
329
351
 
352
+ const clearConnectGhostListener = useCallback(() => {
353
+ const listener = connectGhostListenerRef.current
354
+ if (!listener) return
355
+ document.removeEventListener('mousemove', listener)
356
+ connectGhostListenerRef.current = null
357
+ }, [])
358
+
330
359
  const stopHandleReconnectDrag = useCallback(() => {
331
360
  clearHandleReconnectListeners()
332
361
  handleReconnectDragRef.current = null
@@ -396,6 +425,7 @@ export function useCanvasInteractions({
396
425
  try {
397
426
  const obj = await api.elements.create({ name, kind: '' })
398
427
  await api.workspace.views.placements.add(viewId, obj.id, flowX - 100, flowY - 40)
428
+ onUnsupportedMutation?.()
399
429
  await refreshElements()
400
430
  const placed = viewElementsRef.current.find((element) => element.element_id === obj.id)
401
431
  if (placed) upsertPlacementGraphSnapshot(viewId, placed)
@@ -410,9 +440,10 @@ export function useCanvasInteractions({
410
440
  })
411
441
  const connector = connectorToConnector(newConnector)
412
442
  await finalizeConnectorCreate(connector)
443
+ onUnsupportedMutation?.()
413
444
  }
414
445
  } catch { /* intentionally empty */ }
415
- }, [addingElementAt, canEdit, finalizeConnectorCreate, refreshElements, rfNodesRef, viewId, viewElementsRef])
446
+ }, [addingElementAt, canEdit, finalizeConnectorCreate, onUnsupportedMutation, refreshElements, rfNodesRef, viewId, viewElementsRef])
416
447
 
417
448
  const handleConfirmExistingElement = useCallback(async (obj: LibraryElement) => {
418
449
  if (!canEdit || viewId === null || !addingElementAt || addingElementAt.mode !== 'add') return
@@ -425,6 +456,7 @@ export function useCanvasInteractions({
425
456
  try {
426
457
  if (!existingElementIds.has(obj.id)) {
427
458
  await api.workspace.views.placements.add(viewId, obj.id, flowX - 100, flowY - 40)
459
+ onUnsupportedMutation?.()
428
460
  await refreshElements()
429
461
  const placed = viewElementsRef.current.find((element) => element.element_id === obj.id)
430
462
  if (placed) upsertPlacementGraphSnapshot(viewId, placed)
@@ -443,9 +475,10 @@ export function useCanvasInteractions({
443
475
  })
444
476
  const connector = connectorToConnector(newConnector)
445
477
  await finalizeConnectorCreate(connector)
478
+ onUnsupportedMutation?.()
446
479
  }
447
480
  } catch { /* intentionally empty */ }
448
- }, [addingElementAt, canEdit, existingElementIds, finalizeConnectorCreate, refreshElements, rfNodesRef, viewId, viewElementsRef])
481
+ }, [addingElementAt, canEdit, existingElementIds, finalizeConnectorCreate, onUnsupportedMutation, refreshElements, rfNodesRef, viewId, viewElementsRef])
449
482
 
450
483
  const handleConfirmConnectExistingElement = useCallback(async (obj: LibraryElement) => {
451
484
  if (!canEdit || viewId === null || !addingElementAt || addingElementAt.mode !== 'connect') return
@@ -470,13 +503,21 @@ export function useCanvasInteractions({
470
503
  })
471
504
  const connector = connectorToConnector(newConnector)
472
505
  await finalizeConnectorCreate(connector)
506
+ onUnsupportedMutation?.()
473
507
  } catch { /* intentionally empty */ }
474
- }, [addingElementAt, canEdit, finalizeConnectorCreate, rfNodesRef, viewId])
508
+ }, [addingElementAt, canEdit, finalizeConnectorCreate, onUnsupportedMutation, rfNodesRef, viewId])
475
509
 
476
510
  // ── Zoom-in / zoom-out stable callbacks ───────────────────────────────────
477
511
  const stableOnZoomIn = useCallback(async (elementId: number) => {
478
512
  const childLinks = linksMapRef.current[elementId] || []
479
- if (childLinks.length > 0) { navigateRef.current(`/views/${childLinks[0].to_view_id}`); return }
513
+ if (childLinks.length > 0) {
514
+ setSelectedElement(null)
515
+ setSelectedEdge(null)
516
+ closeElementPanel()
517
+ closeConnectorPanel()
518
+ navigateRef.current(`/views/${childLinks[0].to_view_id}`)
519
+ return
520
+ }
480
521
 
481
522
  const obj = viewElementsRef.current.find((o) => o.element_id === elementId)
482
523
  if (obj?.has_view) {
@@ -491,6 +532,10 @@ export function useCanvasInteractions({
491
532
  }
492
533
  const existingView = findInTree(treeDataRef.current)
493
534
  if (existingView) {
535
+ setSelectedElement(null)
536
+ setSelectedEdge(null)
537
+ closeElementPanel()
538
+ closeConnectorPanel()
494
539
  navigateRef.current(`/views/${existingView.id}`)
495
540
  return
496
541
  }
@@ -506,9 +551,13 @@ export function useCanvasInteractions({
506
551
  [elementId]: [...(prev[elementId] || []),
507
552
  { id: 0, element_id: elementId, from_view_id: cid, to_view_id: newView.id, to_view_name: newView.name, relation_type: 'child' as const }],
508
553
  }))
554
+ setSelectedElement(null)
555
+ setSelectedEdge(null)
556
+ closeElementPanel()
557
+ closeConnectorPanel()
509
558
  navigateRef.current(`/views/${newView.id}`)
510
559
  } catch { /* intentionally empty */ }
511
- }, [canEdit, linksMapRef, viewIdRef, viewElementsRef, navigateRef, setLinksMap, treeDataRef])
560
+ }, [canEdit, linksMapRef, viewIdRef, viewElementsRef, navigateRef, setLinksMap, treeDataRef, setSelectedElement, setSelectedEdge, closeElementPanel, closeConnectorPanel])
512
561
 
513
562
  const stableOnZoomOut = useCallback(async (elementId: number) => {
514
563
  const parentLinks = parentLinksMapRef.current[elementId] || []
@@ -517,7 +566,14 @@ export function useCanvasInteractions({
517
566
  // from the clicked element's ID for elements like functions/classes that
518
567
  // don't own a view themselves).
519
568
  const anyParentLink = parentLinks[0] ?? Object.values(parentLinksMapRef.current).flat()[0]
520
- if (anyParentLink) { navigateRef.current(`/views/${anyParentLink.from_view_id}`); return }
569
+ if (anyParentLink) {
570
+ setSelectedElement(null)
571
+ setSelectedEdge(null)
572
+ closeElementPanel()
573
+ closeConnectorPanel()
574
+ navigateRef.current(`/views/${anyParentLink.from_view_id}`)
575
+ return
576
+ }
521
577
 
522
578
  // Final fallback: use current view's parent_view_id if available
523
579
  const findInTreeById = (nodes: ViewTreeNode[], id: number): ViewTreeNode | null => {
@@ -530,13 +586,21 @@ export function useCanvasInteractions({
530
586
  }
531
587
  const currentView = findInTreeById(treeDataRef.current, viewIdRef.current || -1)
532
588
  if (currentView?.parent_view_id) {
589
+ setSelectedElement(null)
590
+ setSelectedEdge(null)
591
+ closeElementPanel()
592
+ closeConnectorPanel()
533
593
  navigateRef.current(`/views/${currentView.parent_view_id}`)
534
594
  }
535
- }, [parentLinksMapRef, navigateRef, treeDataRef, viewIdRef])
595
+ }, [parentLinksMapRef, navigateRef, treeDataRef, viewIdRef, setSelectedElement, setSelectedEdge, closeElementPanel, closeConnectorPanel])
536
596
 
537
597
  const stableOnNavigateToView = useCallback((id: number) => {
598
+ setSelectedElement(null)
599
+ setSelectedEdge(null)
600
+ closeElementPanel()
601
+ closeConnectorPanel()
538
602
  navigateRef.current(`/views/${id}`)
539
- }, [navigateRef])
603
+ }, [navigateRef, setSelectedElement, setSelectedEdge, closeElementPanel, closeConnectorPanel])
540
604
 
541
605
  const stableOnHoverZoom = useCallback((elementId: number, type: 'in' | 'out' | null) => {
542
606
  const prev = hoveredZoomRef.current
@@ -555,15 +619,17 @@ export function useCanvasInteractions({
555
619
 
556
620
  const stableOnRemoveElement = useCallback(async (elementId: number) => {
557
621
  if (!canEdit || viewId === null) return
622
+ const removedPlacement = viewElementsRef.current.find((element) => element.element_id === elementId) ?? null
558
623
  try {
559
624
  await api.workspace.views.placements.remove(viewId, elementId)
560
625
  removePlacementGraphSnapshot(viewId, elementId)
561
626
  removeElementPlacement(elementId)
627
+ if (removedPlacement) onPlacementRemoved?.(removedPlacement)
562
628
  handleElementDeleted(elementId)
563
629
  setInteractionSourceId(null)
564
630
  pendingConnectionSourceHandleRef.current = null
565
631
  } catch { /* intentionally empty */ }
566
- }, [canEdit, viewId, removeElementPlacement, handleElementDeleted])
632
+ }, [canEdit, viewId, viewElementsRef, removeElementPlacement, onPlacementRemoved, handleElementDeleted])
567
633
 
568
634
  const connectClickModeToHandle = useCallback(async (targetElementId: number, targetHandle: string) => {
569
635
  if (!canEdit) return
@@ -594,8 +660,9 @@ export function useCanvasInteractions({
594
660
  })
595
661
  const connector = connectorToConnector(newConnector)
596
662
  await finalizeConnectorCreate(connector)
663
+ onUnsupportedMutation?.()
597
664
  } catch { /* intentionally empty */ }
598
- }, [canEdit, finalizeConnectorCreate, interactionSourceIdRef, viewIdRef])
665
+ }, [canEdit, finalizeConnectorCreate, interactionSourceIdRef, onUnsupportedMutation, viewIdRef])
599
666
 
600
667
  const stableOnInteractionStart = useCallback((elementId: number, options?: InteractionStartOptions) => {
601
668
  if (!canEdit) return
@@ -684,15 +751,20 @@ export function useCanvasInteractions({
684
751
  return
685
752
  }
686
753
 
754
+ const beforePlacement = currentObj ? { ...currentObj, position_x: startPos?.x ?? currentObj.position_x, position_y: startPos?.y ?? currentObj.position_y } : null
755
+ const afterPlacement = currentObj ? { ...currentObj, position_x: node.position.x, position_y: node.position.y } : null
687
756
  updateElementPosition(elementId, node.position.x, node.position.y)
688
757
  clearTimeout(positionTimers.current[node.id])
689
758
  positionTimers.current[node.id] = setTimeout(() => {
690
759
  api.workspace.views.placements
691
760
  .updatePosition(viewId, elementId, node.position.x, node.position.y)
761
+ .then(() => {
762
+ if (beforePlacement && afterPlacement) onPlacementMoved?.(beforePlacement, afterPlacement)
763
+ })
692
764
  .catch(() => { /* intentionally empty */ })
693
765
  }, 400)
694
766
  delete dragStartPositionsRef.current[node.id]
695
- }, [canEdit, updateElementPosition, viewId, viewElementsRef])
767
+ }, [canEdit, updateElementPosition, viewId, viewElementsRef, onPlacementMoved])
696
768
 
697
769
  // ── Connections ────────────────────────────────────────────────────────────
698
770
  const onConnect: OnConnect = useCallback(async (params: Connection) => {
@@ -712,11 +784,13 @@ export function useCanvasInteractions({
712
784
  })
713
785
  const connector = connectorToConnector(newConnector)
714
786
  await finalizeConnectorCreate(connector)
787
+ onUnsupportedMutation?.()
715
788
  } catch { /* intentionally empty */ }
716
- }, [canEdit, finalizeConnectorCreate, viewId])
789
+ }, [canEdit, finalizeConnectorCreate, onUnsupportedMutation, viewId])
717
790
 
718
791
  const onConnectStart = useCallback((_: React.MouseEvent | React.TouchEvent, { nodeId }: OnConnectStartParams) => {
719
792
  if (!canEdit || isReconnectingRef.current) return
793
+ clearConnectGhostListener()
720
794
  connectingSourceRef.current = nodeId
721
795
  connectWasValidRef.current = false
722
796
  const handleTargets = collectHandleTargets(nodeId ?? undefined)
@@ -730,13 +804,10 @@ export function useCanvasInteractions({
730
804
  }
731
805
  connectGhostListenerRef.current = listener
732
806
  document.addEventListener('mousemove', listener)
733
- }, [canEdit])
807
+ }, [canEdit, clearConnectGhostListener])
734
808
 
735
809
  const onConnectEnd = useCallback((event: MouseEvent | TouchEvent) => {
736
- if (connectGhostListenerRef.current) {
737
- document.removeEventListener('mousemove', connectGhostListenerRef.current)
738
- connectGhostListenerRef.current = null
739
- }
810
+ clearConnectGhostListener()
740
811
  setConnectGhostPos(null)
741
812
  if (!canEdit || isReconnectingRef.current) return
742
813
  const sourceId = connectingSourceRef.current
@@ -774,13 +845,14 @@ export function useCanvasInteractions({
774
845
  }).then((connector) => {
775
846
  const next = connectorToConnector(connector)
776
847
  void finalizeConnectorCreate(next)
848
+ onUnsupportedMutation?.()
777
849
  }).catch(() => { /* intentionally empty */ })
778
850
  } else {
779
851
  setPendingConnectionSource(sourceElementId)
780
852
  suppressNextPaneClickRef.current = true
781
853
  showAddingElementAt(clientX, clientY, true, 'connect', 'shiftKey' in event && event.shiftKey)
782
854
  }
783
- }, [canEdit, finalizeConnectorCreate, showAddingElementAt, rfNodesRef, viewIdRef])
855
+ }, [canEdit, clearConnectGhostListener, finalizeConnectorCreate, onUnsupportedMutation, showAddingElementAt, rfNodesRef, viewIdRef])
784
856
 
785
857
  // ── Reconnect ──────────────────────────────────────────────────────────────
786
858
  const performReconnect = useCallback(async (oldConnector: RFEdge, newConnection: Connection) => {
@@ -806,8 +878,9 @@ export function useCanvasInteractions({
806
878
  const connector = connectorToConnector(updated)
807
879
  upsertConnectorGraphSnapshot(connector)
808
880
  replaceConnector(connector)
881
+ if (existingData) onConnectorUpdated?.(existingData, connector)
809
882
  } catch { /* intentionally empty */ }
810
- }, [canEdit, replaceConnector, viewId, setRfEdges])
883
+ }, [canEdit, replaceConnector, viewId, setRfEdges, onConnectorUpdated])
811
884
  const onReconnect = useCallback(async (oldConnector: RFEdge, newConnection: Connection) => {
812
885
  await performReconnect(oldConnector, newConnection)
813
886
  }, [performReconnect])
@@ -978,8 +1051,9 @@ export function useCanvasInteractions({
978
1051
  }, [interactionSourceId])
979
1052
 
980
1053
  useEffect(() => () => {
1054
+ clearConnectGhostListener()
981
1055
  stopHandleReconnectDrag()
982
- }, [stopHandleReconnectDrag])
1056
+ }, [clearConnectGhostListener, stopHandleReconnectDrag])
983
1057
 
984
1058
  // ── Connector interactions ─────────────────────────────────────────────────────
985
1059
  const onEdgeContextMenu = useCallback((e: React.MouseEvent, rfConnector: RFEdge) => {
@@ -1012,7 +1086,7 @@ export function useCanvasInteractions({
1012
1086
  setSelectedProxyConnectorDetails(null)
1013
1087
  setSelectedEdge(connector)
1014
1088
  openConnectorPanelRef.current()
1015
- }, [closeElementPanel, connectors, openProxyConnectorPanel, setSelectedEdge, setSelectedElement, setSelectedProxyConnectorDetails])
1089
+ }, [closeConnectorPanel, closeElementPanel, connectors, openProxyConnectorPanel, setSelectedEdge, setSelectedElement, setSelectedProxyConnectorDetails])
1016
1090
 
1017
1091
  // ── Pane interactions ─────────────────────────────────────────────────────
1018
1092
  const onPaneClick = useCallback((e: React.MouseEvent) => {
@@ -1221,7 +1295,11 @@ export function useCanvasInteractions({
1221
1295
  } else {
1222
1296
  const connectorId = getConnectorDeletionTarget(selectedConnector)
1223
1297
  if (connectorId === null) return
1298
+ const deletedConnector = selectedConnector?.id === connectorId
1299
+ ? selectedConnector
1300
+ : connectors.find((connector) => connector.id === connectorId) ?? null
1224
1301
  api.workspace.connectors.delete('', connectorId).then(() => {
1302
+ if (deletedConnector) onConnectorDeleted?.(deletedConnector)
1225
1303
  handleConnectorDeleted(connectorId)
1226
1304
  setSelectedEdge(null)
1227
1305
  closeConnectorPanel()
@@ -1265,7 +1343,7 @@ export function useCanvasInteractions({
1265
1343
  const cid = viewIdRef.current
1266
1344
  if (!cid) return
1267
1345
  const incoming = incomingLinksRef.current
1268
- const tree = treeDataRef.current
1346
+ const tree = flattenViewTree(treeDataRef.current)
1269
1347
  const nav = navigateRef.current
1270
1348
  const links = linksMapRef.current
1271
1349
  const treeNode = tree.find((n) => n.id === cid)
@@ -1332,7 +1410,7 @@ export function useCanvasInteractions({
1332
1410
  }
1333
1411
  window.addEventListener('keydown', handler)
1334
1412
  return () => window.removeEventListener('keydown', handler)
1335
- }, [canEdit, refreshGrid, selectedElement, selectedConnector, viewId, stableOnRemoveElement, handleConnectorDeleted, handleElementPermanentlyDeleted, closeElementPanel, closeConnectorPanel, viewIdRef, incomingLinksRef, treeDataRef, navigateRef, rfNodesRef, viewElementsRef, setLinksMap, showAddingElementAt, setSelectedElement, setSelectedEdge, containerRef, linksMapRef])
1413
+ }, [canEdit, refreshGrid, selectedElement, selectedConnector, connectors, viewId, stableOnRemoveElement, handleConnectorDeleted, handleElementPermanentlyDeleted, onConnectorDeleted, closeElementPanel, closeConnectorPanel, viewIdRef, incomingLinksRef, treeDataRef, navigateRef, rfNodesRef, viewElementsRef, setLinksMap, showAddingElementAt, setSelectedElement, setSelectedEdge, containerRef, linksMapRef])
1336
1414
 
1337
1415
  // ── DnD handlers ──────────────────────────────────────────────────────────
1338
1416
  const onDragOver = useCallback((e: React.DragEvent) => {
@@ -1351,6 +1429,7 @@ export function useCanvasInteractions({
1351
1429
  const pos = screenToFlowPositionRef.current({ x: e.clientX, y: e.clientY })
1352
1430
  try {
1353
1431
  await api.workspace.views.placements.add(viewId, obj.id, pos.x - 100, pos.y - 40)
1432
+ onUnsupportedMutation?.()
1354
1433
  await refreshElements()
1355
1434
  const placed = viewElementsRef.current.find((element) => element.element_id === obj.id)
1356
1435
  if (placed) upsertPlacementGraphSnapshot(viewId, placed)
@@ -1402,7 +1481,7 @@ export function useCanvasInteractions({
1402
1481
  }
1403
1482
  }
1404
1483
  }
1405
- }, [canEdit, viewId, existingElementIds, refreshElements, rfNodesRef, viewElementsRef, layers, handleUpdateTags])
1484
+ }, [canEdit, viewId, existingElementIds, onUnsupportedMutation, refreshElements, rfNodesRef, viewElementsRef, layers, handleUpdateTags])
1406
1485
 
1407
1486
  const onWheelCapture = useCallback((e: React.WheelEvent) => {
1408
1487
  if (touchStateRef.current.touches.size === 2) return
@@ -1,6 +1,6 @@
1
1
  import { useMemo } from 'react'
2
2
  import { type Edge as RFEdge, type Node as RFNode } from 'reactflow'
3
- import type { PlacedElement } from '../../../types'
3
+ import type { Connector, PlacedElement } from '../../../types'
4
4
  import type { CrossBranchContextSettings, ProxyConnectorDetails, WorkspaceGraphSnapshot } from '../../../crossBranch/types'
5
5
  import { resolveViewProxyGraph } from '../../../crossBranch/resolve'
6
6
 
@@ -69,6 +69,61 @@ function isAncestorContextNode(
69
69
  (snapshot.descendantsByViewId[ownedViewId]?.includes(descendant.placementViewId) ?? false)
70
70
  }
71
71
 
72
+ function canonicalElementPairKey(leftId: number, rightId: number) {
73
+ return leftId <= rightId ? `${leftId}::${rightId}` : `${rightId}::${leftId}`
74
+ }
75
+
76
+ function canonicalNodePairKey(leftId: string, rightId: string) {
77
+ return leftId <= rightId ? `${leftId}::${rightId}` : `${rightId}::${leftId}`
78
+ }
79
+
80
+ function buildDirectConnectorPairSet(connectors: Connector[], visibleElementIds: Set<number>) {
81
+ const pairs = new Set<string>()
82
+ for (const connector of connectors) {
83
+ if (!visibleElementIds.has(connector.source_element_id) || !visibleElementIds.has(connector.target_element_id)) continue
84
+ pairs.add(canonicalElementPairKey(connector.source_element_id, connector.target_element_id))
85
+ }
86
+ return pairs
87
+ }
88
+
89
+ function mergeHiddenProxyDetails(
90
+ existing: ProxyConnectorDetails | undefined,
91
+ next: ProxyConnectorDetails,
92
+ ): ProxyConnectorDetails {
93
+ if (!existing) {
94
+ return {
95
+ ...next,
96
+ ownerViewIds: [...next.ownerViewIds],
97
+ ownerViewNames: [...next.ownerViewNames],
98
+ connectors: [...next.connectors],
99
+ }
100
+ }
101
+
102
+ const ownerViews = new Map<number, string>()
103
+ existing.ownerViewIds.forEach((ownerViewId, index) => {
104
+ ownerViews.set(ownerViewId, existing.ownerViewNames[index] ?? `View ${ownerViewId}`)
105
+ })
106
+ next.ownerViewIds.forEach((ownerViewId, index) => {
107
+ ownerViews.set(ownerViewId, next.ownerViewNames[index] ?? `View ${ownerViewId}`)
108
+ })
109
+
110
+ const connectors = [...existing.connectors, ...next.connectors]
111
+ const count = connectors.length
112
+
113
+ return {
114
+ key: existing.key,
115
+ label: count === 1 ? connectors[0]?.connector.label?.trim() || connectors[0]?.connector.relationship?.trim() || 'Cross-branch' : `${count} connectors`,
116
+ count,
117
+ sourceAnchorId: existing.sourceAnchorId,
118
+ targetAnchorId: existing.targetAnchorId,
119
+ sourceAnchorName: existing.sourceAnchorName,
120
+ targetAnchorName: existing.targetAnchorName,
121
+ ownerViewIds: Array.from(ownerViews.keys()),
122
+ ownerViewNames: Array.from(ownerViews.values()),
123
+ connectors,
124
+ }
125
+ }
126
+
72
127
  export function useViewContextNeighbours({
73
128
  snapshot,
74
129
  settings,
@@ -82,18 +137,38 @@ export function useViewContextNeighbours({
82
137
  }: Props) {
83
138
  return useMemo(() => {
84
139
  if (!snapshot || viewId == null || !settings.enabled) {
85
- return { contextNodes: [] as RFNode[], contextConnectors: [] as RFEdge[], proxyConnectorDetailsByKey: {} as Record<string, ProxyConnectorDetails> }
140
+ return {
141
+ contextNodes: [] as RFNode[],
142
+ contextConnectors: [] as RFEdge[],
143
+ proxyConnectorDetailsByKey: {} as Record<string, ProxyConnectorDetails>,
144
+ hiddenProxyCountsByPair: {} as Record<string, number>,
145
+ hiddenProxyDetailsByPair: {} as Record<string, ProxyConnectorDetails>,
146
+ }
86
147
  }
87
148
 
88
149
  const { proxyNodes, proxyConnectors, proxyConnectorDetailsByKey } = resolveViewProxyGraph(snapshot, viewId, viewElements, settings)
89
150
  if (proxyNodes.length === 0 && proxyConnectors.length === 0) {
90
- return { contextNodes: [] as RFNode[], contextConnectors: [] as RFEdge[], proxyConnectorDetailsByKey }
151
+ return {
152
+ contextNodes: [] as RFNode[],
153
+ contextConnectors: [] as RFEdge[],
154
+ proxyConnectorDetailsByKey,
155
+ hiddenProxyCountsByPair: {} as Record<string, number>,
156
+ hiddenProxyDetailsByPair: {} as Record<string, ProxyConnectorDetails>,
157
+ }
91
158
  }
92
159
 
93
160
  const mainNodes = rfNodes.filter((node) => node.type === 'elementNode')
94
161
  if (mainNodes.length === 0) {
95
- return { contextNodes: [] as RFNode[], contextConnectors: [] as RFEdge[], proxyConnectorDetailsByKey }
162
+ return {
163
+ contextNodes: [] as RFNode[],
164
+ contextConnectors: [] as RFEdge[],
165
+ proxyConnectorDetailsByKey,
166
+ hiddenProxyCountsByPair: {} as Record<string, number>,
167
+ hiddenProxyDetailsByPair: {} as Record<string, ProxyConnectorDetails>,
168
+ }
96
169
  }
170
+ const visibleElementIds = new Set(viewElements.map((element) => element.element_id))
171
+ const directConnectorPairs = buildDirectConnectorPairSet(snapshot.connectorsByViewId[viewId] ?? [], visibleElementIds)
97
172
 
98
173
  let minX = Infinity
99
174
  let minY = Infinity
@@ -460,6 +535,8 @@ export function useViewContextNeighbours({
460
535
  })
461
536
 
462
537
  const seenCollapsedPairs = new Set<string>()
538
+ const hiddenProxyCountsByPair: Record<string, number> = {}
539
+ const hiddenProxyDetailsByPair: Record<string, ProxyConnectorDetails> = {}
463
540
  const contextConnectors: RFEdge[] = proxyConnectors.flatMap((connector) => {
464
541
  let sourceId = connector.sourceAnchorId
465
542
  let targetId = connector.targetAnchorId
@@ -472,7 +549,20 @@ export function useViewContextNeighbours({
472
549
 
473
550
  if (sourceId === targetId) return []
474
551
 
475
- const pairKey = `${sourceId}::${targetId}`
552
+ const pairKey = canonicalNodePairKey(sourceId, targetId)
553
+ if (directConnectorPairs.has(pairKey)) {
554
+ hiddenProxyCountsByPair[pairKey] = (hiddenProxyCountsByPair[pairKey] ?? 0) + connector.details.count
555
+ hiddenProxyDetailsByPair[pairKey] = mergeHiddenProxyDetails(
556
+ hiddenProxyDetailsByPair[pairKey],
557
+ {
558
+ ...connector.details,
559
+ key: `hidden:${pairKey}`,
560
+ sourceAnchorId: sourceId,
561
+ targetAnchorId: targetId,
562
+ },
563
+ )
564
+ return []
565
+ }
476
566
  if (seenCollapsedPairs.has(pairKey)) return []
477
567
  seenCollapsedPairs.add(pairKey)
478
568
 
@@ -496,6 +586,12 @@ export function useViewContextNeighbours({
496
586
  }]
497
587
  })
498
588
 
499
- return { contextNodes: [ContextBoundaryElement, ...contextNodes], contextConnectors, proxyConnectorDetailsByKey }
589
+ return {
590
+ contextNodes: [ContextBoundaryElement, ...contextNodes],
591
+ contextConnectors,
592
+ proxyConnectorDetailsByKey,
593
+ hiddenProxyCountsByPair,
594
+ hiddenProxyDetailsByPair,
595
+ }
500
596
  }, [snapshot, settings, viewId, viewElements, rfNodes, stableOnNavigateToView, onSelectProxyDetails, expandedAncestorGroups, onToggleAncestorGroup])
501
597
  }