@andespindola/brainlink 0.1.0-beta.98 → 1.0.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.
Files changed (46) hide show
  1. package/AGENTS.md +6 -6
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +186 -38
  4. package/dist/application/add-note.js +13 -44
  5. package/dist/application/analyze-vault.js +1 -1
  6. package/dist/application/auto-migrate-configured-vault.js +37 -0
  7. package/dist/application/build-context.js +119 -20
  8. package/dist/application/canonical-context-links.js +209 -0
  9. package/dist/application/frontend/client-css.js +212 -42
  10. package/dist/application/frontend/client-html.js +42 -28
  11. package/dist/application/frontend/client-js.js +1294 -3217
  12. package/dist/application/frontend/client-render-worker-js.js +676 -0
  13. package/dist/application/get-graph-contexts.js +33 -0
  14. package/dist/application/get-graph-layout.js +62 -8
  15. package/dist/application/get-graph-stream-chunk.js +326 -0
  16. package/dist/application/get-graph-view.js +246 -0
  17. package/dist/application/graph-view-state.js +66 -0
  18. package/dist/application/import-legacy-sqlite.js +3 -33
  19. package/dist/application/index-vault.js +35 -22
  20. package/dist/application/migrate-context-links.js +79 -0
  21. package/dist/application/search-graph-node-ids.js +63 -3
  22. package/dist/application/server/routes.js +197 -12
  23. package/dist/cli/commands/read-commands.js +39 -3
  24. package/dist/cli/commands/vault-commands.js +182 -0
  25. package/dist/cli/commands/write-commands.js +147 -12
  26. package/dist/cli/main.js +2 -0
  27. package/dist/cli/runtime.js +10 -2
  28. package/dist/domain/context.js +1 -0
  29. package/dist/domain/graph-contexts.js +180 -0
  30. package/dist/domain/graph-layout.js +347 -21
  31. package/dist/domain/markdown.js +53 -9
  32. package/dist/infrastructure/config.js +105 -6
  33. package/dist/infrastructure/context-packs.js +122 -0
  34. package/dist/infrastructure/file-index.js +6 -3
  35. package/dist/infrastructure/index-state.js +2 -0
  36. package/dist/infrastructure/vault-migration-state.js +69 -0
  37. package/dist/infrastructure/volatile-memory.js +100 -0
  38. package/dist/mcp/http-server.js +97 -0
  39. package/dist/mcp/runtime.js +20 -0
  40. package/dist/mcp/server.js +36 -13
  41. package/dist/mcp/tools.js +203 -14
  42. package/docs/AGENT_USAGE.md +50 -5
  43. package/docs/ARCHITECTURE.md +11 -0
  44. package/docs/QUICKSTART.md +3 -1
  45. package/docs/RELEASE.md +4 -3
  46. package/package.json +3 -1
