@andespindola/brainlink 0.1.0-beta.136 → 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.
@@ -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
  }
@@ -1622,12 +1686,23 @@ const computeHierarchyRenderVisibility = (viewport) => {
1622
1686
  const focusChildGroups = focus.childGroupIds
1623
1687
  .map(groupId => state.groupById.get(groupId))
1624
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
+ }
1625
1694
  const childTargetLimit = Math.min(renderNodeBudget, Math.max(1, Math.floor(renderNodeBudget * revealProgress)))
1626
1695
  const childLimit = updateHierarchyChildRevealBudget(focus.id, childTargetLimit)
1627
1696
  const focusRenderNode = groupNodes.find(node => node.groupId === focus.id) ?? createGroupRenderNode(focus)
1628
- const arrangedChildren = focusChildGroups.length > 0
1629
- ? arrangeChildGroupNodes(focusChildGroups, focus, focusRenderNode)
1630
- : 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
+ })()
1631
1706
  const childNodes = selectStableSampleNodes(arrangedChildren, childLimit)
1632
1707
  .map(node => interpolateNodeFromGroup(node, focusRenderNode, revealProgress))
1633
1708
  const childIds = new Set(childNodes.map(node => node.id))
@@ -2121,6 +2196,7 @@ const fitView = (options = { useFiltered: true, preferHubCenter: true }) => {
2121
2196
  const resetView = () => fitView({ useFiltered: false, preferHubCenter: false })
2122
2197
 
2123
2198
  const resetHierarchyFocus = () => {
2199
+ state.leafFocusRootNodeId = null
2124
2200
  state.hierarchyFocusGroupId = null
2125
2201
  state.hierarchyFocusStack = []
2126
2202
  state.hierarchyRevealFocusGroupId = null
@@ -3128,6 +3204,22 @@ const expandGroupNode = node => {
3128
3204
  }
3129
3205
  }
3130
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
+ }
3131
3223
  const handleGroupNodePrimaryClick = node => {
3132
3224
  if (!node?.isGroupNode) return
3133
3225
  const alreadyFocused = state.selected?.isGroupNode && state.selected.groupId === node.groupId
@@ -3138,8 +3230,19 @@ const handleGroupNodePrimaryClick = node => {
3138
3230
  expandGroupNode(node)
3139
3231
  }
3140
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
+
3141
3243
  const selectNode = (node, options = { openContent: false }) => {
3142
3244
  if (!node) {
3245
+ state.leafFocusRootNodeId = null
3143
3246
  state.selected = null
3144
3247
  if (elements.contentDialog.open) {
3145
3248
  elements.contentDialog.close()
@@ -3376,6 +3479,8 @@ const bindEvents = () => {
3376
3479
  const clickedNode = draggedNode ?? state.hovered
3377
3480
  if (clickedNode?.isGroupNode) {
3378
3481
  handleGroupNodePrimaryClick(clickedNode)
3482
+ } else if (clickedNode && state.hierarchyFocusGroupId) {
3483
+ handleLeafNodePrimaryClick(clickedNode)
3379
3484
  } else {
3380
3485
  selectNode(clickedNode, { openContent: true })
3381
3486
  }
@@ -3465,6 +3570,7 @@ const loadGraph = async (options = { reset: false }) => {
3465
3570
  state.graph = graph
3466
3571
  state.nodes = layout.nodes
3467
3572
  state.groups = layout.groups
3573
+ state.leafFocusRootNodeId = null
3468
3574
  state.hierarchyFocusGroupId = null
3469
3575
  state.hierarchyFocusStack = []
3470
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.136",
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",