@andespindola/brainlink 0.1.0-beta.135 → 0.1.0-beta.137

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.
@@ -30,7 +30,7 @@ const worldCoordinateLimit = 5_000_000
30
30
  const transformCoordinateLimit = 20_000_000
31
31
  const hoverHitTestIntervalMs = 64
32
32
  const zoomRecoveryGuardMs = 4200
33
- const hierarchyGroupEdgeLimit = 900
33
+ const hierarchyAbsoluteEdgeSafetyCap = 24_000
34
34
  const dragNeighborhoodMaxAffected = 180
35
35
  const dragSettleRounds = 3
36
36
  const wheelZoomExponent = 0.0009
@@ -89,6 +89,7 @@ const state = {
89
89
  lastHoverHitAt: 0,
90
90
  lastManualZoomAt: 0,
91
91
  lastZoomFocus: { x: 0, y: 0, at: 0 },
92
+ leafFocusRootNodeId: null,
92
93
  hierarchyFocusGroupId: null,
93
94
  hierarchyFocusStack: [],
94
95
  hierarchyRevealFocusGroupId: null,
@@ -1337,6 +1338,68 @@ const arrangeChildGraphNodes = (nodes, group, origin = group) => {
1337
1338
  return arranged
1338
1339
  }
1339
1340
 
1341
+ const projectNodesIntoChildGraph = (nodes, focusRenderNode, group) => {
1342
+ if (nodes.length <= 1) {
1343
+ return nodes.map(node => ({ ...node, x: focusRenderNode.x, y: focusRenderNode.y }))
1344
+ }
1345
+
1346
+ let minX = Number.POSITIVE_INFINITY
1347
+ let maxX = Number.NEGATIVE_INFINITY
1348
+ let minY = Number.POSITIVE_INFINITY
1349
+ let maxY = Number.NEGATIVE_INFINITY
1350
+
1351
+ for (let index = 0; index < nodes.length; index += 1) {
1352
+ const node = nodes[index]
1353
+ minX = Math.min(minX, node.x)
1354
+ maxX = Math.max(maxX, node.x)
1355
+ minY = Math.min(minY, node.y)
1356
+ maxY = Math.max(maxY, node.y)
1357
+ }
1358
+
1359
+ const centerX = (minX + maxX) / 2
1360
+ const centerY = (minY + maxY) / 2
1361
+ const spanX = Math.max(1, maxX - minX)
1362
+ const spanY = Math.max(1, maxY - minY)
1363
+ const targetDiameter = Math.max(1, childGraphRenderRadius(group) * 2 * 0.9)
1364
+ const scale = targetDiameter / Math.max(spanX, spanY)
1365
+
1366
+ return nodes.map(node => ({
1367
+ ...node,
1368
+ x: focusRenderNode.x + (node.x - centerX) * scale,
1369
+ y: focusRenderNode.y + (node.y - centerY) * scale
1370
+ }))
1371
+ }
1372
+
1373
+ const recursiveLeafSubgraphNodes = (rootNodeId, maxNodes = renderNodeBudget) => {
1374
+ const root = state.nodeById.get(rootNodeId)
1375
+ if (!root) {
1376
+ return []
1377
+ }
1378
+
1379
+ const visited = new Set([rootNodeId])
1380
+ const queue = [rootNodeId]
1381
+
1382
+ while (queue.length > 0 && visited.size < maxNodes) {
1383
+ const currentId = queue.shift()
1384
+ const edges = [...(state.visibleEdgeByNode.get(currentId) ?? [])]
1385
+ .filter(edge => edge.target)
1386
+ .sort((left, right) => edgeWeight(right) - edgeWeight(left))
1387
+ for (let edgeIndex = 0; edgeIndex < edges.length && visited.size < maxNodes; edgeIndex += 1) {
1388
+ const edge = edges[edgeIndex]
1389
+ const nextId = edge.source === currentId ? edge.target : edge.source
1390
+ if (!nextId || visited.has(nextId) || !state.nodeById.has(nextId)) {
1391
+ continue
1392
+ }
1393
+ visited.add(nextId)
1394
+ queue.push(nextId)
1395
+ }
1396
+ }
1397
+
1398
+ return [...visited]
1399
+ .map(id => state.nodeById.get(id))
1400
+ .filter(Boolean)
1401
+ }
1402
+
1340
1403
  const arrangeChildGroupNodes = (groups, parentGroup, origin) => {
1341
1404
  if (groups.length <= 1) {
1342
1405
  return groups.map(group => ({
@@ -1502,6 +1565,7 @@ const updateHierarchyFocusGroup = (groups, _viewport) => {
1502
1565
  state.selected = null
1503
1566
  }
1504
1567
  }
1568
+ state.leafFocusRootNodeId = null
1505
1569
  state.hierarchyFocusGroupId = null
1506
1570
  return null
1507
1571
  }
@@ -1542,7 +1606,7 @@ const hierarchyViewportProgress = (group, _viewport) => {
1542
1606
  return Math.pow(Math.max(0, Math.min(1, transitionScaleProgress)), 2.8)
1543
1607
  }
1544
1608
 
1545
- const groupEdgesForRenderedGroups = (groupNodes) => {
1609
+ const groupEdgesForRenderedGroups = (groupNodes, options = { preferComplete: false }) => {
1546
1610
  if (groupNodes.length <= 1) {
1547
1611
  return []
1548
1612
  }
@@ -1586,9 +1650,12 @@ const groupEdgesForRenderedGroups = (groupNodes) => {
1586
1650
  })
1587
1651
  }
1588
1652
 
1589
- return Array.from(selected.values())
1653
+ const sorted = Array.from(selected.values())
1590
1654
  .sort((left, right) => edgeWeight(right) - edgeWeight(left) || left.source.localeCompare(right.source) || left.target.localeCompare(right.target))
1591
- .slice(0, Math.min(hierarchyGroupEdgeLimit, edgeBudgetForCurrentFrame()))
1655
+ if (options.preferComplete) {
1656
+ return sorted.slice(0, Math.min(sorted.length, hierarchyAbsoluteEdgeSafetyCap))
1657
+ }
1658
+ return sorted.slice(0, Math.min(edgeBudgetForCurrentFrame(), hierarchyAbsoluteEdgeSafetyCap))
1592
1659
  }
1593
1660
 
1594
1661
  const computeHierarchyRenderVisibility = (viewport) => {
@@ -1610,7 +1677,7 @@ const computeHierarchyRenderVisibility = (viewport) => {
1610
1677
  state.hierarchyRevealFocusGroupId = null
1611
1678
  state.hierarchyRevealBudget = 1
1612
1679
  state.renderNodes = groupNodes
1613
- state.renderEdges = groupEdgesForRenderedGroups(groupNodes)
1680
+ state.renderEdges = groupEdgesForRenderedGroups(groupNodes, { preferComplete: true })
1614
1681
  return true
1615
1682
  }
1616
1683
 
@@ -1619,12 +1686,23 @@ const computeHierarchyRenderVisibility = (viewport) => {
1619
1686
  const focusChildGroups = focus.childGroupIds
1620
1687
  .map(groupId => state.groupById.get(groupId))
1621
1688
  .filter(Boolean)
1689
+ if (focusChildGroups.length > 0) {
1690
+ state.leafFocusRootNodeId = null
1691
+ } else if (state.leafFocusRootNodeId && !focusIds.has(state.leafFocusRootNodeId)) {
1692
+ state.leafFocusRootNodeId = null
1693
+ }
1622
1694
  const childTargetLimit = Math.min(renderNodeBudget, Math.max(1, Math.floor(renderNodeBudget * revealProgress)))
1623
1695
  const childLimit = updateHierarchyChildRevealBudget(focus.id, childTargetLimit)
1624
1696
  const focusRenderNode = groupNodes.find(node => node.groupId === focus.id) ?? createGroupRenderNode(focus)
1625
- const arrangedChildren = focusChildGroups.length > 0
1626
- ? arrangeChildGroupNodes(focusChildGroups, focus, focusRenderNode)
1627
- : arrangeChildGraphNodes(rawChildNodes, focus, focusRenderNode)
1697
+ const arrangedChildren = (() => {
1698
+ if (focusChildGroups.length > 0) {
1699
+ return arrangeChildGroupNodes(focusChildGroups, focus, focusRenderNode)
1700
+ }
1701
+ const recursiveNodes = state.leafFocusRootNodeId
1702
+ ? recursiveLeafSubgraphNodes(state.leafFocusRootNodeId, renderNodeBudget)
1703
+ : rawChildNodes
1704
+ return projectNodesIntoChildGraph(recursiveNodes, focusRenderNode, focus)
1705
+ })()
1628
1706
  const childNodes = selectStableSampleNodes(arrangedChildren, childLimit)
1629
1707
  .map(node => interpolateNodeFromGroup(node, focusRenderNode, revealProgress))
1630
1708
  const childIds = new Set(childNodes.map(node => node.id))
@@ -2118,6 +2196,7 @@ const fitView = (options = { useFiltered: true, preferHubCenter: true }) => {
2118
2196
  const resetView = () => fitView({ useFiltered: false, preferHubCenter: false })
2119
2197
 
2120
2198
  const resetHierarchyFocus = () => {
2199
+ state.leafFocusRootNodeId = null
2121
2200
  state.hierarchyFocusGroupId = null
2122
2201
  state.hierarchyFocusStack = []
2123
2202
  state.hierarchyRevealFocusGroupId = null
@@ -3125,6 +3204,22 @@ const expandGroupNode = node => {
3125
3204
  }
3126
3205
  }
3127
3206
 
3207
+ const expandLeafNodeGraph = node => {
3208
+ if (!node || node.isGroupNode) return
3209
+ state.selected = node
3210
+ state.leafFocusRootNodeId = node.id
3211
+ state.zoomTransition = {
3212
+ active: true,
3213
+ source: 'group',
3214
+ screenX: state.viewport.width / 2,
3215
+ screenY: state.viewport.height / 2,
3216
+ worldX: node.x,
3217
+ worldY: node.y,
3218
+ targetScale: clampScale(Math.max(state.transform.scale * 1.08, hierarchyMicroExitScale * 1.02))
3219
+ }
3220
+ state.lastZoomFocus = { x: node.x, y: node.y, at: performance.now() }
3221
+ markRenderDirty()
3222
+ }
3128
3223
  const handleGroupNodePrimaryClick = node => {
3129
3224
  if (!node?.isGroupNode) return
3130
3225
  const alreadyFocused = state.selected?.isGroupNode && state.selected.groupId === node.groupId
@@ -3135,8 +3230,19 @@ const handleGroupNodePrimaryClick = node => {
3135
3230
  expandGroupNode(node)
3136
3231
  }
3137
3232
 
3233
+ const handleLeafNodePrimaryClick = node => {
3234
+ if (!node || node.isGroupNode) return
3235
+ const sameFocusedNode = state.leafFocusRootNodeId === node.id
3236
+ if (!sameFocusedNode) {
3237
+ expandLeafNodeGraph(node)
3238
+ return
3239
+ }
3240
+ selectNode(node, { openContent: true })
3241
+ }
3242
+
3138
3243
  const selectNode = (node, options = { openContent: false }) => {
3139
3244
  if (!node) {
3245
+ state.leafFocusRootNodeId = null
3140
3246
  state.selected = null
3141
3247
  if (elements.contentDialog.open) {
3142
3248
  elements.contentDialog.close()
@@ -3373,6 +3479,8 @@ const bindEvents = () => {
3373
3479
  const clickedNode = draggedNode ?? state.hovered
3374
3480
  if (clickedNode?.isGroupNode) {
3375
3481
  handleGroupNodePrimaryClick(clickedNode)
3482
+ } else if (clickedNode && state.hierarchyFocusGroupId) {
3483
+ handleLeafNodePrimaryClick(clickedNode)
3376
3484
  } else {
3377
3485
  selectNode(clickedNode, { openContent: true })
3378
3486
  }
@@ -3462,6 +3570,7 @@ const loadGraph = async (options = { reset: false }) => {
3462
3570
  state.graph = graph
3463
3571
  state.nodes = layout.nodes
3464
3572
  state.groups = layout.groups
3573
+ state.leafFocusRootNodeId = null
3465
3574
  state.hierarchyFocusGroupId = null
3466
3575
  state.hierarchyFocusStack = []
3467
3576
  state.hierarchyRevealFocusGroupId = null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.135",
3
+ "version": "0.1.0-beta.137",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",