@tldiagram/core-ui 1.87.0

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 (272) hide show
  1. package/dist/App.d.ts +1 -0
  2. package/dist/api/client.d.ts +143 -0
  3. package/dist/api/transport-vscode.d.ts +8 -0
  4. package/dist/api/transport.d.ts +1 -0
  5. package/dist/components/CodePreviewPanel-vscode.d.ts +7 -0
  6. package/dist/components/CodePreviewPanel.d.ts +9 -0
  7. package/dist/components/ConfirmDialog.d.ts +12 -0
  8. package/dist/components/ConnectorPanel.d.ts +21 -0
  9. package/dist/components/ContextBoundaryElement.d.ts +11 -0
  10. package/dist/components/ContextNeighborElement.d.ts +29 -0
  11. package/dist/components/ContextStraightConnector.d.ts +4 -0
  12. package/dist/components/CrossBranchControls.d.ts +9 -0
  13. package/dist/components/DependenciesOnboarding.d.ts +5 -0
  14. package/dist/components/DrawingCanvas.d.ts +39 -0
  15. package/dist/components/ElementLibrary-vscode.d.ts +7 -0
  16. package/dist/components/ElementLibrary.d.ts +22 -0
  17. package/dist/components/ElementNode.d.ts +36 -0
  18. package/dist/components/ElementPanel.d.ts +25 -0
  19. package/dist/components/ExploreOnboarding.d.ts +5 -0
  20. package/dist/components/ExplorePageOnboarding.d.ts +5 -0
  21. package/dist/components/ExportModal.d.ts +16 -0
  22. package/dist/components/FloatingEdge.d.ts +9 -0
  23. package/dist/components/GitSourceLinker.d.ts +8 -0
  24. package/dist/components/HeaderContext.d.ts +16 -0
  25. package/dist/components/Icons.d.ts +95 -0
  26. package/dist/components/ImportModal.d.ts +10 -0
  27. package/dist/components/InlineElementAdder.d.ts +17 -0
  28. package/dist/components/LayoutSection.d.ts +7 -0
  29. package/dist/components/LocalSourceLinker.d.ts +8 -0
  30. package/dist/components/MiniZoomOnboarding.d.ts +5 -0
  31. package/dist/components/NavBreadcrumb.d.ts +6 -0
  32. package/dist/components/NodeBody.d.ts +12 -0
  33. package/dist/components/NodeContainer.d.ts +8 -0
  34. package/dist/components/NodeHoverCard.d.ts +10 -0
  35. package/dist/components/PanelHeader.d.ts +8 -0
  36. package/dist/components/PanelUI.d.ts +3 -0
  37. package/dist/components/ProxyConnectorEdge.d.ts +4 -0
  38. package/dist/components/ProxyConnectorPanel.d.ts +9 -0
  39. package/dist/components/SafeBackground.d.ts +13 -0
  40. package/dist/components/ScrollIndicatorWrapper.d.ts +8 -0
  41. package/dist/components/SetChildModal.d.ts +10 -0
  42. package/dist/components/SetParentModal.d.ts +10 -0
  43. package/dist/components/SlidingPanel.d.ts +16 -0
  44. package/dist/components/TagUpsert.d.ts +8 -0
  45. package/dist/components/TopMenuBar.d.ts +8 -0
  46. package/dist/components/ViewBezierConnector.d.ts +4 -0
  47. package/dist/components/ViewDrawMenu.d.ts +22 -0
  48. package/dist/components/ViewEditorEdgeLabelLayout.d.ts +16 -0
  49. package/dist/components/ViewEditorOnboarding.d.ts +5 -0
  50. package/dist/components/ViewExplorer/TagManager/ColorPicker.d.ts +7 -0
  51. package/dist/components/ViewExplorer/TagManager/GroupNamingPopover.d.ts +10 -0
  52. package/dist/components/ViewExplorer/TagManager/LayerItem.d.ts +27 -0
  53. package/dist/components/ViewExplorer/TagManager/TagItem.d.ts +25 -0
  54. package/dist/components/ViewExplorer/TagManager/index.d.ts +21 -0
  55. package/dist/components/ViewExplorer/ViewNavigator.d.ts +11 -0
  56. package/dist/components/ViewExplorer/ViewSearch.d.ts +8 -0
  57. package/dist/components/ViewExplorer/ViewTree.d.ts +18 -0
  58. package/dist/components/ViewExplorer/index.d.ts +31 -0
  59. package/dist/components/ViewExplorer/types.d.ts +11 -0
  60. package/dist/components/ViewExplorer/utils.d.ts +6 -0
  61. package/dist/components/ViewExplorer-vscode.d.ts +6 -0
  62. package/dist/components/ViewFloatingMenu-vscode.d.ts +27 -0
  63. package/dist/components/ViewFloatingMenu.d.ts +39 -0
  64. package/dist/components/ViewGridNode.d.ts +29 -0
  65. package/dist/components/ViewHeaderButton.d.ts +11 -0
  66. package/dist/components/ViewPanel.d.ts +18 -0
  67. package/dist/components/ViewsGridOnboarding.d.ts +5 -0
  68. package/dist/components/ZUI/ZUICanvas.d.ts +18 -0
  69. package/dist/components/ZUI/index.d.ts +2 -0
  70. package/dist/components/ZUI/layout.d.ts +18 -0
  71. package/dist/components/ZUI/proxy.d.ts +25 -0
  72. package/dist/components/ZUI/renderer.d.ts +30 -0
  73. package/dist/components/ZUI/types.d.ts +140 -0
  74. package/dist/components/ZUI/useZUIInteraction.d.ts +21 -0
  75. package/dist/config/runtime-vscode.d.ts +22 -0
  76. package/dist/config/runtime.d.ts +5 -0
  77. package/dist/constants/colors.d.ts +27 -0
  78. package/dist/constants/diagramColors.d.ts +1 -0
  79. package/dist/context/ThemeContext.d.ts +27 -0
  80. package/dist/crossBranch/graph.d.ts +13 -0
  81. package/dist/crossBranch/resolve.d.ts +22 -0
  82. package/dist/crossBranch/settings.d.ts +6 -0
  83. package/dist/crossBranch/store.d.ts +11 -0
  84. package/dist/crossBranch/types.d.ts +96 -0
  85. package/dist/demo/DemoPage.d.ts +9 -0
  86. package/dist/demo/seed.d.ts +9 -0
  87. package/dist/demo/store.d.ts +137 -0
  88. package/dist/demo/viewEditor.d.ts +26 -0
  89. package/dist/favicon.svg +35 -0
  90. package/dist/hooks/useSafeFitView.d.ts +16 -0
  91. package/dist/index.css +1 -0
  92. package/dist/index.d.ts +115 -0
  93. package/dist/index.js +19966 -0
  94. package/dist/lib/vscodeBridge-vscode.d.ts +13 -0
  95. package/dist/lib/vscodeBridge.d.ts +5 -0
  96. package/dist/logo-120.png +0 -0
  97. package/dist/logo-bw.png +0 -0
  98. package/dist/logo-bw.svg +15 -0
  99. package/dist/logo-text.svg +51 -0
  100. package/dist/logo.svg +35 -0
  101. package/dist/pages/AppearanceSettings.d.ts +3 -0
  102. package/dist/pages/Dependencies.d.ts +1 -0
  103. package/dist/pages/InfiniteZoom.d.ts +7 -0
  104. package/dist/pages/Settings.d.ts +7 -0
  105. package/dist/pages/ViewEditor/components/EditorMenus.d.ts +24 -0
  106. package/dist/pages/ViewEditor/components/EditorOverlays.d.ts +30 -0
  107. package/dist/pages/ViewEditor/components/EmptyCanvasState.d.ts +7 -0
  108. package/dist/pages/ViewEditor/context.d.ts +13 -0
  109. package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +201 -0
  110. package/dist/pages/ViewEditor/hooks/useDrawingEngine.d.ts +40 -0
  111. package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +20 -0
  112. package/dist/pages/ViewEditor/hooks/useViewData.d.ts +74 -0
  113. package/dist/pages/ViewEditor/index.d.ts +8 -0
  114. package/dist/pages/ViewEditor/utils.d.ts +14 -0
  115. package/dist/pages/Views.d.ts +6 -0
  116. package/dist/pages/ViewsGrid.d.ts +6 -0
  117. package/dist/pkg/importer/mermaid.d.ts +7 -0
  118. package/dist/pkg/importer/mermaid.test.d.ts +1 -0
  119. package/dist/platform/PlatformContext.d.ts +6 -0
  120. package/dist/platform/context.d.ts +3 -0
  121. package/dist/platform/local.d.ts +2 -0
  122. package/dist/platform/types.d.ts +17 -0
  123. package/dist/slots.d.ts +67 -0
  124. package/dist/theme.d.ts +2 -0
  125. package/dist/types/index.d.ts +193 -0
  126. package/dist/types/vscode-messages.d.ts +60 -0
  127. package/dist/utils/edgeDistribution.d.ts +34 -0
  128. package/dist/utils/githubApi.d.ts +4 -0
  129. package/dist/utils/githubCache.d.ts +17 -0
  130. package/dist/utils/ids.d.ts +2 -0
  131. package/dist/utils/technologyCatalog.d.ts +15 -0
  132. package/dist/utils/toast.d.ts +15 -0
  133. package/dist/utils/treesitter.d.ts +13 -0
  134. package/dist/utils/url.d.ts +12 -0
  135. package/package.json +159 -0
  136. package/src/App.tsx +141 -0
  137. package/src/api/client.ts +618 -0
  138. package/src/api/transport-vscode.ts +28 -0
  139. package/src/api/transport.ts +7 -0
  140. package/src/assets/logo-mark.svg +31 -0
  141. package/src/assets/logo-wordmark.svg +22 -0
  142. package/src/assets/logo.svg +35 -0
  143. package/src/components/CodePreviewPanel-vscode.tsx +85 -0
  144. package/src/components/CodePreviewPanel.tsx +384 -0
  145. package/src/components/ConfirmDialog.tsx +66 -0
  146. package/src/components/ConnectorPanel.tsx +403 -0
  147. package/src/components/ContextBoundaryElement.tsx +35 -0
  148. package/src/components/ContextNeighborElement.tsx +282 -0
  149. package/src/components/ContextStraightConnector.tsx +144 -0
  150. package/src/components/CrossBranchControls.tsx +105 -0
  151. package/src/components/DependenciesOnboarding.tsx +427 -0
  152. package/src/components/DrawingCanvas.tsx +391 -0
  153. package/src/components/ElementLibrary-vscode.tsx +9 -0
  154. package/src/components/ElementLibrary.tsx +512 -0
  155. package/src/components/ElementNode.tsx +1033 -0
  156. package/src/components/ElementPanel.tsx +928 -0
  157. package/src/components/ExploreOnboarding.tsx +347 -0
  158. package/src/components/ExplorePageOnboarding.tsx +383 -0
  159. package/src/components/ExportModal.tsx +132 -0
  160. package/src/components/FloatingEdge.tsx +115 -0
  161. package/src/components/GitSourceLinker.tsx +1053 -0
  162. package/src/components/HeaderContext.tsx +30 -0
  163. package/src/components/Icons.tsx +245 -0
  164. package/src/components/ImportModal.tsx +219 -0
  165. package/src/components/InlineElementAdder.tsx +216 -0
  166. package/src/components/LayoutSection.tsx +624 -0
  167. package/src/components/LocalSourceLinker.tsx +330 -0
  168. package/src/components/MiniZoomOnboarding.tsx +78 -0
  169. package/src/components/NavBreadcrumb.tsx +24 -0
  170. package/src/components/NodeBody.tsx +89 -0
  171. package/src/components/NodeContainer.tsx +58 -0
  172. package/src/components/NodeHoverCard.tsx +135 -0
  173. package/src/components/PanelHeader.tsx +36 -0
  174. package/src/components/PanelUI.tsx +24 -0
  175. package/src/components/ProxyConnectorEdge.tsx +169 -0
  176. package/src/components/ProxyConnectorPanel.tsx +130 -0
  177. package/src/components/SafeBackground.tsx +19 -0
  178. package/src/components/ScrollIndicatorWrapper.tsx +117 -0
  179. package/src/components/SetChildModal.tsx +191 -0
  180. package/src/components/SetParentModal.tsx +187 -0
  181. package/src/components/SlidingPanel.tsx +114 -0
  182. package/src/components/TagUpsert.tsx +142 -0
  183. package/src/components/TopMenuBar.tsx +380 -0
  184. package/src/components/ViewBezierConnector.tsx +143 -0
  185. package/src/components/ViewDrawMenu.tsx +270 -0
  186. package/src/components/ViewEditorEdgeLabelLayout.ts +189 -0
  187. package/src/components/ViewEditorOnboarding.tsx +445 -0
  188. package/src/components/ViewExplorer/TagManager/ColorPicker.tsx +49 -0
  189. package/src/components/ViewExplorer/TagManager/GroupNamingPopover.tsx +96 -0
  190. package/src/components/ViewExplorer/TagManager/LayerItem.tsx +228 -0
  191. package/src/components/ViewExplorer/TagManager/TagItem.tsx +242 -0
  192. package/src/components/ViewExplorer/TagManager/index.tsx +418 -0
  193. package/src/components/ViewExplorer/ViewNavigator.tsx +121 -0
  194. package/src/components/ViewExplorer/ViewSearch.tsx +33 -0
  195. package/src/components/ViewExplorer/ViewTree.tsx +98 -0
  196. package/src/components/ViewExplorer/index.tsx +384 -0
  197. package/src/components/ViewExplorer/types.ts +13 -0
  198. package/src/components/ViewExplorer/utils.ts +56 -0
  199. package/src/components/ViewExplorer-vscode.tsx +8 -0
  200. package/src/components/ViewFloatingMenu-vscode.tsx +248 -0
  201. package/src/components/ViewFloatingMenu.tsx +379 -0
  202. package/src/components/ViewGridNode.tsx +451 -0
  203. package/src/components/ViewHeaderButton.tsx +60 -0
  204. package/src/components/ViewPanel.tsx +162 -0
  205. package/src/components/ViewsGridOnboarding.tsx +400 -0
  206. package/src/components/ZUI/ZUICanvas.tsx +853 -0
  207. package/src/components/ZUI/index.ts +3 -0
  208. package/src/components/ZUI/layout.ts +323 -0
  209. package/src/components/ZUI/proxy.ts +278 -0
  210. package/src/components/ZUI/renderer.ts +1189 -0
  211. package/src/components/ZUI/types.ts +150 -0
  212. package/src/components/ZUI/useZUIInteraction.ts +720 -0
  213. package/src/config/runtime-vscode.ts +46 -0
  214. package/src/config/runtime.ts +30 -0
  215. package/src/constants/colors.ts +80 -0
  216. package/src/constants/diagramColors.ts +9 -0
  217. package/src/context/ThemeContext.tsx +158 -0
  218. package/src/crossBranch/graph.ts +207 -0
  219. package/src/crossBranch/resolve.ts +643 -0
  220. package/src/crossBranch/settings.ts +59 -0
  221. package/src/crossBranch/store.ts +71 -0
  222. package/src/crossBranch/types.ts +102 -0
  223. package/src/demo/DemoPage.tsx +184 -0
  224. package/src/demo/seed.ts +67 -0
  225. package/src/demo/store.ts +536 -0
  226. package/src/demo/viewEditor.ts +110 -0
  227. package/src/hooks/useSafeFitView.ts +60 -0
  228. package/src/index.css +309 -0
  229. package/src/index.ts +184 -0
  230. package/src/kafka-ss.png +0 -0
  231. package/src/lib/vscodeBridge-vscode.ts +27 -0
  232. package/src/lib/vscodeBridge.ts +7 -0
  233. package/src/main.tsx +46 -0
  234. package/src/pages/AppearanceSettings.tsx +135 -0
  235. package/src/pages/Dependencies.tsx +926 -0
  236. package/src/pages/InfiniteZoom.tsx +404 -0
  237. package/src/pages/Settings.tsx +91 -0
  238. package/src/pages/ViewEditor/EDGE_DISTRIBUTION.md +64 -0
  239. package/src/pages/ViewEditor/components/EditorMenus.tsx +112 -0
  240. package/src/pages/ViewEditor/components/EditorOverlays.tsx +172 -0
  241. package/src/pages/ViewEditor/components/EmptyCanvasState.tsx +42 -0
  242. package/src/pages/ViewEditor/context.tsx +21 -0
  243. package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +1349 -0
  244. package/src/pages/ViewEditor/hooks/useDrawingEngine.ts +127 -0
  245. package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +501 -0
  246. package/src/pages/ViewEditor/hooks/useViewData.ts +491 -0
  247. package/src/pages/ViewEditor/index.tsx +1366 -0
  248. package/src/pages/ViewEditor/utils.ts +88 -0
  249. package/src/pages/Views.tsx +171 -0
  250. package/src/pages/ViewsGrid.tsx +1310 -0
  251. package/src/pkg/importer/mermaid.test.ts +141 -0
  252. package/src/pkg/importer/mermaid.ts +76 -0
  253. package/src/platform/PlatformContext.tsx +17 -0
  254. package/src/platform/context.ts +9 -0
  255. package/src/platform/local.tsx +15 -0
  256. package/src/platform/types.ts +19 -0
  257. package/src/slots.ts +92 -0
  258. package/src/styles/editor-panels.css +66 -0
  259. package/src/styles/theme.css +56 -0
  260. package/src/theme.ts +336 -0
  261. package/src/types/index.ts +234 -0
  262. package/src/types/offline-ambient.d.ts +14 -0
  263. package/src/types/vscode-messages.ts +32 -0
  264. package/src/utils/edgeDistribution.ts +103 -0
  265. package/src/utils/githubApi.ts +121 -0
  266. package/src/utils/githubCache.ts +108 -0
  267. package/src/utils/ids.ts +9 -0
  268. package/src/utils/technologyCatalog.ts +143 -0
  269. package/src/utils/toast.ts +100 -0
  270. package/src/utils/treesitter.ts +147 -0
  271. package/src/utils/url.ts +72 -0
  272. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,643 @@
