@andespindola/brainlink 0.1.0-beta.130 → 0.1.0-beta.132
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.
- package/README.md +1 -1
- package/dist/application/frontend/client-js.js +152 -40
- package/docs/AGENT_USAGE.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -606,7 +606,7 @@ The graph UI shows:
|
|
|
606
606
|
- graph rendering safeguards (batched canvas drawing across graph sizes, edge draw caps, lower redraw rate, zoom-aware interaction)
|
|
607
607
|
- adaptive CPU safeguards for large graphs: idle frame pacing, throttled background physics updates and cached viewport dimensions to reduce redraw/layout overhead while preserving interaction responsiveness
|
|
608
608
|
- WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
|
|
609
|
-
- large graph LOD keeps a recursive graph-of-graphs model: zoom-out fills the projected macro level toward 1000 lightweight group nodes with
|
|
609
|
+
- large graph LOD keeps a recursive graph-of-graphs model: zoom-out fills the projected macro level toward 1000 lightweight group nodes with every aggregated link between visible groups, zoom-in grows the focused node into a local child graph anchored at the same point, once the child graph is framed the sibling groups stop rendering, groups with child groups open another graph level instead of jumping to leaf notes, and zoom-out restores sibling groups
|
|
610
610
|
|
|
611
611
|
The server indexes before starting by default. Use `--no-index` to skip that step:
|
|
612
612
|
|
|
@@ -12,10 +12,13 @@ const massiveSegmentedScaleThreshold = 0.45
|
|
|
12
12
|
const massiveSegmentRepresentativeBudget = 760
|
|
13
13
|
const massiveAutoFitMacroScale = 0.018
|
|
14
14
|
const hierarchyExpansionStartScale = 0.18
|
|
15
|
-
const hierarchyMicroEnterCoverage = 0.
|
|
16
|
-
const hierarchyMicroExitCoverage = 0.
|
|
17
|
-
const hierarchyMicroEnterScale = 0.
|
|
18
|
-
const hierarchyMicroExitScale = 0.
|
|
15
|
+
const hierarchyMicroEnterCoverage = 0.58
|
|
16
|
+
const hierarchyMicroExitCoverage = 0.38
|
|
17
|
+
const hierarchyMicroEnterScale = 0.18
|
|
18
|
+
const hierarchyMicroExitScale = 0.12
|
|
19
|
+
const hierarchyFocusedOnlyProgress = 0.5
|
|
20
|
+
const hierarchyFocusedOnlyCoverage = 0.4
|
|
21
|
+
const hierarchyChildGraphFitMargin = 1.28
|
|
19
22
|
const minNodePixelRadius = 2.3
|
|
20
23
|
const viewportPaddingPx = 280
|
|
21
24
|
const worldCoordinateLimit = 5_000_000
|
|
@@ -82,6 +85,7 @@ const state = {
|
|
|
82
85
|
lastManualZoomAt: 0,
|
|
83
86
|
lastZoomFocus: { x: 0, y: 0, at: 0 },
|
|
84
87
|
hierarchyFocusGroupId: null,
|
|
88
|
+
hierarchyFocusStack: [],
|
|
85
89
|
zoomTransition: {
|
|
86
90
|
active: false,
|
|
87
91
|
source: 'generic',
|
|
@@ -645,7 +649,7 @@ const createVisibleEdgeLookup = edges => {
|
|
|
645
649
|
|
|
646
650
|
const edgeBudgetForCurrentFrame = () => {
|
|
647
651
|
const zoom = state.transform.scale
|
|
648
|
-
if (zoom < 0.12) return 380
|
|
652
|
+
if (zoom < 0.12) return state.groups.length > 0 ? 1200 : 380
|
|
649
653
|
if (zoom < 0.18) return 900
|
|
650
654
|
if (zoom < 0.28) return 1700
|
|
651
655
|
if (zoom < 0.45) return 2800
|
|
@@ -1317,6 +1321,43 @@ const arrangeChildGraphNodes = (nodes, group, origin = group) => {
|
|
|
1317
1321
|
return arranged
|
|
1318
1322
|
}
|
|
1319
1323
|
|
|
1324
|
+
const arrangeChildGroupNodes = (groups, parentGroup, origin) => {
|
|
1325
|
+
if (groups.length <= 1) {
|
|
1326
|
+
return groups.map(group => ({
|
|
1327
|
+
...createGroupRenderNode(group),
|
|
1328
|
+
x: origin.x,
|
|
1329
|
+
y: origin.y
|
|
1330
|
+
}))
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const targetRadius = childGraphRenderRadius(parentGroup)
|
|
1334
|
+
const centerGroup = groups
|
|
1335
|
+
.map(group => ({
|
|
1336
|
+
group,
|
|
1337
|
+
score: Math.max(group.nodeIds.length, group.childGroupIds.length, 1)
|
|
1338
|
+
}))
|
|
1339
|
+
.sort((left, right) => right.score - left.score || left.group.title.localeCompare(right.group.title))[0]?.group
|
|
1340
|
+
const outerGroups = groups
|
|
1341
|
+
.filter(group => group.id !== centerGroup?.id)
|
|
1342
|
+
.sort((left, right) => left.segment.localeCompare(right.segment) || left.title.localeCompare(right.title))
|
|
1343
|
+
const goldenAngle = Math.PI * (3 - Math.sqrt(5))
|
|
1344
|
+
const arranged = centerGroup
|
|
1345
|
+
? [{ ...createGroupRenderNode(centerGroup), x: origin.x, y: origin.y }]
|
|
1346
|
+
: []
|
|
1347
|
+
|
|
1348
|
+
outerGroups.forEach((group, index) => {
|
|
1349
|
+
const ringRadius = targetRadius * Math.sqrt((index + 1) / Math.max(outerGroups.length, 1))
|
|
1350
|
+
const angle = index * goldenAngle
|
|
1351
|
+
arranged.push({
|
|
1352
|
+
...createGroupRenderNode(group),
|
|
1353
|
+
x: origin.x + Math.cos(angle) * ringRadius,
|
|
1354
|
+
y: origin.y + Math.sin(angle) * ringRadius
|
|
1355
|
+
})
|
|
1356
|
+
})
|
|
1357
|
+
|
|
1358
|
+
return arranged
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1320
1361
|
const interpolateNodeFromGroup = (node, origin, progress) => {
|
|
1321
1362
|
return {
|
|
1322
1363
|
...node,
|
|
@@ -1330,20 +1371,39 @@ const interpolateNodeFromGroup = (node, origin, progress) => {
|
|
|
1330
1371
|
const parentHierarchyGroups = () =>
|
|
1331
1372
|
state.groups.filter(group => group.parentId === null)
|
|
1332
1373
|
|
|
1374
|
+
const activeHierarchyParentGroup = () => {
|
|
1375
|
+
for (let index = state.hierarchyFocusStack.length - 1; index >= 0; index -= 1) {
|
|
1376
|
+
const group = state.groupById.get(state.hierarchyFocusStack[index])
|
|
1377
|
+
if (group && group.childGroupIds.length > 0) {
|
|
1378
|
+
return group
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return null
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const groupsBelongToParent = (groups, parentGroup) =>
|
|
1385
|
+
Boolean(parentGroup) && groups.some(group =>
|
|
1386
|
+
group.parentId === parentGroup.id || parentGroup.childGroupIds.includes(group.id)
|
|
1387
|
+
)
|
|
1388
|
+
|
|
1333
1389
|
const hierarchyGroupsForScale = () => {
|
|
1334
1390
|
if (state.groups.length === 0) {
|
|
1335
1391
|
return []
|
|
1336
1392
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1393
|
+
const parentGroup = activeHierarchyParentGroup()
|
|
1394
|
+
if (parentGroup) {
|
|
1395
|
+
const childGroups = parentGroup.childGroupIds
|
|
1396
|
+
.map(groupId => state.groupById.get(groupId))
|
|
1397
|
+
.filter(Boolean)
|
|
1398
|
+
return arrangeGraphLevelNodes(childGroups, groupRenderRadius)
|
|
1339
1399
|
}
|
|
1340
|
-
return arrangeGraphLevelNodes(
|
|
1400
|
+
return arrangeGraphLevelNodes(parentHierarchyGroups(), groupRenderRadius)
|
|
1341
1401
|
}
|
|
1342
1402
|
|
|
1343
1403
|
const groupViewportCoverage = (group, viewport) => {
|
|
1344
1404
|
const viewportWidth = Math.max(viewport.maxX - viewport.minX, 1)
|
|
1345
1405
|
const viewportHeight = Math.max(viewport.maxY - viewport.minY, 1)
|
|
1346
|
-
const viewportRadius = Math.
|
|
1406
|
+
const viewportRadius = Math.min(viewportWidth, viewportHeight) / 2
|
|
1347
1407
|
const centerX = (viewport.minX + viewport.maxX) / 2
|
|
1348
1408
|
const centerY = (viewport.minY + viewport.maxY) / 2
|
|
1349
1409
|
const centerDistance = Math.hypot(group.x - centerX, group.y - centerY)
|
|
@@ -1386,6 +1446,17 @@ const selectViewportItemsWithFill = (items, viewport, limit = renderNodeBudget)
|
|
|
1386
1446
|
}
|
|
1387
1447
|
|
|
1388
1448
|
const updateHierarchyFocusGroup = (groups, viewport) => {
|
|
1449
|
+
const activeGroupIds = new Set(groups.map(group => group.id))
|
|
1450
|
+
state.hierarchyFocusStack = state.hierarchyFocusStack.filter(groupId => state.groupById.has(groupId))
|
|
1451
|
+
const parentGroup = activeHierarchyParentGroup()
|
|
1452
|
+
if (!groupsBelongToParent(groups, parentGroup)) {
|
|
1453
|
+
while (
|
|
1454
|
+
state.hierarchyFocusStack.length > 0 &&
|
|
1455
|
+
!activeGroupIds.has(state.hierarchyFocusStack[state.hierarchyFocusStack.length - 1])
|
|
1456
|
+
) {
|
|
1457
|
+
state.hierarchyFocusStack.pop()
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1389
1460
|
const current = state.hierarchyFocusGroupId
|
|
1390
1461
|
? groups.find(group => group.id === state.hierarchyFocusGroupId) ?? null
|
|
1391
1462
|
: null
|
|
@@ -1396,6 +1467,9 @@ const updateHierarchyFocusGroup = (groups, viewport) => {
|
|
|
1396
1467
|
state.transform.scale >= hierarchyMicroExitScale &&
|
|
1397
1468
|
currentCoverage >= hierarchyMicroExitCoverage
|
|
1398
1469
|
) {
|
|
1470
|
+
if (current.childGroupIds.length > 0 && state.hierarchyFocusStack.at(-1) !== current.id) {
|
|
1471
|
+
state.hierarchyFocusStack = [...state.hierarchyFocusStack, current.id]
|
|
1472
|
+
}
|
|
1399
1473
|
return current
|
|
1400
1474
|
}
|
|
1401
1475
|
|
|
@@ -1405,13 +1479,19 @@ const updateHierarchyFocusGroup = (groups, viewport) => {
|
|
|
1405
1479
|
|
|
1406
1480
|
if (
|
|
1407
1481
|
candidate &&
|
|
1408
|
-
state.transform.scale >=
|
|
1409
|
-
candidate.coverage >=
|
|
1482
|
+
state.transform.scale >= hierarchyMicroExitScale &&
|
|
1483
|
+
candidate.coverage >= hierarchyMicroExitCoverage
|
|
1410
1484
|
) {
|
|
1411
1485
|
state.hierarchyFocusGroupId = candidate.group.id
|
|
1486
|
+
if (candidate.group.childGroupIds.length > 0 && state.hierarchyFocusStack.at(-1) !== candidate.group.id) {
|
|
1487
|
+
state.hierarchyFocusStack = [...state.hierarchyFocusStack, candidate.group.id]
|
|
1488
|
+
}
|
|
1412
1489
|
return candidate.group
|
|
1413
1490
|
}
|
|
1414
1491
|
|
|
1492
|
+
if (state.hierarchyFocusStack.length > 0 && state.transform.scale < hierarchyMicroExitScale) {
|
|
1493
|
+
state.hierarchyFocusStack = state.hierarchyFocusStack.slice(0, -1)
|
|
1494
|
+
}
|
|
1415
1495
|
state.hierarchyFocusGroupId = null
|
|
1416
1496
|
return null
|
|
1417
1497
|
}
|
|
@@ -1467,25 +1547,15 @@ const groupEdgesForRenderedGroups = (groupNodes) => {
|
|
|
1467
1547
|
})
|
|
1468
1548
|
}
|
|
1469
1549
|
|
|
1470
|
-
const degreeCounts = new Map()
|
|
1471
1550
|
return Array.from(selected.values())
|
|
1472
1551
|
.sort((left, right) => edgeWeight(right) - edgeWeight(left) || left.source.localeCompare(right.source) || left.target.localeCompare(right.target))
|
|
1473
|
-
.filter((edge) => {
|
|
1474
|
-
const sourceCount = degreeCounts.get(edge.source) ?? 0
|
|
1475
|
-
const targetCount = degreeCounts.get(edge.target) ?? 0
|
|
1476
|
-
if (sourceCount >= 3 || targetCount >= 3) {
|
|
1477
|
-
return false
|
|
1478
|
-
}
|
|
1479
|
-
degreeCounts.set(edge.source, sourceCount + 1)
|
|
1480
|
-
degreeCounts.set(edge.target, targetCount + 1)
|
|
1481
|
-
return true
|
|
1482
|
-
})
|
|
1483
1552
|
.slice(0, Math.min(hierarchyGroupEdgeLimit, edgeBudgetForCurrentFrame()))
|
|
1484
1553
|
}
|
|
1485
1554
|
|
|
1486
1555
|
const computeHierarchyRenderVisibility = (viewport) => {
|
|
1487
1556
|
if (state.groups.length === 0 || state.visibleNodes.length <= 1000) {
|
|
1488
1557
|
state.hierarchyFocusGroupId = null
|
|
1558
|
+
state.hierarchyFocusStack = []
|
|
1489
1559
|
return false
|
|
1490
1560
|
}
|
|
1491
1561
|
|
|
@@ -1502,25 +1572,33 @@ const computeHierarchyRenderVisibility = (viewport) => {
|
|
|
1502
1572
|
|
|
1503
1573
|
const focusIds = new Set(focus.nodeIds)
|
|
1504
1574
|
const rawChildNodes = state.visibleNodes.filter(node => focusIds.has(node.id))
|
|
1505
|
-
const
|
|
1575
|
+
const focusChildGroups = focus.childGroupIds
|
|
1576
|
+
.map(groupId => state.groupById.get(groupId))
|
|
1577
|
+
.filter(Boolean)
|
|
1578
|
+
const childLimit = Math.min(renderNodeBudget, Math.max(1, Math.floor(renderNodeBudget * progress)))
|
|
1506
1579
|
const focusRenderNode = groupNodes.find(node => node.groupId === focus.id) ?? createGroupRenderNode(focus)
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1580
|
+
const arrangedChildren = focusChildGroups.length > 0
|
|
1581
|
+
? arrangeChildGroupNodes(focusChildGroups, focus, focusRenderNode)
|
|
1582
|
+
: arrangeChildGraphNodes(rawChildNodes, focus, focusRenderNode)
|
|
1583
|
+
const childNodes = selectStableSampleNodes(arrangedChildren, childLimit)
|
|
1584
|
+
.map(node => interpolateNodeFromGroup(node, focusRenderNode, progress))
|
|
1511
1585
|
const childIds = new Set(childNodes.map(node => node.id))
|
|
1512
1586
|
const childById = new Map(childNodes.map(node => [node.id, node]))
|
|
1513
|
-
const isMicroView =
|
|
1587
|
+
const isMicroView =
|
|
1588
|
+
progress >= hierarchyFocusedOnlyProgress ||
|
|
1589
|
+
groupViewportCoverage(focus, viewport) >= hierarchyFocusedOnlyCoverage
|
|
1514
1590
|
const visibleGroupNodes = isMicroView
|
|
1515
1591
|
? []
|
|
1516
1592
|
: groupNodes.filter(node => node.groupId !== focus.id || progress < 0.92)
|
|
1517
1593
|
const groupEdges = isMicroView ? [] : groupEdgesForRenderedGroups(visibleGroupNodes)
|
|
1518
|
-
const childEdges = progress > 0.
|
|
1519
|
-
?
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1594
|
+
const childEdges = (isMicroView || progress > 0.02)
|
|
1595
|
+
? focusChildGroups.length > 0
|
|
1596
|
+
? groupEdgesForRenderedGroups(childNodes)
|
|
1597
|
+
: collectVisibleEdgesForNodes(childIds).map(edge => ({
|
|
1598
|
+
...edge,
|
|
1599
|
+
sourceNode: childById.get(edge.source) ?? edge.sourceNode,
|
|
1600
|
+
targetNode: childById.get(edge.target) ?? edge.targetNode
|
|
1601
|
+
}))
|
|
1524
1602
|
: []
|
|
1525
1603
|
|
|
1526
1604
|
state.renderNodes = mergeUniqueNodes(childNodes, visibleGroupNodes, Math.max(renderNodeBudget, childLimit + visibleGroupNodes.length))
|
|
@@ -1996,6 +2074,11 @@ const fitView = (options = { useFiltered: true, preferHubCenter: true }) => {
|
|
|
1996
2074
|
|
|
1997
2075
|
const resetView = () => fitView({ useFiltered: false, preferHubCenter: false })
|
|
1998
2076
|
|
|
2077
|
+
const resetHierarchyFocus = () => {
|
|
2078
|
+
state.hierarchyFocusGroupId = null
|
|
2079
|
+
state.hierarchyFocusStack = []
|
|
2080
|
+
}
|
|
2081
|
+
|
|
1999
2082
|
const focusPrimaryHub = () => {
|
|
2000
2083
|
const hub = state.primaryHub
|
|
2001
2084
|
if (!hub) {
|
|
@@ -2018,6 +2101,30 @@ const focusPrimaryHub = () => {
|
|
|
2018
2101
|
markRenderDirty()
|
|
2019
2102
|
}
|
|
2020
2103
|
|
|
2104
|
+
const childGraphFitScaleForGroup = (group, viewportWidth, viewportHeight) => {
|
|
2105
|
+
const graphDiameter = Math.max(childGraphRenderRadius(group) * 2 * hierarchyChildGraphFitMargin, 1)
|
|
2106
|
+
const available = Math.max(Math.min(viewportWidth, viewportHeight) - 96, 240)
|
|
2107
|
+
return clampScale(available / graphDiameter)
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
const childGraphFitTransition = (node, group) => {
|
|
2111
|
+
const rect = canvas.getBoundingClientRect()
|
|
2112
|
+
const width = Math.max(rect.width, 320)
|
|
2113
|
+
const height = Math.max(rect.height, 320)
|
|
2114
|
+
const fitScale = childGraphFitScaleForGroup(group, width, height)
|
|
2115
|
+
const nextScale = clampScale(Math.max(fitScale, state.transform.scale * 1.08, hierarchyMicroExitScale * 1.02))
|
|
2116
|
+
|
|
2117
|
+
return {
|
|
2118
|
+
active: true,
|
|
2119
|
+
source: 'group',
|
|
2120
|
+
screenX: width / 2,
|
|
2121
|
+
screenY: height / 2,
|
|
2122
|
+
worldX: node.x,
|
|
2123
|
+
worldY: node.y,
|
|
2124
|
+
targetScale: nextScale
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2021
2128
|
const layoutDensityScaleForNodeCount = (nodeCount) => {
|
|
2022
2129
|
if (nodeCount > 50000) return 0.26
|
|
2023
2130
|
if (nodeCount > 20000) return 0.3
|
|
@@ -2857,19 +2964,21 @@ const openContentDialog = async node => {
|
|
|
2857
2964
|
const selectNode = (node, options = { openContent: false }) => {
|
|
2858
2965
|
if (node?.isGroupNode) {
|
|
2859
2966
|
state.selected = node
|
|
2860
|
-
const
|
|
2861
|
-
|
|
2862
|
-
state.zoomTransition = {
|
|
2967
|
+
const group = state.groupById.get(node.groupId)
|
|
2968
|
+
state.zoomTransition = group ? childGraphFitTransition(node, group) : {
|
|
2863
2969
|
active: true,
|
|
2864
2970
|
source: 'group',
|
|
2865
|
-
screenX:
|
|
2866
|
-
screenY:
|
|
2971
|
+
screenX: state.viewport.width / 2,
|
|
2972
|
+
screenY: state.viewport.height / 2,
|
|
2867
2973
|
worldX: node.x,
|
|
2868
2974
|
worldY: node.y,
|
|
2869
|
-
targetScale
|
|
2975
|
+
targetScale: clampScale(Math.max(state.transform.scale * 1.08, hierarchyMicroExitScale * 1.02))
|
|
2870
2976
|
}
|
|
2871
2977
|
state.lastZoomFocus = { x: node.x, y: node.y, at: performance.now() }
|
|
2872
2978
|
state.hierarchyFocusGroupId = node.groupId
|
|
2979
|
+
if (group?.childGroupIds.length && state.hierarchyFocusStack.at(-1) !== group.id) {
|
|
2980
|
+
state.hierarchyFocusStack = [...state.hierarchyFocusStack, group.id]
|
|
2981
|
+
}
|
|
2873
2982
|
markRenderDirty()
|
|
2874
2983
|
return
|
|
2875
2984
|
}
|
|
@@ -3003,10 +3112,12 @@ const bindEvents = () => {
|
|
|
3003
3112
|
})
|
|
3004
3113
|
if (elements.fit) {
|
|
3005
3114
|
elements.fit.addEventListener('click', () => {
|
|
3115
|
+
resetHierarchyFocus()
|
|
3006
3116
|
focusPrimaryHub()
|
|
3007
3117
|
})
|
|
3008
3118
|
}
|
|
3009
3119
|
elements.reset.addEventListener('click', () => {
|
|
3120
|
+
resetHierarchyFocus()
|
|
3010
3121
|
resetView()
|
|
3011
3122
|
})
|
|
3012
3123
|
elements.contentClose.addEventListener('click', () => elements.contentDialog.close())
|
|
@@ -3175,6 +3286,7 @@ const loadGraph = async (options = { reset: false }) => {
|
|
|
3175
3286
|
state.nodes = layout.nodes
|
|
3176
3287
|
state.groups = layout.groups
|
|
3177
3288
|
state.hierarchyFocusGroupId = null
|
|
3289
|
+
state.hierarchyFocusStack = []
|
|
3178
3290
|
state.groupById = new Map(state.groups.map(group => [group.id, group]))
|
|
3179
3291
|
state.leafGroups = state.groups.filter(group => group.nodeIds.length > 0)
|
|
3180
3292
|
state.nodeLeafGroupById = new Map(state.leafGroups.flatMap(group => group.nodeIds.map(nodeId => [nodeId, group])))
|
package/docs/AGENT_USAGE.md
CHANGED
|
@@ -607,7 +607,7 @@ Without `--vault`, the graph UI serves `$HOME/.brainlink/vault`.
|
|
|
607
607
|
|
|
608
608
|
The frontend includes an agent selector that shows only the agent id. Selecting an agent calls the same read APIs with `agent=<agent-id>` and renders that namespace instead of merging every agent into one graph.
|
|
609
609
|
|
|
610
|
-
Graph navigation controls include zoom in, zoom out, fit visible nodes and reset-to-fit-all nodes. Mouse wheel zoom (including `cmd+scroll` and `ctrl+scroll`) is anchored to the cursor. Keyboard shortcuts are `+` (zoom in), `-` (zoom out) and `0` (reset fit). Double-click on canvas zooms in at cursor position. Totals for notes, links and tags stay visible as floating metrics under the Brainlink title, and node details open on click in a modal (tags, outgoing links, backlinks and Markdown content). Vaults above 1000 notes also expose stable hierarchy groups that fill each visible graph level toward 1000 nodes while keeping every group capped at 1000 child nodes; zoom-out renders the macro level
|
|
610
|
+
Graph navigation controls include zoom in, zoom out, fit visible nodes and reset-to-fit-all nodes. Mouse wheel zoom (including `cmd+scroll` and `ctrl+scroll`) is anchored to the cursor. Keyboard shortcuts are `+` (zoom in), `-` (zoom out) and `0` (reset fit). Double-click on canvas zooms in at cursor position. Totals for notes, links and tags stay visible as floating metrics under the Brainlink title, and node details open on click in a modal (tags, outgoing links, backlinks and Markdown content). Vaults above 1000 notes also expose stable hierarchy groups that fill each visible graph level toward 1000 nodes while keeping every group capped at 1000 child nodes; zoom-out renders the macro level with every aggregated link between visible group nodes, zoom-in grows the focused node into a local child graph anchored at the same point, once the child graph is framed the sibling groups stop rendering, and groups with child groups open another graph level instead of jumping directly to leaf notes.
|
|
611
611
|
During graph filtering, Brainlink keeps hub context nodes visible (`Memory Hub`/`MOC`/high-degree fallback) so filtered views still show relationship anchors.
|
|
612
612
|
|
|
613
613
|
The command reindexes by default, then serves:
|
package/package.json
CHANGED