@andespindola/brainlink 1.0.5 → 1.0.6

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 (51) hide show
  1. package/README.md +8 -0
  2. package/dist/application/add-note.js +2 -2
  3. package/dist/application/build-context.js +16 -10
  4. package/dist/application/canonical-context-links.js +44 -5
  5. package/dist/application/check-package-update.js +105 -0
  6. package/dist/application/frontend/client/chunk-fetch.js +236 -0
  7. package/dist/application/frontend/client/controls.js +178 -0
  8. package/dist/application/frontend/client/elements.js +122 -0
  9. package/dist/application/frontend/client/input.js +202 -0
  10. package/dist/application/frontend/client/node-details.js +191 -0
  11. package/dist/application/frontend/client/rendering.js +296 -0
  12. package/dist/application/frontend/client/scope-theme.js +114 -0
  13. package/dist/application/frontend/client/spatial.js +98 -0
  14. package/dist/application/frontend/client/storage.js +215 -0
  15. package/dist/application/frontend/client/upload.js +90 -0
  16. package/dist/application/frontend/client/worker-bootstrap.js +147 -0
  17. package/dist/application/frontend/client-js.js +24 -1837
  18. package/dist/application/frontend/client-render-worker-js.js +1 -1
  19. package/dist/application/index-vault-phases.js +189 -0
  20. package/dist/application/index-vault.js +44 -165
  21. package/dist/cli/commands/write/dedupe-commands.js +59 -0
  22. package/dist/cli/commands/write/index-commands.js +205 -0
  23. package/dist/cli/commands/write/link-commands.js +68 -0
  24. package/dist/cli/commands/write/note-commands.js +146 -0
  25. package/dist/cli/commands/write/server-commands.js +553 -0
  26. package/dist/cli/commands/write/shared.js +35 -0
  27. package/dist/cli/commands/write/vault-lifecycle-commands.js +270 -0
  28. package/dist/cli/commands/write-commands.js +12 -1303
  29. package/dist/cli/main.js +39 -3
  30. package/dist/domain/context.js +39 -3
  31. package/dist/domain/embeddings.js +31 -5
  32. package/dist/domain/graph-contexts.js +62 -57
  33. package/dist/domain/graph-layout/cauliflower-layout.js +116 -0
  34. package/dist/domain/graph-layout/collisions.js +100 -0
  35. package/dist/domain/graph-layout/hierarchy.js +135 -0
  36. package/dist/domain/graph-layout/metrics.js +111 -0
  37. package/dist/domain/graph-layout/segments.js +76 -0
  38. package/dist/domain/graph-layout/star-layout.js +110 -0
  39. package/dist/domain/graph-layout.js +4 -625
  40. package/dist/infrastructure/config.js +6 -0
  41. package/dist/infrastructure/file-index.js +13 -4
  42. package/dist/infrastructure/semantic-prefilter.js +24 -0
  43. package/dist/mcp/server.js +7 -0
  44. package/dist/mcp/tool-guard.js +29 -0
  45. package/dist/mcp/tools/maintenance-tools.js +409 -0
  46. package/dist/mcp/tools/read-tools.js +504 -0
  47. package/dist/mcp/tools/shared.js +216 -0
  48. package/dist/mcp/tools/write-tools.js +247 -0
  49. package/dist/mcp/tools.js +3 -1357
  50. package/docs/QUICKSTART.md +4 -0
  51. package/package.json +2 -2
