@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.deltaY < 0 ? 1.08 : 0.92
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 === 'shared' ? agent.id : agent.id + ' · ' + agent.documentCount
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>'
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.22",
3
+ "version": "0.1.0-beta.24",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",