@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.
- package/AGENTS.md +6 -6
- package/CHANGELOG.md +14 -0
- package/README.md +186 -38
- package/dist/application/add-note.js +13 -44
- package/dist/application/analyze-vault.js +1 -1
- package/dist/application/auto-migrate-configured-vault.js +37 -0
- package/dist/application/build-context.js +119 -20
- package/dist/application/canonical-context-links.js +209 -0
- package/dist/application/frontend/client-css.js +212 -42
- package/dist/application/frontend/client-html.js +42 -28
- package/dist/application/frontend/client-js.js +1294 -3217
- package/dist/application/frontend/client-render-worker-js.js +676 -0
- package/dist/application/get-graph-contexts.js +33 -0
- package/dist/application/get-graph-layout.js +62 -8
- package/dist/application/get-graph-stream-chunk.js +326 -0
- package/dist/application/get-graph-view.js +246 -0
- package/dist/application/graph-view-state.js +66 -0
- package/dist/application/import-legacy-sqlite.js +3 -33
- package/dist/application/index-vault.js +35 -22
- package/dist/application/migrate-context-links.js +79 -0
- package/dist/application/search-graph-node-ids.js +63 -3
- package/dist/application/server/routes.js +197 -12
- package/dist/cli/commands/read-commands.js +39 -3
- package/dist/cli/commands/vault-commands.js +182 -0
- package/dist/cli/commands/write-commands.js +147 -12
- package/dist/cli/main.js +2 -0
- package/dist/cli/runtime.js +10 -2
- package/dist/domain/context.js +1 -0
- package/dist/domain/graph-contexts.js +180 -0
- package/dist/domain/graph-layout.js +347 -21
- package/dist/domain/markdown.js +53 -9
- package/dist/infrastructure/config.js +105 -6
- package/dist/infrastructure/context-packs.js +122 -0
- package/dist/infrastructure/file-index.js +6 -3
- package/dist/infrastructure/index-state.js +2 -0
- package/dist/infrastructure/vault-migration-state.js +69 -0
- package/dist/infrastructure/volatile-memory.js +100 -0
- package/dist/mcp/http-server.js +97 -0
- package/dist/mcp/runtime.js +20 -0
- package/dist/mcp/server.js +36 -13
- package/dist/mcp/tools.js +203 -14
- package/docs/AGENT_USAGE.md +50 -5
- package/docs/ARCHITECTURE.md +11 -0
- package/docs/QUICKSTART.md +3 -1
- package/docs/RELEASE.md +4 -3
- 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
|
+
};
|