@andespindola/brainlink 1.4.0 → 1.5.0
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/CHANGELOG.md +7 -0
- package/COPYRIGHT.md +1 -1
- package/README.md +2 -2
- package/assets/brainlink-logo.png +0 -0
- package/assets/brainlink-logo.svg +96 -21
- package/assets/fonts/CrowquillMono-Bold.woff2 +0 -0
- package/assets/fonts/CrowquillMono-BoldItalic.woff2 +0 -0
- package/assets/fonts/CrowquillMono-Italic.woff2 +0 -0
- package/assets/fonts/CrowquillMono-Regular.woff2 +0 -0
- package/dist/application/frontend/client/rendering.js +65 -21
- package/dist/application/frontend/client/scope-theme.js +2 -2
- package/dist/application/frontend/client-css.js +40 -13
- package/dist/application/frontend/client-html.js +1 -1
- package/dist/application/frontend/client-render-worker-js.js +7 -5
- package/dist/application/get-graph-stream-chunk.js +18 -9
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
- **Graph shows every node at any zoom.** The viewport stream previously replaced nodes with per-context clusters below a zoom threshold, so nodes vanished when you zoomed out and reappeared on approach. Graphs that fit a cap (6000 nodes) now render every node in near mode at all zoom levels; larger graphs keep the far/mid/near level-of-detail.
|
|
6
|
+
- **Graph visual refresh.** Bigger nodes with a screen-space radius floor so they stay visible dots at any zoom, brighter and more legible edges, and labels decluttered by collision (placed by importance, skipping overlaps) so density scales smoothly with zoom instead of flipping on at a hard threshold. Labels reposition live during interaction (no flicker) and are throttled to keep pan/zoom fluid.
|
|
7
|
+
- **Crowquill Mono UI font.** The graph UI now ships and uses Crowquill Mono, inlined as base64 woff2 in the served CSS so it is self-contained (graceful monospace fallback if unavailable). The header metrics were made compact so the Notes/Links counters no longer wrap.
|
|
8
|
+
- **New logo and npm image fix.** The README logo is now a brain drawn as a knowledge graph, exported to PNG and referenced by absolute URL so it renders on the npm package page (npm does not render SVG). Copyright holder set to Anderson Espindola.
|
|
9
|
+
|
|
3
10
|
## 1.4.0
|
|
4
11
|
|
|
5
12
|
- **Shared namespace by default.** `defaultAgent` now defaults to `shared`, so every agent (Claude, Codex, GPT, …) reads and writes the same namespace and sees each other's memory unless a call passes an explicit `--agent`/`agent`. Set `defaultAgent` to another id, or pass `--agent` per call, to scope to a private namespace.
|
package/COPYRIGHT.md
CHANGED
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="
|
|
2
|
+
<img src="https://raw.githubusercontent.com/andersonflima/brainlink/main/assets/brainlink-logo.png" alt="Brainlink" width="720">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# Brainlink
|
|
@@ -1456,7 +1456,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
|
1456
1456
|
## License
|
|
1457
1457
|
|
|
1458
1458
|
MIT. See [LICENSE](LICENSE).
|
|
1459
|
-
Copyright (c) 2026
|
|
1459
|
+
Copyright (c) 2026 Anderson Espindola. See [COPYRIGHT.md](COPYRIGHT.md).
|
|
1460
1460
|
|
|
1461
1461
|
### Memory Optimization Loop (1-7)
|
|
1462
1462
|
|
|
Binary file
|
|
@@ -1,25 +1,100 @@
|
|
|
1
|
-
<svg width="
|
|
1
|
+
<svg width="1240" height="440" viewBox="0 0 1240 440" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
|
|
2
2
|
<title id="title">Brainlink</title>
|
|
3
|
-
<desc id="desc">Brainlink logo
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
<desc id="desc">Brainlink logo: a brain drawn as a knowledge graph of connected memory nodes, for AI agents.</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<radialGradient id="bg" cx="26%" cy="30%" r="95%">
|
|
6
|
+
<stop offset="0%" stop-color="#0e2233"/>
|
|
7
|
+
<stop offset="55%" stop-color="#0a1926"/>
|
|
8
|
+
<stop offset="100%" stop-color="#06111b"/>
|
|
9
|
+
</radialGradient>
|
|
10
|
+
<linearGradient id="brainStroke" x1="0" y1="0" x2="1" y2="1">
|
|
11
|
+
<stop offset="0%" stop-color="#5aa8ff"/>
|
|
12
|
+
<stop offset="100%" stop-color="#59d0dd"/>
|
|
13
|
+
</linearGradient>
|
|
14
|
+
<linearGradient id="wordmark" x1="0" y1="0" x2="0" y2="1">
|
|
15
|
+
<stop offset="0%" stop-color="#eef5ff"/>
|
|
16
|
+
<stop offset="100%" stop-color="#bcd4f2"/>
|
|
17
|
+
</linearGradient>
|
|
18
|
+
<filter id="glow" x="-60%" y="-60%" width="220%" height="220%">
|
|
19
|
+
<feGaussianBlur stdDeviation="5" result="b"/>
|
|
20
|
+
<feMerge>
|
|
21
|
+
<feMergeNode in="b"/>
|
|
22
|
+
<feMergeNode in="SourceGraphic"/>
|
|
23
|
+
</feMerge>
|
|
24
|
+
</filter>
|
|
25
|
+
</defs>
|
|
26
|
+
|
|
27
|
+
<rect width="1240" height="440" rx="34" fill="url(#bg)"/>
|
|
28
|
+
|
|
29
|
+
<!-- Brain drawn as a graph -->
|
|
30
|
+
<g transform="translate(70 26)" filter="url(#glow)">
|
|
31
|
+
<!-- brain silhouette -->
|
|
32
|
+
<path d="M270,58 C232,40 190,50 168,84 C138,74 110,96 116,130 C88,140 82,180 108,200
|
|
33
|
+
C86,214 92,256 122,266 C120,306 158,336 202,326 C224,352 286,352 312,326
|
|
34
|
+
C356,340 398,312 392,270 C420,258 424,214 398,196 C420,180 416,138 386,132
|
|
35
|
+
C392,98 360,72 322,84 C308,64 288,58 270,58 Z"
|
|
36
|
+
fill="#5aa8ff" fill-opacity="0.05" stroke="url(#brainStroke)" stroke-width="3" stroke-opacity="0.65"/>
|
|
37
|
+
<!-- central sulcus + a few gyri -->
|
|
38
|
+
<path d="M268,72 C246,120 300,150 270,196 C242,240 300,272 272,318"
|
|
39
|
+
fill="none" stroke="#5aa8ff" stroke-width="2.2" stroke-opacity="0.32"/>
|
|
40
|
+
<path d="M150,110 C176,128 176,156 150,176" fill="none" stroke="#5aa8ff" stroke-width="2" stroke-opacity="0.2"/>
|
|
41
|
+
<path d="M360,120 C334,140 336,168 362,186" fill="none" stroke="#5aa8ff" stroke-width="2" stroke-opacity="0.2"/>
|
|
42
|
+
<path d="M196,250 C222,262 250,258 268,240" fill="none" stroke="#5aa8ff" stroke-width="2" stroke-opacity="0.2"/>
|
|
43
|
+
|
|
44
|
+
<!-- graph edges -->
|
|
45
|
+
<g stroke="#7fb2e6" stroke-width="2" stroke-opacity="0.42" stroke-linecap="round">
|
|
46
|
+
<line x1="150" y1="120" x2="210" y2="96"/>
|
|
47
|
+
<line x1="210" y1="96" x2="250" y2="150"/>
|
|
48
|
+
<line x1="150" y1="120" x2="190" y2="170"/>
|
|
49
|
+
<line x1="190" y1="170" x2="250" y2="150"/>
|
|
50
|
+
<line x1="190" y1="170" x2="140" y2="200"/>
|
|
51
|
+
<line x1="140" y1="200" x2="172" y2="248"/>
|
|
52
|
+
<line x1="172" y1="248" x2="225" y2="228"/>
|
|
53
|
+
<line x1="225" y1="228" x2="250" y2="150"/>
|
|
54
|
+
<line x1="172" y1="248" x2="208" y2="298"/>
|
|
55
|
+
<line x1="208" y1="298" x2="266" y2="262"/>
|
|
56
|
+
<line x1="266" y1="262" x2="225" y2="228"/>
|
|
57
|
+
<line x1="250" y1="150" x2="270" y2="205"/>
|
|
58
|
+
<line x1="270" y1="205" x2="315" y2="200"/>
|
|
59
|
+
<line x1="300" y1="112" x2="340" y2="152"/>
|
|
60
|
+
<line x1="300" y1="112" x2="250" y2="150"/>
|
|
61
|
+
<line x1="340" y1="152" x2="315" y2="200"/>
|
|
62
|
+
<line x1="315" y1="200" x2="362" y2="236"/>
|
|
63
|
+
<line x1="362" y1="236" x2="345" y2="292"/>
|
|
64
|
+
<line x1="345" y1="292" x2="300" y2="300"/>
|
|
65
|
+
<line x1="300" y1="300" x2="266" y2="262"/>
|
|
66
|
+
<line x1="300" y1="300" x2="208" y2="298"/>
|
|
67
|
+
<line x1="270" y1="205" x2="300" y2="300"/>
|
|
68
|
+
<line x1="270" y1="205" x2="266" y2="262"/>
|
|
69
|
+
</g>
|
|
70
|
+
|
|
71
|
+
<!-- graph nodes -->
|
|
72
|
+
<g>
|
|
73
|
+
<circle cx="270" cy="205" r="12" fill="#5aa8ff"/>
|
|
74
|
+
<circle cx="210" cy="96" r="8" fill="#59d0dd"/>
|
|
75
|
+
<circle cx="150" cy="120" r="7" fill="#5aa8ff"/>
|
|
76
|
+
<circle cx="250" cy="150" r="10" fill="#7cb6ff"/>
|
|
77
|
+
<circle cx="190" cy="170" r="7" fill="#5ecf92"/>
|
|
78
|
+
<circle cx="140" cy="200" r="7" fill="#5aa8ff"/>
|
|
79
|
+
<circle cx="172" cy="248" r="8" fill="#a88fff"/>
|
|
80
|
+
<circle cx="225" cy="228" r="9" fill="#5aa8ff"/>
|
|
81
|
+
<circle cx="208" cy="298" r="7" fill="#ffb65c"/>
|
|
82
|
+
<circle cx="266" cy="262" r="8" fill="#59d0dd"/>
|
|
83
|
+
<circle cx="300" cy="112" r="8" fill="#ff7dac"/>
|
|
84
|
+
<circle cx="340" cy="152" r="7" fill="#5aa8ff"/>
|
|
85
|
+
<circle cx="315" cy="200" r="10" fill="#5ecf92"/>
|
|
86
|
+
<circle cx="362" cy="236" r="7" fill="#7cb6ff"/>
|
|
87
|
+
<circle cx="345" cy="292" r="8" fill="#5aa8ff"/>
|
|
88
|
+
<circle cx="300" cy="300" r="9" fill="#ff8f6a"/>
|
|
89
|
+
</g>
|
|
18
90
|
</g>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
<
|
|
91
|
+
|
|
92
|
+
<!-- wordmark -->
|
|
93
|
+
<g transform="translate(560 0)" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
|
|
94
|
+
<text x="0" y="212" fill="url(#wordmark)" font-size="118" font-weight="780" letter-spacing="-3">Brainlink</text>
|
|
95
|
+
<text x="4" y="262" fill="#7f9ad0" font-size="30" font-weight="500">Canonical memory graph for AI agents</text>
|
|
96
|
+
<line x1="6" y1="292" x2="150" y2="292" stroke="#5aa8ff" stroke-width="6" stroke-linecap="round"/>
|
|
97
|
+
<line x1="166" y1="292" x2="238" y2="292" stroke="#59d0dd" stroke-width="6" stroke-linecap="round"/>
|
|
98
|
+
<line x1="254" y1="292" x2="298" y2="292" stroke="#5ecf92" stroke-width="6" stroke-linecap="round"/>
|
|
24
99
|
</g>
|
|
25
100
|
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -23,8 +23,8 @@ const drawFallback = () => {
|
|
|
23
23
|
nodeById.set(nodes[i][0], nodes[i])
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
ctx2dFallback.strokeStyle = 'rgba(
|
|
27
|
-
ctx2dFallback.lineWidth = 1
|
|
26
|
+
ctx2dFallback.strokeStyle = 'rgba(158,188,220,0.32)'
|
|
27
|
+
ctx2dFallback.lineWidth = 1.2
|
|
28
28
|
for (let i = 0; i < edges.length; i += 1) {
|
|
29
29
|
const edge = edges[i]
|
|
30
30
|
const source = nodeById.get(edge[0])
|
|
@@ -43,7 +43,7 @@ const drawFallback = () => {
|
|
|
43
43
|
const p = worldToScreen(node[2], node[3])
|
|
44
44
|
const selected = state.selectedNodeId === node[0]
|
|
45
45
|
const color = segmentColor(node[5] || node[4] || node[1])
|
|
46
|
-
const radius = Math.max(3.
|
|
46
|
+
const radius = Math.max(3.8, Math.min(18, 5.5 + node[7] * 0.7))
|
|
47
47
|
|
|
48
48
|
ctx2dFallback.beginPath()
|
|
49
49
|
ctx2dFallback.fillStyle = selected ? '#edf4ff' : color
|
|
@@ -179,25 +179,57 @@ const hideTooltip = () => {
|
|
|
179
179
|
|
|
180
180
|
const labelCandidates = () => {
|
|
181
181
|
const nodes = normalizeList(state.chunk.nodes)
|
|
182
|
-
const
|
|
182
|
+
const focused = state.focusedNodeIds
|
|
183
|
+
const isPinned = (node) => focused.has(node[0]) || node[0] === state.hoveredNodeId || node[0] === state.selectedNodeId
|
|
184
|
+
const margin = 80
|
|
185
|
+
|
|
186
|
+
// Collect on-screen nodes with their screen position, ranked by importance
|
|
187
|
+
// (pinned first, then relevance, deterministic id tie-break).
|
|
188
|
+
const scored = []
|
|
189
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
190
|
+
const node = nodes[i]
|
|
183
191
|
const x = Number(node?.[2])
|
|
184
192
|
const y = Number(node?.[3])
|
|
185
|
-
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
193
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue
|
|
186
194
|
const point = worldToScreen(x, y)
|
|
187
|
-
|
|
195
|
+
if (point.x < -margin || point.x > state.viewport.width + margin || point.y < -margin || point.y > state.viewport.height + margin) continue
|
|
196
|
+
scored.push({ node, x: point.x, y: point.y, pinned: isPinned(node), relevance: Number(node?.[7] ?? 0) })
|
|
197
|
+
}
|
|
198
|
+
scored.sort((left, right) => {
|
|
199
|
+
const focusDelta = (right.pinned ? 1 : 0) - (left.pinned ? 1 : 0)
|
|
200
|
+
if (focusDelta !== 0) return focusDelta
|
|
201
|
+
const relevanceDelta = right.relevance - left.relevance
|
|
202
|
+
if (relevanceDelta !== 0) return relevanceDelta
|
|
203
|
+
return String(left.node[0]).localeCompare(String(right.node[0]))
|
|
188
204
|
})
|
|
189
|
-
const shouldShowMany = state.camera.scale >= 0.72 || visible.length <= 120
|
|
190
|
-
const focused = state.focusedNodeIds
|
|
191
205
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
206
|
+
// Greedy overlap-free placement: a label is shown only if its box does not
|
|
207
|
+
// collide with an already-placed one, so density scales smoothly with zoom
|
|
208
|
+
// (few when nodes are packed, more as they spread out) instead of flipping on
|
|
209
|
+
// at a hard zoom threshold. Pinned labels are always kept.
|
|
210
|
+
const gap = 4
|
|
211
|
+
const height = 22
|
|
212
|
+
const maxLabels = state.camera.scale >= 0.72 ? 220 : 120
|
|
213
|
+
const placed = []
|
|
214
|
+
const result = []
|
|
215
|
+
for (let i = 0; i < scored.length && result.length < maxLabels; i += 1) {
|
|
216
|
+
const item = scored[i]
|
|
217
|
+
const title = String(item.node[1] || item.node[0])
|
|
218
|
+
const width = Math.min(220, 18 + title.length * 6.6)
|
|
219
|
+
const box = { left: item.x - width / 2, right: item.x + width / 2, bottom: item.y - 6, top: item.y - 6 - height }
|
|
220
|
+
let overlaps = false
|
|
221
|
+
for (let j = 0; j < placed.length; j += 1) {
|
|
222
|
+
const other = placed[j]
|
|
223
|
+
if (box.left < other.right + gap && box.right > other.left - gap && box.top < other.bottom + gap && box.bottom > other.top - gap) {
|
|
224
|
+
overlaps = true
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (overlaps && !item.pinned) continue
|
|
229
|
+
placed.push(box)
|
|
230
|
+
result.push(item.node)
|
|
231
|
+
}
|
|
232
|
+
return result
|
|
201
233
|
}
|
|
202
234
|
|
|
203
235
|
const drawLabels = () => {
|
|
@@ -268,6 +300,12 @@ const drawMiniMap = () => {
|
|
|
268
300
|
|
|
269
301
|
const shouldDeferGraphOverlays = () => state.pointer.down || performance.now() - state.lastWheelAt < 150
|
|
270
302
|
|
|
303
|
+
// Rebuilding the label overlay is the main per-frame cost. During interaction we
|
|
304
|
+
// cap it to ~25fps (the canvas keeps 60fps in the worker), which halves the DOM
|
|
305
|
+
// churn while labels still track the nodes. A trailing timer guarantees a final
|
|
306
|
+
// pass so labels settle in the right place when interaction stops.
|
|
307
|
+
const graphOverlayInteractionThrottleMs = 40
|
|
308
|
+
|
|
271
309
|
const updateGraphOverlays = () => {
|
|
272
310
|
if (state.overlayScheduled) {
|
|
273
311
|
return
|
|
@@ -275,19 +313,25 @@ const updateGraphOverlays = () => {
|
|
|
275
313
|
state.overlayScheduled = true
|
|
276
314
|
requestAnimationFrame(() => {
|
|
277
315
|
state.overlayScheduled = false
|
|
278
|
-
|
|
279
|
-
|
|
316
|
+
const interacting = shouldDeferGraphOverlays()
|
|
317
|
+
|
|
318
|
+
if (interacting && performance.now() - (state.lastOverlayDrawAt || 0) < graphOverlayInteractionThrottleMs) {
|
|
280
319
|
if (!state.overlayIdleTimer) {
|
|
281
320
|
state.overlayIdleTimer = setTimeout(() => {
|
|
282
321
|
state.overlayIdleTimer = null
|
|
283
322
|
updateGraphOverlays()
|
|
284
|
-
},
|
|
323
|
+
}, graphOverlayInteractionThrottleMs)
|
|
285
324
|
}
|
|
286
325
|
return
|
|
287
326
|
}
|
|
327
|
+
|
|
288
328
|
elements.labels?.classList.remove('is-stale')
|
|
289
329
|
drawLabels()
|
|
290
|
-
|
|
330
|
+
state.lastOverlayDrawAt = performance.now()
|
|
331
|
+
|
|
332
|
+
// The minimap is heavier and irrelevant mid-gesture, so only refresh it once
|
|
333
|
+
// the camera settles.
|
|
334
|
+
if (!interacting && state.miniMapDirty) {
|
|
291
335
|
drawMiniMap()
|
|
292
336
|
state.miniMapDirty = false
|
|
293
337
|
}
|
|
@@ -63,8 +63,8 @@ const graphTheme = {
|
|
|
63
63
|
parseColor('#c9945f'),
|
|
64
64
|
parseColor('#7cb6ff')
|
|
65
65
|
],
|
|
66
|
-
edge: [0.
|
|
67
|
-
edgeHeavy: [0.
|
|
66
|
+
edge: [0.62, 0.74, 0.86, 0.24],
|
|
67
|
+
edgeHeavy: [0.66, 0.78, 0.9, 0.46],
|
|
68
68
|
clear: parseColor('#08131d')
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
// Crowquill Mono ships with the package under assets/fonts. Inline each face as a
|
|
4
|
+
// base64 woff2 data URI so the served CSS is self-contained (no extra route, no
|
|
5
|
+
// path resolution) and works identically for a global npm install. A missing
|
|
6
|
+
// file degrades gracefully to the monospace fallback stack.
|
|
7
|
+
const embedFontFace = (weight, style, file) => {
|
|
8
|
+
try {
|
|
9
|
+
const path = fileURLToPath(new URL(`../../../assets/fonts/${file}`, import.meta.url));
|
|
10
|
+
const base64 = readFileSync(path).toString('base64');
|
|
11
|
+
return `@font-face{font-family:"Crowquill Mono";font-style:${style};font-weight:${weight};font-display:swap;src:url("data:font/woff2;base64,${base64}") format("woff2")}`;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const crowquillFontFaces = () => [
|
|
18
|
+
embedFontFace(400, 'normal', 'CrowquillMono-Regular.woff2'),
|
|
19
|
+
embedFontFace(700, 'normal', 'CrowquillMono-Bold.woff2'),
|
|
20
|
+
embedFontFace(400, 'italic', 'CrowquillMono-Italic.woff2'),
|
|
21
|
+
embedFontFace(700, 'italic', 'CrowquillMono-BoldItalic.woff2')
|
|
22
|
+
]
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.join('\n');
|
|
25
|
+
export const createClientCss = () => `${crowquillFontFaces()}
|
|
26
|
+
:root {
|
|
2
27
|
color-scheme: dark;
|
|
3
28
|
--bg: #071019;
|
|
4
29
|
--panel: #0d1823;
|
|
@@ -22,7 +47,7 @@ body {
|
|
|
22
47
|
margin: 0;
|
|
23
48
|
background: var(--bg);
|
|
24
49
|
color: var(--text);
|
|
25
|
-
font-family:
|
|
50
|
+
font-family: "Crowquill Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
26
51
|
}
|
|
27
52
|
|
|
28
53
|
body {
|
|
@@ -256,28 +281,30 @@ select {
|
|
|
256
281
|
|
|
257
282
|
.floating-metrics {
|
|
258
283
|
display: flex;
|
|
259
|
-
gap:
|
|
260
|
-
flex-wrap:
|
|
284
|
+
gap: 8px;
|
|
285
|
+
flex-wrap: nowrap;
|
|
286
|
+
flex: 0 0 auto;
|
|
261
287
|
}
|
|
262
288
|
|
|
263
289
|
.metric-chip {
|
|
264
|
-
|
|
265
|
-
padding: 10px 12px;
|
|
290
|
+
padding: 6px 10px;
|
|
266
291
|
border: 1px solid var(--line);
|
|
267
|
-
border-radius:
|
|
292
|
+
border-radius: 9px;
|
|
268
293
|
background: rgba(12, 24, 36, 0.94);
|
|
269
|
-
display:
|
|
270
|
-
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: baseline;
|
|
296
|
+
gap: 6px;
|
|
297
|
+
white-space: nowrap;
|
|
271
298
|
}
|
|
272
299
|
|
|
273
300
|
.metric-chip strong {
|
|
274
|
-
font-size:
|
|
301
|
+
font-size: 15px;
|
|
275
302
|
line-height: 1;
|
|
276
303
|
}
|
|
277
304
|
|
|
278
305
|
.metric-chip small {
|
|
279
306
|
color: var(--muted);
|
|
280
|
-
font-size:
|
|
307
|
+
font-size: 10px;
|
|
281
308
|
letter-spacing: 0.03em;
|
|
282
309
|
text-transform: uppercase;
|
|
283
310
|
}
|
|
@@ -344,7 +371,7 @@ li small {
|
|
|
344
371
|
color: var(--text);
|
|
345
372
|
white-space: pre-wrap;
|
|
346
373
|
overflow: auto;
|
|
347
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
374
|
+
font-family: "Crowquill Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
348
375
|
font-size: 12px;
|
|
349
376
|
line-height: 1.5;
|
|
350
377
|
}
|
|
@@ -742,7 +769,7 @@ li small {
|
|
|
742
769
|
}
|
|
743
770
|
|
|
744
771
|
.metric-chip {
|
|
745
|
-
min-width:
|
|
772
|
+
min-width: 0;
|
|
746
773
|
}
|
|
747
774
|
|
|
748
775
|
.content-meta {
|
|
@@ -124,7 +124,7 @@ export const createClientHtml = () => `<!doctype html>
|
|
|
124
124
|
</section>
|
|
125
125
|
</main>
|
|
126
126
|
<footer class="app-footer" aria-label="Copyright notice">
|
|
127
|
-
<small>Copyright © 2026
|
|
127
|
+
<small>Copyright © 2026 Anderson Espindola</small>
|
|
128
128
|
</footer>
|
|
129
129
|
<script src="/app.js"></script>
|
|
130
130
|
</body>
|
|
@@ -56,8 +56,8 @@ const defaultTheme = {
|
|
|
56
56
|
[0.72, 0.51, 0.33, 1],
|
|
57
57
|
[0.44, 0.62, 0.85, 1]
|
|
58
58
|
],
|
|
59
|
-
edge: [0.
|
|
60
|
-
edgeHeavy: [0.
|
|
59
|
+
edge: [0.36, 0.46, 0.60, 0.26],
|
|
60
|
+
edgeHeavy: [0.40, 0.52, 0.68, 0.5],
|
|
61
61
|
clear: [0.031, 0.075, 0.114, 1]
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -213,8 +213,8 @@ const ensureEdgeCapacity = (count) => {
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
const nodeRadius = (relevance, kind) => {
|
|
216
|
-
const base = kind === 'cluster' ?
|
|
217
|
-
const modifier = Math.min(
|
|
216
|
+
const base = kind === 'cluster' ? 11.2 : 7.2
|
|
217
|
+
const modifier = Math.min(7.6, Math.max(0, relevance * 0.78))
|
|
218
218
|
return base + modifier
|
|
219
219
|
}
|
|
220
220
|
|
|
@@ -352,7 +352,9 @@ const drawNodeLayer = (predicate, color, radiusBoost = 1) => {
|
|
|
352
352
|
const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
|
|
353
353
|
pointPositionsBuffer[positionCursor] = sx
|
|
354
354
|
pointPositionsBuffer[positionCursor + 1] = sy
|
|
355
|
-
|
|
355
|
+
// Screen-space floor so every node stays a visible dot at any zoom (a low
|
|
356
|
+
// floor made nodes sub-pixel until zoomed in). Scaled by devicePixelRatio.
|
|
357
|
+
pointSizesBuffer[sizeCursor] = Math.max(3.0 * devicePixelRatio, state.radius[index] * camera.scale * devicePixelRatio * radiusBoost)
|
|
356
358
|
positionCursor += 2
|
|
357
359
|
sizeCursor += 1
|
|
358
360
|
}
|
|
@@ -3,6 +3,9 @@ const layoutCacheBySignature = new Map();
|
|
|
3
3
|
const maxLayoutCacheEntries = 6;
|
|
4
4
|
const farScaleThreshold = 0.22;
|
|
5
5
|
const midScaleThreshold = 0.78;
|
|
6
|
+
// Graphs with up to this many nodes render every node at all zoom levels (no
|
|
7
|
+
// zoom-based clustering). Above it, the far/mid/near LOD keeps huge graphs fast.
|
|
8
|
+
const renderAllNodeCap = 6000;
|
|
6
9
|
const viewportPaddingFactor = 0.18;
|
|
7
10
|
const maxNearEdgePerNode = 24;
|
|
8
11
|
const maxMidEdgePerNode = 12;
|
|
@@ -296,23 +299,29 @@ export const getGraphStreamChunk = async (vaultPath, input) => {
|
|
|
296
299
|
}
|
|
297
300
|
};
|
|
298
301
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
// When the whole graph fits a sane cap, render every node at every zoom (no
|
|
303
|
+
// clustering / budget-shrink), so nodes never disappear as you zoom out and
|
|
304
|
+
// reappear on approach. Only genuinely large graphs keep the far/mid/near LOD.
|
|
305
|
+
const rendersAll = layout.nodes.length <= renderAllNodeCap;
|
|
306
|
+
const effectiveNodeBudget = rendersAll ? Math.max(nodeBudget, layout.nodes.length) : nodeBudget;
|
|
307
|
+
const effectiveEdgeBudget = rendersAll ? Math.max(edgeBudget, layout.edges.length) : edgeBudget;
|
|
308
|
+
const mode = rendersAll || input.scale >= midScaleThreshold
|
|
309
|
+
? 'near'
|
|
310
|
+
: input.scale < farScaleThreshold
|
|
311
|
+
? 'far'
|
|
312
|
+
: 'mid';
|
|
304
313
|
const nodes = mode === 'far'
|
|
305
|
-
? selectFarClusters(layout.nodes, input,
|
|
314
|
+
? selectFarClusters(layout.nodes, input, effectiveNodeBudget)
|
|
306
315
|
: mode === 'mid'
|
|
307
316
|
? selectMidNodes(layout.nodes, cache, {
|
|
308
317
|
...input,
|
|
309
|
-
nodeBudget
|
|
318
|
+
nodeBudget: effectiveNodeBudget
|
|
310
319
|
})
|
|
311
320
|
: selectNearNodes(layout.nodes, cache, {
|
|
312
321
|
...input,
|
|
313
|
-
nodeBudget
|
|
322
|
+
nodeBudget: effectiveNodeBudget
|
|
314
323
|
});
|
|
315
|
-
const edges = collectEdgesForNodes(layout.edges, cache, nodes,
|
|
324
|
+
const edges = collectEdgesForNodes(layout.edges, cache, nodes, effectiveEdgeBudget, mode === 'near' ? maxNearEdgePerNode : mode === 'mid' ? maxMidEdgePerNode : maxFarEdgePerCluster);
|
|
316
325
|
return {
|
|
317
326
|
signature,
|
|
318
327
|
mode,
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andespindola/brainlink",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"author": "
|
|
7
|
+
"author": "Anderson Espindola",
|
|
8
8
|
"homepage": "https://github.com/andersonflima/brainlink#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|