@andespindola/brainlink 0.1.0-beta.15 → 0.1.0-beta.150
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/AGENTS.md +3 -0
- package/CHANGELOG.md +24 -0
- package/COPYRIGHT.md +5 -0
- package/README.md +135 -7
- package/dist/application/auto-migrate-configured-vault.js +37 -0
- package/dist/application/build-context.js +64 -3
- package/dist/application/dedupe-notes.js +226 -0
- package/dist/application/frontend/client-css.js +111 -47
- package/dist/application/frontend/client-html.js +42 -26
- package/dist/application/frontend/client-js.js +788 -554
- package/dist/application/frontend/client-render-worker-js.js +569 -0
- package/dist/application/frontend/client-worker-js.js +66 -0
- package/dist/application/get-graph-layout.js +38 -5
- package/dist/application/get-graph-stream-chunk.js +289 -0
- package/dist/application/get-graph-view.js +243 -0
- package/dist/application/import-legacy-sqlite.js +296 -0
- package/dist/application/index-vault.js +249 -21
- package/dist/application/offline-pack-backup.js +44 -0
- package/dist/application/server/routes.js +187 -5
- package/dist/application/start-server.js +75 -4
- package/dist/application/watch-vault.js +23 -2
- package/dist/cli/commands/agent-commands.js +7 -0
- package/dist/cli/commands/write-commands.js +842 -8
- package/dist/cli/runtime.js +10 -2
- package/dist/domain/context.js +54 -11
- package/dist/domain/graph-layout.js +275 -3
- package/dist/domain/markdown.js +29 -9
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +117 -4
- package/dist/infrastructure/file-index.js +70 -3
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/index-state.js +58 -0
- package/dist/infrastructure/private-pack-codec.js +71 -10
- package/dist/infrastructure/search-packs.js +286 -15
- package/dist/infrastructure/vault-migration-state.js +69 -0
- package/dist/infrastructure/volatile-memory.js +100 -0
- package/dist/mcp/runtime.js +20 -0
- package/dist/mcp/server.js +28 -10
- package/dist/mcp/tools.js +110 -0
- package/docs/AGENT_USAGE.md +87 -3
- package/docs/ARCHITECTURE.md +6 -0
- package/docs/QUICKSTART.md +7 -0
- package/package.json +7 -2
|
@@ -0,0 +1,569 @@
|
|
|
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
|
+
visible: new Uint8Array(0),
|
|
18
|
+
highlighted: new Uint8Array(0),
|
|
19
|
+
selected: new Uint8Array(0),
|
|
20
|
+
edgeSource: new Uint32Array(0),
|
|
21
|
+
edgeTarget: new Uint32Array(0),
|
|
22
|
+
edgeWeight: new Float32Array(0)
|
|
23
|
+
}
|
|
24
|
+
const nodeIndexById = new Map()
|
|
25
|
+
const highlightedIds = new Set()
|
|
26
|
+
let selectedNodeId = null
|
|
27
|
+
let dirty = true
|
|
28
|
+
let renderScheduled = false
|
|
29
|
+
let hoverX = null
|
|
30
|
+
let hoverY = null
|
|
31
|
+
let lastFrameAt = 0
|
|
32
|
+
let lastVisibleEdges = 0
|
|
33
|
+
let edgePositionsBuffer = new Float32Array(0)
|
|
34
|
+
let pointPositionsBuffer = new Float32Array(0)
|
|
35
|
+
let pointSizesBuffer = new Float32Array(0)
|
|
36
|
+
|
|
37
|
+
const defaultTheme = {
|
|
38
|
+
node: [0.68, 0.72, 0.78, 1],
|
|
39
|
+
nodeCluster: [0.42, 0.76, 0.92, 1],
|
|
40
|
+
nodeHighlight: [0.95, 0.76, 0.22, 1],
|
|
41
|
+
nodeSelected: [0.99, 0.99, 1, 1],
|
|
42
|
+
edge: [0.58, 0.64, 0.74, 0.24],
|
|
43
|
+
edgeHeavy: [0.78, 0.84, 0.92, 0.44],
|
|
44
|
+
clear: [0.05, 0.06, 0.08, 1]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const theme = { ...defaultTheme }
|
|
48
|
+
|
|
49
|
+
const createShader = (type, source) => {
|
|
50
|
+
const shader = gl.createShader(type)
|
|
51
|
+
if (!shader) return null
|
|
52
|
+
gl.shaderSource(shader, source)
|
|
53
|
+
gl.compileShader(shader)
|
|
54
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
55
|
+
gl.deleteShader(shader)
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
return shader
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const createProgram = (vertexSource, fragmentSource) => {
|
|
62
|
+
const vertexShader = createShader(gl.VERTEX_SHADER, vertexSource)
|
|
63
|
+
const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragmentSource)
|
|
64
|
+
if (!vertexShader || !fragmentShader) return null
|
|
65
|
+
const program = gl.createProgram()
|
|
66
|
+
if (!program) return null
|
|
67
|
+
gl.attachShader(program, vertexShader)
|
|
68
|
+
gl.attachShader(program, fragmentShader)
|
|
69
|
+
gl.linkProgram(program)
|
|
70
|
+
gl.deleteShader(vertexShader)
|
|
71
|
+
gl.deleteShader(fragmentShader)
|
|
72
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
73
|
+
gl.deleteProgram(program)
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
return program
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let lineProgram = null
|
|
80
|
+
let pointProgram = null
|
|
81
|
+
let lineBuffer = null
|
|
82
|
+
let pointPositionBuffer = null
|
|
83
|
+
let pointSizeBuffer = null
|
|
84
|
+
let linePositionLocation = -1
|
|
85
|
+
let lineResolutionLocation = null
|
|
86
|
+
let lineColorLocation = null
|
|
87
|
+
let pointPositionLocation = -1
|
|
88
|
+
let pointSizeLocation = -1
|
|
89
|
+
let pointResolutionLocation = null
|
|
90
|
+
let pointColorLocation = null
|
|
91
|
+
|
|
92
|
+
const initWebGl = () => {
|
|
93
|
+
if (!canvas) {
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
gl = canvas.getContext('webgl2', { alpha: false, antialias: true, depth: false, stencil: false }) ||
|
|
98
|
+
canvas.getContext('webgl', { alpha: false, antialias: true, depth: false, stencil: false })
|
|
99
|
+
|
|
100
|
+
if (!gl) {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
lineProgram = createProgram(
|
|
105
|
+
'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); }',
|
|
106
|
+
'precision mediump float; uniform vec4 u_color; void main() { gl_FragColor = u_color; }'
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
pointProgram = createProgram(
|
|
110
|
+
'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; }',
|
|
111
|
+
'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); }'
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if (!lineProgram || !pointProgram) {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
lineBuffer = gl.createBuffer()
|
|
119
|
+
pointPositionBuffer = gl.createBuffer()
|
|
120
|
+
pointSizeBuffer = gl.createBuffer()
|
|
121
|
+
if (!lineBuffer || !pointPositionBuffer || !pointSizeBuffer) {
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
linePositionLocation = gl.getAttribLocation(lineProgram, 'a_position')
|
|
126
|
+
lineResolutionLocation = gl.getUniformLocation(lineProgram, 'u_resolution')
|
|
127
|
+
lineColorLocation = gl.getUniformLocation(lineProgram, 'u_color')
|
|
128
|
+
pointPositionLocation = gl.getAttribLocation(pointProgram, 'a_position')
|
|
129
|
+
pointSizeLocation = gl.getAttribLocation(pointProgram, 'a_size')
|
|
130
|
+
pointResolutionLocation = gl.getUniformLocation(pointProgram, 'u_resolution')
|
|
131
|
+
pointColorLocation = gl.getUniformLocation(pointProgram, 'u_color')
|
|
132
|
+
|
|
133
|
+
gl.enable(gl.BLEND)
|
|
134
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
|
135
|
+
gl.disable(gl.DEPTH_TEST)
|
|
136
|
+
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const ensureFloat32Capacity = (buffer, neededLength) => {
|
|
141
|
+
if (buffer.length >= neededLength) {
|
|
142
|
+
return buffer
|
|
143
|
+
}
|
|
144
|
+
const next = Math.max(neededLength, Math.ceil(buffer.length * 1.6), 1024)
|
|
145
|
+
return new Float32Array(next)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const resizeCanvas = (width, height, ratio) => {
|
|
149
|
+
viewportWidth = Math.max(320, Number.isFinite(width) ? width : viewportWidth)
|
|
150
|
+
viewportHeight = Math.max(320, Number.isFinite(height) ? height : viewportHeight)
|
|
151
|
+
devicePixelRatio = Math.max(1, Number.isFinite(ratio) ? ratio : devicePixelRatio)
|
|
152
|
+
|
|
153
|
+
if (!canvas) return
|
|
154
|
+
|
|
155
|
+
canvas.width = Math.floor(viewportWidth * devicePixelRatio)
|
|
156
|
+
canvas.height = Math.floor(viewportHeight * devicePixelRatio)
|
|
157
|
+
if (gl) {
|
|
158
|
+
gl.viewport(0, 0, canvas.width, canvas.height)
|
|
159
|
+
}
|
|
160
|
+
dirty = true
|
|
161
|
+
requestRender()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const toScreenPoint = (x, y) => {
|
|
165
|
+
const sx = (x * camera.scale + camera.x) * devicePixelRatio
|
|
166
|
+
const sy = (y * camera.scale + camera.y) * devicePixelRatio
|
|
167
|
+
return [sx, sy]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const ensureNodeCapacity = (count) => {
|
|
171
|
+
if (state.x.length >= count) {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const nextCapacity = Math.max(count, Math.ceil(state.x.length * 1.5), 512)
|
|
176
|
+
state.x = new Float32Array(nextCapacity)
|
|
177
|
+
state.y = new Float32Array(nextCapacity)
|
|
178
|
+
state.relevance = new Float32Array(nextCapacity)
|
|
179
|
+
state.radius = new Float32Array(nextCapacity)
|
|
180
|
+
state.visible = new Uint8Array(nextCapacity)
|
|
181
|
+
state.highlighted = new Uint8Array(nextCapacity)
|
|
182
|
+
state.selected = new Uint8Array(nextCapacity)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const ensureEdgeCapacity = (count) => {
|
|
186
|
+
if (state.edgeSource.length >= count) {
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const nextCapacity = Math.max(count, Math.ceil(state.edgeSource.length * 1.5), 1024)
|
|
191
|
+
state.edgeSource = new Uint32Array(nextCapacity)
|
|
192
|
+
state.edgeTarget = new Uint32Array(nextCapacity)
|
|
193
|
+
state.edgeWeight = new Float32Array(nextCapacity)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const nodeRadius = (relevance, kind) => {
|
|
197
|
+
const base = kind === 'cluster' ? 7.8 : 4.6
|
|
198
|
+
const modifier = Math.min(4.8, Math.max(0, relevance * 0.55))
|
|
199
|
+
return base + modifier
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const loadChunk = (chunk) => {
|
|
203
|
+
const nodes = Array.isArray(chunk?.nodes) ? chunk.nodes : []
|
|
204
|
+
const edges = Array.isArray(chunk?.edges) ? chunk.edges : []
|
|
205
|
+
|
|
206
|
+
ensureNodeCapacity(nodes.length)
|
|
207
|
+
nodeIndexById.clear()
|
|
208
|
+
state.ids = new Array(nodes.length)
|
|
209
|
+
state.titles = new Array(nodes.length)
|
|
210
|
+
state.kinds = new Array(nodes.length)
|
|
211
|
+
|
|
212
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
213
|
+
const row = nodes[index]
|
|
214
|
+
const id = typeof row?.[0] === 'string' ? row[0] : ''
|
|
215
|
+
if (!id) {
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
const title = typeof row?.[1] === 'string' ? row[1] : id
|
|
219
|
+
const x = Number.isFinite(row?.[2]) ? Number(row[2]) : 0
|
|
220
|
+
const y = Number.isFinite(row?.[3]) ? Number(row[3]) : 0
|
|
221
|
+
const kind = row?.[6] === 'cluster' ? 'cluster' : 'node'
|
|
222
|
+
const relevance = Number.isFinite(row?.[7]) ? Number(row[7]) : 0
|
|
223
|
+
|
|
224
|
+
state.ids[index] = id
|
|
225
|
+
state.titles[index] = title
|
|
226
|
+
state.kinds[index] = kind
|
|
227
|
+
state.x[index] = x
|
|
228
|
+
state.y[index] = y
|
|
229
|
+
state.relevance[index] = relevance
|
|
230
|
+
state.radius[index] = nodeRadius(relevance, kind)
|
|
231
|
+
state.visible[index] = 0
|
|
232
|
+
state.highlighted[index] = highlightedIds.has(id) ? 1 : 0
|
|
233
|
+
state.selected[index] = selectedNodeId === id ? 1 : 0
|
|
234
|
+
nodeIndexById.set(id, index)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
state.nodeCount = nodes.length
|
|
238
|
+
|
|
239
|
+
ensureEdgeCapacity(edges.length)
|
|
240
|
+
let edgeCount = 0
|
|
241
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
242
|
+
const row = edges[index]
|
|
243
|
+
const sourceId = typeof row?.[0] === 'string' ? row[0] : ''
|
|
244
|
+
const targetId = typeof row?.[1] === 'string' ? row[1] : ''
|
|
245
|
+
const source = nodeIndexById.get(sourceId)
|
|
246
|
+
const target = nodeIndexById.get(targetId)
|
|
247
|
+
if (source === undefined || target === undefined || source === target) {
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
state.edgeSource[edgeCount] = source
|
|
251
|
+
state.edgeTarget[edgeCount] = target
|
|
252
|
+
state.edgeWeight[edgeCount] = Number.isFinite(row?.[2]) ? Number(row[2]) : 1
|
|
253
|
+
edgeCount += 1
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
state.edgeCount = edgeCount
|
|
257
|
+
dirty = true
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const cullVisibleNodes = () => {
|
|
261
|
+
const minX = -280
|
|
262
|
+
const minY = -280
|
|
263
|
+
const maxX = viewportWidth + 280
|
|
264
|
+
const maxY = viewportHeight + 280
|
|
265
|
+
|
|
266
|
+
for (let index = 0; index < state.nodeCount; index += 1) {
|
|
267
|
+
const radius = state.radius[index] * camera.scale
|
|
268
|
+
const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
|
|
269
|
+
const screenX = sx / devicePixelRatio
|
|
270
|
+
const screenY = sy / devicePixelRatio
|
|
271
|
+
state.visible[index] = screenX + radius >= minX && screenX - radius <= maxX && screenY + radius >= minY && screenY - radius <= maxY ? 1 : 0
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const drawEdges = () => {
|
|
276
|
+
if (!gl || state.edgeCount === 0) return
|
|
277
|
+
|
|
278
|
+
edgePositionsBuffer = ensureFloat32Capacity(edgePositionsBuffer, state.edgeCount * 4)
|
|
279
|
+
let cursor = 0
|
|
280
|
+
let visibleEdges = 0
|
|
281
|
+
for (let index = 0; index < state.edgeCount; index += 1) {
|
|
282
|
+
const source = state.edgeSource[index]
|
|
283
|
+
const target = state.edgeTarget[index]
|
|
284
|
+
if (state.visible[source] === 0 && state.visible[target] === 0) {
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
const [sx, sy] = toScreenPoint(state.x[source], state.y[source])
|
|
288
|
+
const [tx, ty] = toScreenPoint(state.x[target], state.y[target])
|
|
289
|
+
edgePositionsBuffer[cursor] = sx
|
|
290
|
+
edgePositionsBuffer[cursor + 1] = sy
|
|
291
|
+
edgePositionsBuffer[cursor + 2] = tx
|
|
292
|
+
edgePositionsBuffer[cursor + 3] = ty
|
|
293
|
+
cursor += 4
|
|
294
|
+
visibleEdges += 1
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
lastVisibleEdges = visibleEdges
|
|
298
|
+
if (cursor === 0) return
|
|
299
|
+
|
|
300
|
+
gl.useProgram(lineProgram)
|
|
301
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, lineBuffer)
|
|
302
|
+
gl.bufferData(gl.ARRAY_BUFFER, edgePositionsBuffer.subarray(0, cursor), gl.STREAM_DRAW)
|
|
303
|
+
gl.enableVertexAttribArray(linePositionLocation)
|
|
304
|
+
gl.vertexAttribPointer(linePositionLocation, 2, gl.FLOAT, false, 0, 0)
|
|
305
|
+
gl.uniform2f(lineResolutionLocation, canvas.width, canvas.height)
|
|
306
|
+
gl.uniform4fv(lineColorLocation, state.nodeCount > 4000 ? theme.edge : theme.edgeHeavy)
|
|
307
|
+
gl.lineWidth(1)
|
|
308
|
+
gl.drawArrays(gl.LINES, 0, cursor / 2)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const drawNodeLayer = (predicate, color, radiusBoost = 1) => {
|
|
312
|
+
if (!gl || state.nodeCount === 0) return
|
|
313
|
+
|
|
314
|
+
pointPositionsBuffer = ensureFloat32Capacity(pointPositionsBuffer, state.nodeCount * 2)
|
|
315
|
+
pointSizesBuffer = ensureFloat32Capacity(pointSizesBuffer, state.nodeCount)
|
|
316
|
+
let positionCursor = 0
|
|
317
|
+
let sizeCursor = 0
|
|
318
|
+
for (let index = 0; index < state.nodeCount; index += 1) {
|
|
319
|
+
if (!predicate(index)) continue
|
|
320
|
+
const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
|
|
321
|
+
pointPositionsBuffer[positionCursor] = sx
|
|
322
|
+
pointPositionsBuffer[positionCursor + 1] = sy
|
|
323
|
+
pointSizesBuffer[sizeCursor] = Math.max(1.2, state.radius[index] * camera.scale * devicePixelRatio * radiusBoost)
|
|
324
|
+
positionCursor += 2
|
|
325
|
+
sizeCursor += 1
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (positionCursor === 0) return
|
|
329
|
+
|
|
330
|
+
gl.useProgram(pointProgram)
|
|
331
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, pointPositionBuffer)
|
|
332
|
+
gl.bufferData(gl.ARRAY_BUFFER, pointPositionsBuffer.subarray(0, positionCursor), gl.STREAM_DRAW)
|
|
333
|
+
gl.enableVertexAttribArray(pointPositionLocation)
|
|
334
|
+
gl.vertexAttribPointer(pointPositionLocation, 2, gl.FLOAT, false, 0, 0)
|
|
335
|
+
|
|
336
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, pointSizeBuffer)
|
|
337
|
+
gl.bufferData(gl.ARRAY_BUFFER, pointSizesBuffer.subarray(0, sizeCursor), gl.STREAM_DRAW)
|
|
338
|
+
gl.enableVertexAttribArray(pointSizeLocation)
|
|
339
|
+
gl.vertexAttribPointer(pointSizeLocation, 1, gl.FLOAT, false, 0, 0)
|
|
340
|
+
|
|
341
|
+
gl.uniform2f(pointResolutionLocation, canvas.width, canvas.height)
|
|
342
|
+
gl.uniform4fv(pointColorLocation, color)
|
|
343
|
+
gl.drawArrays(gl.POINTS, 0, positionCursor / 2)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const clear = () => {
|
|
347
|
+
if (!gl || !canvas) return
|
|
348
|
+
gl.viewport(0, 0, canvas.width, canvas.height)
|
|
349
|
+
gl.clearColor(theme.clear[0], theme.clear[1], theme.clear[2], theme.clear[3])
|
|
350
|
+
gl.clear(gl.COLOR_BUFFER_BIT)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const renderFrame = (now) => {
|
|
354
|
+
renderScheduled = false
|
|
355
|
+
if (!dirty) {
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const delta = now - lastFrameAt
|
|
360
|
+
const minInterval = state.nodeCount > 20000 ? 22 : 16
|
|
361
|
+
if (delta < minInterval) {
|
|
362
|
+
requestRender()
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
lastFrameAt = now
|
|
366
|
+
|
|
367
|
+
if (!gl) {
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
cullVisibleNodes()
|
|
372
|
+
clear()
|
|
373
|
+
drawEdges()
|
|
374
|
+
|
|
375
|
+
drawNodeLayer(
|
|
376
|
+
(index) => state.visible[index] === 1 && state.selected[index] === 0 && state.highlighted[index] === 0,
|
|
377
|
+
theme.node,
|
|
378
|
+
1
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
drawNodeLayer(
|
|
382
|
+
(index) => state.visible[index] === 1 && state.kinds[index] === 'cluster' && state.selected[index] === 0,
|
|
383
|
+
theme.nodeCluster,
|
|
384
|
+
1.15
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
drawNodeLayer(
|
|
388
|
+
(index) => state.visible[index] === 1 && state.highlighted[index] === 1,
|
|
389
|
+
theme.nodeHighlight,
|
|
390
|
+
1.22
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
drawNodeLayer(
|
|
394
|
+
(index) => state.visible[index] === 1 && state.selected[index] === 1,
|
|
395
|
+
theme.nodeSelected,
|
|
396
|
+
1.32
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if (dirty) {
|
|
400
|
+
postMessage({
|
|
401
|
+
type: 'frame-stats',
|
|
402
|
+
visibleNodes: (() => {
|
|
403
|
+
let count = 0
|
|
404
|
+
for (let index = 0; index < state.nodeCount; index += 1) {
|
|
405
|
+
if (state.visible[index] === 1) count += 1
|
|
406
|
+
}
|
|
407
|
+
return count
|
|
408
|
+
})(),
|
|
409
|
+
visibleEdges: lastVisibleEdges
|
|
410
|
+
})
|
|
411
|
+
dirty = false
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const requestRender = () => {
|
|
416
|
+
if (renderScheduled) {
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
renderScheduled = true
|
|
420
|
+
const raf = self.requestAnimationFrame
|
|
421
|
+
if (typeof raf === 'function') {
|
|
422
|
+
raf.call(self, renderFrame)
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
setTimeout(() => renderFrame(performance.now()), 16)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const setCamera = (nextCamera) => {
|
|
429
|
+
if (!nextCamera || typeof nextCamera !== 'object') {
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
camera.x = Number.isFinite(nextCamera.x) ? Number(nextCamera.x) : camera.x
|
|
433
|
+
camera.y = Number.isFinite(nextCamera.y) ? Number(nextCamera.y) : camera.y
|
|
434
|
+
camera.scale = Number.isFinite(nextCamera.scale) ? Math.max(0.0002, Math.min(8, Number(nextCamera.scale))) : camera.scale
|
|
435
|
+
dirty = true
|
|
436
|
+
requestRender()
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const worldAtScreen = (screenX, screenY) => {
|
|
440
|
+
const x = (screenX - camera.x) / camera.scale
|
|
441
|
+
const y = (screenY - camera.y) / camera.scale
|
|
442
|
+
return [x, y]
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const pickNode = (screenX, screenY) => {
|
|
446
|
+
const [worldX, worldY] = worldAtScreen(screenX, screenY)
|
|
447
|
+
let bestIndex = -1
|
|
448
|
+
let bestDistance = Infinity
|
|
449
|
+
|
|
450
|
+
for (let index = 0; index < state.nodeCount; index += 1) {
|
|
451
|
+
if (state.visible[index] === 0) continue
|
|
452
|
+
const dx = state.x[index] - worldX
|
|
453
|
+
const dy = state.y[index] - worldY
|
|
454
|
+
const distance = Math.hypot(dx, dy)
|
|
455
|
+
const maxDistance = state.radius[index] * 1.2
|
|
456
|
+
if (distance <= maxDistance && distance < bestDistance) {
|
|
457
|
+
bestDistance = distance
|
|
458
|
+
bestIndex = index
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (bestIndex < 0) {
|
|
463
|
+
return null
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
id: state.ids[bestIndex],
|
|
468
|
+
title: state.titles[bestIndex],
|
|
469
|
+
kind: state.kinds[bestIndex],
|
|
470
|
+
x: state.x[bestIndex],
|
|
471
|
+
y: state.y[bestIndex]
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const setHighlights = (ids) => {
|
|
476
|
+
highlightedIds.clear()
|
|
477
|
+
const list = Array.isArray(ids) ? ids : []
|
|
478
|
+
for (let index = 0; index < list.length; index += 1) {
|
|
479
|
+
const id = list[index]
|
|
480
|
+
if (typeof id === 'string' && id.length > 0) {
|
|
481
|
+
highlightedIds.add(id)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
for (let index = 0; index < state.nodeCount; index += 1) {
|
|
486
|
+
state.highlighted[index] = highlightedIds.has(state.ids[index]) ? 1 : 0
|
|
487
|
+
}
|
|
488
|
+
dirty = true
|
|
489
|
+
requestRender()
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const setSelected = (id) => {
|
|
493
|
+
selectedNodeId = typeof id === 'string' && id.length > 0 ? id : null
|
|
494
|
+
for (let index = 0; index < state.nodeCount; index += 1) {
|
|
495
|
+
state.selected[index] = selectedNodeId === state.ids[index] ? 1 : 0
|
|
496
|
+
}
|
|
497
|
+
dirty = true
|
|
498
|
+
requestRender()
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
self.onmessage = (event) => {
|
|
502
|
+
const payload = event.data
|
|
503
|
+
if (!payload || typeof payload !== 'object') {
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (payload.type === 'init') {
|
|
508
|
+
canvas = payload.canvas
|
|
509
|
+
if (payload.theme && typeof payload.theme === 'object') {
|
|
510
|
+
Object.assign(theme, payload.theme)
|
|
511
|
+
}
|
|
512
|
+
const initialized = initWebGl()
|
|
513
|
+
if (!initialized) {
|
|
514
|
+
postMessage({ type: 'fatal', message: 'WebGL is not available in render worker.' })
|
|
515
|
+
return
|
|
516
|
+
}
|
|
517
|
+
resizeCanvas(payload.width, payload.height, payload.devicePixelRatio)
|
|
518
|
+
setCamera(payload.camera)
|
|
519
|
+
requestRender()
|
|
520
|
+
postMessage({ type: 'ready' })
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (payload.type === 'resize') {
|
|
525
|
+
resizeCanvas(payload.width, payload.height, payload.devicePixelRatio)
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (payload.type === 'camera') {
|
|
530
|
+
setCamera(payload.camera)
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (payload.type === 'chunk') {
|
|
535
|
+
loadChunk(payload.chunk)
|
|
536
|
+
requestRender()
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (payload.type === 'highlight') {
|
|
541
|
+
setHighlights(payload.ids)
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (payload.type === 'select') {
|
|
546
|
+
setSelected(payload.id)
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (payload.type === 'pick') {
|
|
551
|
+
const node = pickNode(
|
|
552
|
+
Number.isFinite(payload.x) ? Number(payload.x) : 0,
|
|
553
|
+
Number.isFinite(payload.y) ? Number(payload.y) : 0
|
|
554
|
+
)
|
|
555
|
+
postMessage({
|
|
556
|
+
type: 'pick-result',
|
|
557
|
+
requestId: payload.requestId,
|
|
558
|
+
node
|
|
559
|
+
})
|
|
560
|
+
return
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (payload.type === 'pointer') {
|
|
564
|
+
hoverX = Number.isFinite(payload.x) ? Number(payload.x) : null
|
|
565
|
+
hoverY = Number.isFinite(payload.y) ? Number(payload.y) : null
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
`;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const createClientWorkerJs = () => `const normalize = value => String(value || '')
|
|
2
|
+
.normalize('NFKD')
|
|
3
|
+
.replace(/\\p{Diacritic}/gu, '')
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
|
|
6
|
+
let nodeIndex = []
|
|
7
|
+
|
|
8
|
+
const toNodeIndex = nodes =>
|
|
9
|
+
(Array.isArray(nodes) ? nodes : [])
|
|
10
|
+
.map(node => {
|
|
11
|
+
const id = typeof node.id === 'string' ? node.id : ''
|
|
12
|
+
if (!id) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
const title = normalize(node.title)
|
|
16
|
+
const path = normalize(node.path)
|
|
17
|
+
const tags = Array.isArray(node.tags) ? node.tags.map(tag => normalize(tag)) : []
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
text: [title, path, ...tags].join(' ')
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
|
|
25
|
+
const scoreText = (text, query) => {
|
|
26
|
+
if (!query) return 0
|
|
27
|
+
if (!text.includes(query)) return 0
|
|
28
|
+
if (text.startsWith(query)) return 4
|
|
29
|
+
return 1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const filterIds = (query, limit) => {
|
|
33
|
+
const normalizedQuery = normalize(query).trim()
|
|
34
|
+
if (!normalizedQuery) {
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
const rows = []
|
|
38
|
+
for (let index = 0; index < nodeIndex.length; index += 1) {
|
|
39
|
+
const row = nodeIndex[index]
|
|
40
|
+
const score = scoreText(row.text, normalizedQuery)
|
|
41
|
+
if (score > 0) {
|
|
42
|
+
rows.push({ id: row.id, score })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
rows.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))
|
|
46
|
+
return rows.slice(0, Math.max(1, Number.isFinite(limit) ? limit : rows.length)).map(row => row.id)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
self.onmessage = event => {
|
|
50
|
+
const payload = event.data
|
|
51
|
+
if (!payload || typeof payload !== 'object') {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
if (payload.type === 'load-nodes') {
|
|
55
|
+
nodeIndex = toNodeIndex(payload.nodes)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
if (payload.type === 'filter') {
|
|
59
|
+
const token = payload.token
|
|
60
|
+
const ids = filterIds(payload.query, payload.limit)
|
|
61
|
+
self.postMessage({ type: 'filter-result', token, ids })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
self.postMessage({ type: 'ready' })
|
|
66
|
+
`;
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { stat } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
2
|
+
import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { createStarGraphLayout } from '../domain/graph-layout.js';
|
|
4
5
|
import { indexStoragePath } from '../infrastructure/file-index.js';
|
|
5
6
|
import { getGraphSummary } from './get-graph-summary.js';
|
|
7
|
+
const graphLayoutVersion = 3;
|
|
6
8
|
const graphLayoutCache = new Map();
|
|
9
|
+
const graphLayoutStoragePath = (vaultPath, agentId) => join(vaultPath, '.brainlink', `graph-layout-${agentId?.replace(/[^a-zA-Z0-9_-]/g, '_') ?? 'all'}.json`);
|
|
10
|
+
const readPersistedLayout = async (vaultPath, databaseSignature, agentId) => {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(await readFile(graphLayoutStoragePath(vaultPath, agentId), 'utf8'));
|
|
13
|
+
return parsed.databaseSignature === databaseSignature && parsed.layoutVersion === graphLayoutVersion ? parsed : null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const writePersistedLayout = async (vaultPath, agentId, cached) => {
|
|
20
|
+
const target = graphLayoutStoragePath(vaultPath, agentId);
|
|
21
|
+
const temp = `${target}.tmp`;
|
|
22
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
23
|
+
await writeFile(temp, `${JSON.stringify(cached)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
24
|
+
await rename(temp, target);
|
|
25
|
+
};
|
|
7
26
|
const readDatabaseSignature = async (vaultPath) => {
|
|
8
27
|
try {
|
|
9
28
|
const info = await stat(indexStoragePath(vaultPath));
|
|
@@ -26,16 +45,30 @@ export const getGraphLayout = async (vaultPath, agentId) => {
|
|
|
26
45
|
const databaseSignature = await readDatabaseSignature(vaultPath);
|
|
27
46
|
const cacheKey = `${vaultPath}:${agentId ?? ''}`;
|
|
28
47
|
const cached = graphLayoutCache.get(cacheKey);
|
|
29
|
-
if (cached?.databaseSignature === databaseSignature) {
|
|
48
|
+
if (cached?.databaseSignature === databaseSignature && cached.layoutVersion === graphLayoutVersion) {
|
|
30
49
|
return {
|
|
31
50
|
signature: cached.signature,
|
|
32
51
|
layout: cached.layout
|
|
33
52
|
};
|
|
34
53
|
}
|
|
54
|
+
const persisted = await readPersistedLayout(vaultPath, databaseSignature, agentId);
|
|
55
|
+
if (persisted) {
|
|
56
|
+
graphLayoutCache.set(cacheKey, persisted);
|
|
57
|
+
return {
|
|
58
|
+
signature: persisted.signature,
|
|
59
|
+
layout: persisted.layout
|
|
60
|
+
};
|
|
61
|
+
}
|
|
35
62
|
const graph = await getGraphSummary(vaultPath, agentId);
|
|
36
63
|
const signature = createGraphSignature(graph);
|
|
37
|
-
const
|
|
38
|
-
|
|
64
|
+
const rawLayout = createStarGraphLayout(graph);
|
|
65
|
+
const layout = {
|
|
66
|
+
...rawLayout,
|
|
67
|
+
nodes: rawLayout.nodes.map((node) => ({ ...node, content: '' }))
|
|
68
|
+
};
|
|
69
|
+
const nextCache = { layoutVersion: graphLayoutVersion, databaseSignature, signature, layout };
|
|
70
|
+
graphLayoutCache.set(cacheKey, nextCache);
|
|
71
|
+
await writePersistedLayout(vaultPath, agentId, nextCache);
|
|
39
72
|
return {
|
|
40
73
|
signature,
|
|
41
74
|
layout
|