1
+ import type { Connector, PlacedElement } from '../types'
2
+ import { CROSS_BRANCH_DEPTH_ALL } from './types'
3
+ import type {
4
+ AggregatedProxyConnector,
5
+ CrossBranchContextSettings,
6
+ GraphPlacementRef,
7
+ ProxyConnectorDetails,
8
+ ProxyConnectorLeaf,
9
+ ProxyContextNode,
10
+ ProxyEndpoint,
11
+ WorkspaceGraphSnapshot,
12
+ } from './types'
13
+ import { allConnectors, findLowestCommonAncestorViewId, isDescendantView, relativeOwnerElementPath, viewName } from './graph'
14
+
15
+ function firstPlacementForElement(snapshot: WorkspaceGraphSnapshot, elementId: number): GraphPlacementRef | null {
16
+ return snapshot.placementsByElementId[elementId]?.[0] ?? null
17
+ }
18
+
19
+ function placementInView(snapshot: WorkspaceGraphSnapshot, viewId: number | null, elementId: number): GraphPlacementRef | null {
20
+ if (viewId == null) return null
21
+ return snapshot.placementsByElementId[elementId]?.find((placement) => placement.viewId === viewId) ?? null
22
+ }
23
+
24
+ function elementDisplayPlacement(snapshot: WorkspaceGraphSnapshot, elementId: number, preferredViewId: number | null = null): GraphPlacementRef | null {
25
+ return placementInView(snapshot, preferredViewId, elementId) ?? firstPlacementForElement(snapshot, elementId)
26
+ }
27
+
28
+ function ancestorDepth(snapshot: WorkspaceGraphSnapshot, viewId: number | null | undefined): number {
29
+ if (viewId == null) return 0
30
+ return snapshot.ancestorsByViewId[viewId]?.length ?? 0
31
+ }
32
+
33
+ function pathDistance(snapshot: WorkspaceGraphSnapshot, leftViewId: number, rightViewId: number): number {
34
+ const lca = findLowestCommonAncestorViewId(snapshot, leftViewId, rightViewId)
35
+ if (lca == null) return Number.MAX_SAFE_INTEGER
36
+ const lcaDepth = ancestorDepth(snapshot, lca)
37
+ return (ancestorDepth(snapshot, leftViewId) - lcaDepth) + (ancestorDepth(snapshot, rightViewId) - lcaDepth)
38
+ }
39
+
40
+ function appendElementToPath(path: number[], elementId: number): number[] {
41
+ if (path[path.length - 1] === elementId) return path
42
+ return [...path, elementId]
43
+ }
44
+
45
+ function explicitOffViewContextPath(
46
+ snapshot: WorkspaceGraphSnapshot,
47
+ currentViewId: number,
48
+ currentVisibleElementIds: Set<number>,
49
+ chosenPlacementViewId: number,
50
+ elementId: number,
51
+ ): number[] {
52
+ if (isDescendantView(snapshot, chosenPlacementViewId, currentViewId)) {
53
+ const owners = relativeOwnerElementPath(snapshot, currentViewId, chosenPlacementViewId)
54
+ const lastVisibleOwnerIndex = owners.reduce((bestIndex, ownerElementId, index) => (
55
+ currentVisibleElementIds.has(ownerElementId) ? index : bestIndex
56
+ ), -1)
57
+ return appendElementToPath(owners.slice(lastVisibleOwnerIndex + 1), elementId)
58
+ }
59
+
60
+ const commonAncestorViewId = findLowestCommonAncestorViewId(snapshot, currentViewId, chosenPlacementViewId)
61
+ const owners = commonAncestorViewId == null
62
+ ? []
63
+ : relativeOwnerElementPath(snapshot, commonAncestorViewId, chosenPlacementViewId)
64
+ return appendElementToPath(owners, elementId)
65
+ }
66
+
67
+ function chooseBestPlacement(snapshot: WorkspaceGraphSnapshot, elementId: number, focusViewId: number, ownerViewId: number): GraphPlacementRef | null {
68
+ const candidates = snapshot.placementsByElementId[elementId] ?? []
69
+ if (candidates.length === 0) return null
70
+
71
+ const score = (placement: GraphPlacementRef) => {
72
+ if (placement.viewId === focusViewId) return 0
73
+ if (placement.viewId === ownerViewId) return 10
74
+ if (isDescendantView(snapshot, placement.viewId, focusViewId)) {
75
+ return 50 + Math.max(0, ancestorDepth(snapshot, placement.viewId) - ancestorDepth(snapshot, focusViewId))
76
+ }
77
+
78
+ const lca = findLowestCommonAncestorViewId(snapshot, focusViewId, placement.viewId)
79
+ const lcaDepth = ancestorDepth(snapshot, lca)
80
+ const focusDistance = pathDistance(snapshot, focusViewId, placement.viewId)
81
+ const ownerDistance = pathDistance(snapshot, ownerViewId, placement.viewId)
82
+ return 200 + focusDistance * 10 + ownerDistance * 2 - lcaDepth
83
+ }
84
+
85
+ return [...candidates].sort((left, right) => {
86
+ const delta = score(left) - score(right)
87
+ if (delta !== 0) return delta
88
+ return left.viewId - right.viewId
89
+ })[0]
90
+ }
91
+
92
+ function buildProxyEndpoint(
93
+ snapshot: WorkspaceGraphSnapshot,
94
+ currentViewId: number,
95
+ currentVisibleElementIds: Set<number>,
96
+ ownerViewId: number,
97
+ elementId: number,
98
+ ): ProxyEndpoint | null {
99
+ const chosenPlacement = chooseBestPlacement(snapshot, elementId, currentViewId, ownerViewId)
100
+ if (!chosenPlacement) return null
101
+
102
+ const actualPlacement = elementDisplayPlacement(snapshot, elementId, chosenPlacement.viewId)
103
+ const actualName = actualPlacement?.element.name ?? `Element ${elementId}`
104
+ const forceExplicitOffViewContext = ownerViewId === currentViewId && chosenPlacement.viewId !== currentViewId
105
+
106
+ if (forceExplicitOffViewContext) {
107
+ const contextPathElementIds = explicitOffViewContextPath(
108
+ snapshot,
109
+ currentViewId,
110
+ currentVisibleElementIds,
111
+ chosenPlacement.viewId,
112
+ elementId,
113
+ )
114
+ const mergeAncestorElementId = contextPathElementIds[0] ?? null
115
+ const commonAncestorViewId = findLowestCommonAncestorViewId(snapshot, currentViewId, chosenPlacement.viewId)
116
+ const currentBranchElementId = commonAncestorViewId == null
117
+ ? null
118
+ : commonAncestorViewId === currentViewId
119
+ ? (relativeOwnerElementPath(snapshot, currentViewId, chosenPlacement.viewId)[0] ?? null)
120
+ : (relativeOwnerElementPath(snapshot, commonAncestorViewId, currentViewId)[0] ?? null)
121
+
122
+ return {
123
+ actualElementId: elementId,
124
+ actualElementName: actualName,
125
+ anchorElementId: elementId,
126
+ anchorElementName: actualName,
127
+ anchorViewId: chosenPlacement.viewId,
128
+ anchorViewName: chosenPlacement.viewName,
129
+ placementViewId: chosenPlacement.viewId,
130
+ placementViewName: chosenPlacement.viewName,
131
+ depth: Math.max(1, pathDistance(snapshot, currentViewId, chosenPlacement.viewId)),
132
+ externalToView: true,
133
+ currentBranchElementId,
134
+ commonAncestorViewId,
135
+ commonAncestorViewName: viewName(snapshot, commonAncestorViewId),
136
+ mergeAncestorElementId,
137
+ contextPathElementIds,
138
+ }
139
+ }
140
+
141
+ if (chosenPlacement.viewId === currentViewId || isDescendantView(snapshot, chosenPlacement.viewId, currentViewId)) {
142
+ const descendantDepth = chosenPlacement.viewId === currentViewId
143
+ ? 0
144
+ : Math.max(1, ancestorDepth(snapshot, chosenPlacement.viewId) - ancestorDepth(snapshot, currentViewId))
145
+ const owners = chosenPlacement.viewId === currentViewId
146
+ ? []
147
+ : relativeOwnerElementPath(snapshot, currentViewId, chosenPlacement.viewId)
148
+
149
+ const candidateIds = [elementId, ...owners.slice().reverse()]
150
+ const visibleAnchorId = candidateIds.find((candidate) => currentVisibleElementIds.has(candidate))
151
+ if (visibleAnchorId == null) return null
152
+ const visibleAnchorPlacement = elementDisplayPlacement(snapshot, visibleAnchorId, currentViewId)
153
+
154
+ return {
155
+ actualElementId: elementId,
156
+ actualElementName: actualName,
157
+ anchorElementId: visibleAnchorId,
158
+ anchorElementName: visibleAnchorPlacement?.element.name ?? actualName,
159
+ anchorViewId: currentViewId,
160
+ anchorViewName: viewName(snapshot, currentViewId),
161
+ placementViewId: chosenPlacement.viewId,
162
+ placementViewName: chosenPlacement.viewName,
163
+ depth: descendantDepth,
164
+ externalToView: false,
165
+ currentBranchElementId: null,
166
+ commonAncestorViewId: currentViewId,
167
+ commonAncestorViewName: viewName(snapshot, currentViewId),
168
+ mergeAncestorElementId: null,
169
+ contextPathElementIds: [],
170
+ }
171
+ }
172
+
173
+ const commonAncestorViewId = findLowestCommonAncestorViewId(snapshot, currentViewId, chosenPlacement.viewId)
174
+ const externalOwners = commonAncestorViewId == null
175
+ ? []
176
+ : relativeOwnerElementPath(snapshot, commonAncestorViewId, chosenPlacement.viewId)
177
+ const anchorElementId = commonAncestorViewId == null
178
+ ? elementId
179
+ : commonAncestorViewId === chosenPlacement.viewId
180
+ ? elementId
181
+ : (externalOwners[0] ?? elementId)
182
+ const anchorPlacement = elementDisplayPlacement(snapshot, anchorElementId, commonAncestorViewId)
183
+ const currentBranchElementId = commonAncestorViewId == null || commonAncestorViewId === currentViewId
184
+ ? null
185
+ : (relativeOwnerElementPath(snapshot, commonAncestorViewId, currentViewId)[0] ?? null)
186
+
187
+ return {
188
+ actualElementId: elementId,
189
+ actualElementName: actualName,
190
+ anchorElementId,
191
+ anchorElementName: anchorPlacement?.element.name ?? actualName,
192
+ anchorViewId: commonAncestorViewId == null ? chosenPlacement.viewId : commonAncestorViewId,
193
+ anchorViewName: commonAncestorViewId == null ? chosenPlacement.viewName : viewName(snapshot, commonAncestorViewId),
194
+ placementViewId: chosenPlacement.viewId,
195
+ placementViewName: chosenPlacement.viewName,
196
+ depth: Math.max(1, externalOwners.length || 1),
197
+ externalToView: true,
198
+ currentBranchElementId,
199
+ commonAncestorViewId,
200
+ commonAncestorViewName: viewName(snapshot, commonAncestorViewId),
201
+ mergeAncestorElementId: null,
202
+ contextPathElementIds: [],
203
+ }
204
+ }
205
+
206
+ function proxyDisplayLabel(connectors: ProxyConnectorLeaf[]): string {
207
+ if (connectors.length === 1) {
208
+ const [leaf] = connectors
209
+ return leaf.connector.label?.trim() || leaf.connector.relationship?.trim() || 'Cross-branch'
210
+ }
211
+ const labels = new Set(connectors.map((leaf) => leaf.connector.label?.trim()).filter(Boolean))
212
+ if (labels.size === 1) return `${connectors.length} × ${Array.from(labels)[0]}`
213
+ return `${connectors.length} connectors`
214
+ }
215
+
216
+ function contextNodeKey(endpoint: ProxyEndpoint): string {
217
+ return [
218
+ 'ctx',
219
+ endpoint.anchorViewId ?? 'none',
220
+ endpoint.anchorElementId,
221
+ endpoint.commonAncestorViewId ?? 'none',
222
+ endpoint.currentBranchElementId ?? 'none',
223
+ ].join(':')
224
+ }
225
+
226
+ function visibleNodeKey(elementId: number): string {
227
+ return String(elementId)
228
+ }
229
+
230
+ function displayNodeId(endpoint: ProxyEndpoint): string {
231
+ return endpoint.externalToView ? contextNodeKey(endpoint) : visibleNodeKey(endpoint.anchorElementId)
232
+ }
233
+
234
+ function canonicalPairIds(leftId: string, rightId: string): [string, string] {
235
+ return leftId <= rightId ? [leftId, rightId] : [rightId, leftId]
236
+ }
237
+
238
+ function canonicalPairElements(leftId: number, rightId: number): [number, number] {
239
+ return leftId <= rightId ? [leftId, rightId] : [rightId, leftId]
240
+ }
241
+
242
+ function proxyDisplayGroupKey(leftId: string, rightId: string): string {
243
+ const [first, second] = canonicalPairIds(leftId, rightId)
244
+ return [first, second].join('::')
245
+ }
246
+
247
+ function ownerViewsFromLeaves(leaves: ProxyConnectorLeaf[]) {
248
+ const ownerViews = new Map<number, string>()
249
+ for (const leaf of leaves) {
250
+ ownerViews.set(leaf.ownerViewId, leaf.ownerViewName)
251
+ }
252
+ return {
253
+ ownerViewIds: Array.from(ownerViews.keys()),
254
+ ownerViewNames: Array.from(ownerViews.values()),
255
+ }
256
+ }
257
+
258
+ function collapseEndpointToAncestor(
259
+ snapshot: WorkspaceGraphSnapshot,
260
+ currentViewId: number,
261
+ currentVisibleElementIds: Set<number>,
262
+ ownerViewId: number,
263
+ endpoint: ProxyEndpoint,
264
+ collapseCounts: Map<number, number>,
265
+ ): ProxyEndpoint {
266
+ if (!endpoint.externalToView) {
267
+ return endpoint
268
+ }
269
+
270
+ const collapseAncestorElementId = [...(endpoint.contextPathElementIds ?? [])]
271
+ .reverse()
272
+ .find((elementId) => (collapseCounts.get(elementId) ?? 0) >= 2)
273
+
274
+ if (collapseAncestorElementId == null) {
275
+ return endpoint
276
+ }
277
+
278
+ const visibleAncestorPlacement = placementInView(snapshot, currentViewId, collapseAncestorElementId)
279
+ if (visibleAncestorPlacement && currentVisibleElementIds.has(collapseAncestorElementId)) {
280
+ return {
281
+ ...endpoint,
282
+ anchorElementId: collapseAncestorElementId,
283
+ anchorElementName: visibleAncestorPlacement.element.name,
284
+ anchorViewId: currentViewId,
285
+ anchorViewName: viewName(snapshot, currentViewId),
286
+ placementViewId: currentViewId,
287
+ placementViewName: viewName(snapshot, currentViewId),
288
+ externalToView: false,
289
+ currentBranchElementId: null,
290
+ commonAncestorViewId: currentViewId,
291
+ commonAncestorViewName: viewName(snapshot, currentViewId),
292
+ }
293
+ }
294
+
295
+ const ancestorPlacement = chooseBestPlacement(snapshot, collapseAncestorElementId, currentViewId, ownerViewId)
296
+ ?? firstPlacementForElement(snapshot, collapseAncestorElementId)
297
+ const commonAncestorViewId = findLowestCommonAncestorViewId(
298
+ snapshot,
299
+ currentViewId,
300
+ ancestorPlacement?.viewId ?? endpoint.anchorViewId ?? endpoint.placementViewId,
301
+ )
302
+ const currentBranchElementId = commonAncestorViewId == null || commonAncestorViewId === currentViewId
303
+ ? null
304
+ : (relativeOwnerElementPath(snapshot, commonAncestorViewId, currentViewId)[0] ?? null)
305
+
306
+ return {
307
+ ...endpoint,
308
+ anchorElementId: collapseAncestorElementId,
309
+ anchorElementName: ancestorPlacement?.element.name ?? endpoint.anchorElementName,
310
+ anchorViewId: ancestorPlacement?.viewId ?? endpoint.anchorViewId,
311
+ anchorViewName: ancestorPlacement?.viewName ?? endpoint.anchorViewName,
312
+ placementViewId: ancestorPlacement?.viewId ?? endpoint.placementViewId,
313
+ placementViewName: ancestorPlacement?.viewName ?? endpoint.placementViewName,
314
+ currentBranchElementId,
315
+ commonAncestorViewId,
316
+ commonAncestorViewName: viewName(snapshot, commonAncestorViewId),
317
+ }
318
+ }
319
+
320
+ function isNativeCurrentViewConnector(connector: Connector, currentViewId: number, currentVisibleElementIds: Set<number>): boolean {
321
+ return connector.view_id === currentViewId &&
322
+ currentVisibleElementIds.has(connector.source_element_id) &&
323
+ currentVisibleElementIds.has(connector.target_element_id)
324
+ }
325
+
326
+ export interface ViewProxyGraphResult {
327
+ proxyNodes: ProxyContextNode[]
328
+ proxyConnectors: AggregatedProxyConnector[]
329
+ proxyConnectorDetailsByKey: Record<string, ProxyConnectorDetails>
330
+ }
331
+
332
+ export function resolveViewProxyGraph(
333
+ snapshot: WorkspaceGraphSnapshot | null,
334
+ currentViewId: number | null,
335
+ currentViewElements: PlacedElement[],
336
+ settings: CrossBranchContextSettings,
337
+ ): ViewProxyGraphResult {
338
+ if (!snapshot || currentViewId == null || !settings.enabled) {
339
+ return { proxyNodes: [], proxyConnectors: [], proxyConnectorDetailsByKey: {} }
340
+ }
341
+
342
+ const currentVisibleElementIds = new Set(currentViewElements.map((element) => element.element_id))
343
+ const currentVisibleElementsById = new Map(currentViewElements.map((element) => [element.element_id, element]))
344
+
345
+ const connectorLeaves: ProxyConnectorLeaf[] = []
346
+ for (const connector of allConnectors(snapshot)) {
347
+ if (isNativeCurrentViewConnector(connector, currentViewId, currentVisibleElementIds)) continue
348
+
349
+ const source = buildProxyEndpoint(snapshot, currentViewId, currentVisibleElementIds, connector.view_id, connector.source_element_id)
350
+ const target = buildProxyEndpoint(snapshot, currentViewId, currentVisibleElementIds, connector.view_id, connector.target_element_id)
351
+ if (!source || !target) continue
352
+ if (source.externalToView && target.externalToView) continue
353
+
354
+ const maxDepth = Math.max(source.depth, target.depth)
355
+ if (settings.depth < CROSS_BRANCH_DEPTH_ALL && maxDepth > settings.depth) continue
356
+
357
+ const sourceAnchorId = displayNodeId(source)
358
+ const targetAnchorId = displayNodeId(target)
359
+ if (sourceAnchorId === targetAnchorId) continue
360
+ if (!source.externalToView && !target.externalToView && connector.view_id === currentViewId) continue
361
+
362
+ connectorLeaves.push({
363
+ connector,
364
+ ownerViewId: connector.view_id,
365
+ ownerViewName: viewName(snapshot, connector.view_id) ?? `View ${connector.view_id}`,
366
+ source,
367
+ target,
368
+ })
369
+ }
370
+
371
+ const collapseAncestorElements = new Map<number, Set<number>>()
372
+ for (const leaf of connectorLeaves) {
373
+ for (const endpoint of [leaf.source, leaf.target]) {
374
+ for (const ancestorElementId of endpoint.contextPathElementIds ?? []) {
375
+ const elements = collapseAncestorElements.get(ancestorElementId) ?? new Set<number>()
376
+ elements.add(endpoint.actualElementId)
377
+ collapseAncestorElements.set(ancestorElementId, elements)
378
+ }
379
+ }
380
+ }
381
+ const collapseCounts = new Map(
382
+ Array.from(collapseAncestorElements.entries()).map(([ancestorElementId, elements]) => [ancestorElementId, elements.size]),
383
+ )
384
+
385
+ const collapsedConnectorLeaves = connectorLeaves.flatMap((leaf) => {
386
+ const source = collapseEndpointToAncestor(
387
+ snapshot,
388
+ currentViewId,
389
+ currentVisibleElementIds,
390
+ leaf.ownerViewId,
391
+ leaf.source,
392
+ collapseCounts,
393
+ )
394
+ const target = collapseEndpointToAncestor(
395
+ snapshot,
396
+ currentViewId,
397
+ currentVisibleElementIds,
398
+ leaf.ownerViewId,
399
+ leaf.target,
400
+ collapseCounts,
401
+ )
402
+ if (displayNodeId(source) === displayNodeId(target)) {
403
+ return []
404
+ }
405
+ return [{ ...leaf, source, target }]
406
+ })
407
+
408
+ const grouped = new Map<string, ProxyConnectorLeaf[]>()
409
+ for (const leaf of collapsedConnectorLeaves) {
410
+ const key = proxyDisplayGroupKey(displayNodeId(leaf.source), displayNodeId(leaf.target))
411
+ const group = grouped.get(key)
412
+ if (group) group.push(leaf)
413
+ else grouped.set(key, [leaf])
414
+ }
415
+
416
+ const proxyNodesMap = new Map<string, ProxyContextNode>()
417
+ const proxyConnectorDetailsByKey: Record<string, ProxyConnectorDetails> = {}
418
+ const proxyConnectors: AggregatedProxyConnector[] = []
419
+
420
+ for (const [key, leaves] of grouped) {
421
+ const [first] = leaves
422
+ const { ownerViewIds, ownerViewNames } = ownerViewsFromLeaves(leaves)
423
+ const [canonicalSourceAnchorId, canonicalTargetAnchorId] = canonicalPairIds(
424
+ displayNodeId(first.source),
425
+ displayNodeId(first.target),
426
+ )
427
+ const canonicalFirstIsSource = canonicalSourceAnchorId === displayNodeId(first.source)
428
+ const details: ProxyConnectorDetails = {
429
+ key,
430
+ label: proxyDisplayLabel(leaves),
431
+ count: leaves.length,
432
+ sourceAnchorId: canonicalSourceAnchorId,
433
+ targetAnchorId: canonicalTargetAnchorId,
434
+ sourceAnchorName: canonicalFirstIsSource ? first.source.anchorElementName : first.target.anchorElementName,
435
+ targetAnchorName: canonicalFirstIsSource ? first.target.anchorElementName : first.source.anchorElementName,
436
+ ownerViewIds,
437
+ ownerViewNames,
438
+ connectors: leaves,
439
+ }
440
+ proxyConnectorDetailsByKey[key] = details
441
+
442
+ for (const endpoint of [first.source, first.target]) {
443
+ if (!endpoint.externalToView) continue
444
+ const nodeId = displayNodeId(endpoint)
445
+ const endpointSortLevel = ancestorDepth(snapshot, endpoint.placementViewId ?? endpoint.anchorViewId ?? endpoint.commonAncestorViewId)
446
+ const existingNode = proxyNodesMap.get(nodeId)
447
+ if (existingNode) {
448
+ const mergedOwners = new Map<number, string>()
449
+ existingNode.ownerViewIds.forEach((ownerViewId, index) => {
450
+ mergedOwners.set(ownerViewId, existingNode.ownerViewNames[index] ?? `View ${ownerViewId}`)
451
+ })
452
+ details.ownerViewIds.forEach((ownerViewId, index) => {
453
+ mergedOwners.set(ownerViewId, details.ownerViewNames[index] ?? `View ${ownerViewId}`)
454
+ })
455
+ existingNode.ownerViewIds = Array.from(mergedOwners.keys())
456
+ existingNode.ownerViewNames = Array.from(mergedOwners.values())
457
+ existingNode.connectorCount += details.count
458
+ existingNode.sortLevel = Math.min(existingNode.sortLevel, endpointSortLevel)
459
+ continue
460
+ }
461
+ const anchorPlacement = elementDisplayPlacement(snapshot, endpoint.anchorElementId, endpoint.anchorViewId)
462
+ proxyNodesMap.set(nodeId, {
463
+ id: nodeId,
464
+ anchorElementId: endpoint.anchorElementId,
465
+ name: endpoint.anchorElementName,
466
+ sortLevel: endpointSortLevel,
467
+ placementViewId: endpoint.placementViewId ?? endpoint.anchorViewId ?? null,
468
+ kind: anchorPlacement?.element.kind ?? null,
469
+ description: anchorPlacement?.element.description ?? null,
470
+ technology: anchorPlacement?.element.technology ?? null,
471
+ logoUrl: anchorPlacement?.element.logo_url ?? null,
472
+ technologyConnectors: anchorPlacement?.element.technology_connectors ?? [],
473
+ ownerViewIds: [...details.ownerViewIds],
474
+ ownerViewNames: [...details.ownerViewNames],
475
+ commonAncestorViewId: endpoint.commonAncestorViewId,
476
+ commonAncestorViewName: endpoint.commonAncestorViewName,
477
+ currentBranchElementId: endpoint.currentBranchElementId,
478
+ connectorCount: details.count,
479
+ })
480
+ }
481
+
482
+ const sourceElement = first.source.externalToView ? null : currentVisibleElementsById.get(first.source.anchorElementId) ?? null
483
+ const targetElement = first.target.externalToView ? null : currentVisibleElementsById.get(first.target.anchorElementId) ?? null
484
+
485
+ proxyConnectors.push({
486
+ key,
487
+ sourceAnchorId: canonicalSourceAnchorId,
488
+ targetAnchorId: canonicalTargetAnchorId,
489
+ direction: 'merged',
490
+ style: first.connector.style || 'bezier',
491
+ label: details.label,
492
+ count: details.count,
493
+ sourceElementId: sourceElement?.element_id ?? null,
494
+ targetElementId: targetElement?.element_id ?? null,
495
+ details,
496
+ })
497
+ }
498
+
499
+ return {
500
+ proxyNodes: [...proxyNodesMap.values()],
501
+ proxyConnectors,
502
+ proxyConnectorDetailsByKey,
503
+ }
504
+ }
505
+
506
+ export interface ZUIResolvedConnector {
507
+ key: string
508
+ sourceElementId: number
509
+ targetElementId: number
510
+ sourceAnchorElementId: number
511
+ targetAnchorElementId: number
512
+ sourceNodeId: string
513
+ targetNodeId: string
514
+ direction: string
515
+ style: string
516
+ label: string
517
+ details: ProxyConnectorDetails
518
+ }
519
+
520
+ function endpointPathForOwnerView(snapshot: WorkspaceGraphSnapshot, ownerViewId: number, elementId: number): number[] {
521
+ const placement = chooseBestPlacement(snapshot, elementId, ownerViewId, ownerViewId)
522
+ if (!placement) return [elementId]
523
+ const owners = relativeOwnerElementPath(snapshot, snapshot.ancestorsByViewId[placement.viewId]?.[0] ?? placement.viewId, placement.viewId)
524
+ const path = [...owners]
525
+ if (path[path.length - 1] !== elementId) path.push(elementId)
526
+ return path.length > 0 ? path : [elementId]
527
+ }
528
+
529
+ export function resolveZUIProxyConnectors(
530
+ snapshot: WorkspaceGraphSnapshot | null,
531
+ visibleNodeIdsByElementId: Map<number, string>,
532
+ settings: CrossBranchContextSettings,
533
+ ): ZUIResolvedConnector[] {
534
+ if (!snapshot || !settings.enabled || visibleNodeIdsByElementId.size === 0) return []
535
+
536
+ const visibleElements = new Set(visibleNodeIdsByElementId.keys())
537
+ const grouped = new Map<string, ProxyConnectorLeaf[]>()
538
+
539
+ for (const connector of allConnectors(snapshot)) {
540
+ const sourcePath = endpointPathForOwnerView(snapshot, connector.view_id, connector.source_element_id)
541
+ const targetPath = endpointPathForOwnerView(snapshot, connector.view_id, connector.target_element_id)
542
+
543
+ const sourceAnchorElementId = [...sourcePath].reverse().find((elementId) => visibleElements.has(elementId))
544
+ const targetAnchorElementId = [...targetPath].reverse().find((elementId) => visibleElements.has(elementId))
545
+ if (sourceAnchorElementId == null || targetAnchorElementId == null) continue
546
+ if (sourceAnchorElementId === targetAnchorElementId) continue
547
+ // If both real endpoints are already visible, the normal edge renderer will
548
+ // draw this connector in-place. Only keep connectors that need anchoring to
549
+ // an ancestor/summary node.
550
+ if (
551
+ sourceAnchorElementId === connector.source_element_id &&
552
+ targetAnchorElementId === connector.target_element_id
553
+ ) continue
554
+
555
+ const sourceDepth = Math.max(0, sourcePath.length - 1 - sourcePath.indexOf(sourceAnchorElementId))
556
+ const targetDepth = Math.max(0, targetPath.length - 1 - targetPath.indexOf(targetAnchorElementId))
557
+ if (settings.depth < CROSS_BRANCH_DEPTH_ALL && Math.max(sourceDepth, targetDepth) > settings.depth) continue
558
+
559
+ const sourceEndpoint: ProxyEndpoint = {
560
+ actualElementId: connector.source_element_id,
561
+ actualElementName: firstPlacementForElement(snapshot, connector.source_element_id)?.element.name ?? `Element ${connector.source_element_id}`,
562
+ anchorElementId: sourceAnchorElementId,
563
+ anchorElementName: firstPlacementForElement(snapshot, sourceAnchorElementId)?.element.name ?? `Element ${sourceAnchorElementId}`,
564
+ anchorViewId: firstPlacementForElement(snapshot, sourceAnchorElementId)?.viewId ?? connector.view_id,
565
+ anchorViewName: firstPlacementForElement(snapshot, sourceAnchorElementId)?.viewName ?? viewName(snapshot, connector.view_id),
566
+ placementViewId: connector.view_id,
567
+ placementViewName: viewName(snapshot, connector.view_id),
568
+ depth: sourceDepth,
569
+ externalToView: sourceAnchorElementId !== connector.source_element_id,
570
+ currentBranchElementId: null,
571
+ commonAncestorViewId: null,
572
+ commonAncestorViewName: null,
573
+ }
574
+ const targetEndpoint: ProxyEndpoint = {
575
+ actualElementId: connector.target_element_id,
576
+ actualElementName: firstPlacementForElement(snapshot, connector.target_element_id)?.element.name ?? `Element ${connector.target_element_id}`,
577
+ anchorElementId: targetAnchorElementId,
578
+ anchorElementName: firstPlacementForElement(snapshot, targetAnchorElementId)?.element.name ?? `Element ${targetAnchorElementId}`,
579
+ anchorViewId: firstPlacementForElement(snapshot, targetAnchorElementId)?.viewId ?? connector.view_id,
580
+ anchorViewName: firstPlacementForElement(snapshot, targetAnchorElementId)?.viewName ?? viewName(snapshot, connector.view_id),
581
+ placementViewId: connector.view_id,
582
+ placementViewName: viewName(snapshot, connector.view_id),
583
+ depth: targetDepth,
584
+ externalToView: targetAnchorElementId !== connector.target_element_id,
585
+ currentBranchElementId: null,
586
+ commonAncestorViewId: null,
587
+ commonAncestorViewName: null,
588
+ }
589
+
590
+ const leaf: ProxyConnectorLeaf = {
591
+ connector,
592
+ ownerViewId: connector.view_id,
593
+ ownerViewName: viewName(snapshot, connector.view_id) ?? `View ${connector.view_id}`,
594
+ source: sourceEndpoint,
595
+ target: targetEndpoint,
596
+ }
597
+
598
+ const [leftAnchorElementId, rightAnchorElementId] = canonicalPairElements(sourceAnchorElementId, targetAnchorElementId)
599
+ const key = [leftAnchorElementId, rightAnchorElementId].join('::')
600
+ const existing = grouped.get(key)
601
+ if (existing) existing.push(leaf)
602
+ else grouped.set(key, [leaf])
603
+ }
604
+
605
+ const resolved: ZUIResolvedConnector[] = []
606
+ for (const [key, leaves] of grouped) {
607
+ const [first] = leaves
608
+ const { ownerViewIds, ownerViewNames } = ownerViewsFromLeaves(leaves)
609
+ const [canonicalSourceAnchorElementId, canonicalTargetAnchorElementId] = canonicalPairElements(
610
+ first.source.anchorElementId,
611
+ first.target.anchorElementId,
612
+ )
613
+ const canonicalFirstIsSource = canonicalSourceAnchorElementId === first.source.anchorElementId
614
+ const details: ProxyConnectorDetails = {
615
+ key,
616
+ label: proxyDisplayLabel(leaves),
617
+ count: leaves.length,
618
+ sourceAnchorId: visibleNodeKey(canonicalSourceAnchorElementId),
619
+ targetAnchorId: visibleNodeKey(canonicalTargetAnchorElementId),
620
+ sourceAnchorName: canonicalFirstIsSource ? first.source.anchorElementName : first.target.anchorElementName,
621
+ targetAnchorName: canonicalFirstIsSource ? first.target.anchorElementName : first.source.anchorElementName,
622
+ ownerViewIds,
623
+ ownerViewNames,
624
+ connectors: leaves,
625
+ }
626
+
627
+ resolved.push({
628
+ key,
629
+ sourceElementId: canonicalFirstIsSource ? first.source.actualElementId : first.target.actualElementId,
630
+ targetElementId: canonicalFirstIsSource ? first.target.actualElementId : first.source.actualElementId,
631
+ sourceAnchorElementId: canonicalSourceAnchorElementId,
632
+ targetAnchorElementId: canonicalTargetAnchorElementId,
633
+ sourceNodeId: visibleNodeIdsByElementId.get(canonicalSourceAnchorElementId) ?? '',
634
+ targetNodeId: visibleNodeIdsByElementId.get(canonicalTargetAnchorElementId) ?? '',
635
+ direction: 'merged',
636
+ style: first.connector.style || 'bezier',
637
+ label: details.label,
638
+ details,
639
+ })
640
+ }
641
+
642
+ return resolved.filter((connector) => connector.sourceNodeId && connector.targetNodeId)
643
+ }