@andespindola/brainlink 0.1.0-beta.138 → 0.1.0-beta.139
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 +4 -5
- package/dist/application/frontend/client-js.js +68 -61
- package/docs/AGENT_USAGE.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,11 +85,10 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
|
|
|
85
85
|
- Graph renderer optimized for large datasets with viewport-driven node culling and edge lookup by visible nodes.
|
|
86
86
|
- Canvas graph rendering uses the same batched node and edge pipeline for every graph size, reducing per-frame draw calls while keeping selected and hovered items highlighted.
|
|
87
87
|
- WebGL acceleration is used when available for dense node and edge drawing, with Canvas 2D preserved as the interaction and fallback layer.
|
|
88
|
-
- Graph rendering keeps the flat node scene and adds stable hierarchical mesh groups for vaults above 1000 notes, with every visible graph level filled toward 1000 nodes, each group capped at 1000 child nodes, and recursive parent groups when a level itself exceeds 1000 groups.
|
|
89
88
|
- Large graph layout API automatically uses compact payload encoding with link-coverage-aware edge selection to reduce initial client load without hiding major relationships.
|
|
90
89
|
- Large-segment layout spacing now grows logarithmically to keep initial visual density consistent between medium and very large vaults (for example, ~1k vs ~50k notes).
|
|
91
90
|
- Graph coordinates are visually compacted across graph sizes so reset starts from a stable fitted scene and zoom-in progressively reveals local detail.
|
|
92
|
-
- Zoomed-out graph LOD
|
|
91
|
+
- Zoomed-out graph LOD keeps a single flat graph scene, with viewport-driven node sampling and edge coverage safeguards to preserve visible relationships without switching to nested subgraphs.
|
|
93
92
|
- Graph reset fits the full graph scene instead of starting in a separate macro overview mode.
|
|
94
93
|
- Graph filtering runs in a dedicated browser worker to keep the UI thread responsive during heavy datasets.
|
|
95
94
|
- Edge rendering budgets adapt to zoom level to prevent frame spikes on large graph panoramas.
|
|
@@ -598,16 +597,16 @@ The graph UI shows:
|
|
|
598
597
|
- realtime refresh while `--watch` is enabled
|
|
599
598
|
- graph controls for zoom in, zoom out, fit visible nodes and reset-to-fit-all
|
|
600
599
|
- wheel zoom (including `cmd+scroll` and `ctrl+scroll`) anchored to cursor position for faster navigation in large graphs
|
|
601
|
-
-
|
|
600
|
+
- wheel/button zoom updates immediately at the cursor anchor without delayed focus-transition interpolation
|
|
602
601
|
- Bloom-like scene navigation: reset fits the current graph scene, wheel zoom stays anchored to the cursor, and WebGL acceleration draws the dense node and edge layer faster
|
|
603
602
|
- zoom-out floor for large and massive graphs to keep the scene reachable without switching into a separate macro graph mode
|
|
604
603
|
- keyboard shortcuts: `+` zoom in, `-` zoom out, `0` reset fit
|
|
605
|
-
- click on a
|
|
604
|
+
- click on a node opens its details panel; double-click on empty canvas zooms in at cursor position
|
|
606
605
|
- floating graph totals (notes, links, tags) below the Brainlink title
|
|
607
606
|
- graph rendering safeguards (batched canvas drawing across graph sizes, edge draw caps, lower redraw rate, zoom-aware interaction)
|
|
608
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
|
|
609
608
|
- WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
|
|
610
|
-
- large graph LOD keeps a
|
|
609
|
+
- large graph LOD keeps a single-level graph model across zoom levels, preserving visual continuity while adjusting node/edge density from the active viewport
|
|
611
610
|
|
|
612
611
|
The server indexes before starting by default. Use `--no-index` to skip that step:
|
|
613
612
|
|
|
@@ -749,13 +749,7 @@ const selectStableSampleNodes = (sourceNodes, limit) => {
|
|
|
749
749
|
return sourceNodes
|
|
750
750
|
}
|
|
751
751
|
|
|
752
|
-
const
|
|
753
|
-
const cursorPoint = cursorWorldPoint()
|
|
754
|
-
const recentZoomFocus =
|
|
755
|
-
now - state.lastZoomFocus.at <= 1500
|
|
756
|
-
? { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
|
|
757
|
-
: null
|
|
758
|
-
const anchor = cursorPoint ?? recentZoomFocus ?? viewportCenterWorldPoint()
|
|
752
|
+
const anchor = viewportCenterWorldPoint()
|
|
759
753
|
const previousIds = new Set(state.renderNodes.map((node) => node.id))
|
|
760
754
|
const preferAnchorDistance = state.visibleNodes.length > massiveGraphNodeThreshold && state.transform.scale >= 0.28
|
|
761
755
|
|
|
@@ -822,6 +816,7 @@ const collectVisibleEdgesForNodes = (nodeIds, options = { preferComplete: false
|
|
|
822
816
|
|
|
823
817
|
const seen = new Set()
|
|
824
818
|
const candidates = []
|
|
819
|
+
const bestEdgeByNode = new Map()
|
|
825
820
|
const limit = options.preferComplete
|
|
826
821
|
? hierarchyAbsoluteEdgeSafetyCap
|
|
827
822
|
: edgeBudgetForCurrentFrame()
|
|
@@ -838,6 +833,15 @@ const collectVisibleEdgesForNodes = (nodeIds, options = { preferComplete: false
|
|
|
838
833
|
|
|
839
834
|
seen.add(key)
|
|
840
835
|
candidates.push(edge)
|
|
836
|
+
const score = edgeRelevanceScore(edge)
|
|
837
|
+
const currentSource = bestEdgeByNode.get(edge.source)
|
|
838
|
+
if (!currentSource || score > currentSource.score) {
|
|
839
|
+
bestEdgeByNode.set(edge.source, { edge, score })
|
|
840
|
+
}
|
|
841
|
+
const currentTarget = bestEdgeByNode.get(edge.target)
|
|
842
|
+
if (!currentTarget || score > currentTarget.score) {
|
|
843
|
+
bestEdgeByNode.set(edge.target, { edge, score })
|
|
844
|
+
}
|
|
841
845
|
}
|
|
842
846
|
})
|
|
843
847
|
|
|
@@ -845,7 +849,7 @@ const collectVisibleEdgesForNodes = (nodeIds, options = { preferComplete: false
|
|
|
845
849
|
return candidates
|
|
846
850
|
}
|
|
847
851
|
|
|
848
|
-
|
|
852
|
+
const ranked = candidates
|
|
849
853
|
.sort((left, right) => {
|
|
850
854
|
const scoreDelta = edgeRelevanceScore(right) - edgeRelevanceScore(left)
|
|
851
855
|
if (scoreDelta !== 0) {
|
|
@@ -855,7 +859,38 @@ const collectVisibleEdgesForNodes = (nodeIds, options = { preferComplete: false
|
|
|
855
859
|
const rightKey = edgeIdentityKey(right)
|
|
856
860
|
return leftKey.localeCompare(rightKey)
|
|
857
861
|
})
|
|
858
|
-
|
|
862
|
+
|
|
863
|
+
if (options.preferComplete) {
|
|
864
|
+
return ranked.slice(0, limit)
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const coverageFloor = Math.min(4_200, Math.max(limit, Math.floor(nodeIds.size * 1.6)))
|
|
868
|
+
const resolvedLimit = Math.min(coverageFloor, ranked.length)
|
|
869
|
+
const selected = []
|
|
870
|
+
const selectedKeys = new Set()
|
|
871
|
+
|
|
872
|
+
nodeIds.forEach(nodeId => {
|
|
873
|
+
const edge = bestEdgeByNode.get(nodeId)?.edge
|
|
874
|
+
if (!edge) return
|
|
875
|
+
const key = edgeIdentityKey(edge)
|
|
876
|
+
if (selectedKeys.has(key)) return
|
|
877
|
+
selectedKeys.add(key)
|
|
878
|
+
selected.push(edge)
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
if (selected.length >= resolvedLimit) {
|
|
882
|
+
return selected.slice(0, resolvedLimit)
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
for (let index = 0; index < ranked.length && selected.length < resolvedLimit; index += 1) {
|
|
886
|
+
const edge = ranked[index]
|
|
887
|
+
const key = edgeIdentityKey(edge)
|
|
888
|
+
if (selectedKeys.has(key)) continue
|
|
889
|
+
selectedKeys.add(key)
|
|
890
|
+
selected.push(edge)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return selected
|
|
859
894
|
}
|
|
860
895
|
|
|
861
896
|
const edgeOpacityForScale = (edge, scale) => {
|
|
@@ -1121,10 +1156,7 @@ const partitionGraphForAcceleratedRenderer = () => {
|
|
|
1121
1156
|
}
|
|
1122
1157
|
|
|
1123
1158
|
const drawGraphLabels = nodes => {
|
|
1124
|
-
const
|
|
1125
|
-
const shouldDrawLabels = isRenderingHierarchyChildGraph
|
|
1126
|
-
? false
|
|
1127
|
-
: state.nodes.length > largeGraphNodeThreshold
|
|
1159
|
+
const shouldDrawLabels = state.nodes.length > largeGraphNodeThreshold
|
|
1128
1160
|
? state.transform.scale >= 1.25 && state.renderNodes.length <= 420
|
|
1129
1161
|
: state.transform.scale >= 0.62 && state.renderNodes.length <= 1200
|
|
1130
1162
|
|
|
@@ -2148,10 +2180,7 @@ const fitView = (options = { useFiltered: true, preferHubCenter: true }) => {
|
|
|
2148
2180
|
const width = Math.max(rect.width, 320)
|
|
2149
2181
|
const height = Math.max(rect.height, 320)
|
|
2150
2182
|
const nodes = options.useFiltered ? filteredNodes() : state.nodes
|
|
2151
|
-
const
|
|
2152
|
-
? arrangeGraphLevelNodes(parentHierarchyGroups(), groupRenderRadius).map(createGroupRenderNode)
|
|
2153
|
-
: null
|
|
2154
|
-
const fitNodes = hierarchyFitNodes ?? nodes
|
|
2183
|
+
const fitNodes = nodes
|
|
2155
2184
|
const bounds = graphBounds(fitNodes)
|
|
2156
2185
|
|
|
2157
2186
|
if (!bounds) {
|
|
@@ -2179,14 +2208,10 @@ const fitView = (options = { useFiltered: true, preferHubCenter: true }) => {
|
|
|
2179
2208
|
const biasedScale = clampScale(fitScale * fitScaleBiasByNodeCount(fitNodes.length))
|
|
2180
2209
|
const scaleRange = autoFitScaleRangeByNodeCount(fitNodes.length)
|
|
2181
2210
|
const baselineScale = clampScale(Math.min(scaleRange.max, Math.max(scaleRange.min, biasedScale)))
|
|
2182
|
-
const resolvedScale = nodes.length > massiveGraphNodeThreshold
|
|
2211
|
+
const resolvedScale = nodes.length > massiveGraphNodeThreshold
|
|
2183
2212
|
? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
|
|
2184
2213
|
: baselineScale
|
|
2185
|
-
const hubCenter =
|
|
2186
|
-
hierarchyFitNodes
|
|
2187
|
-
? null
|
|
2188
|
-
:
|
|
2189
|
-
options.preferHubCenter && isDominantHub(state.primaryHub, nodes.length) && nodes.some((node) => node.id === state.primaryHub.id)
|
|
2214
|
+
const hubCenter = options.preferHubCenter && isDominantHub(state.primaryHub, nodes.length) && nodes.some((node) => node.id === state.primaryHub.id)
|
|
2190
2215
|
? state.primaryHub
|
|
2191
2216
|
: null
|
|
2192
2217
|
const centerX = hubCenter ? hubCenter.x : (bounds.minX + bounds.maxX) / 2
|
|
@@ -2750,14 +2775,10 @@ const computeRenderVisibility = () => {
|
|
|
2750
2775
|
state.lastViewportKey = viewportKey
|
|
2751
2776
|
state.renderVisibilityDirty = false
|
|
2752
2777
|
|
|
2753
|
-
if (computeHierarchyRenderVisibility(viewport)) {
|
|
2754
|
-
return
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
2778
|
if (state.visibleNodes.length <= 2000) {
|
|
2758
2779
|
state.renderNodes = state.visibleNodes
|
|
2759
2780
|
const ids = new Set(state.renderNodes.map((node) => node.id))
|
|
2760
|
-
state.renderEdges = limitRenderEdges(state.renderNodes, collectVisibleEdgesForNodes(ids))
|
|
2781
|
+
state.renderEdges = limitRenderEdges(state.renderNodes, collectVisibleEdgesForNodes(ids, { preferComplete: true }))
|
|
2761
2782
|
return
|
|
2762
2783
|
}
|
|
2763
2784
|
|
|
@@ -2782,12 +2803,17 @@ const computeRenderVisibility = () => {
|
|
|
2782
2803
|
: viewportNodes.length > 0
|
|
2783
2804
|
? viewportNodes
|
|
2784
2805
|
: sampleMassiveOverviewNodes(sampleLimit)
|
|
2785
|
-
const
|
|
2806
|
+
const shouldCarryOverNodes = state.transform.scale < 0.18
|
|
2807
|
+
const carryMargin = Math.max(180, Math.min(640, 260 / Math.max(state.transform.scale, 0.0001)))
|
|
2786
2808
|
const carryViewport = expandViewportBounds(viewport, carryMargin)
|
|
2787
|
-
const carryOverLimit =
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2809
|
+
const carryOverLimit = shouldCarryOverNodes
|
|
2810
|
+
? Math.max(80, Math.min(sampleLimit, Math.floor(sampleLimit * 0.24)))
|
|
2811
|
+
: 0
|
|
2812
|
+
const carryOverNodes = shouldCarryOverNodes
|
|
2813
|
+
? (state.renderNodes ?? [])
|
|
2814
|
+
.filter((node) => isNodeInViewport(node, carryViewport))
|
|
2815
|
+
.slice(0, carryOverLimit)
|
|
2816
|
+
: []
|
|
2791
2817
|
const sourceWithCarry = mergeUniqueNodes(
|
|
2792
2818
|
sourceNodes,
|
|
2793
2819
|
carryOverNodes,
|
|
@@ -3286,23 +3312,15 @@ const selectNodeById = id => {
|
|
|
3286
3312
|
|
|
3287
3313
|
const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
|
|
3288
3314
|
state.lastManualZoomAt = performance.now()
|
|
3289
|
-
const baseScale = state.
|
|
3290
|
-
? state.zoomTransition.targetScale
|
|
3291
|
-
: state.transform.scale
|
|
3315
|
+
const baseScale = state.transform.scale
|
|
3292
3316
|
const boundedFactor = source === 'wheel'
|
|
3293
3317
|
? Math.max(wheelZoomInputFloorCap, Math.min(wheelZoomInputCeilCap, factor))
|
|
3294
3318
|
: factor
|
|
3295
3319
|
const nextScale = clampScale(baseScale * boundedFactor)
|
|
3296
|
-
if (nextScale === baseScale
|
|
3320
|
+
if (nextScale === baseScale) {
|
|
3297
3321
|
return
|
|
3298
3322
|
}
|
|
3299
|
-
const worldPointAtCursor =
|
|
3300
|
-
state.zoomTransition.active &&
|
|
3301
|
-
state.zoomTransition.source === source &&
|
|
3302
|
-
state.zoomTransition.screenX === screenX &&
|
|
3303
|
-
state.zoomTransition.screenY === screenY
|
|
3304
|
-
? { x: state.zoomTransition.worldX, y: state.zoomTransition.worldY }
|
|
3305
|
-
: resolveZoomAnchorWorldPoint(screenX, screenY)
|
|
3323
|
+
const worldPointAtCursor = resolveZoomAnchorWorldPoint(screenX, screenY)
|
|
3306
3324
|
const worldX = worldPointAtCursor.x
|
|
3307
3325
|
const worldY = worldPointAtCursor.y
|
|
3308
3326
|
state.lastZoomFocus = {
|
|
@@ -3310,15 +3328,10 @@ const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
|
|
|
3310
3328
|
y: worldY,
|
|
3311
3329
|
at: performance.now()
|
|
3312
3330
|
}
|
|
3313
|
-
state.
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
screenY,
|
|
3318
|
-
worldX,
|
|
3319
|
-
worldY,
|
|
3320
|
-
targetScale: nextScale
|
|
3321
|
-
}
|
|
3331
|
+
state.transform.scale = nextScale
|
|
3332
|
+
state.transform.x = clampTransformCoordinate(screenX - worldX * nextScale)
|
|
3333
|
+
state.transform.y = clampTransformCoordinate(screenY - worldY * nextScale)
|
|
3334
|
+
clearZoomTransition()
|
|
3322
3335
|
state.offscreenFrameCount = 0
|
|
3323
3336
|
markRenderDirty()
|
|
3324
3337
|
}
|
|
@@ -3398,7 +3411,7 @@ const bindEvents = () => {
|
|
|
3398
3411
|
if (elements.fit) {
|
|
3399
3412
|
elements.fit.addEventListener('click', () => {
|
|
3400
3413
|
resetHierarchyFocus()
|
|
3401
|
-
|
|
3414
|
+
fitView({ useFiltered: true, preferHubCenter: false })
|
|
3402
3415
|
})
|
|
3403
3416
|
}
|
|
3404
3417
|
elements.reset.addEventListener('click', () => {
|
|
@@ -3487,13 +3500,7 @@ const bindEvents = () => {
|
|
|
3487
3500
|
}
|
|
3488
3501
|
if (!state.pointer.moved) {
|
|
3489
3502
|
const clickedNode = draggedNode ?? state.hovered
|
|
3490
|
-
|
|
3491
|
-
handleGroupNodePrimaryClick(clickedNode)
|
|
3492
|
-
} else if (clickedNode && state.hierarchyFocusGroupId) {
|
|
3493
|
-
handleLeafNodePrimaryClick(clickedNode)
|
|
3494
|
-
} else {
|
|
3495
|
-
selectNode(clickedNode, { openContent: true })
|
|
3496
|
-
}
|
|
3503
|
+
selectNode(clickedNode, { openContent: true })
|
|
3497
3504
|
}
|
|
3498
3505
|
state.pointer = { x: 0, y: 0, down: false, dragNode: null, moved: false }
|
|
3499
3506
|
canvas.releasePointerCapture(event.pointerId)
|
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 empty canvas zooms in at cursor position. Clicking a
|
|
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 and applied immediately without delayed focus interpolation. Keyboard shortcuts are `+` (zoom in), `-` (zoom out) and `0` (reset fit). Double-click on empty canvas zooms in at cursor position. Clicking a node opens its details panel. Totals for notes, links and tags stay visible as floating metrics under the Brainlink title, and node details open in a non-modal side panel (tags, outgoing links, backlinks and Markdown content), so zoom and pan remain available during inspection. Vaults above 1000 notes keep the same single graph scene and use viewport-driven LOD sampling with edge-coverage safeguards to preserve visible relationships without switching into nested subgraph views.
|
|
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