@andespindola/brainlink 0.1.0-beta.22 → 0.1.0-beta.24
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
CHANGED
|
@@ -560,11 +560,11 @@ The graph UI shows:
|
|
|
560
560
|
- `[[wiki links]]` as weighted edges
|
|
561
561
|
- details opened on node click (tags, outgoing links, backlinks, full Markdown content)
|
|
562
562
|
- neutral graph nodes with segment/group metadata
|
|
563
|
-
- agent selector for isolated views
|
|
563
|
+
- agent selector (id-only labels) for isolated views
|
|
564
564
|
- graph filter matches title, path, tags and note content
|
|
565
565
|
- realtime refresh while `--watch` is enabled
|
|
566
566
|
- graph controls for zoom in, zoom out, fit visible nodes and reset-to-fit-all
|
|
567
|
-
- wheel zoom anchored to cursor position for faster navigation in large graphs
|
|
567
|
+
- wheel zoom (including `cmd+scroll` and `ctrl+scroll`) anchored to cursor position for faster navigation in large graphs
|
|
568
568
|
- floating graph totals (notes, links, tags) below the Brainlink title
|
|
569
569
|
- large-graph rendering safeguards (edge draw caps, lower redraw rate, zoom-aware interaction)
|
|
570
570
|
|
|
@@ -33,7 +33,7 @@ select {
|
|
|
33
33
|
|
|
34
34
|
.shell {
|
|
35
35
|
width: 100%;
|
|
36
|
-
height: 100svh;
|
|
36
|
+
height: calc(100svh - 28px);
|
|
37
37
|
overflow: hidden;
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -104,14 +104,6 @@ select {
|
|
|
104
104
|
margin-left: auto;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
.license-badge {
|
|
108
|
-
margin-left: 10px;
|
|
109
|
-
color: var(--muted);
|
|
110
|
-
font-size: 11px;
|
|
111
|
-
letter-spacing: 0.02em;
|
|
112
|
-
white-space: nowrap;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
107
|
.search input,
|
|
116
108
|
.agent-filter select {
|
|
117
109
|
width: 100%;
|
|
@@ -353,6 +345,20 @@ li small {
|
|
|
353
345
|
padding: 22px;
|
|
354
346
|
}
|
|
355
347
|
|
|
348
|
+
.app-footer {
|
|
349
|
+
height: 28px;
|
|
350
|
+
display: flex;
|
|
351
|
+
align-items: center;
|
|
352
|
+
justify-content: center;
|
|
353
|
+
background: transparent;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.app-footer small {
|
|
357
|
+
color: var(--muted);
|
|
358
|
+
font-size: 11px;
|
|
359
|
+
letter-spacing: 0.02em;
|
|
360
|
+
}
|
|
361
|
+
|
|
356
362
|
@media (max-width: 860px) {
|
|
357
363
|
.graph-header {
|
|
358
364
|
align-items: stretch;
|
|
@@ -378,13 +384,6 @@ li small {
|
|
|
378
384
|
order: 4;
|
|
379
385
|
}
|
|
380
386
|
|
|
381
|
-
.license-badge {
|
|
382
|
-
width: 100%;
|
|
383
|
-
margin-left: 0;
|
|
384
|
-
order: 5;
|
|
385
|
-
white-space: normal;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
387
|
.content-dialog header {
|
|
389
388
|
align-items: stretch;
|
|
390
389
|
flex-direction: column;
|
|
@@ -42,11 +42,13 @@ export const createClientHtml = () => `<!doctype html>
|
|
|
42
42
|
<button id="reset" type="button" title="Reset view">⌂</button>
|
|
43
43
|
</div>
|
|
44
44
|
</div>
|
|
45
|
-
<small class="license-badge" aria-label="License notice">MIT License · Copyright © 2026 Anderson Espindola</small>
|
|
46
45
|
</header>
|
|
47
46
|
<canvas id="graph" aria-label="Brainlink knowledge graph"></canvas>
|
|
48
47
|
</section>
|
|
49
48
|
</main>
|
|
49
|
+
<footer class="app-footer" aria-label="License notice">
|
|
50
|
+
<small>MIT License · Copyright © 2026 Anderson Espindola</small>
|
|
51
|
+
</footer>
|
|
50
52
|
<dialog id="contentDialog" class="content-dialog" aria-labelledby="contentTitle">
|
|
51
53
|
<article>
|
|
52
54
|
<header>
|
|
@@ -201,7 +201,9 @@ const createLayout = graph => {
|
|
|
201
201
|
const nodes = graph.nodes.map(node => ({
|
|
202
202
|
...node,
|
|
203
203
|
x: Number.isFinite(node.x) ? node.x : 0,
|
|
204
|
-
y: Number.isFinite(node.y) ? node.y : 0
|
|
204
|
+
y: Number.isFinite(node.y) ? node.y : 0,
|
|
205
|
+
vx: Number.isFinite(node.vx) ? node.vx : 0,
|
|
206
|
+
vy: Number.isFinite(node.vy) ? node.vy : 0
|
|
205
207
|
}))
|
|
206
208
|
const nodeMap = new Map(nodes.map(node => [node.id, node]))
|
|
207
209
|
const edges = graph.edges
|
|
@@ -296,6 +298,10 @@ const tick = delta => {
|
|
|
296
298
|
edges.forEach(edge => {
|
|
297
299
|
const source = edge.sourceNode
|
|
298
300
|
const target = edge.targetNode
|
|
301
|
+
source.vx = Number.isFinite(source.vx) ? source.vx : 0
|
|
302
|
+
source.vy = Number.isFinite(source.vy) ? source.vy : 0
|
|
303
|
+
target.vx = Number.isFinite(target.vx) ? target.vx : 0
|
|
304
|
+
target.vy = Number.isFinite(target.vy) ? target.vy : 0
|
|
299
305
|
const dx = target.x - source.x
|
|
300
306
|
const dy = target.y - source.y
|
|
301
307
|
const distance = Math.max(Math.hypot(dx, dy), 1)
|
|
@@ -312,6 +318,10 @@ const tick = delta => {
|
|
|
312
318
|
for (let j = i + 1; j < nodes.length; j += 1) {
|
|
313
319
|
const a = nodes[i]
|
|
314
320
|
const b = nodes[j]
|
|
321
|
+
a.vx = Number.isFinite(a.vx) ? a.vx : 0
|
|
322
|
+
a.vy = Number.isFinite(a.vy) ? a.vy : 0
|
|
323
|
+
b.vx = Number.isFinite(b.vx) ? b.vx : 0
|
|
324
|
+
b.vy = Number.isFinite(b.vy) ? b.vy : 0
|
|
315
325
|
const dx = b.x - a.x
|
|
316
326
|
const dy = b.y - a.y
|
|
317
327
|
const distance = Math.max(Math.hypot(dx, dy), 1)
|
|
@@ -326,6 +336,10 @@ const tick = delta => {
|
|
|
326
336
|
}
|
|
327
337
|
|
|
328
338
|
nodes.forEach(node => {
|
|
339
|
+
node.vx = Number.isFinite(node.vx) ? node.vx : 0
|
|
340
|
+
node.vy = Number.isFinite(node.vy) ? node.vy : 0
|
|
341
|
+
node.x = Number.isFinite(node.x) ? node.x : 0
|
|
342
|
+
node.y = Number.isFinite(node.y) ? node.y : 0
|
|
329
343
|
if (state.pointer.dragNode === node) {
|
|
330
344
|
node.vx = 0
|
|
331
345
|
node.vy = 0
|
|
@@ -411,6 +425,13 @@ const viewportNodeStride = () => {
|
|
|
411
425
|
}
|
|
412
426
|
|
|
413
427
|
const computeRenderVisibility = () => {
|
|
428
|
+
if (state.visibleNodes.length <= 2000) {
|
|
429
|
+
state.renderNodes = state.visibleNodes
|
|
430
|
+
const ids = new Set(state.renderNodes.map((node) => node.id))
|
|
431
|
+
state.renderEdges = state.visibleEdges.filter((edge) => ids.has(edge.source) && edge.target && ids.has(edge.target))
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
414
435
|
const viewport = worldViewportBounds()
|
|
415
436
|
const stride = viewportNodeStride()
|
|
416
437
|
const picked = []
|
|
@@ -433,6 +454,25 @@ const computeRenderVisibility = () => {
|
|
|
433
454
|
const nodes = picked.length > renderNodeBudget
|
|
434
455
|
? picked.slice(0, renderNodeBudget)
|
|
435
456
|
: picked
|
|
457
|
+
if (nodes.length === 0 && state.visibleNodes.length > 0) {
|
|
458
|
+
const centerX = (viewport.minX + viewport.maxX) / 2
|
|
459
|
+
const centerY = (viewport.minY + viewport.maxY) / 2
|
|
460
|
+
const closest = [...state.visibleNodes]
|
|
461
|
+
.sort((left, right) => {
|
|
462
|
+
const leftDistance = (left.x - centerX) ** 2 + (left.y - centerY) ** 2
|
|
463
|
+
const rightDistance = (right.x - centerX) ** 2 + (right.y - centerY) ** 2
|
|
464
|
+
return leftDistance - rightDistance
|
|
465
|
+
})
|
|
466
|
+
.slice(0, Math.min(renderNodeBudget, 180))
|
|
467
|
+
const closestIds = new Set(closest.map((node) => node.id))
|
|
468
|
+
|
|
469
|
+
state.renderNodes = closest
|
|
470
|
+
state.renderEdges = state.visibleEdges.filter(
|
|
471
|
+
(edge) => closestIds.has(edge.source) && edge.target && closestIds.has(edge.target)
|
|
472
|
+
)
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
|
|
436
476
|
const nodeIds = new Set(nodes.map((node) => node.id))
|
|
437
477
|
const edges = state.visibleEdges.filter((edge) => nodeIds.has(edge.source) && edge.target && nodeIds.has(edge.target))
|
|
438
478
|
|
|
@@ -611,6 +651,20 @@ const zoomAtPoint = (screenX, screenY, factor) => {
|
|
|
611
651
|
state.transform.y = screenY - worldY * nextScale
|
|
612
652
|
}
|
|
613
653
|
|
|
654
|
+
const wheelZoomFactor = event => {
|
|
655
|
+
const isModifierZoom = event.metaKey || event.ctrlKey
|
|
656
|
+
const delta = Math.abs(event.deltaY)
|
|
657
|
+
|
|
658
|
+
if (delta < 1) {
|
|
659
|
+
return event.deltaY < 0 ? 1.04 : 0.96
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const zoomInFactor = isModifierZoom ? 1.12 : 1.08
|
|
663
|
+
const zoomOutFactor = isModifierZoom ? 0.88 : 0.92
|
|
664
|
+
|
|
665
|
+
return event.deltaY < 0 ? zoomInFactor : zoomOutFactor
|
|
666
|
+
}
|
|
667
|
+
|
|
614
668
|
const bindEvents = () => {
|
|
615
669
|
window.addEventListener('resize', resize)
|
|
616
670
|
elements.search.addEventListener('input', event => {
|
|
@@ -659,7 +713,7 @@ const bindEvents = () => {
|
|
|
659
713
|
const rect = canvas.getBoundingClientRect()
|
|
660
714
|
const cursorX = event.clientX - rect.left
|
|
661
715
|
const cursorY = event.clientY - rect.top
|
|
662
|
-
const factor = event
|
|
716
|
+
const factor = wheelZoomFactor(event)
|
|
663
717
|
zoomAtPoint(cursorX, cursorY, factor)
|
|
664
718
|
}, { passive: false })
|
|
665
719
|
canvas.addEventListener('pointerdown', event => {
|
|
@@ -709,7 +763,7 @@ const loadAgents = async () => {
|
|
|
709
763
|
|
|
710
764
|
state.agentId = selected
|
|
711
765
|
if (signature !== state.agentsSignature) {
|
|
712
|
-
const formatAgentLabel = (agent) => agent.id
|
|
766
|
+
const formatAgentLabel = (agent) => agent.id
|
|
713
767
|
elements.agent.innerHTML = agents.length
|
|
714
768
|
? agents.map(agent => '<option value="' + escapeHtml(agent.id) + '">' + escapeHtml(formatAgentLabel(agent)) + '</option>').join('')
|
|
715
769
|
: '<option value="shared">shared</option>'
|
package/docs/AGENT_USAGE.md
CHANGED
|
@@ -530,9 +530,9 @@ This starts a local frontend for inspecting the knowledge graph.
|
|
|
530
530
|
|
|
531
531
|
Without `--vault`, the graph UI serves `$HOME/.brainlink/vault`.
|
|
532
532
|
|
|
533
|
-
The frontend includes an agent selector. Selecting an agent calls the same read APIs with `agent=<agent-id>` and renders that namespace instead of merging every agent into one graph.
|
|
533
|
+
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.
|
|
534
534
|
|
|
535
|
-
Graph navigation controls include zoom in, zoom out, fit visible nodes and reset-to-fit-all nodes. Mouse wheel zoom is anchored to the cursor. 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).
|
|
535
|
+
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. 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).
|
|
536
536
|
|
|
537
537
|
The command reindexes by default, then serves:
|
|
538
538
|
|
package/package.json
CHANGED