@@ -0,0 +1,676 @@
1
+ export const createClientRenderWorkerJs = () => `let canvas = null
2
+ let gl = null
3
+ let viewportWidth = 320
4
+ let viewportHeight = 320
5
+ let devicePixelRatio = 1
6
+ const camera = { x: 0, y: 0, scale: 1 }
7
+ const state = {
8
+ nodeCount: 0,
9
+ edgeCount: 0,
10
+ ids: [],
11
+ titles: [],
12
+ kinds: [],
13
+ x: new Float32Array(0),
14
+ y: new Float32Array(0),
15
+ relevance: new Float32Array(0),
16
+ radius: new Float32Array(0),
17
+ colorIndex: new Uint8Array(0),
18
+ visible: new Uint8Array(0),
19
+ highlighted: new Uint8Array(0),
20
+ focused: new Uint8Array(0),
21
+ selected: new Uint8Array(0),
22
+ edgeSource: new Uint32Array(0),
23
+ edgeTarget: new Uint32Array(0),
24
+ edgeWeight: new Float32Array(0)
25
+ }
26
+ const nodeIndexById = new Map()
27
+ const highlightedIds = new Set()
28
+ const focusedIds = new Set()
29
+ let selectedNodeId = null
30
+ let dirty = true
31
+ let renderScheduled = false
32
+ let hoverX = null
33
+ let hoverY = null
34
+ let lastFrameAt = 0
35
+ let lastVisibleEdges = 0
36
+ let interactionUntil = 0
37
+ let settledRenderTimer = null
38
+ let edgePositionsBuffer = new Float32Array(0)
39
+ let pointPositionsBuffer = new Float32Array(0)
40
+ let pointSizesBuffer = new Float32Array(0)
41
+
42
+ const defaultTheme = {
43
+ node: [0.30, 0.56, 0.85, 1],
44
+ nodeCluster: [0.18, 0.44, 0.71, 1],
45
+ nodeHighlight: [0.95, 0.70, 0.25, 1],
46
+ nodeSelected: [0.09, 0.13, 0.20, 1],
47
+ nodePalette: [
48
+ [0.30, 0.56, 0.85, 1],
49
+ [0.40, 0.73, 0.43, 1],
50
+ [0.94, 0.64, 0.23, 1],
51
+ [0.85, 0.37, 0.55, 1],
52
+ [0.55, 0.45, 0.85, 1],
53
+ [0.33, 0.75, 0.77, 1],
54
+ [0.93, 0.42, 0.34, 1],
55
+ [0.60, 0.65, 0.70, 1],
56
+ [0.72, 0.51, 0.33, 1],
57
+ [0.44, 0.62, 0.85, 1]
58
+ ],
59
+ edge: [0.23, 0.31, 0.42, 0.18],
60
+ edgeHeavy: [0.23, 0.31, 0.42, 0.34],
61
+ clear: [0.96, 0.97, 0.98, 1]
62
+ }
63
+
64
+ const theme = { ...defaultTheme }
65
+
66
+ const createShader = (type, source) => {
67
+ const shader = gl.createShader(type)
68
+ if (!shader) return null
69
+ gl.shaderSource(shader, source)
70
+ gl.compileShader(shader)
71
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
72
+ gl.deleteShader(shader)
73
+ return null
74
+ }
75
+ return shader
76
+ }
77
+
78
+ const createProgram = (vertexSource, fragmentSource) => {
79
+ const vertexShader = createShader(gl.VERTEX_SHADER, vertexSource)
80
+ const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragmentSource)
81
+ if (!vertexShader || !fragmentShader) return null
82
+ const program = gl.createProgram()
83
+ if (!program) return null
84
+ gl.attachShader(program, vertexShader)
85
+ gl.attachShader(program, fragmentShader)
86
+ gl.linkProgram(program)
87
+ gl.deleteShader(vertexShader)
88
+ gl.deleteShader(fragmentShader)
89
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
90
+ gl.deleteProgram(program)
91
+ return null
92
+ }
93
+ return program
94
+ }
95
+
96
+ let lineProgram = null
97
+ let pointProgram = null
98
+ let lineBuffer = null
99
+ let pointPositionBuffer = null
100
+ let pointSizeBuffer = null
101
+ let linePositionLocation = -1
102
+ let lineResolutionLocation = null
103
+ let lineColorLocation = null
104
+ let pointPositionLocation = -1
105
+ let pointSizeLocation = -1
106
+ let pointResolutionLocation = null
107
+ let pointColorLocation = null
108
+
109
+ const initWebGl = () => {
110
+ if (!canvas) {
111
+ return false
112
+ }
113
+
114
+ gl = canvas.getContext('webgl2', { alpha: false, antialias: true, depth: false, stencil: false }) ||
115
+ canvas.getContext('webgl', { alpha: false, antialias: true, depth: false, stencil: false })
116
+
117
+ if (!gl) {
118
+ return false
119
+ }
120
+
121
+ lineProgram = createProgram(
122
+ 'attribute vec2 a_position; uniform vec2 u_resolution; void main() { vec2 zeroToOne = a_position / u_resolution; vec2 clip = zeroToOne * 2.0 - 1.0; gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); }',
123
+ 'precision mediump float; uniform vec4 u_color; void main() { gl_FragColor = u_color; }'
124
+ )
125
+
126
+ pointProgram = createProgram(
127
+ 'attribute vec2 a_position; attribute float a_size; uniform vec2 u_resolution; void main() { vec2 zeroToOne = a_position / u_resolution; vec2 clip = zeroToOne * 2.0 - 1.0; gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); gl_PointSize = a_size; }',
128
+ 'precision mediump float; uniform vec4 u_color; void main() { vec2 center = gl_PointCoord - vec2(0.5); float distanceFromCenter = length(center); if (distanceFromCenter > 0.5) discard; float alpha = smoothstep(0.5, 0.38, distanceFromCenter); gl_FragColor = vec4(u_color.rgb, u_color.a * alpha); }'
129
+ )
130
+
131
+ if (!lineProgram || !pointProgram) {
132
+ return false
133
+ }
134
+
135
+ lineBuffer = gl.createBuffer()
136
+ pointPositionBuffer = gl.createBuffer()
137
+ pointSizeBuffer = gl.createBuffer()
138
+ if (!lineBuffer || !pointPositionBuffer || !pointSizeBuffer) {
139
+ return false
140
+ }
141
+
142
+ linePositionLocation = gl.getAttribLocation(lineProgram, 'a_position')
143
+ lineResolutionLocation = gl.getUniformLocation(lineProgram, 'u_resolution')
144
+ lineColorLocation = gl.getUniformLocation(lineProgram, 'u_color')
145
+ pointPositionLocation = gl.getAttribLocation(pointProgram, 'a_position')
146
+ pointSizeLocation = gl.getAttribLocation(pointProgram, 'a_size')
147
+ pointResolutionLocation = gl.getUniformLocation(pointProgram, 'u_resolution')
148
+ pointColorLocation = gl.getUniformLocation(pointProgram, 'u_color')
149
+
150
+ gl.enable(gl.BLEND)
151
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
152
+ gl.disable(gl.DEPTH_TEST)
153
+
154
+ return true
155
+ }
156
+
157
+ const ensureFloat32Capacity = (buffer, neededLength) => {
158
+ if (buffer.length >= neededLength) {
159
+ return buffer
160
+ }
161
+ const next = Math.max(neededLength, Math.ceil(buffer.length * 1.6), 1024)
162
+ return new Float32Array(next)
163
+ }
164
+
165
+ const resizeCanvas = (width, height, ratio) => {
166
+ viewportWidth = Math.max(320, Number.isFinite(width) ? width : viewportWidth)
167
+ viewportHeight = Math.max(320, Number.isFinite(height) ? height : viewportHeight)
168
+ devicePixelRatio = Math.max(1, Number.isFinite(ratio) ? ratio : devicePixelRatio)
169
+
170
+ if (!canvas) return
171
+
172
+ canvas.width = Math.floor(viewportWidth * devicePixelRatio)
173
+ canvas.height = Math.floor(viewportHeight * devicePixelRatio)
174
+ if (gl) {
175
+ gl.viewport(0, 0, canvas.width, canvas.height)
176
+ }
177
+ dirty = true
178
+ requestRender()
179
+ }
180
+
181
+ const toScreenPoint = (x, y) => {
182
+ const sx = (x * camera.scale + camera.x) * devicePixelRatio
183
+ const sy = (y * camera.scale + camera.y) * devicePixelRatio
184
+ return [sx, sy]
185
+ }
186
+
187
+ const ensureNodeCapacity = (count) => {
188
+ if (state.x.length >= count) {
189
+ return
190
+ }
191
+
192
+ const nextCapacity = Math.max(count, Math.ceil(state.x.length * 1.5), 512)
193
+ state.x = new Float32Array(nextCapacity)
194
+ state.y = new Float32Array(nextCapacity)
195
+ state.relevance = new Float32Array(nextCapacity)
196
+ state.radius = new Float32Array(nextCapacity)
197
+ state.colorIndex = new Uint8Array(nextCapacity)
198
+ state.visible = new Uint8Array(nextCapacity)
199
+ state.highlighted = new Uint8Array(nextCapacity)
200
+ state.focused = new Uint8Array(nextCapacity)
201
+ state.selected = new Uint8Array(nextCapacity)
202
+ }
203
+
204
+ const ensureEdgeCapacity = (count) => {
205
+ if (state.edgeSource.length >= count) {
206
+ return
207
+ }
208
+
209
+ const nextCapacity = Math.max(count, Math.ceil(state.edgeSource.length * 1.5), 1024)
210
+ state.edgeSource = new Uint32Array(nextCapacity)
211
+ state.edgeTarget = new Uint32Array(nextCapacity)
212
+ state.edgeWeight = new Float32Array(nextCapacity)
213
+ }
214
+
215
+ const nodeRadius = (relevance, kind) => {
216
+ const base = kind === 'cluster' ? 10.2 : 6.4
217
+ const modifier = Math.min(6.6, Math.max(0, relevance * 0.72))
218
+ return base + modifier
219
+ }
220
+
221
+ const segmentColorIndex = (segment) => {
222
+ const value = String(segment || '')
223
+ let hash = 0
224
+ for (let index = 0; index < value.length; index += 1) {
225
+ hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0
226
+ }
227
+ const palette = Array.isArray(theme.nodePalette) && theme.nodePalette.length > 0 ? theme.nodePalette : [theme.node]
228
+ return Math.abs(hash) % palette.length
229
+ }
230
+
231
+ const loadChunk = (chunk) => {
232
+ const nodes = Array.isArray(chunk?.nodes) ? chunk.nodes : []
233
+ const edges = Array.isArray(chunk?.edges) ? chunk.edges : []
234
+
235
+ ensureNodeCapacity(nodes.length)
236
+ nodeIndexById.clear()
237
+ state.ids = new Array(nodes.length)
238
+ state.titles = new Array(nodes.length)
239
+ state.kinds = new Array(nodes.length)
240
+
241
+ for (let index = 0; index < nodes.length; index += 1) {
242
+ const row = nodes[index]
243
+ const id = typeof row?.[0] === 'string' ? row[0] : ''
244
+ if (!id) {
245
+ continue
246
+ }
247
+ const title = typeof row?.[1] === 'string' ? row[1] : id
248
+ const x = Number.isFinite(row?.[2]) ? Number(row[2]) : 0
249
+ const y = Number.isFinite(row?.[3]) ? Number(row[3]) : 0
250
+ const segment = typeof row?.[5] === 'string' ? row[5] : ''
251
+ const kind = row?.[6] === 'cluster' ? 'cluster' : 'node'
252
+ const relevance = Number.isFinite(row?.[7]) ? Number(row[7]) : 0
253
+
254
+ state.ids[index] = id
255
+ state.titles[index] = title
256
+ state.kinds[index] = kind
257
+ state.x[index] = x
258
+ state.y[index] = y
259
+ state.relevance[index] = relevance
260
+ state.radius[index] = nodeRadius(relevance, kind)
261
+ state.colorIndex[index] = segmentColorIndex(segment || title)
262
+ state.visible[index] = 0
263
+ state.highlighted[index] = highlightedIds.has(id) ? 1 : 0
264
+ state.focused[index] = focusedIds.has(id) ? 1 : 0
265
+ state.selected[index] = selectedNodeId === id ? 1 : 0
266
+ nodeIndexById.set(id, index)
267
+ }
268
+
269
+ state.nodeCount = nodes.length
270
+
271
+ ensureEdgeCapacity(edges.length)
272
+ let edgeCount = 0
273
+ for (let index = 0; index < edges.length; index += 1) {
274
+ const row = edges[index]
275
+ const sourceId = typeof row?.[0] === 'string' ? row[0] : ''
276
+ const targetId = typeof row?.[1] === 'string' ? row[1] : ''
277
+ const source = nodeIndexById.get(sourceId)
278
+ const target = nodeIndexById.get(targetId)
279
+ if (source === undefined || target === undefined || source === target) {
280
+ continue
281
+ }
282
+ state.edgeSource[edgeCount] = source
283
+ state.edgeTarget[edgeCount] = target
284
+ state.edgeWeight[edgeCount] = Number.isFinite(row?.[2]) ? Number(row[2]) : 1
285
+ edgeCount += 1
286
+ }
287
+
288
+ state.edgeCount = edgeCount
289
+ dirty = true
290
+ }
291
+
292
+ const cullVisibleNodes = () => {
293
+ const minX = -280
294
+ const minY = -280
295
+ const maxX = viewportWidth + 280
296
+ const maxY = viewportHeight + 280
297
+
298
+ for (let index = 0; index < state.nodeCount; index += 1) {
299
+ const radius = state.radius[index] * camera.scale
300
+ const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
301
+ const screenX = sx / devicePixelRatio
302
+ const screenY = sy / devicePixelRatio
303
+ state.visible[index] = screenX + radius >= minX && screenX - radius <= maxX && screenY + radius >= minY && screenY - radius <= maxY ? 1 : 0
304
+ }
305
+ }
306
+
307
+ const drawEdges = () => {
308
+ if (!gl || state.edgeCount === 0) return
309
+
310
+ edgePositionsBuffer = ensureFloat32Capacity(edgePositionsBuffer, state.edgeCount * 4)
311
+ let cursor = 0
312
+ let visibleEdges = 0
313
+ for (let index = 0; index < state.edgeCount; index += 1) {
314
+ const source = state.edgeSource[index]
315
+ const target = state.edgeTarget[index]
316
+ if (state.visible[source] === 0 && state.visible[target] === 0) {
317
+ continue
318
+ }
319
+ const [sx, sy] = toScreenPoint(state.x[source], state.y[source])
320
+ const [tx, ty] = toScreenPoint(state.x[target], state.y[target])
321
+ edgePositionsBuffer[cursor] = sx
322
+ edgePositionsBuffer[cursor + 1] = sy
323
+ edgePositionsBuffer[cursor + 2] = tx
324
+ edgePositionsBuffer[cursor + 3] = ty
325
+ cursor += 4
326
+ visibleEdges += 1
327
+ }
328
+
329
+ lastVisibleEdges = visibleEdges
330
+ if (cursor === 0) return
331
+
332
+ gl.useProgram(lineProgram)
333
+ gl.bindBuffer(gl.ARRAY_BUFFER, lineBuffer)
334
+ gl.bufferData(gl.ARRAY_BUFFER, edgePositionsBuffer.subarray(0, cursor), gl.STREAM_DRAW)
335
+ gl.enableVertexAttribArray(linePositionLocation)
336
+ gl.vertexAttribPointer(linePositionLocation, 2, gl.FLOAT, false, 0, 0)
337
+ gl.uniform2f(lineResolutionLocation, canvas.width, canvas.height)
338
+ gl.uniform4fv(lineColorLocation, state.nodeCount > 4000 ? theme.edge : theme.edgeHeavy)
339
+ gl.lineWidth(1)
340
+ gl.drawArrays(gl.LINES, 0, cursor / 2)
341
+ }
342
+
343
+ const drawNodeLayer = (predicate, color, radiusBoost = 1) => {
344
+ if (!gl || state.nodeCount === 0) return
345
+
346
+ pointPositionsBuffer = ensureFloat32Capacity(pointPositionsBuffer, state.nodeCount * 2)
347
+ pointSizesBuffer = ensureFloat32Capacity(pointSizesBuffer, state.nodeCount)
348
+ let positionCursor = 0
349
+ let sizeCursor = 0
350
+ for (let index = 0; index < state.nodeCount; index += 1) {
351
+ if (!predicate(index)) continue
352
+ const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
353
+ pointPositionsBuffer[positionCursor] = sx
354
+ pointPositionsBuffer[positionCursor + 1] = sy
355
+ pointSizesBuffer[sizeCursor] = Math.max(1.2, state.radius[index] * camera.scale * devicePixelRatio * radiusBoost)
356
+ positionCursor += 2
357
+ sizeCursor += 1
358
+ }
359
+
360
+ if (positionCursor === 0) return
361
+
362
+ gl.useProgram(pointProgram)
363
+ gl.bindBuffer(gl.ARRAY_BUFFER, pointPositionBuffer)
364
+ gl.bufferData(gl.ARRAY_BUFFER, pointPositionsBuffer.subarray(0, positionCursor), gl.STREAM_DRAW)
365
+ gl.enableVertexAttribArray(pointPositionLocation)
366
+ gl.vertexAttribPointer(pointPositionLocation, 2, gl.FLOAT, false, 0, 0)
367
+
368
+ gl.bindBuffer(gl.ARRAY_BUFFER, pointSizeBuffer)
369
+ gl.bufferData(gl.ARRAY_BUFFER, pointSizesBuffer.subarray(0, sizeCursor), gl.STREAM_DRAW)
370
+ gl.enableVertexAttribArray(pointSizeLocation)
371
+ gl.vertexAttribPointer(pointSizeLocation, 1, gl.FLOAT, false, 0, 0)
372
+
373
+ gl.uniform2f(pointResolutionLocation, canvas.width, canvas.height)
374
+ gl.uniform4fv(pointColorLocation, color)
375
+ gl.drawArrays(gl.POINTS, 0, positionCursor / 2)
376
+ }
377
+
378
+ const drawColoredNodeLayer = (predicate, radiusBoost = 1) => {
379
+ const palette = Array.isArray(theme.nodePalette) && theme.nodePalette.length > 0 ? theme.nodePalette : [theme.node]
380
+ for (let colorIndex = 0; colorIndex < palette.length; colorIndex += 1) {
381
+ drawNodeLayer((index) => predicate(index) && state.colorIndex[index] === colorIndex, palette[colorIndex], radiusBoost)
382
+ }
383
+ }
384
+
385
+ const clear = () => {
386
+ if (!gl || !canvas) return
387
+ gl.viewport(0, 0, canvas.width, canvas.height)
388
+ gl.clearColor(theme.clear[0], theme.clear[1], theme.clear[2], theme.clear[3])
389
+ gl.clear(gl.COLOR_BUFFER_BIT)
390
+ }
391
+
392
+ const isCameraInteracting = (now) => now < interactionUntil
393
+
394
+ const scheduleSettledRender = (now) => {
395
+ if (settledRenderTimer) {
396
+ return
397
+ }
398
+ const delay = Math.max(32, interactionUntil - now + 16)
399
+ settledRenderTimer = setTimeout(() => {
400
+ settledRenderTimer = null
401
+ dirty = true
402
+ requestRender()
403
+ }, delay)
404
+ }
405
+
406
+ const renderFrame = (now) => {
407
+ renderScheduled = false
408
+ if (!dirty) {
409
+ return
410
+ }
411
+
412
+ const delta = now - lastFrameAt
413
+ const minInterval = state.nodeCount > 20000 ? 22 : 16
414
+ if (delta < minInterval) {
415
+ requestRender()
416
+ return
417
+ }
418
+ lastFrameAt = now
419
+
420
+ if (!gl) {
421
+ return
422
+ }
423
+
424
+ cullVisibleNodes()
425
+ clear()
426
+ const cameraInteracting = isCameraInteracting(now)
427
+ if (!cameraInteracting || state.edgeCount < 1200) {
428
+ drawEdges()
429
+ } else {
430
+ lastVisibleEdges = 0
431
+ scheduleSettledRender(now)
432
+ }
433
+
434
+ drawColoredNodeLayer(
435
+ (index) => state.visible[index] === 1 && state.kinds[index] !== 'cluster' && state.selected[index] === 0 && state.highlighted[index] === 0 && state.focused[index] === 0,
436
+ 1
437
+ )
438
+
439
+ drawColoredNodeLayer(
440
+ (index) => state.visible[index] === 1 && state.kinds[index] === 'cluster' && state.selected[index] === 0,
441
+ 1.15
442
+ )
443
+
444
+ drawNodeLayer(
445
+ (index) => state.visible[index] === 1 && state.highlighted[index] === 1,
446
+ theme.nodeHighlight,
447
+ 1.22
448
+ )
449
+
450
+ drawNodeLayer(
451
+ (index) => state.visible[index] === 1 && state.focused[index] === 1,
452
+ theme.nodeHighlight,
453
+ 1.12
454
+ )
455
+
456
+ drawNodeLayer(
457
+ (index) => state.visible[index] === 1 && state.selected[index] === 1,
458
+ theme.nodeSelected,
459
+ 1.32
460
+ )
461
+
462
+ if (dirty) {
463
+ postMessage({
464
+ type: 'frame-stats',
465
+ visibleNodes: (() => {
466
+ let count = 0
467
+ for (let index = 0; index < state.nodeCount; index += 1) {
468
+ if (state.visible[index] === 1) count += 1
469
+ }
470
+ return count
471
+ })(),
472
+ visibleEdges: lastVisibleEdges
473
+ })
474
+ dirty = false
475
+ }
476
+ }
477
+
478
+ const requestRender = () => {
479
+ if (renderScheduled) {
480
+ return
481
+ }
482
+ renderScheduled = true
483
+ const raf = self.requestAnimationFrame
484
+ if (typeof raf === 'function') {
485
+ raf.call(self, renderFrame)
486
+ return
487
+ }
488
+ setTimeout(() => renderFrame(performance.now()), 16)
489
+ }
490
+
491
+ const setCamera = (nextCamera) => {
492
+ if (!nextCamera || typeof nextCamera !== 'object') {
493
+ return
494
+ }
495
+ camera.x = Number.isFinite(nextCamera.x) ? Number(nextCamera.x) : camera.x
496
+ camera.y = Number.isFinite(nextCamera.y) ? Number(nextCamera.y) : camera.y
497
+ camera.scale = Number.isFinite(nextCamera.scale) ? Math.max(0.0002, Math.min(8, Number(nextCamera.scale))) : camera.scale
498
+ interactionUntil = performance.now() + 140
499
+ dirty = true
500
+ requestRender()
501
+ }
502
+
503
+ const worldAtScreen = (screenX, screenY) => {
504
+ const x = (screenX - camera.x) / camera.scale
505
+ const y = (screenY - camera.y) / camera.scale
506
+ return [x, y]
507
+ }
508
+
509
+ const pickNode = (screenX, screenY) => {
510
+ const [worldX, worldY] = worldAtScreen(screenX, screenY)
511
+ let bestIndex = -1
512
+ let bestDistance = Infinity
513
+
514
+ for (let index = 0; index < state.nodeCount; index += 1) {
515
+ if (state.visible[index] === 0) continue
516
+ const dx = state.x[index] - worldX
517
+ const dy = state.y[index] - worldY
518
+ const distance = Math.hypot(dx, dy)
519
+ const maxDistance = state.radius[index] * 1.2
520
+ if (distance <= maxDistance && distance < bestDistance) {
521
+ bestDistance = distance
522
+ bestIndex = index
523
+ }
524
+ }
525
+
526
+ if (bestIndex < 0) {
527
+ return null
528
+ }
529
+
530
+ return {
531
+ id: state.ids[bestIndex],
532
+ title: state.titles[bestIndex],
533
+ kind: state.kinds[bestIndex],
534
+ x: state.x[bestIndex],
535
+ y: state.y[bestIndex]
536
+ }
537
+ }
538
+
539
+ const setHighlights = (ids) => {
540
+ highlightedIds.clear()
541
+ const list = Array.isArray(ids) ? ids : []
542
+ for (let index = 0; index < list.length; index += 1) {
543
+ const id = list[index]
544
+ if (typeof id === 'string' && id.length > 0) {
545
+ highlightedIds.add(id)
546
+ }
547
+ }
548
+
549
+ for (let index = 0; index < state.nodeCount; index += 1) {
550
+ state.highlighted[index] = highlightedIds.has(state.ids[index]) ? 1 : 0
551
+ }
552
+ dirty = true
553
+ requestRender()
554
+ }
555
+
556
+ const setFocus = (ids) => {
557
+ focusedIds.clear()
558
+ const list = Array.isArray(ids) ? ids : []
559
+ for (let index = 0; index < list.length; index += 1) {
560
+ const id = list[index]
561
+ if (typeof id === 'string' && id.length > 0) {
562
+ focusedIds.add(id)
563
+ }
564
+ }
565
+
566
+ for (let index = 0; index < state.nodeCount; index += 1) {
567
+ state.focused[index] = focusedIds.has(state.ids[index]) ? 1 : 0
568
+ }
569
+ dirty = true
570
+ requestRender()
571
+ }
572
+
573
+ const setSelected = (id) => {
574
+ selectedNodeId = typeof id === 'string' && id.length > 0 ? id : null
575
+ for (let index = 0; index < state.nodeCount; index += 1) {
576
+ state.selected[index] = selectedNodeId === state.ids[index] ? 1 : 0
577
+ }
578
+ dirty = true
579
+ requestRender()
580
+ }
581
+
582
+ const moveNode = (id, x, y) => {
583
+ if (typeof id !== 'string' || !Number.isFinite(x) || !Number.isFinite(y)) {
584
+ return
585
+ }
586
+
587
+ const index = nodeIndexById.get(id)
588
+ if (index === undefined) {
589
+ return
590
+ }
591
+
592
+ state.x[index] = x
593
+ state.y[index] = y
594
+ dirty = true
595
+ requestRender()
596
+ }
597
+
598
+ self.onmessage = (event) => {
599
+ const payload = event.data
600
+ if (!payload || typeof payload !== 'object') {
601
+ return
602
+ }
603
+
604
+ if (payload.type === 'init') {
605
+ canvas = payload.canvas
606
+ if (payload.theme && typeof payload.theme === 'object') {
607
+ Object.assign(theme, payload.theme)
608
+ }
609
+ const initialized = initWebGl()
610
+ if (!initialized) {
611
+ postMessage({ type: 'fatal', message: 'WebGL is not available in render worker.' })
612
+ return
613
+ }
614
+ resizeCanvas(payload.width, payload.height, payload.devicePixelRatio)
615
+ setCamera(payload.camera)
616
+ requestRender()
617
+ postMessage({ type: 'ready' })
618
+ return
619
+ }
620
+
621
+ if (payload.type === 'resize') {
622
+ resizeCanvas(payload.width, payload.height, payload.devicePixelRatio)
623
+ return
624
+ }
625
+
626
+ if (payload.type === 'camera') {
627
+ setCamera(payload.camera)
628
+ return
629
+ }
630
+
631
+ if (payload.type === 'chunk') {
632
+ loadChunk(payload.chunk)
633
+ requestRender()
634
+ return
635
+ }
636
+
637
+ if (payload.type === 'highlight') {
638
+ setHighlights(payload.ids)
639
+ return
640
+ }
641
+
642
+ if (payload.type === 'focus') {
643
+ setFocus(payload.ids)
644
+ return
645
+ }
646
+
647
+ if (payload.type === 'select') {
648
+ setSelected(payload.id)
649
+ return
650
+ }
651
+
652
+ if (payload.type === 'move-node') {
653
+ moveNode(payload.id, Number(payload.x), Number(payload.y))
654
+ return
655
+ }
656
+
657
+ if (payload.type === 'pick') {
658
+ const node = pickNode(
659
+ Number.isFinite(payload.x) ? Number(payload.x) : 0,
660
+ Number.isFinite(payload.y) ? Number(payload.y) : 0
661
+ )
662
+ postMessage({
663
+ type: 'pick-result',
664
+ requestId: payload.requestId,
665
+ node
666
+ })
667
+ return
668
+ }
669
+
670
+ if (payload.type === 'pointer') {
671
+ hoverX = Number.isFinite(payload.x) ? Number(payload.x) : null
672
+ hoverY = Number.isFinite(payload.y) ? Number(payload.y) : null
673
+ return
674
+ }
675
+ }
676
+ `;
@@ -0,0 +1,33 @@
1
+ import { getGraphLayout } from './get-graph-layout.js';
2
+ export const getGraphContexts = async (vaultPath, agentId) => {
3
+ const { layout } = await getGraphLayout(vaultPath, { agentId });
4
+ const nodeIdsByContext = new Map();
5
+ const contextByNodeId = new Map();
6
+ layout.nodes.forEach((node) => {
7
+ const title = node.segment || node.group || 'root';
8
+ const nodeIds = nodeIdsByContext.get(title) ?? new Set();
9
+ nodeIds.add(node.id);
10
+ nodeIdsByContext.set(title, nodeIds);
11
+ contextByNodeId.set(node.id, title);
12
+ });
13
+ const edgeCountByContext = new Map();
14
+ layout.edges.forEach((edge) => {
15
+ if (!edge.target) {
16
+ return;
17
+ }
18
+ const sourceContext = contextByNodeId.get(edge.source);
19
+ const targetContext = contextByNodeId.get(edge.target);
20
+ if (!sourceContext || sourceContext !== targetContext) {
21
+ return;
22
+ }
23
+ edgeCountByContext.set(sourceContext, (edgeCountByContext.get(sourceContext) ?? 0) + 1);
24
+ });
25
+ return Array.from(nodeIdsByContext.entries())
26
+ .map(([title, nodeIds]) => ({
27
+ id: title,
28
+ title,
29
+ nodeCount: nodeIds.size,
30
+ edgeCount: edgeCountByContext.get(title) ?? 0
31
+ }))
32
+ .sort((left, right) => right.nodeCount - left.nodeCount || left.title.localeCompare(right.title));
33
+ };