@andespindola/brainlink 0.1.0-beta.131 → 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 +52 -26
- 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
|
|
|
@@ -16,7 +16,9 @@ const hierarchyMicroEnterCoverage = 0.58
|
|
|
16
16
|
const hierarchyMicroExitCoverage = 0.38
|
|
17
17
|
const hierarchyMicroEnterScale = 0.18
|
|
18
18
|
const hierarchyMicroExitScale = 0.12
|
|
19
|
-
const
|
|
19
|
+
const hierarchyFocusedOnlyProgress = 0.5
|
|
20
|
+
const hierarchyFocusedOnlyCoverage = 0.4
|
|
21
|
+
const hierarchyChildGraphFitMargin = 1.28
|
|
20
22
|
const minNodePixelRadius = 2.3
|
|
21
23
|
const viewportPaddingPx = 280
|
|
22
24
|
const worldCoordinateLimit = 5_000_000
|
|
@@ -647,7 +649,7 @@ const createVisibleEdgeLookup = edges => {
|
|
|
647
649
|
|
|
648
650
|
const edgeBudgetForCurrentFrame = () => {
|
|
649
651
|
const zoom = state.transform.scale
|
|
650
|
-
if (zoom < 0.12) return 380
|
|
652
|
+
if (zoom < 0.12) return state.groups.length > 0 ? 1200 : 380
|
|
651
653
|
if (zoom < 0.18) return 900
|
|
652
654
|
if (zoom < 0.28) return 1700
|
|
653
655
|
if (zoom < 0.45) return 2800
|
|
@@ -1319,6 +1321,43 @@ const arrangeChildGraphNodes = (nodes, group, origin = group) => {
|
|
|
1319
1321
|
return arranged
|
|
1320
1322
|
}
|
|
1321
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
|
+
|
|
1322
1361
|
const interpolateNodeFromGroup = (node, origin, progress) => {
|
|
1323
1362
|
return {
|
|
1324
1363
|
...node,
|
|
@@ -1364,7 +1403,7 @@ const hierarchyGroupsForScale = () => {
|
|
|
1364
1403
|
const groupViewportCoverage = (group, viewport) => {
|
|
1365
1404
|
const viewportWidth = Math.max(viewport.maxX - viewport.minX, 1)
|
|
1366
1405
|
const viewportHeight = Math.max(viewport.maxY - viewport.minY, 1)
|
|
1367
|
-
const viewportRadius = Math.
|
|
1406
|
+
const viewportRadius = Math.min(viewportWidth, viewportHeight) / 2
|
|
1368
1407
|
const centerX = (viewport.minX + viewport.maxX) / 2
|
|
1369
1408
|
const centerY = (viewport.minY + viewport.maxY) / 2
|
|
1370
1409
|
const centerDistance = Math.hypot(group.x - centerX, group.y - centerY)
|
|
@@ -1440,8 +1479,8 @@ const updateHierarchyFocusGroup = (groups, viewport) => {
|
|
|
1440
1479
|
|
|
1441
1480
|
if (
|
|
1442
1481
|
candidate &&
|
|
1443
|
-
state.transform.scale >=
|
|
1444
|
-
candidate.coverage >=
|
|
1482
|
+
state.transform.scale >= hierarchyMicroExitScale &&
|
|
1483
|
+
candidate.coverage >= hierarchyMicroExitCoverage
|
|
1445
1484
|
) {
|
|
1446
1485
|
state.hierarchyFocusGroupId = candidate.group.id
|
|
1447
1486
|
if (candidate.group.childGroupIds.length > 0 && state.hierarchyFocusStack.at(-1) !== candidate.group.id) {
|
|
@@ -1508,19 +1547,8 @@ const groupEdgesForRenderedGroups = (groupNodes) => {
|
|
|
1508
1547
|
})
|
|
1509
1548
|
}
|
|
1510
1549
|
|
|
1511
|
-
const degreeCounts = new Map()
|
|
1512
1550
|
return Array.from(selected.values())
|
|
1513
1551
|
.sort((left, right) => edgeWeight(right) - edgeWeight(left) || left.source.localeCompare(right.source) || left.target.localeCompare(right.target))
|
|
1514
|
-
.filter((edge) => {
|
|
1515
|
-
const sourceCount = degreeCounts.get(edge.source) ?? 0
|
|
1516
|
-
const targetCount = degreeCounts.get(edge.target) ?? 0
|
|
1517
|
-
if (sourceCount >= 3 || targetCount >= 3) {
|
|
1518
|
-
return false
|
|
1519
|
-
}
|
|
1520
|
-
degreeCounts.set(edge.source, sourceCount + 1)
|
|
1521
|
-
degreeCounts.set(edge.target, targetCount + 1)
|
|
1522
|
-
return true
|
|
1523
|
-
})
|
|
1524
1552
|
.slice(0, Math.min(hierarchyGroupEdgeLimit, edgeBudgetForCurrentFrame()))
|
|
1525
1553
|
}
|
|
1526
1554
|
|
|
@@ -1547,25 +1575,23 @@ const computeHierarchyRenderVisibility = (viewport) => {
|
|
|
1547
1575
|
const focusChildGroups = focus.childGroupIds
|
|
1548
1576
|
.map(groupId => state.groupById.get(groupId))
|
|
1549
1577
|
.filter(Boolean)
|
|
1550
|
-
const childLimit = Math.
|
|
1578
|
+
const childLimit = Math.min(renderNodeBudget, Math.max(1, Math.floor(renderNodeBudget * progress)))
|
|
1551
1579
|
const focusRenderNode = groupNodes.find(node => node.groupId === focus.id) ?? createGroupRenderNode(focus)
|
|
1552
1580
|
const arrangedChildren = focusChildGroups.length > 0
|
|
1553
|
-
?
|
|
1554
|
-
...createGroupRenderNode(group),
|
|
1555
|
-
x: focusRenderNode.x + group.x,
|
|
1556
|
-
y: focusRenderNode.y + group.y
|
|
1557
|
-
}))
|
|
1581
|
+
? arrangeChildGroupNodes(focusChildGroups, focus, focusRenderNode)
|
|
1558
1582
|
: arrangeChildGraphNodes(rawChildNodes, focus, focusRenderNode)
|
|
1559
1583
|
const childNodes = selectStableSampleNodes(arrangedChildren, childLimit)
|
|
1560
1584
|
.map(node => interpolateNodeFromGroup(node, focusRenderNode, progress))
|
|
1561
1585
|
const childIds = new Set(childNodes.map(node => node.id))
|
|
1562
1586
|
const childById = new Map(childNodes.map(node => [node.id, node]))
|
|
1563
|
-
const isMicroView =
|
|
1587
|
+
const isMicroView =
|
|
1588
|
+
progress >= hierarchyFocusedOnlyProgress ||
|
|
1589
|
+
groupViewportCoverage(focus, viewport) >= hierarchyFocusedOnlyCoverage
|
|
1564
1590
|
const visibleGroupNodes = isMicroView
|
|
1565
1591
|
? []
|
|
1566
1592
|
: groupNodes.filter(node => node.groupId !== focus.id || progress < 0.92)
|
|
1567
1593
|
const groupEdges = isMicroView ? [] : groupEdgesForRenderedGroups(visibleGroupNodes)
|
|
1568
|
-
const childEdges = progress > 0.
|
|
1594
|
+
const childEdges = (isMicroView || progress > 0.02)
|
|
1569
1595
|
? focusChildGroups.length > 0
|
|
1570
1596
|
? groupEdgesForRenderedGroups(childNodes)
|
|
1571
1597
|
: collectVisibleEdgesForNodes(childIds).map(edge => ({
|
|
@@ -2086,7 +2112,7 @@ const childGraphFitTransition = (node, group) => {
|
|
|
2086
2112
|
const width = Math.max(rect.width, 320)
|
|
2087
2113
|
const height = Math.max(rect.height, 320)
|
|
2088
2114
|
const fitScale = childGraphFitScaleForGroup(group, width, height)
|
|
2089
|
-
const nextScale = clampScale(Math.max(fitScale, state.transform.scale * 1.
|
|
2115
|
+
const nextScale = clampScale(Math.max(fitScale, state.transform.scale * 1.08, hierarchyMicroExitScale * 1.02))
|
|
2090
2116
|
|
|
2091
2117
|
return {
|
|
2092
2118
|
active: true,
|
|
@@ -2946,7 +2972,7 @@ const selectNode = (node, options = { openContent: false }) => {
|
|
|
2946
2972
|
screenY: state.viewport.height / 2,
|
|
2947
2973
|
worldX: node.x,
|
|
2948
2974
|
worldY: node.y,
|
|
2949
|
-
targetScale: clampScale(Math.max(state.transform.scale * 1.
|
|
2975
|
+
targetScale: clampScale(Math.max(state.transform.scale * 1.08, hierarchyMicroExitScale * 1.02))
|
|
2950
2976
|
}
|
|
2951
2977
|
state.lastZoomFocus = { x: node.x, y: node.y, at: performance.now() }
|
|
2952
2978
|
state.hierarchyFocusGroupId = node.groupId
|
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