@@ -0,0 +1,296 @@
1
+ export const createRenderingJs = () => `
2
+ const drawFallback = () => {
3
+ if (state.rendererMode !== 'fallback') {
4
+ return
5
+ }
6
+ ctx2dFallback = ctx2dFallback ?? canvas.getContext('2d')
7
+ if (!ctx2dFallback) {
8
+ return
9
+ }
10
+ const width = state.viewport.width
11
+ const height = state.viewport.height
12
+ const ratio = state.viewport.ratio
13
+ canvas.width = Math.floor(width * ratio)
14
+ canvas.height = Math.floor(height * ratio)
15
+ ctx2dFallback.setTransform(ratio, 0, 0, ratio, 0, 0)
16
+ ctx2dFallback.fillStyle = '#08131d'
17
+ ctx2dFallback.fillRect(0, 0, width, height)
18
+
19
+ const nodes = Array.isArray(state.chunk.nodes) ? state.chunk.nodes : []
20
+ const edges = Array.isArray(state.chunk.edges) ? state.chunk.edges : []
21
+ const nodeById = new Map()
22
+ for (let i = 0; i < nodes.length; i += 1) {
23
+ nodeById.set(nodes[i][0], nodes[i])
24
+ }
25
+
26
+ ctx2dFallback.strokeStyle = 'rgba(151,181,212,0.18)'
27
+ ctx2dFallback.lineWidth = 1
28
+ for (let i = 0; i < edges.length; i += 1) {
29
+ const edge = edges[i]
30
+ const source = nodeById.get(edge[0])
31
+ const target = nodeById.get(edge[1])
32
+ if (!source || !target) continue
33
+ const from = worldToScreen(source[2], source[3])
34
+ const to = worldToScreen(target[2], target[3])
35
+ ctx2dFallback.beginPath()
36
+ ctx2dFallback.moveTo(from.x, from.y)
37
+ ctx2dFallback.lineTo(to.x, to.y)
38
+ ctx2dFallback.stroke()
39
+ }
40
+
41
+ for (let i = 0; i < nodes.length; i += 1) {
42
+ const node = nodes[i]
43
+ const p = worldToScreen(node[2], node[3])
44
+ const selected = state.selectedNodeId === node[0]
45
+ const color = segmentColor(node[5] || node[4] || node[1])
46
+ const radius = Math.max(3.2, Math.min(16.5, 5 + node[7] * 0.65))
47
+
48
+ ctx2dFallback.beginPath()
49
+ ctx2dFallback.fillStyle = selected ? '#edf4ff' : color
50
+ ctx2dFallback.arc(p.x, p.y, radius, 0, Math.PI * 2)
51
+ ctx2dFallback.fill()
52
+ }
53
+
54
+ ctx2dFallback.fillStyle = '#97a9bd'
55
+ ctx2dFallback.font = '12px Inter, system-ui, sans-serif'
56
+ ctx2dFallback.textAlign = 'center'
57
+ ctx2dFallback.fillText('Fallback canvas mode', Math.max(width, 320) / 2, 24)
58
+ }
59
+
60
+ const updateTotals = () => {
61
+ elements.nodeCount.textContent = String(state.totals.nodes)
62
+ elements.edgeCount.textContent = String(state.totals.edges)
63
+ }
64
+
65
+ const updateWorkerCamera = () => {
66
+ updateGraphOverlays()
67
+ if (!state.renderWorker || !state.workerReady) {
68
+ return
69
+ }
70
+ if (state.cameraSyncScheduled) {
71
+ return
72
+ }
73
+ state.cameraSyncScheduled = true
74
+ requestAnimationFrame(() => {
75
+ state.cameraSyncScheduled = false
76
+ if (!state.renderWorker || !state.workerReady) {
77
+ return
78
+ }
79
+ state.renderWorker.postMessage({
80
+ type: 'camera',
81
+ camera: state.camera
82
+ })
83
+ })
84
+ }
85
+
86
+ const updateWorkerSize = () => {
87
+ updateGraphOverlays()
88
+ if (!state.renderWorker || !state.workerReady) {
89
+ return
90
+ }
91
+ state.renderWorker.postMessage({
92
+ type: 'resize',
93
+ width: state.viewport.width,
94
+ height: state.viewport.height,
95
+ devicePixelRatio: state.viewport.ratio
96
+ })
97
+ }
98
+
99
+ const normalizeList = (items) => Array.isArray(items) ? items : []
100
+
101
+ const applyManualNodePositions = (nodes) => normalizeList(nodes).map((node) => {
102
+ const id = typeof node?.[0] === 'string' ? node[0] : ''
103
+ const position = id ? state.nodePositions.get(id) : null
104
+ if (!position || !Number.isFinite(position.x) || !Number.isFinite(position.y)) {
105
+ return node
106
+ }
107
+
108
+ const next = [...node]
109
+ next[2] = position.x
110
+ next[3] = position.y
111
+ return next
112
+ })
113
+
114
+ const updateNodePositionInChunk = (nodeId, x, y) => {
115
+ if (!nodeId || !Number.isFinite(x) || !Number.isFinite(y)) {
116
+ return
117
+ }
118
+
119
+ state.chunk = {
120
+ ...state.chunk,
121
+ nodes: normalizeList(state.chunk.nodes).map((node) => {
122
+ if (node?.[0] !== nodeId) {
123
+ return node
124
+ }
125
+ const next = [...node]
126
+ next[2] = x
127
+ next[3] = y
128
+ return next
129
+ })
130
+ }
131
+ state.spatialIndex.key = ''
132
+
133
+ if (state.renderWorker && state.workerReady) {
134
+ state.renderWorker.postMessage({ type: 'move-node', id: nodeId, x, y })
135
+ }
136
+ updateGraphOverlays()
137
+ }
138
+
139
+ const focusNodeInViewport = (nodeId, nextScale = null) => {
140
+ const node = nodeByIdFromChunk().get(nodeId)
141
+ if (!node) {
142
+ return false
143
+ }
144
+
145
+ const x = Number(node[2])
146
+ const y = Number(node[3])
147
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
148
+ return false
149
+ }
150
+
151
+ if (Number.isFinite(nextScale)) {
152
+ state.camera.scale = clampScale(Number(nextScale))
153
+ }
154
+ state.camera.x = state.viewport.width / 2 - x * state.camera.scale
155
+ state.camera.y = state.viewport.height / 2 - y * state.camera.scale
156
+ updateWorkerCamera()
157
+ scheduleChunkFetch()
158
+ return true
159
+ }
160
+
161
+ const showTooltip = (node, pointer) => {
162
+ if (!elements.tooltip || !node) {
163
+ return
164
+ }
165
+
166
+ elements.tooltip.hidden = false
167
+ elements.tooltip.innerHTML =
168
+ '<strong>' + escapeHtml(node[1] || node[0]) + '</strong>' +
169
+ '<small>' + escapeHtml(node[4] || node[5] || '') + '</small>'
170
+ elements.tooltip.style.left = Math.min(state.viewport.width - 24, pointer.x + 14) + 'px'
171
+ elements.tooltip.style.top = Math.min(state.viewport.height - 24, pointer.y + 14) + 'px'
172
+ }
173
+
174
+ const hideTooltip = () => {
175
+ if (elements.tooltip) {
176
+ elements.tooltip.hidden = true
177
+ }
178
+ }
179
+
180
+ const labelCandidates = () => {
181
+ const nodes = normalizeList(state.chunk.nodes)
182
+ const visible = nodes.filter((node) => {
183
+ const x = Number(node?.[2])
184
+ const y = Number(node?.[3])
185
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return false
186
+ const point = worldToScreen(x, y)
187
+ return point.x >= -80 && point.x <= state.viewport.width + 80 && point.y >= -80 && point.y <= state.viewport.height + 80
188
+ })
189
+ const shouldShowMany = state.camera.scale >= 0.72 || visible.length <= 120
190
+ const focused = state.focusedNodeIds
191
+
192
+ return visible
193
+ .filter((node) => shouldShowMany || focused.has(node[0]) || node[0] === state.hoveredNodeId || node[0] === state.selectedNodeId || Number(node?.[7]) > 5.5)
194
+ .sort((left, right) => {
195
+ const leftFocused = focused.has(left[0]) || left[0] === state.hoveredNodeId || left[0] === state.selectedNodeId ? 1 : 0
196
+ const rightFocused = focused.has(right[0]) || right[0] === state.hoveredNodeId || right[0] === state.selectedNodeId ? 1 : 0
197
+ if (rightFocused !== leftFocused) return rightFocused - leftFocused
198
+ return Number(right?.[7] ?? 0) - Number(left?.[7] ?? 0)
199
+ })
200
+ .slice(0, state.camera.scale >= 0.72 ? 160 : 48)
201
+ }
202
+
203
+ const drawLabels = () => {
204
+ if (!elements.labels) {
205
+ return
206
+ }
207
+
208
+ elements.labels.innerHTML = labelCandidates().map((node) => {
209
+ const point = worldToScreen(Number(node[2]), Number(node[3]))
210
+ const focused = state.focusedNodeIds.has(node[0]) || node[0] === state.hoveredNodeId || node[0] === state.selectedNodeId
211
+ return '<span class="graph-label' + (focused ? ' is-focused' : '') + '" style="left:' +
212
+ point.x.toFixed(1) + 'px;top:' + point.y.toFixed(1) + 'px">' + escapeHtml(node[1] || node[0]) + '</span>'
213
+ }).join('')
214
+ }
215
+
216
+ const drawMiniMap = () => {
217
+ const miniMap = elements.miniMap
218
+ if (!(miniMap instanceof HTMLCanvasElement)) {
219
+ return
220
+ }
221
+ const nodes = normalizeList(state.chunk.nodes)
222
+ const ctx = miniMap.getContext('2d')
223
+ if (!ctx || nodes.length === 0) {
224
+ return
225
+ }
226
+
227
+ const ratio = window.devicePixelRatio || 1
228
+ const width = miniMap.clientWidth || 180
229
+ const height = miniMap.clientHeight || 120
230
+ miniMap.width = Math.floor(width * ratio)
231
+ miniMap.height = Math.floor(height * ratio)
232
+ ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
233
+ ctx.clearRect(0, 0, width, height)
234
+ ctx.fillStyle = 'rgba(8, 19, 29, 0.88)'
235
+ ctx.fillRect(0, 0, width, height)
236
+
237
+ const xs = nodes.map((node) => Number(node[2])).filter(Number.isFinite)
238
+ const ys = nodes.map((node) => Number(node[3])).filter(Number.isFinite)
239
+ const minX = Math.min(...xs)
240
+ const maxX = Math.max(...xs)
241
+ const minY = Math.min(...ys)
242
+ const maxY = Math.max(...ys)
243
+ const graphWidth = Math.max(1, maxX - minX)
244
+ const graphHeight = Math.max(1, maxY - minY)
245
+ const scale = Math.min((width - 18) / graphWidth, (height - 18) / graphHeight)
246
+ const offsetX = (width - graphWidth * scale) / 2
247
+ const offsetY = (height - graphHeight * scale) / 2
248
+ const toMini = (x, y) => ({
249
+ x: offsetX + (x - minX) * scale,
250
+ y: offsetY + (y - minY) * scale
251
+ })
252
+ state.miniMapView = { minX, minY, scale, offsetX, offsetY, width, height }
253
+
254
+ ctx.fillStyle = 'rgba(90, 168, 255, 0.62)'
255
+ nodes.forEach((node) => {
256
+ const point = toMini(Number(node[2]), Number(node[3]))
257
+ ctx.fillRect(point.x - 1, point.y - 1, 2, 2)
258
+ })
259
+
260
+ const worldTopLeft = screenToWorld(0, 0)
261
+ const worldBottomRight = screenToWorld(state.viewport.width, state.viewport.height)
262
+ const topLeft = toMini(Math.min(worldTopLeft.x, worldBottomRight.x), Math.min(worldTopLeft.y, worldBottomRight.y))
263
+ const bottomRight = toMini(Math.max(worldTopLeft.x, worldBottomRight.x), Math.max(worldTopLeft.y, worldBottomRight.y))
264
+ ctx.strokeStyle = 'rgba(90, 168, 255, 0.86)'
265
+ ctx.lineWidth = 1
266
+ ctx.strokeRect(topLeft.x, topLeft.y, Math.max(3, bottomRight.x - topLeft.x), Math.max(3, bottomRight.y - topLeft.y))
267
+ }
268
+
269
+ const shouldDeferGraphOverlays = () => state.pointer.down || performance.now() - state.lastWheelAt < 150
270
+
271
+ const updateGraphOverlays = () => {
272
+ if (state.overlayScheduled) {
273
+ return
274
+ }
275
+ state.overlayScheduled = true
276
+ requestAnimationFrame(() => {
277
+ state.overlayScheduled = false
278
+ if (shouldDeferGraphOverlays()) {
279
+ elements.labels?.classList.add('is-stale')
280
+ if (!state.overlayIdleTimer) {
281
+ state.overlayIdleTimer = setTimeout(() => {
282
+ state.overlayIdleTimer = null
283
+ updateGraphOverlays()
284
+ }, 170)
285
+ }
286
+ return
287
+ }
288
+ elements.labels?.classList.remove('is-stale')
289
+ drawLabels()
290
+ if (state.miniMapDirty) {
291
+ drawMiniMap()
292
+ state.miniMapDirty = false
293
+ }
294
+ })
295
+ }
296
+ `;
@@ -0,0 +1,114 @@
1
+ export const createScopeThemeJs = () => `
2
+ const initialAgentFromUrl = (() => {
3
+ try {
4
+ const raw = new URL(window.location.href).searchParams.get('agent')
5
+ const value = raw?.trim() ?? ''
6
+ return value.length > 0 ? value : ''
7
+ } catch {
8
+ return ''
9
+ }
10
+ })()
11
+
12
+ const initialContextFromUrl = (() => {
13
+ try {
14
+ const raw = new URL(window.location.href).searchParams.get('context')
15
+ const value = raw?.trim() ?? ''
16
+ return value.length > 0 ? value : ''
17
+ } catch {
18
+ return ''
19
+ }
20
+ })()
21
+
22
+ const scopeQuery = (separator = '?') => {
23
+ const params = new URLSearchParams()
24
+ if (state.agentId) {
25
+ params.set('agent', state.agentId)
26
+ }
27
+ if (state.contextId) {
28
+ params.set('context', state.contextId)
29
+ }
30
+ const query = params.toString()
31
+
32
+ return query ? separator + query : ''
33
+ }
34
+
35
+ const parseColor = (hex) => {
36
+ const normalized = String(hex || '#ffffff').replace('#', '')
37
+ const expanded = normalized.length === 3
38
+ ? normalized.split('').map((char) => char + char).join('')
39
+ : normalized.padEnd(6, 'f')
40
+ const value = Number.parseInt(expanded, 16)
41
+ return [
42
+ ((value >> 16) & 255) / 255,
43
+ ((value >> 8) & 255) / 255,
44
+ (value & 255) / 255,
45
+ 1
46
+ ]
47
+ }
48
+
49
+ const graphTheme = {
50
+ node: parseColor('#5aa8ff'),
51
+ nodeCluster: parseColor('#3f7fbd'),
52
+ nodeHighlight: parseColor('#ffcb67'),
53
+ nodeSelected: parseColor('#edf4ff'),
54
+ nodePalette: [
55
+ parseColor('#5aa8ff'),
56
+ parseColor('#5ecf92'),
57
+ parseColor('#ffb65c'),
58
+ parseColor('#ff7dac'),
59
+ parseColor('#a88fff'),
60
+ parseColor('#59d0dd'),
61
+ parseColor('#ff8f6a'),
62
+ parseColor('#a4b3c3'),
63
+ parseColor('#c9945f'),
64
+ parseColor('#7cb6ff')
65
+ ],
66
+ edge: [0.59, 0.71, 0.83, 0.14],
67
+ edgeHeavy: [0.59, 0.71, 0.83, 0.3],
68
+ clear: parseColor('#08131d')
69
+ }
70
+
71
+ const segmentPalette = ['#5aa8ff', '#5ecf92', '#ffb65c', '#ff7dac', '#a88fff', '#59d0dd', '#ff8f6a', '#a4b3c3', '#c9945f', '#7cb6ff']
72
+
73
+ const segmentColorIndex = (segment) => {
74
+ const value = String(segment || '')
75
+ let hash = 0
76
+ for (let index = 0; index < value.length; index += 1) {
77
+ hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0
78
+ }
79
+ return Math.abs(hash) % segmentPalette.length
80
+ }
81
+
82
+ const segmentColor = (segment) => segmentPalette[segmentColorIndex(segment)] || segmentPalette[0]
83
+ const nodeKind = (node) => node?.[6] === 'cluster' ? 'cluster' : 'node'
84
+ const isRealGraphNode = (node) => nodeKind(node) === 'node'
85
+
86
+ const clampScale = (scale) => Math.max(zoomRange.min, Math.min(zoomRange.max, scale))
87
+
88
+ const getZoomNodeBudget = () => {
89
+ const scale = state.camera.scale
90
+ if (scale < 0.06) return 900
91
+ if (scale < 0.12) return 1600
92
+ if (scale < 0.24) return 2600
93
+ if (scale < 0.7) return 4000
94
+ return 6000
95
+ }
96
+
97
+ const getZoomEdgeBudget = () => {
98
+ const scale = state.camera.scale
99
+ if (scale < 0.06) return 2000
100
+ if (scale < 0.12) return 4800
101
+ if (scale < 0.24) return 9000
102
+ if (scale < 0.7) return 15000
103
+ return 26000
104
+ }
105
+
106
+ const zoomDetailBand = () => {
107
+ const scale = state.camera.scale
108
+ if (scale < 0.06) return 'far'
109
+ if (scale < 0.12) return 'wide'
110
+ if (scale < 0.24) return 'mid'
111
+ if (scale < 0.7) return 'near'
112
+ return 'detail'
113
+ }
114
+ `;
@@ -0,0 +1,98 @@
1
+ export const createSpatialJs = () => `
2
+ const graphStreamRequestKey = ({ x, y, w, h }) => {
3
+ const grid = Math.max(80, Math.min(720, Math.max(w, h) / 6))
4
+ return [
5
+ state.agentId || '*',
6
+ state.contextId || '*',
7
+ zoomDetailBand(),
8
+ getZoomNodeBudget(),
9
+ getZoomEdgeBudget(),
10
+ Math.round(x / grid),
11
+ Math.round(y / grid),
12
+ Math.round(w / grid),
13
+ Math.round(h / grid)
14
+ ].join(':')
15
+ }
16
+
17
+ const screenToWorld = (screenX, screenY) => ({
18
+ x: (screenX - state.camera.x) / state.camera.scale,
19
+ y: (screenY - state.camera.y) / state.camera.scale
20
+ })
21
+
22
+ const worldToScreen = (x, y) => ({
23
+ x: x * state.camera.scale + state.camera.x,
24
+ y: y * state.camera.scale + state.camera.y
25
+ })
26
+
27
+ const spatialIndexKey = () => [
28
+ state.graphSignature,
29
+ state.camera.x.toFixed(1),
30
+ state.camera.y.toFixed(1),
31
+ state.camera.scale.toFixed(4),
32
+ normalizeList(state.chunk.nodes).length
33
+ ].join(':')
34
+
35
+ const rebuildSpatialIndex = () => {
36
+ const key = spatialIndexKey()
37
+ if (state.spatialIndex.key === key) {
38
+ return
39
+ }
40
+
41
+ const cellSize = 44
42
+ const cells = new Map()
43
+ normalizeList(state.chunk.nodes).forEach((node) => {
44
+ const id = typeof node?.[0] === 'string' ? node[0] : ''
45
+ if (!id) return
46
+ const x = Number(node?.[2])
47
+ const y = Number(node?.[3])
48
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return
49
+ const point = worldToScreen(x, y)
50
+ const cellX = Math.floor(point.x / cellSize)
51
+ const cellY = Math.floor(point.y / cellSize)
52
+ const key = cellX + ',' + cellY
53
+ const bucket = cells.get(key)
54
+ if (bucket) {
55
+ bucket.push(node)
56
+ return
57
+ }
58
+ cells.set(key, [node])
59
+ })
60
+
61
+ state.spatialIndex = { key, cells }
62
+ }
63
+
64
+ const spatialCandidates = (screenX, screenY) => {
65
+ rebuildSpatialIndex()
66
+ const cellSize = 44
67
+ const cellX = Math.floor(screenX / cellSize)
68
+ const cellY = Math.floor(screenY / cellSize)
69
+ const nodes = []
70
+
71
+ for (let y = cellY - 1; y <= cellY + 1; y += 1) {
72
+ for (let x = cellX - 1; x <= cellX + 1; x += 1) {
73
+ nodes.push(...(state.spatialIndex.cells.get(x + ',' + y) ?? []))
74
+ }
75
+ }
76
+
77
+ return nodes
78
+ }
79
+
80
+ const nodeByIdFromChunk = () => new Map(normalizeList(state.chunk.nodes).map((node) => [node[0], node]))
81
+
82
+ const linkedNodeIds = (nodeId) => {
83
+ const ids = new Set(nodeId ? [nodeId] : [])
84
+ normalizeList(state.chunk.edges).forEach((edge) => {
85
+ if (edge?.[0] === nodeId && typeof edge?.[1] === 'string') ids.add(edge[1])
86
+ if (edge?.[1] === nodeId && typeof edge?.[0] === 'string') ids.add(edge[0])
87
+ })
88
+ return ids
89
+ }
90
+
91
+ const setFocusedNodeIds = (ids) => {
92
+ state.focusedNodeIds = ids
93
+ if (state.renderWorker && state.workerReady) {
94
+ state.renderWorker.postMessage({ type: 'focus', ids: Array.from(ids) })
95
+ }
96
+ updateGraphOverlays()
97
+ }
98
+ `;