@andespindola/brainlink 0.1.0-beta.157 → 0.1.0-beta.159
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/application/frontend/client-js.js +50 -23
- package/dist/application/get-graph-layout.js +1 -1
- package/dist/application/get-graph-stream-chunk.js +15 -13
- package/dist/domain/graph-contexts.js +24 -3
- package/dist/domain/graph-layout.js +44 -8
- package/docs/AGENT_USAGE.md +1 -1
- package/docs/ARCHITECTURE.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,9 +82,9 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
|
|
|
82
82
|
- Built-in MCP stdio server for agent tool integration.
|
|
83
83
|
- Local HTTP API.
|
|
84
84
|
- Realtime graph UI with agent selector and colored knowledge groups.
|
|
85
|
-
- Graph renderer uses a cauliflower-style hub layout: the primary hub stays centered, segment hubs anchor surrounding lobes, and
|
|
86
|
-
-
|
|
87
|
-
- Graph exploration uses
|
|
85
|
+
- Graph renderer uses a cauliflower-style hub layout: the primary hub stays centered, segment hubs anchor surrounding lobes, and visual edges are simplified to root hub -> segment hubs -> local context nodes.
|
|
86
|
+
- Real weighted `[[wiki link]]` edges stay preserved in the indexed graph APIs for backlinks, ranking and context traversal; the browser layout uses a separate visual edge layer for readability.
|
|
87
|
+
- Graph exploration uses stable chunk streaming (`/api/graph-stream`) with explicit node/edge budgets, returning the full mode-level scene while it fits the budget.
|
|
88
88
|
- Render pipeline uses WebGL in a dedicated worker through `OffscreenCanvas`, keeping the main thread focused on UI controls and details panels.
|
|
89
89
|
- Large graph layout API automatically uses compact payload encoding with link-coverage-aware edge selection to reduce initial client load without hiding major relationships.
|
|
90
90
|
- Large-segment layout spacing now grows logarithmically to keep initial visual density consistent between medium and very large vaults (for example, ~1k vs ~50k notes).
|
|
@@ -599,8 +599,8 @@ When native GUI is used, the GUI window automatically closes when the `blink ser
|
|
|
599
599
|
The graph UI shows:
|
|
600
600
|
|
|
601
601
|
- notes as nodes
|
|
602
|
-
- all non-self `[[wiki links]]` inside `## Context Links` as weighted edges
|
|
603
|
-
- default cauliflower-style layout centered on the primary hub, with segment hubs anchoring surrounding lobes and
|
|
602
|
+
- all non-self `[[wiki links]]` inside `## Context Links` as weighted indexed edges
|
|
603
|
+
- default cauliflower-style visual layout centered on the primary hub, with segment hubs anchoring surrounding lobes and visual edges simplified to root hub -> segment hubs -> local context nodes
|
|
604
604
|
- details opened in a non-modal side panel (tags, outgoing links, backlinks, full Markdown content), so zoom and pan remain available while inspecting data
|
|
605
605
|
- neutral graph nodes with segment/group metadata
|
|
606
606
|
- agent selector (id-only labels) for isolated views
|
|
@@ -619,7 +619,7 @@ The graph UI shows:
|
|
|
619
619
|
- graph rendering safeguards (batched GPU draw calls, lower redraw rate, zoom-aware interaction)
|
|
620
620
|
- adaptive CPU safeguards for large graphs: idle frame pacing, throttled background physics updates and cached viewport dimensions to reduce redraw/layout overhead while preserving interaction responsiveness
|
|
621
621
|
- worker-first WebGL rendering with Canvas fallback when `OffscreenCanvas` or worker rendering is unavailable
|
|
622
|
-
- large graph view keeps one
|
|
622
|
+
- large graph view keeps one indexed graph model across zoom levels, uses a stable visual hierarchy for rendering, uses segment clusters at high zoom-out, and shows node titles as zoom approaches readable scale
|
|
623
623
|
|
|
624
624
|
The server indexes before starting by default. Use `--no-index` to skip that step:
|
|
625
625
|
|
|
@@ -73,6 +73,7 @@ const state = {
|
|
|
73
73
|
cells: new Map()
|
|
74
74
|
},
|
|
75
75
|
miniMapView: null,
|
|
76
|
+
miniMapDirty: true,
|
|
76
77
|
overlayScheduled: false,
|
|
77
78
|
chunk: {
|
|
78
79
|
nodes: [],
|
|
@@ -80,6 +81,8 @@ const state = {
|
|
|
80
81
|
},
|
|
81
82
|
selectedNodeId: null,
|
|
82
83
|
searchToken: 0,
|
|
84
|
+
searchTimer: null,
|
|
85
|
+
searchResultIds: new Set(),
|
|
83
86
|
fetchToken: 0,
|
|
84
87
|
fetchTimer: null,
|
|
85
88
|
fetchAbortController: null,
|
|
@@ -732,7 +735,10 @@ const updateGraphOverlays = () => {
|
|
|
732
735
|
requestAnimationFrame(() => {
|
|
733
736
|
state.overlayScheduled = false
|
|
734
737
|
drawLabels()
|
|
735
|
-
|
|
738
|
+
if (state.miniMapDirty) {
|
|
739
|
+
drawMiniMap()
|
|
740
|
+
state.miniMapDirty = false
|
|
741
|
+
}
|
|
736
742
|
})
|
|
737
743
|
}
|
|
738
744
|
|
|
@@ -963,6 +969,7 @@ const fetchChunk = async ({ fit } = { fit: false }) => {
|
|
|
963
969
|
nodes: chunkNodes,
|
|
964
970
|
edges: normalizeList(chunk.edges)
|
|
965
971
|
}
|
|
972
|
+
state.miniMapDirty = true
|
|
966
973
|
state.spatialIndex.key = ''
|
|
967
974
|
const renderChunk = { ...chunk, nodes: chunkNodes }
|
|
968
975
|
state.totals = {
|
|
@@ -980,6 +987,7 @@ const fetchChunk = async ({ fit } = { fit: false }) => {
|
|
|
980
987
|
if (state.renderWorker && state.workerReady) {
|
|
981
988
|
state.renderWorker.postMessage({ type: 'chunk', chunk: renderChunk })
|
|
982
989
|
state.renderWorker.postMessage({ type: 'select', id: state.selectedNodeId })
|
|
990
|
+
state.renderWorker.postMessage({ type: 'highlight', ids: Array.from(state.searchResultIds) })
|
|
983
991
|
}
|
|
984
992
|
|
|
985
993
|
updateGraphOverlays()
|
|
@@ -1010,6 +1018,7 @@ const setViewportFromCanvas = () => {
|
|
|
1010
1018
|
state.viewport.width = Math.max(320, rect.width)
|
|
1011
1019
|
state.viewport.height = Math.max(320, rect.height)
|
|
1012
1020
|
state.viewport.ratio = window.devicePixelRatio || 1
|
|
1021
|
+
state.miniMapDirty = true
|
|
1013
1022
|
updateWorkerSize()
|
|
1014
1023
|
drawFallback()
|
|
1015
1024
|
}
|
|
@@ -1278,32 +1287,50 @@ const setupControls = () => {
|
|
|
1278
1287
|
})
|
|
1279
1288
|
|
|
1280
1289
|
elements.search.addEventListener('input', () => {
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
if (!query) {
|
|
1284
|
-
if (state.renderWorker && state.workerReady) {
|
|
1285
|
-
state.renderWorker.postMessage({ type: 'highlight', ids: [] })
|
|
1286
|
-
}
|
|
1287
|
-
return
|
|
1290
|
+
if (state.searchTimer) {
|
|
1291
|
+
clearTimeout(state.searchTimer)
|
|
1288
1292
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
.
|
|
1292
|
-
|
|
1293
|
-
if (token !== state.searchToken) {
|
|
1294
|
-
return
|
|
1295
|
-
}
|
|
1296
|
-
const ids = Array.isArray(payload?.nodeIds) ? payload.nodeIds : []
|
|
1297
|
-
if (state.renderWorker && state.workerReady) {
|
|
1298
|
-
state.renderWorker.postMessage({ type: 'highlight', ids })
|
|
1299
|
-
}
|
|
1300
|
-
})
|
|
1301
|
-
.catch((error) => {
|
|
1302
|
-
console.error(error)
|
|
1303
|
-
})
|
|
1293
|
+
state.searchTimer = setTimeout(() => {
|
|
1294
|
+
state.searchTimer = null
|
|
1295
|
+
runGraphSearch().catch((error) => console.error(error))
|
|
1296
|
+
}, 160)
|
|
1304
1297
|
})
|
|
1305
1298
|
}
|
|
1306
1299
|
|
|
1300
|
+
const runGraphSearch = async () => {
|
|
1301
|
+
const token = ++state.searchToken
|
|
1302
|
+
const query = (elements.search.value || '').trim()
|
|
1303
|
+
if (!query) {
|
|
1304
|
+
state.searchResultIds = new Set()
|
|
1305
|
+
setFocusedNodeIds(new Set())
|
|
1306
|
+
if (state.renderWorker && state.workerReady) {
|
|
1307
|
+
state.renderWorker.postMessage({ type: 'highlight', ids: [] })
|
|
1308
|
+
}
|
|
1309
|
+
return
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const response = await fetch('/api/graph-filter?q=' + encodeURIComponent(query) + '&limit=1800' + scopeQuery('&'))
|
|
1313
|
+
if (!response.ok) {
|
|
1314
|
+
throw new Error('Failed to search graph')
|
|
1315
|
+
}
|
|
1316
|
+
const payload = await response.json()
|
|
1317
|
+
if (token !== state.searchToken) {
|
|
1318
|
+
return
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const ids = Array.isArray(payload?.nodeIds) ? payload.nodeIds.filter((id) => typeof id === 'string' && id.length > 0) : []
|
|
1322
|
+
state.searchResultIds = new Set(ids)
|
|
1323
|
+
setFocusedNodeIds(state.searchResultIds)
|
|
1324
|
+
if (state.renderWorker && state.workerReady) {
|
|
1325
|
+
state.renderWorker.postMessage({ type: 'highlight', ids })
|
|
1326
|
+
}
|
|
1327
|
+
if (ids.length > 0 && state.graphMode === 'far') {
|
|
1328
|
+
state.camera.scale = Math.max(state.camera.scale, 0.82)
|
|
1329
|
+
updateWorkerCamera()
|
|
1330
|
+
scheduleChunkFetch()
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1307
1334
|
const loadAgents = async () => {
|
|
1308
1335
|
const response = await fetch('/api/agents')
|
|
1309
1336
|
if (!response.ok) {
|
|
@@ -5,7 +5,7 @@ import { addVisualContextEdges } from '../domain/graph-contexts.js';
|
|
|
5
5
|
import { createCauliflowerGraphLayout } from '../domain/graph-layout.js';
|
|
6
6
|
import { indexStoragePath } from '../infrastructure/file-index.js';
|
|
7
7
|
import { getGraphSummary } from './get-graph-summary.js';
|
|
8
|
-
const graphLayoutVersion =
|
|
8
|
+
const graphLayoutVersion = 9;
|
|
9
9
|
const graphLayoutCache = new Map();
|
|
10
10
|
const safeCacheSegment = (value, fallback) => value?.replace(/[^a-zA-Z0-9_-]/g, '_') || fallback;
|
|
11
11
|
const graphLayoutStoragePath = (vaultPath, options) => {
|
|
@@ -107,13 +107,15 @@ const selectTopByRelevance = (items, relevanceById, budget) => [...items]
|
|
|
107
107
|
.slice(0, Math.max(1, budget));
|
|
108
108
|
const selectNearNodes = (nodes, cache, input) => {
|
|
109
109
|
const padding = Math.max(input.width, input.height) * viewportPaddingFactor;
|
|
110
|
-
const
|
|
110
|
+
const candidateNodes = nodes.length <= input.nodeBudget
|
|
111
|
+
? nodes
|
|
112
|
+
: nodes.filter((node) => inViewport(node, input, padding));
|
|
111
113
|
const relevance = new Map();
|
|
112
|
-
for (let index = 0; index <
|
|
113
|
-
const node =
|
|
114
|
+
for (let index = 0; index < candidateNodes.length; index += 1) {
|
|
115
|
+
const node = candidateNodes[index];
|
|
114
116
|
relevance.set(node.id, rankNodeRelevance(node, input, cache.degrees));
|
|
115
117
|
}
|
|
116
|
-
const selected = selectTopByRelevance(
|
|
118
|
+
const selected = selectTopByRelevance(candidateNodes, relevance, input.nodeBudget);
|
|
117
119
|
return selected.map((node) => [
|
|
118
120
|
node.id,
|
|
119
121
|
node.title,
|
|
@@ -127,13 +129,15 @@ const selectNearNodes = (nodes, cache, input) => {
|
|
|
127
129
|
};
|
|
128
130
|
const selectMidNodes = (nodes, cache, input) => {
|
|
129
131
|
const padding = Math.max(input.width, input.height) * (viewportPaddingFactor + 0.08);
|
|
130
|
-
const
|
|
132
|
+
const candidateNodes = nodes.length <= input.nodeBudget
|
|
133
|
+
? nodes
|
|
134
|
+
: nodes.filter((node) => inViewport(node, input, padding));
|
|
131
135
|
const relevance = new Map();
|
|
132
|
-
for (let index = 0; index <
|
|
133
|
-
const node =
|
|
136
|
+
for (let index = 0; index < candidateNodes.length; index += 1) {
|
|
137
|
+
const node = candidateNodes[index];
|
|
134
138
|
relevance.set(node.id, rankNodeRelevance(node, input, cache.degrees) * 1.1);
|
|
135
139
|
}
|
|
136
|
-
const selected = selectTopByRelevance(
|
|
140
|
+
const selected = selectTopByRelevance(candidateNodes, relevance, input.nodeBudget);
|
|
137
141
|
return selected.map((node) => [
|
|
138
142
|
node.id,
|
|
139
143
|
node.title,
|
|
@@ -147,14 +151,12 @@ const selectMidNodes = (nodes, cache, input) => {
|
|
|
147
151
|
};
|
|
148
152
|
const selectFarClusters = (nodes, input, nodeBudget) => {
|
|
149
153
|
const roots = createSegmentClusters(nodes);
|
|
150
|
-
const padding = Math.max(input.width, input.height) * 0.12;
|
|
151
|
-
const candidates = roots.filter((group) => inViewport(group, input, padding));
|
|
152
154
|
const relevance = new Map();
|
|
153
|
-
for (let index = 0; index <
|
|
154
|
-
const group =
|
|
155
|
+
for (let index = 0; index < roots.length; index += 1) {
|
|
156
|
+
const group = roots[index];
|
|
155
157
|
relevance.set(group.id, rankGroupRelevance(group, input));
|
|
156
158
|
}
|
|
157
|
-
const selected = selectTopByRelevance(
|
|
159
|
+
const selected = selectTopByRelevance(roots, relevance, nodeBudget);
|
|
158
160
|
return selected.map((group) => [
|
|
159
161
|
`cluster:${group.id}`,
|
|
160
162
|
group.title,
|
|
@@ -25,6 +25,8 @@ export const inferExplicitVisualGraphContext = (node) => {
|
|
|
25
25
|
return context('Git Workflow');
|
|
26
26
|
if (includesAny(text, [/\bagent memory hub\b/]))
|
|
27
27
|
return context('Agent Memory');
|
|
28
|
+
if (includesAny(text, [/pingu_ai_codding_pair_programming/, /\bpingu\b/]))
|
|
29
|
+
return context('Pingu');
|
|
28
30
|
if (path.startsWith('github-repos/'))
|
|
29
31
|
return context('GitHub Repositories');
|
|
30
32
|
if (path.startsWith('github-org-repos/'))
|
|
@@ -41,21 +43,40 @@ export const inferExplicitVisualGraphContext = (node) => {
|
|
|
41
43
|
return context('Nebula');
|
|
42
44
|
if (includesAny(text, [/\bsnippets?\b/, /\bupgrader\b/, /\bversion-map\b/]))
|
|
43
45
|
return context('Snippets');
|
|
44
|
-
if (includesAny(text, [/\binkdrop\b/]))
|
|
45
|
-
return context('Inkdrop');
|
|
46
46
|
if (includesAny(text, [
|
|
47
47
|
/\bpreference\b/,
|
|
48
|
+
/\bpreferences\b/,
|
|
48
49
|
/\bpreferencia\b/,
|
|
49
50
|
/\bpreferencias\b/,
|
|
51
|
+
/\bpreferência\b/,
|
|
52
|
+
/\bpreferências\b/,
|
|
50
53
|
/\bplaybook\b/,
|
|
51
54
|
/\bdirective\b/,
|
|
55
|
+
/\bdirectives\b/,
|
|
56
|
+
/\bdiretiva\b/,
|
|
57
|
+
/\bdiretivas\b/,
|
|
52
58
|
/\bengineering-style\b/,
|
|
53
59
|
/\bglobal-engineering\b/,
|
|
54
60
|
/\bcoding-identity\b/,
|
|
55
|
-
/\bagents\.md\b
|
|
61
|
+
/\bagents\.md\b/,
|
|
62
|
+
/\bagents-md\b/,
|
|
63
|
+
/\bordem direta\b/,
|
|
64
|
+
/\bordem-direta\b/,
|
|
65
|
+
/\bman-in-the-loop\b/,
|
|
66
|
+
/\bconfig geral\b/,
|
|
67
|
+
/\bconfig-geral\b/,
|
|
68
|
+
/\bsync config_files\b/,
|
|
69
|
+
/\bsync-config-files\b/,
|
|
70
|
+
/\bregra operacional\b/,
|
|
71
|
+
/\bregras operacionais\b/,
|
|
72
|
+
/\boperational rule\b/,
|
|
73
|
+
/\boperational rules\b/,
|
|
74
|
+
/\boperational policy\b/
|
|
56
75
|
])) {
|
|
57
76
|
return context('User Preferences');
|
|
58
77
|
}
|
|
78
|
+
if (includesAny(text, [/\binkdrop\b/]))
|
|
79
|
+
return context('Inkdrop');
|
|
59
80
|
if (includesAny(text, [/\blazyvim\b/, /\bneovim\b/, /\bnvim\b/, /\bmason\b/, /\bwrapper\b/]))
|
|
60
81
|
return context('Neovim LazyVim');
|
|
61
82
|
if (includesAny(text, [/\bgit-flow\b/, /\borigin-sync\b/, /\bgit-identidade\b/, /\bcommit\b/, /\bpush\b/]))
|
|
@@ -219,13 +219,13 @@ const segmentCenterRadius = (segments) => {
|
|
|
219
219
|
const circumference = segments.reduce((total, [, nodes]) => total + petalRadiusForSegmentSize(nodes.length) * 2 + 180, 0);
|
|
220
220
|
return Math.max(520, circumference / (Math.PI * 2));
|
|
221
221
|
};
|
|
222
|
-
const createCauliflowerSegmentNodes = (segments, degrees,
|
|
222
|
+
const createCauliflowerSegmentNodes = (segments, degrees, rootHubId, segmentGroups) => ([segment, nodes], segmentIndex) => {
|
|
223
223
|
const sortedNodes = [...nodes].sort(byDegreeThenTitle(degrees));
|
|
224
|
-
const segmentHub = selectSegmentHub(sortedNodes, degrees,
|
|
224
|
+
const segmentHub = selectSegmentHub(sortedNodes, degrees, rootHubId);
|
|
225
225
|
const angle = segmentAngle(segment, segmentIndex, segmentGroups.length);
|
|
226
226
|
const globalRadius = segmentCenterRadius(segmentGroups);
|
|
227
227
|
const petalRadius = petalRadiusForSegmentSize(sortedNodes.length);
|
|
228
|
-
const isPrimarySegment = Boolean(segmentHub && segmentHub.id ===
|
|
228
|
+
const isPrimarySegment = Boolean(segmentHub && segmentHub.id === rootHubId);
|
|
229
229
|
const centerX = isPrimarySegment || globalRadius === 0 ? 0 : Math.cos(angle) * globalRadius;
|
|
230
230
|
const centerY = isPrimarySegment || globalRadius === 0 ? 0 : Math.sin(angle) * (globalRadius * 0.86);
|
|
231
231
|
const nonHubNodes = sortedNodes.filter((node) => node.id !== segmentHub?.id);
|
|
@@ -254,6 +254,41 @@ const createCauliflowerSegmentNodes = (segments, degrees, primaryHubId, segmentG
|
|
|
254
254
|
});
|
|
255
255
|
return [...hubNode, ...petalNodes];
|
|
256
256
|
};
|
|
257
|
+
const createVisualEdge = (source, target, weight, priority) => ({
|
|
258
|
+
source: source.id,
|
|
259
|
+
target: target.id,
|
|
260
|
+
targetTitle: target.title,
|
|
261
|
+
weight,
|
|
262
|
+
priority
|
|
263
|
+
});
|
|
264
|
+
const createCauliflowerVisualEdges = (segmentGroups, degrees, rootHubId) => {
|
|
265
|
+
const nodeById = new Map(segmentGroups.flatMap(([, nodes]) => nodes.map((node) => [node.id, node])));
|
|
266
|
+
const rootHub = rootHubId ? nodeById.get(rootHubId) ?? null : null;
|
|
267
|
+
const edges = new Map();
|
|
268
|
+
const addEdge = (edge) => {
|
|
269
|
+
if (!edge.target || edge.source === edge.target) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
edges.set(`${edge.source}|${edge.target}`, edge);
|
|
273
|
+
};
|
|
274
|
+
segmentGroups.forEach(([, nodes]) => {
|
|
275
|
+
const segmentHub = selectSegmentHub(nodes, degrees, rootHubId);
|
|
276
|
+
const parent = segmentHub ?? rootHub;
|
|
277
|
+
if (!parent) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (rootHub && parent.id !== rootHub.id) {
|
|
281
|
+
addEdge(createVisualEdge(rootHub, parent, 6, 'high'));
|
|
282
|
+
}
|
|
283
|
+
nodes.forEach((node) => {
|
|
284
|
+
if (node.id === parent.id || node.id === rootHub?.id) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
addEdge(createVisualEdge(parent, node, 1, 'low'));
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
return Array.from(edges.values());
|
|
291
|
+
};
|
|
257
292
|
const distanceBetween = (left, right) => Math.hypot(right.x - left.x, right.y - left.y);
|
|
258
293
|
const layoutBounds = (nodes) => {
|
|
259
294
|
if (nodes.length === 0) {
|
|
@@ -576,13 +611,14 @@ export const createCauliflowerGraphLayout = (graph) => {
|
|
|
576
611
|
const segments = assignSegments(graph.nodes, graph.edges, degrees);
|
|
577
612
|
const segmentGroups = Array.from(groupNodesBySegment(graph.nodes, segments).entries())
|
|
578
613
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
579
|
-
const
|
|
580
|
-
const nodes = relaxCollisions(segmentGroups.flatMap(createCauliflowerSegmentNodes(segments, degrees,
|
|
581
|
-
const centeredNodes = centerLayoutByNode(nodes,
|
|
582
|
-
const
|
|
614
|
+
const rootHubId = selectPrimaryHubId(graph.nodes, degrees) ?? selectHighestDegreeNodeId(graph.nodes, degrees);
|
|
615
|
+
const nodes = relaxCollisions(segmentGroups.flatMap(createCauliflowerSegmentNodes(segments, degrees, rootHubId, segmentGroups)), 156, 28);
|
|
616
|
+
const centeredNodes = centerLayoutByNode(nodes, rootHubId);
|
|
617
|
+
const visualEdges = createCauliflowerVisualEdges(segmentGroups, degrees, rootHubId);
|
|
618
|
+
const groups = createGraphLayoutHierarchy(centeredNodes, visualEdges, degrees);
|
|
583
619
|
return {
|
|
584
620
|
nodes: centeredNodes,
|
|
585
|
-
edges:
|
|
621
|
+
edges: visualEdges,
|
|
586
622
|
...(groups.length > 0 ? { groups } : {})
|
|
587
623
|
};
|
|
588
624
|
};
|
package/docs/AGENT_USAGE.md
CHANGED
|
@@ -610,7 +610,7 @@ Without `--vault`, the graph UI serves `$HOME/.brainlink/vault`.
|
|
|
610
610
|
|
|
611
611
|
The frontend includes an agent selector that shows only the agent id. Selecting an agent calls the same read APIs with `agent=<agent-id>` and renders that namespace instead of merging every agent into one graph.
|
|
612
612
|
|
|
613
|
-
Graph navigation controls include zoom in, zoom out, fit visible nodes and reset-to-fit-all nodes. Mouse wheel zoom (including `cmd+scroll` and `ctrl+scroll`) is anchored to the cursor and applied immediately without delayed focus interpolation. Keyboard shortcuts are `+` (zoom in), `-` (zoom out) and `0` (reset fit). Double-click on empty canvas zooms in at cursor position. Clicking a node opens its details panel. Totals for notes, links and tags stay visible as floating metrics under the Brainlink title, and node details open in a non-modal side panel (tags, outgoing links, backlinks and Markdown content), so zoom and pan remain available during inspection. The visual layout is a cauliflower hub layout: the primary hub stays centered, segment hubs anchor surrounding lobes, and
|
|
613
|
+
Graph navigation controls include zoom in, zoom out, fit visible nodes and reset-to-fit-all nodes. Mouse wheel zoom (including `cmd+scroll` and `ctrl+scroll`) is anchored to the cursor and applied immediately without delayed focus interpolation. Keyboard shortcuts are `+` (zoom in), `-` (zoom out) and `0` (reset fit). Double-click on empty canvas zooms in at cursor position. Clicking a node opens its details panel. Totals for notes, links and tags stay visible as floating metrics under the Brainlink title, and node details open in a non-modal side panel (tags, outgoing links, backlinks and Markdown content), so zoom and pan remain available during inspection. The visual layout is a cauliflower hub layout: the primary hub stays centered, segment hubs anchor surrounding lobes, and visual edges are simplified to root hub -> segment hubs -> local context nodes. Indexed weighted wiki-link edges remain available for backlinks, ranking and context traversal outside the render layer. At high zoom-out, graph streams show segment hub clusters first; zooming in progressively reveals the individual nodes inside those lobes. Node titles appear as zoom approaches readable scale, limited to on-screen nodes in very large graphs.
|
|
614
614
|
During graph filtering, Brainlink keeps hub context nodes visible (`Memory Hub`/`MOC`/high-degree fallback) so filtered views still show relationship anchors.
|
|
615
615
|
|
|
616
616
|
The command reindexes by default, then serves:
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -162,7 +162,7 @@ server command
|
|
|
162
162
|
```
|
|
163
163
|
|
|
164
164
|
The graph UI is intentionally read-only. Markdown remains the write interface and index artifacts remain derived data.
|
|
165
|
-
The cauliflower layout is visual
|
|
165
|
+
The cauliflower layout is a visual projection over the indexed graph. Indexed weighted wiki-link edges remain unchanged for backlinks, ranking, graph reads and context traversal. The browser layout renders a simplified hierarchy instead: primary hub -> segment hubs -> local context nodes. This avoids unstable cross-context visual edges while preserving the real relationship model outside the render layer. Zoomed-out graph streams summarize those lobes as segment clusters before revealing individual nodes on zoom-in.
|
|
166
166
|
|
|
167
167
|
## HTTP API Flow
|
|
168
168
|
|
package/package.json
CHANGED