@andespindola/brainlink 0.1.0-beta.130 → 0.1.0-beta.131
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 +108 -22
- 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 sparse strongest-link edges, zoom-in
|
|
609
|
+
- large graph LOD keeps a recursive graph-of-graphs model: zoom-out fills the projected macro level toward 1000 lightweight group nodes with sparse strongest-link edges, zoom-in fits the framed node's full child-graph circumference with extra margin before expanding, 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,11 @@ 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 hierarchyChildGraphFitMargin = 1.38
|
|
19
20
|
const minNodePixelRadius = 2.3
|
|
20
21
|
const viewportPaddingPx = 280
|
|
21
22
|
const worldCoordinateLimit = 5_000_000
|
|
@@ -82,6 +83,7 @@ const state = {
|
|
|
82
83
|
lastManualZoomAt: 0,
|
|
83
84
|
lastZoomFocus: { x: 0, y: 0, at: 0 },
|
|
84
85
|
hierarchyFocusGroupId: null,
|
|
86
|
+
hierarchyFocusStack: [],
|
|
85
87
|
zoomTransition: {
|
|
86
88
|
active: false,
|
|
87
89
|
source: 'generic',
|
|
@@ -1330,14 +1332,33 @@ const interpolateNodeFromGroup = (node, origin, progress) => {
|
|
|
1330
1332
|
const parentHierarchyGroups = () =>
|
|
1331
1333
|
state.groups.filter(group => group.parentId === null)
|
|
1332
1334
|
|
|
1335
|
+
const activeHierarchyParentGroup = () => {
|
|
1336
|
+
for (let index = state.hierarchyFocusStack.length - 1; index >= 0; index -= 1) {
|
|
1337
|
+
const group = state.groupById.get(state.hierarchyFocusStack[index])
|
|
1338
|
+
if (group && group.childGroupIds.length > 0) {
|
|
1339
|
+
return group
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return null
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const groupsBelongToParent = (groups, parentGroup) =>
|
|
1346
|
+
Boolean(parentGroup) && groups.some(group =>
|
|
1347
|
+
group.parentId === parentGroup.id || parentGroup.childGroupIds.includes(group.id)
|
|
1348
|
+
)
|
|
1349
|
+
|
|
1333
1350
|
const hierarchyGroupsForScale = () => {
|
|
1334
1351
|
if (state.groups.length === 0) {
|
|
1335
1352
|
return []
|
|
1336
1353
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1354
|
+
const parentGroup = activeHierarchyParentGroup()
|
|
1355
|
+
if (parentGroup) {
|
|
1356
|
+
const childGroups = parentGroup.childGroupIds
|
|
1357
|
+
.map(groupId => state.groupById.get(groupId))
|
|
1358
|
+
.filter(Boolean)
|
|
1359
|
+
return arrangeGraphLevelNodes(childGroups, groupRenderRadius)
|
|
1339
1360
|
}
|
|
1340
|
-
return arrangeGraphLevelNodes(
|
|
1361
|
+
return arrangeGraphLevelNodes(parentHierarchyGroups(), groupRenderRadius)
|
|
1341
1362
|
}
|
|
1342
1363
|
|
|
1343
1364
|
const groupViewportCoverage = (group, viewport) => {
|
|
@@ -1386,6 +1407,17 @@ const selectViewportItemsWithFill = (items, viewport, limit = renderNodeBudget)
|
|
|
1386
1407
|
}
|
|
1387
1408
|
|
|
1388
1409
|
const updateHierarchyFocusGroup = (groups, viewport) => {
|
|
1410
|
+
const activeGroupIds = new Set(groups.map(group => group.id))
|
|
1411
|
+
state.hierarchyFocusStack = state.hierarchyFocusStack.filter(groupId => state.groupById.has(groupId))
|
|
1412
|
+
const parentGroup = activeHierarchyParentGroup()
|
|
1413
|
+
if (!groupsBelongToParent(groups, parentGroup)) {
|
|
1414
|
+
while (
|
|
1415
|
+
state.hierarchyFocusStack.length > 0 &&
|
|
1416
|
+
!activeGroupIds.has(state.hierarchyFocusStack[state.hierarchyFocusStack.length - 1])
|
|
1417
|
+
) {
|
|
1418
|
+
state.hierarchyFocusStack.pop()
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1389
1421
|
const current = state.hierarchyFocusGroupId
|
|
1390
1422
|
? groups.find(group => group.id === state.hierarchyFocusGroupId) ?? null
|
|
1391
1423
|
: null
|
|
@@ -1396,6 +1428,9 @@ const updateHierarchyFocusGroup = (groups, viewport) => {
|
|
|
1396
1428
|
state.transform.scale >= hierarchyMicroExitScale &&
|
|
1397
1429
|
currentCoverage >= hierarchyMicroExitCoverage
|
|
1398
1430
|
) {
|
|
1431
|
+
if (current.childGroupIds.length > 0 && state.hierarchyFocusStack.at(-1) !== current.id) {
|
|
1432
|
+
state.hierarchyFocusStack = [...state.hierarchyFocusStack, current.id]
|
|
1433
|
+
}
|
|
1399
1434
|
return current
|
|
1400
1435
|
}
|
|
1401
1436
|
|
|
@@ -1409,9 +1444,15 @@ const updateHierarchyFocusGroup = (groups, viewport) => {
|
|
|
1409
1444
|
candidate.coverage >= hierarchyMicroEnterCoverage
|
|
1410
1445
|
) {
|
|
1411
1446
|
state.hierarchyFocusGroupId = candidate.group.id
|
|
1447
|
+
if (candidate.group.childGroupIds.length > 0 && state.hierarchyFocusStack.at(-1) !== candidate.group.id) {
|
|
1448
|
+
state.hierarchyFocusStack = [...state.hierarchyFocusStack, candidate.group.id]
|
|
1449
|
+
}
|
|
1412
1450
|
return candidate.group
|
|
1413
1451
|
}
|
|
1414
1452
|
|
|
1453
|
+
if (state.hierarchyFocusStack.length > 0 && state.transform.scale < hierarchyMicroExitScale) {
|
|
1454
|
+
state.hierarchyFocusStack = state.hierarchyFocusStack.slice(0, -1)
|
|
1455
|
+
}
|
|
1415
1456
|
state.hierarchyFocusGroupId = null
|
|
1416
1457
|
return null
|
|
1417
1458
|
}
|
|
@@ -1486,6 +1527,7 @@ const groupEdgesForRenderedGroups = (groupNodes) => {
|
|
|
1486
1527
|
const computeHierarchyRenderVisibility = (viewport) => {
|
|
1487
1528
|
if (state.groups.length === 0 || state.visibleNodes.length <= 1000) {
|
|
1488
1529
|
state.hierarchyFocusGroupId = null
|
|
1530
|
+
state.hierarchyFocusStack = []
|
|
1489
1531
|
return false
|
|
1490
1532
|
}
|
|
1491
1533
|
|
|
@@ -1502,12 +1544,20 @@ const computeHierarchyRenderVisibility = (viewport) => {
|
|
|
1502
1544
|
|
|
1503
1545
|
const focusIds = new Set(focus.nodeIds)
|
|
1504
1546
|
const rawChildNodes = state.visibleNodes.filter(node => focusIds.has(node.id))
|
|
1547
|
+
const focusChildGroups = focus.childGroupIds
|
|
1548
|
+
.map(groupId => state.groupById.get(groupId))
|
|
1549
|
+
.filter(Boolean)
|
|
1505
1550
|
const childLimit = Math.max(90, Math.min(renderNodeBudget, Math.floor(renderNodeBudget * progress)))
|
|
1506
1551
|
const focusRenderNode = groupNodes.find(node => node.groupId === focus.id) ?? createGroupRenderNode(focus)
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1552
|
+
const arrangedChildren = focusChildGroups.length > 0
|
|
1553
|
+
? arrangeGraphLevelNodes(focusChildGroups, groupRenderRadius).map(group => ({
|
|
1554
|
+
...createGroupRenderNode(group),
|
|
1555
|
+
x: focusRenderNode.x + group.x,
|
|
1556
|
+
y: focusRenderNode.y + group.y
|
|
1557
|
+
}))
|
|
1558
|
+
: arrangeChildGraphNodes(rawChildNodes, focus, focusRenderNode)
|
|
1559
|
+
const childNodes = selectStableSampleNodes(arrangedChildren, childLimit)
|
|
1560
|
+
.map(node => interpolateNodeFromGroup(node, focusRenderNode, progress))
|
|
1511
1561
|
const childIds = new Set(childNodes.map(node => node.id))
|
|
1512
1562
|
const childById = new Map(childNodes.map(node => [node.id, node]))
|
|
1513
1563
|
const isMicroView = progress >= 0.72
|
|
@@ -1516,11 +1566,13 @@ const computeHierarchyRenderVisibility = (viewport) => {
|
|
|
1516
1566
|
: groupNodes.filter(node => node.groupId !== focus.id || progress < 0.92)
|
|
1517
1567
|
const groupEdges = isMicroView ? [] : groupEdgesForRenderedGroups(visibleGroupNodes)
|
|
1518
1568
|
const childEdges = progress > 0.32
|
|
1519
|
-
?
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1569
|
+
? focusChildGroups.length > 0
|
|
1570
|
+
? groupEdgesForRenderedGroups(childNodes)
|
|
1571
|
+
: collectVisibleEdgesForNodes(childIds).map(edge => ({
|
|
1572
|
+
...edge,
|
|
1573
|
+
sourceNode: childById.get(edge.source) ?? edge.sourceNode,
|
|
1574
|
+
targetNode: childById.get(edge.target) ?? edge.targetNode
|
|
1575
|
+
}))
|
|
1524
1576
|
: []
|
|
1525
1577
|
|
|
1526
1578
|
state.renderNodes = mergeUniqueNodes(childNodes, visibleGroupNodes, Math.max(renderNodeBudget, childLimit + visibleGroupNodes.length))
|
|
@@ -1996,6 +2048,11 @@ const fitView = (options = { useFiltered: true, preferHubCenter: true }) => {
|
|
|
1996
2048
|
|
|
1997
2049
|
const resetView = () => fitView({ useFiltered: false, preferHubCenter: false })
|
|
1998
2050
|
|
|
2051
|
+
const resetHierarchyFocus = () => {
|
|
2052
|
+
state.hierarchyFocusGroupId = null
|
|
2053
|
+
state.hierarchyFocusStack = []
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1999
2056
|
const focusPrimaryHub = () => {
|
|
2000
2057
|
const hub = state.primaryHub
|
|
2001
2058
|
if (!hub) {
|
|
@@ -2018,6 +2075,30 @@ const focusPrimaryHub = () => {
|
|
|
2018
2075
|
markRenderDirty()
|
|
2019
2076
|
}
|
|
2020
2077
|
|
|
2078
|
+
const childGraphFitScaleForGroup = (group, viewportWidth, viewportHeight) => {
|
|
2079
|
+
const graphDiameter = Math.max(childGraphRenderRadius(group) * 2 * hierarchyChildGraphFitMargin, 1)
|
|
2080
|
+
const available = Math.max(Math.min(viewportWidth, viewportHeight) - 96, 240)
|
|
2081
|
+
return clampScale(available / graphDiameter)
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
const childGraphFitTransition = (node, group) => {
|
|
2085
|
+
const rect = canvas.getBoundingClientRect()
|
|
2086
|
+
const width = Math.max(rect.width, 320)
|
|
2087
|
+
const height = Math.max(rect.height, 320)
|
|
2088
|
+
const fitScale = childGraphFitScaleForGroup(group, width, height)
|
|
2089
|
+
const nextScale = clampScale(Math.max(fitScale, state.transform.scale * 1.16, hierarchyMicroEnterScale * 1.04))
|
|
2090
|
+
|
|
2091
|
+
return {
|
|
2092
|
+
active: true,
|
|
2093
|
+
source: 'group',
|
|
2094
|
+
screenX: width / 2,
|
|
2095
|
+
screenY: height / 2,
|
|
2096
|
+
worldX: node.x,
|
|
2097
|
+
worldY: node.y,
|
|
2098
|
+
targetScale: nextScale
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2021
2102
|
const layoutDensityScaleForNodeCount = (nodeCount) => {
|
|
2022
2103
|
if (nodeCount > 50000) return 0.26
|
|
2023
2104
|
if (nodeCount > 20000) return 0.3
|
|
@@ -2857,19 +2938,21 @@ const openContentDialog = async node => {
|
|
|
2857
2938
|
const selectNode = (node, options = { openContent: false }) => {
|
|
2858
2939
|
if (node?.isGroupNode) {
|
|
2859
2940
|
state.selected = node
|
|
2860
|
-
const
|
|
2861
|
-
|
|
2862
|
-
state.zoomTransition = {
|
|
2941
|
+
const group = state.groupById.get(node.groupId)
|
|
2942
|
+
state.zoomTransition = group ? childGraphFitTransition(node, group) : {
|
|
2863
2943
|
active: true,
|
|
2864
2944
|
source: 'group',
|
|
2865
|
-
screenX:
|
|
2866
|
-
screenY:
|
|
2945
|
+
screenX: state.viewport.width / 2,
|
|
2946
|
+
screenY: state.viewport.height / 2,
|
|
2867
2947
|
worldX: node.x,
|
|
2868
2948
|
worldY: node.y,
|
|
2869
|
-
targetScale
|
|
2949
|
+
targetScale: clampScale(Math.max(state.transform.scale * 1.16, hierarchyMicroEnterScale * 1.04))
|
|
2870
2950
|
}
|
|
2871
2951
|
state.lastZoomFocus = { x: node.x, y: node.y, at: performance.now() }
|
|
2872
2952
|
state.hierarchyFocusGroupId = node.groupId
|
|
2953
|
+
if (group?.childGroupIds.length && state.hierarchyFocusStack.at(-1) !== group.id) {
|
|
2954
|
+
state.hierarchyFocusStack = [...state.hierarchyFocusStack, group.id]
|
|
2955
|
+
}
|
|
2873
2956
|
markRenderDirty()
|
|
2874
2957
|
return
|
|
2875
2958
|
}
|
|
@@ -3003,10 +3086,12 @@ const bindEvents = () => {
|
|
|
3003
3086
|
})
|
|
3004
3087
|
if (elements.fit) {
|
|
3005
3088
|
elements.fit.addEventListener('click', () => {
|
|
3089
|
+
resetHierarchyFocus()
|
|
3006
3090
|
focusPrimaryHub()
|
|
3007
3091
|
})
|
|
3008
3092
|
}
|
|
3009
3093
|
elements.reset.addEventListener('click', () => {
|
|
3094
|
+
resetHierarchyFocus()
|
|
3010
3095
|
resetView()
|
|
3011
3096
|
})
|
|
3012
3097
|
elements.contentClose.addEventListener('click', () => elements.contentDialog.close())
|
|
@@ -3175,6 +3260,7 @@ const loadGraph = async (options = { reset: false }) => {
|
|
|
3175
3260
|
state.nodes = layout.nodes
|
|
3176
3261
|
state.groups = layout.groups
|
|
3177
3262
|
state.hierarchyFocusGroupId = null
|
|
3263
|
+
state.hierarchyFocusStack = []
|
|
3178
3264
|
state.groupById = new Map(state.groups.map(group => [group.id, group]))
|
|
3179
3265
|
state.leafGroups = state.groups.filter(group => group.nodeIds.length > 0)
|
|
3180
3266
|
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 as a sparse strongest-link graph of group nodes, zoom-in
|
|
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 as a sparse strongest-link graph of group nodes, zoom-in fits the focused node's full child-graph circumference with extra margin before expanding, 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