@kage-core/kage-graph-mcp 1.1.23 → 1.1.24

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/dist/daemon.js CHANGED
@@ -232,6 +232,7 @@ async function startViewer(projectDir, options = {}) {
232
232
  const host = options.host ?? DEFAULT_HOST;
233
233
  const port = options.port ?? DEFAULT_VIEWER_PORT;
234
234
  const viewerDir = (0, node_path_1.resolve)(__dirname, "..", "viewer");
235
+ const threeDir = (0, node_path_1.resolve)(__dirname, "..", "node_modules", "three");
235
236
  const projectRoot = (0, node_path_1.resolve)(projectDir);
236
237
  const graphPath = (0, node_path_1.join)(projectRoot, ".agent_memory", "graph", "graph.json");
237
238
  const codePath = (0, node_path_1.join)(projectRoot, ".agent_memory", "code_graph", "graph.json");
@@ -262,13 +263,16 @@ async function startViewer(projectDir, options = {}) {
262
263
  else if (requestUrl.pathname.startsWith("/viewer/")) {
263
264
  filePath = (0, node_path_1.join)(viewerDir, (0, node_path_1.normalize)(requestUrl.pathname.replace(/^\/viewer\//, "")));
264
265
  }
266
+ else if (requestUrl.pathname.startsWith("/vendor/three/")) {
267
+ filePath = (0, node_path_1.join)(threeDir, (0, node_path_1.normalize)(requestUrl.pathname.replace(/^\/vendor\/three\//, "")));
268
+ }
265
269
  else {
266
270
  const decoded = decodeURIComponent(requestUrl.pathname);
267
271
  filePath = (0, node_path_1.resolve)(decoded);
268
272
  if (!isInside(projectRoot, filePath))
269
273
  filePath = null;
270
274
  }
271
- if (!filePath || (!isInside(viewerDir, filePath) && !isInside(projectRoot, filePath)) || !(0, node_fs_1.existsSync)(filePath)) {
275
+ if (!filePath || (!isInside(viewerDir, filePath) && !isInside(projectRoot, filePath) && !isInside(threeDir, filePath)) || !(0, node_fs_1.existsSync)(filePath)) {
272
276
  json(res, 404, { ok: false, error: "not_found" });
273
277
  return;
274
278
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.1.23",
3
+ "version": "1.1.24",
4
4
  "description": "Local-first repo memory, code graph, and recall MCP server for coding agents",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -37,6 +37,7 @@
37
37
  "license": "GPL-3.0-only",
38
38
  "dependencies": {
39
39
  "@modelcontextprotocol/sdk": "^1.10.2",
40
+ "three": "^0.184.0",
40
41
  "typescript": "^5.0.0"
41
42
  },
42
43
  "devDependencies": {
package/viewer/app.js CHANGED
@@ -37,6 +37,32 @@
37
37
  idleFrames: 0,
38
38
  adjacency: new Map()
39
39
  },
40
+ three: {
41
+ THREE: null,
42
+ loading: null,
43
+ failed: false,
44
+ renderer: null,
45
+ scene: null,
46
+ camera: null,
47
+ root: null,
48
+ nodeGroup: null,
49
+ edgeGroup: null,
50
+ nodeById: new Map(),
51
+ edgeRefs: [],
52
+ nodeTextureCache: new Map(),
53
+ physicsTick: 0,
54
+ physicsIdle: 0,
55
+ hoverNode: null,
56
+ pointer: null,
57
+ drag: null,
58
+ raycaster: null,
59
+ raf: null,
60
+ distance: 850,
61
+ target: { x: 0, y: 0, z: 0 },
62
+ rotationX: -0.20,
63
+ rotationY: 0.24,
64
+ lastPointerEvent: null
65
+ },
40
66
  pan: null
41
67
  };
42
68
 
@@ -84,6 +110,7 @@
84
110
  selectionStatus: document.getElementById("selectionStatus"),
85
111
  searchInput: document.getElementById("searchInput"),
86
112
  viewMode: document.getElementById("viewMode"),
113
+ renderMode: document.getElementById("renderMode"),
87
114
  typeFilter: document.getElementById("typeFilter"),
88
115
  relationFilter: document.getElementById("relationFilter"),
89
116
  scopeFilter: document.getElementById("scopeFilter"),
@@ -93,7 +120,10 @@
93
120
  zoomOut: document.getElementById("zoomOut"),
94
121
  zoomIn: document.getElementById("zoomIn"),
95
122
  fitView: document.getElementById("fitView"),
123
+ interactionHint: document.getElementById("interactionHint"),
124
+ graphWrap: document.getElementById("graphCanvasWrap"),
96
125
  canvas: document.getElementById("graphCanvas"),
126
+ threeGraph: document.getElementById("threeGraph"),
97
127
  tooltip: document.getElementById("graphTooltip"),
98
128
  svg: document.getElementById("graphSvg"),
99
129
  nodeLayer: document.getElementById("nodeLayer"),
@@ -122,33 +152,42 @@
122
152
  els.graphFile.addEventListener("change", handleFile);
123
153
  els.searchInput.addEventListener("input", scheduleRender);
124
154
  els.viewMode.addEventListener("change", render);
155
+ els.renderMode.addEventListener("change", function () {
156
+ state.lastVisibleSignature = "";
157
+ render();
158
+ });
125
159
  els.typeFilter.addEventListener("change", render);
126
160
  els.relationFilter.addEventListener("change", render);
127
161
  els.scopeFilter.addEventListener("change", render);
128
162
  els.maxNodes.addEventListener("change", render);
129
163
  els.showDependencies.addEventListener("change", render);
130
- els.zoomOut.addEventListener("click", function () { zoomCanvas(0.82); });
131
- els.zoomIn.addEventListener("click", function () { zoomCanvas(1.22); });
132
- els.fitView.addEventListener("click", function () { fitCanvas(); drawCanvasGraph(); });
164
+ els.zoomOut.addEventListener("click", function () { zoomGraph(0.82); });
165
+ els.zoomIn.addEventListener("click", function () { zoomGraph(1.22); });
166
+ els.fitView.addEventListener("click", fitActiveGraph);
133
167
  els.canvas.addEventListener("mousedown", startCanvasPointer);
134
168
  els.canvas.addEventListener("mousemove", moveCanvasPointer);
135
169
  els.canvas.addEventListener("mouseup", endCanvasPointer);
136
170
  els.canvas.addEventListener("mouseleave", leaveCanvasPointer);
137
171
  els.canvas.addEventListener("wheel", handleCanvasWheel, { passive: false });
138
172
  els.canvas.addEventListener("dblclick", handleCanvasDoubleClick);
173
+ els.threeGraph.addEventListener("mousedown", startThreePointer);
174
+ els.threeGraph.addEventListener("mousemove", moveThreePointer);
175
+ els.threeGraph.addEventListener("mouseup", endThreePointer);
176
+ els.threeGraph.addEventListener("mouseleave", leaveThreePointer);
177
+ els.threeGraph.addEventListener("wheel", handleThreeWheel, { passive: false });
178
+ els.threeGraph.addEventListener("dblclick", handleThreeDoubleClick);
139
179
  els.svg.addEventListener("mousedown", startPan);
140
180
  els.svg.addEventListener("click", handleSvgClick);
141
181
  els.svg.addEventListener("wheel", handleWheelZoom, { passive: false });
142
182
  window.addEventListener("mousemove", continuePan);
143
183
  window.addEventListener("mouseup", endPan);
144
184
  window.addEventListener("resize", function () {
145
- resizeCanvas();
146
- fitCanvas();
147
- drawCanvasGraph();
185
+ resizeActiveGraph();
148
186
  });
149
187
  els.resetView.addEventListener("click", function () {
150
188
  els.searchInput.value = "";
151
189
  els.viewMode.value = "combined";
190
+ els.renderMode.value = "2d";
152
191
  els.typeFilter.value = "";
153
192
  els.relationFilter.value = "";
154
193
  els.scopeFilter.value = "signal";
@@ -227,6 +266,7 @@
227
266
  function loadFromUrlParams() {
228
267
  var params = new URLSearchParams(window.location.search);
229
268
  applyRequestedView(params.get("view") || params.get("mode"));
269
+ applyRequestedRenderMode(params.get("render") || params.get("graphMode"));
230
270
  var memoryGraphPaths = splitParamValues(params.getAll("graph"));
231
271
  var codeGraphPaths = splitParamValues(params.getAll("code"));
232
272
  var graphPaths = memoryGraphPaths.concat(codeGraphPaths);
@@ -829,7 +869,7 @@
829
869
  var graphChanged = nextSignature !== state.lastVisibleSignature;
830
870
  state.lastVisibleSignature = nextSignature;
831
871
 
832
- renderCanvasGraph(graphChanged);
872
+ renderActiveGraph(graphChanged);
833
873
  renderLists();
834
874
  renderDetails();
835
875
  renderMetrics();
@@ -1088,9 +1128,69 @@
1088
1128
  }));
1089
1129
  }
1090
1130
 
1091
- function renderCanvasGraph(graphChanged) {
1092
- resizeCanvas();
1131
+ function activeRenderMode() {
1132
+ return els.renderMode && els.renderMode.value === "3d" ? "3d" : "2d";
1133
+ }
1134
+
1135
+ function renderActiveGraph(graphChanged) {
1136
+ var mode = activeRenderMode();
1137
+ updateGraphSurfaceMode(mode);
1093
1138
  syncSimulationGraph(graphChanged);
1139
+ if (mode === "3d") {
1140
+ stopSimulation();
1141
+ renderThreeGraph(graphChanged);
1142
+ return;
1143
+ }
1144
+ stopThreeGraph();
1145
+ renderCanvasGraph(graphChanged, true);
1146
+ }
1147
+
1148
+ function updateGraphSurfaceMode(mode) {
1149
+ if (els.graphWrap) {
1150
+ els.graphWrap.classList.toggle("mode-3d", mode === "3d");
1151
+ els.graphWrap.classList.toggle("mode-2d", mode !== "3d");
1152
+ }
1153
+ if (els.interactionHint) {
1154
+ els.interactionHint.textContent = mode === "3d"
1155
+ ? "drag orb or node / wheel zoom / click node"
1156
+ : "drag canvas / wheel zoom / click node";
1157
+ }
1158
+ if (els.tooltip) els.tooltip.classList.remove("visible");
1159
+ }
1160
+
1161
+ function resizeActiveGraph() {
1162
+ if (activeRenderMode() === "3d") {
1163
+ resizeThreeGraph();
1164
+ renderThreeFrame();
1165
+ return;
1166
+ }
1167
+ resizeCanvas();
1168
+ fitCanvas();
1169
+ drawCanvasGraph();
1170
+ }
1171
+
1172
+ function fitActiveGraph() {
1173
+ if (activeRenderMode() === "3d") {
1174
+ fitThreeGraph();
1175
+ renderThreeFrame();
1176
+ return;
1177
+ }
1178
+ fitCanvas();
1179
+ drawCanvasGraph();
1180
+ }
1181
+
1182
+ function zoomGraph(factor) {
1183
+ if (activeRenderMode() === "3d") {
1184
+ zoomThreeGraph(factor);
1185
+ renderThreeFrame();
1186
+ return;
1187
+ }
1188
+ zoomCanvas(factor);
1189
+ }
1190
+
1191
+ function renderCanvasGraph(graphChanged, alreadySynced) {
1192
+ resizeCanvas();
1193
+ if (!alreadySynced) syncSimulationGraph(graphChanged);
1094
1194
  if (graphChanged) fitCanvas();
1095
1195
  startSimulation();
1096
1196
  drawCanvasGraph();
@@ -1817,6 +1917,15 @@
1817
1917
  els.viewMode.value = normalized;
1818
1918
  }
1819
1919
 
1920
+ function applyRequestedRenderMode(mode) {
1921
+ if (!mode) return;
1922
+ var normalized = String(mode).toLowerCase();
1923
+ if (normalized === "canvas") normalized = "2d";
1924
+ if (normalized === "space") normalized = "3d";
1925
+ if (["2d", "3d"].indexOf(normalized) === -1) return;
1926
+ els.renderMode.value = normalized;
1927
+ }
1928
+
1820
1929
  function isMemoryCodeEdge(edge) {
1821
1930
  return Boolean(edge && (edge.memory_code_link || isMemoryCodeRelation(edge.relation)));
1822
1931
  }
@@ -2317,6 +2426,579 @@
2317
2426
  state.sim.panY = height / 2 - ((minY + maxY) / 2) * state.sim.zoom;
2318
2427
  }
2319
2428
 
2429
+ function ensureThree() {
2430
+ if (state.three.THREE) return Promise.resolve(state.three.THREE);
2431
+ if (state.three.loading) return state.three.loading;
2432
+ state.three.failed = false;
2433
+ state.three.loading = import("/vendor/three/build/three.module.min.js")
2434
+ .catch(function () {
2435
+ return import("https://unpkg.com/three@0.184.0/build/three.module.min.js");
2436
+ })
2437
+ .then(function (mod) {
2438
+ state.three.THREE = mod;
2439
+ return mod;
2440
+ })
2441
+ .catch(function (error) {
2442
+ state.three.failed = true;
2443
+ throw error;
2444
+ });
2445
+ return state.three.loading;
2446
+ }
2447
+
2448
+ function renderThreeGraph(graphChanged) {
2449
+ drawThreeStatus("Loading 3D graph...");
2450
+ ensureThree().then(function () {
2451
+ if (activeRenderMode() !== "3d") return;
2452
+ setupThreeScene();
2453
+ rebuildThreeScene();
2454
+ if (graphChanged || state.three.distance <= 0) fitThreeGraph();
2455
+ startThreeGraph();
2456
+ renderThreeFrame();
2457
+ }).catch(function () {
2458
+ drawThreeStatus("3D renderer unavailable. Falling back to 2D canvas.");
2459
+ if (els.renderMode) els.renderMode.value = "2d";
2460
+ updateGraphSurfaceMode("2d");
2461
+ renderCanvasGraph(true);
2462
+ });
2463
+ }
2464
+
2465
+ function drawThreeStatus(message) {
2466
+ if (!els.threeGraph || state.three.renderer) return;
2467
+ els.threeGraph.innerHTML = "<div class=\"three-status\"></div>";
2468
+ var status = els.threeGraph.querySelector(".three-status");
2469
+ if (status) status.textContent = message;
2470
+ }
2471
+
2472
+ function setupThreeScene() {
2473
+ if (state.three.renderer) {
2474
+ resizeThreeGraph();
2475
+ return;
2476
+ }
2477
+ var THREE = state.three.THREE;
2478
+ els.threeGraph.textContent = "";
2479
+ state.three.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
2480
+ state.three.renderer.setClearColor(new THREE.Color(graphPalette.background), 1);
2481
+ state.three.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
2482
+ els.threeGraph.appendChild(state.three.renderer.domElement);
2483
+ state.three.scene = new THREE.Scene();
2484
+ state.three.scene.fog = new THREE.FogExp2(new THREE.Color(graphPalette.background), 0.00016);
2485
+ state.three.camera = new THREE.PerspectiveCamera(50, 1, 1, 5000);
2486
+ state.three.root = new THREE.Group();
2487
+ state.three.nodeGroup = new THREE.Group();
2488
+ state.three.edgeGroup = new THREE.Group();
2489
+ state.three.root.add(state.three.edgeGroup);
2490
+ state.three.root.add(state.three.nodeGroup);
2491
+ state.three.scene.add(state.three.root);
2492
+ state.three.scene.add(new THREE.AmbientLight(0x8fffb1, 0.54));
2493
+ var key = new THREE.PointLight(0x6ad7ff, 1.2, 2200);
2494
+ key.position.set(280, 360, 520);
2495
+ state.three.scene.add(key);
2496
+ var fill = new THREE.PointLight(0xb88cff, 0.72, 1800);
2497
+ fill.position.set(-420, -220, 360);
2498
+ state.three.scene.add(fill);
2499
+ state.three.raycaster = new THREE.Raycaster();
2500
+ state.three.pointer = new THREE.Vector2();
2501
+ resizeThreeGraph();
2502
+ }
2503
+
2504
+ function rebuildThreeScene() {
2505
+ var THREE = state.three.THREE;
2506
+ clearThreeGroup(state.three.nodeGroup);
2507
+ clearThreeGroup(state.three.edgeGroup);
2508
+ state.three.nodeById = new Map();
2509
+ state.three.edgeRefs = [];
2510
+ state.three.physicsTick = 0;
2511
+ state.three.physicsIdle = 0;
2512
+
2513
+ state.sim.nodes.forEach(function (node, index) {
2514
+ var entity = node.entity;
2515
+ var selected = state.selected && state.selected.kind === "entity" && state.selected.id === node.id;
2516
+ var material = new THREE.SpriteMaterial({
2517
+ map: threeNodeTexture(entity, selected),
2518
+ transparent: true,
2519
+ opacity: isDependencyEntity(entity) ? 0.70 : 1,
2520
+ depthWrite: false
2521
+ });
2522
+ var mesh = new THREE.Sprite(material);
2523
+ var position = threePosition(node, index);
2524
+ mesh.position.set(position.x, position.y, position.z);
2525
+ var size = threeNodeSize(node, selected);
2526
+ mesh.scale.set(size, size, 1);
2527
+ mesh.userData.node = node;
2528
+ state.three.nodeGroup.add(mesh);
2529
+ state.three.nodeById.set(node.id, { node: node, mesh: mesh, vx: 0, vy: 0, vz: 0 });
2530
+ });
2531
+
2532
+ state.sim.edges.forEach(function (edge) {
2533
+ var from = state.three.nodeById.get(edge.from);
2534
+ var to = state.three.nodeById.get(edge.to);
2535
+ if (!from || !to) return;
2536
+ var fromEntity = from.node.entity;
2537
+ var toEntity = to.node.entity;
2538
+ var connected = state.selected && state.selected.kind === "entity" && (state.selected.id === edge.from || state.selected.id === edge.to);
2539
+ var geometry = new THREE.BufferGeometry().setFromPoints([from.mesh.position, to.mesh.position]);
2540
+ var material = new THREE.LineBasicMaterial({
2541
+ color: new THREE.Color(edgeThemeColor(edge, fromEntity, toEntity)),
2542
+ transparent: true,
2543
+ opacity: threeEdgeOpacity(edge, fromEntity, toEntity, connected),
2544
+ depthWrite: false,
2545
+ depthTest: false
2546
+ });
2547
+ var line = new THREE.Line(geometry, material);
2548
+ line.userData.edge = edge;
2549
+ line.userData.from = edge.from;
2550
+ line.userData.to = edge.to;
2551
+ state.three.edgeGroup.add(line);
2552
+ state.three.edgeRefs.push(line);
2553
+ });
2554
+
2555
+ if (state.three.root) {
2556
+ state.three.root.rotation.x = state.three.rotationX;
2557
+ state.three.root.rotation.y = state.three.rotationY;
2558
+ }
2559
+ }
2560
+
2561
+ function clearThreeGroup(group) {
2562
+ if (!group) return;
2563
+ while (group.children.length) {
2564
+ var child = group.children.pop();
2565
+ if (!child) continue;
2566
+ if (child.geometry && child.geometry.dispose) child.geometry.dispose();
2567
+ if (child.material) {
2568
+ if (Array.isArray(child.material)) child.material.forEach(function (material) { if (material.dispose) material.dispose(); });
2569
+ else if (child.material.dispose) child.material.dispose();
2570
+ }
2571
+ }
2572
+ }
2573
+
2574
+ function threeNodeTexture(entity, selected) {
2575
+ var THREE = state.three.THREE;
2576
+ var color = nodeThemeColor(entity);
2577
+ var key = [color, entity.graph_kind || "", entity.type || "", selected ? "selected" : "normal", isDependencyEntity(entity) ? "dependency" : "primary"].join("|");
2578
+ if (state.three.nodeTextureCache.has(key)) return state.three.nodeTextureCache.get(key);
2579
+ var canvas = document.createElement("canvas");
2580
+ canvas.width = 128;
2581
+ canvas.height = 128;
2582
+ var ctx = canvas.getContext("2d");
2583
+ var rgb = hexToRgb(color);
2584
+ var fill = nodeFillColor(entity);
2585
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
2586
+ var halo = ctx.createRadialGradient(64, 64, 8, 64, 64, 58);
2587
+ halo.addColorStop(0, "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + "," + (selected ? 0.62 : 0.38) + ")");
2588
+ halo.addColorStop(0.52, "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + "," + (selected ? 0.24 : 0.13) + ")");
2589
+ halo.addColorStop(1, "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + ",0)");
2590
+ ctx.fillStyle = halo;
2591
+ ctx.beginPath();
2592
+ ctx.arc(64, 64, 60, 0, Math.PI * 2);
2593
+ ctx.fill();
2594
+ ctx.fillStyle = fill;
2595
+ ctx.beginPath();
2596
+ ctx.arc(64, 64, selected ? 36 : 31, 0, Math.PI * 2);
2597
+ ctx.fill();
2598
+ ctx.strokeStyle = color;
2599
+ ctx.lineWidth = selected ? 8 : 5;
2600
+ ctx.beginPath();
2601
+ ctx.arc(64, 64, selected ? 38 : 33, 0, Math.PI * 2);
2602
+ ctx.stroke();
2603
+ if (entity.graph_kind === "memory") {
2604
+ ctx.fillStyle = color;
2605
+ ctx.globalAlpha = 0.95;
2606
+ ctx.beginPath();
2607
+ ctx.arc(64, 64, 8, 0, Math.PI * 2);
2608
+ ctx.fill();
2609
+ ctx.globalAlpha = 1;
2610
+ }
2611
+ var texture = new THREE.CanvasTexture(canvas);
2612
+ texture.needsUpdate = true;
2613
+ state.three.nodeTextureCache.set(key, texture);
2614
+ return texture;
2615
+ }
2616
+
2617
+ function threeNodeSize(node, selected) {
2618
+ var entity = node.entity;
2619
+ var size = clamp(19 + degreeOf(node.id) * 1.65, 22, entity.graph_kind === "memory" ? 46 : 40);
2620
+ if (entity.type === "file" || entity.type === "repo") size += 5;
2621
+ if (selected) size *= 1.22;
2622
+ return size;
2623
+ }
2624
+
2625
+ function threeEdgeOpacity(edge, fromEntity, toEntity, connected) {
2626
+ if (connected) return 0.48;
2627
+ if (edge.memory_code_link || isMemoryCodeRelation(edge.relation)) return 0.34;
2628
+ if (fromEntity.graph_kind === "code" && toEntity.graph_kind === "code") return 0.20;
2629
+ if (fromEntity.graph_kind === "memory" && toEntity.graph_kind === "memory") return 0.12;
2630
+ return 0.15;
2631
+ }
2632
+
2633
+ function threePosition(node, index) {
2634
+ var entity = node.entity;
2635
+ var total = Math.max(1, state.sim.nodes.length);
2636
+ var rank = index + 0.5;
2637
+ var golden = Math.PI * (3 - Math.sqrt(5));
2638
+ var theta = golden * rank + (hashString(node.id || index) % 360) * 0.004;
2639
+ var yUnit = 1 - (2 * rank) / total;
2640
+ var ring = Math.sqrt(Math.max(0, 1 - yUnit * yUnit));
2641
+ var radius = threeOrbRadius(total);
2642
+ var inner = threeOrbScale(entity);
2643
+ var kindOffset = threeKindDepthBias(entity);
2644
+ return {
2645
+ x: Math.cos(theta) * ring * radius * inner,
2646
+ y: yUnit * radius * inner,
2647
+ z: (Math.sin(theta) * ring + kindOffset) * radius * inner
2648
+ };
2649
+ }
2650
+
2651
+ function threeOrbRadius(total) {
2652
+ return clamp(245 + Math.max(1, total) * 2.1, 280, 455);
2653
+ }
2654
+
2655
+ function threeOrbScale(entity) {
2656
+ if (entity.graph_kind === "memory") return 0.88;
2657
+ if (entity.type === "file" || entity.type === "repo") return 0.76;
2658
+ if (isDependencyEntity(entity)) return 1.08;
2659
+ return 1;
2660
+ }
2661
+
2662
+ function threeKindDepthBias(entity) {
2663
+ if (entity.graph_kind === "memory") return -0.22;
2664
+ if (entity.graph_kind === "code") return 0.18;
2665
+ return 0;
2666
+ }
2667
+
2668
+ function resizeThreeGraph() {
2669
+ if (!state.three.renderer || !els.threeGraph) return;
2670
+ var rect = els.threeGraph.getBoundingClientRect();
2671
+ var width = Math.max(320, rect.width);
2672
+ var height = Math.max(360, rect.height);
2673
+ state.three.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
2674
+ state.three.renderer.setSize(width, height, false);
2675
+ state.three.camera.aspect = width / height;
2676
+ state.three.camera.updateProjectionMatrix();
2677
+ }
2678
+
2679
+ function fitThreeGraph() {
2680
+ var entries = Array.from(state.three.nodeById.values());
2681
+ if (!entries.length) {
2682
+ state.three.distance = 850;
2683
+ return;
2684
+ }
2685
+ var maxRadius = entries.reduce(function (max, entry) {
2686
+ var p = entry.mesh.position;
2687
+ return Math.max(max, Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z));
2688
+ }, 260);
2689
+ state.three.distance = clamp(maxRadius * 2.35, 760, 2600);
2690
+ }
2691
+
2692
+ function zoomThreeGraph(factor) {
2693
+ state.three.distance = clamp(state.three.distance / factor, 320, 3200);
2694
+ }
2695
+
2696
+ function startThreeGraph() {
2697
+ if (state.three.raf) return;
2698
+ function frame() {
2699
+ if (activeRenderMode() !== "3d" || !state.three.renderer) {
2700
+ state.three.raf = null;
2701
+ return;
2702
+ }
2703
+ if (!state.three.drag && state.three.root) {
2704
+ state.three.rotationY += 0.0017;
2705
+ state.three.root.rotation.y = state.three.rotationY;
2706
+ }
2707
+ stepThreePhysics();
2708
+ renderThreeFrame();
2709
+ state.three.raf = window.requestAnimationFrame(frame);
2710
+ }
2711
+ state.three.raf = window.requestAnimationFrame(frame);
2712
+ }
2713
+
2714
+ function stopThreeGraph() {
2715
+ if (state.three.raf) window.cancelAnimationFrame(state.three.raf);
2716
+ state.three.raf = null;
2717
+ if (els.tooltip) els.tooltip.classList.remove("visible");
2718
+ }
2719
+
2720
+ function renderThreeFrame() {
2721
+ if (!state.three.renderer || !state.three.scene || !state.three.camera) return;
2722
+ resizeThreeGraph();
2723
+ state.three.camera.position.set(state.three.target.x, state.three.target.y, state.three.distance);
2724
+ state.three.camera.lookAt(state.three.target.x, state.three.target.y, state.three.target.z);
2725
+ updateThreeEdges();
2726
+ state.three.renderer.render(state.three.scene, state.three.camera);
2727
+ }
2728
+
2729
+ function stepThreePhysics() {
2730
+ if (!state.three.nodeById || state.three.physicsTick > 180) return;
2731
+ if (state.three.drag && state.three.drag.type === "node") return;
2732
+ var entries = Array.from(state.three.nodeById.values());
2733
+ var count = entries.length;
2734
+ if (!count) return;
2735
+ var forces = new Map(entries.map(function (entry) {
2736
+ return [entry.node.id, { x: 0, y: 0, z: 0 }];
2737
+ }));
2738
+ var repulsion = count > 110 ? 1800 : count > 70 ? 2600 : 3400;
2739
+ for (var i = 0; i < entries.length; i++) {
2740
+ for (var j = i + 1; j < entries.length; j++) {
2741
+ var a = entries[i];
2742
+ var b = entries[j];
2743
+ var dx = a.mesh.position.x - b.mesh.position.x;
2744
+ var dy = a.mesh.position.y - b.mesh.position.y;
2745
+ var dz = a.mesh.position.z - b.mesh.position.z;
2746
+ var distSq = Math.max(1200, dx * dx + dy * dy + dz * dz);
2747
+ var dist = Math.sqrt(distSq);
2748
+ var force = repulsion / distSq;
2749
+ var ax = (dx / dist) * force;
2750
+ var ay = (dy / dist) * force;
2751
+ var az = (dz / dist) * force;
2752
+ var fa = forces.get(a.node.id);
2753
+ var fb = forces.get(b.node.id);
2754
+ fa.x += ax;
2755
+ fa.y += ay;
2756
+ fa.z += az;
2757
+ fb.x -= ax;
2758
+ fb.y -= ay;
2759
+ fb.z -= az;
2760
+ }
2761
+ }
2762
+
2763
+ state.sim.edges.forEach(function (edge) {
2764
+ var from = state.three.nodeById.get(edge.from);
2765
+ var to = state.three.nodeById.get(edge.to);
2766
+ if (!from || !to) return;
2767
+ var dx = to.mesh.position.x - from.mesh.position.x;
2768
+ var dy = to.mesh.position.y - from.mesh.position.y;
2769
+ var dz = to.mesh.position.z - from.mesh.position.z;
2770
+ var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy + dz * dz));
2771
+ var memoryCode = edge.memory_code_link || isMemoryCodeRelation(edge.relation);
2772
+ var target = memoryCode ? 190 : (from.node.entity.graph_kind === "code" && to.node.entity.graph_kind === "code" ? 240 : 220);
2773
+ var strength = memoryCode ? 0.0032 : 0.0018;
2774
+ var force = (dist - target) * strength;
2775
+ var fx = (dx / dist) * force;
2776
+ var fy = (dy / dist) * force;
2777
+ var fz = (dz / dist) * force;
2778
+ var ff = forces.get(from.node.id);
2779
+ var ft = forces.get(to.node.id);
2780
+ ff.x += fx;
2781
+ ff.y += fy;
2782
+ ff.z += fz;
2783
+ ft.x -= fx;
2784
+ ft.y -= fy;
2785
+ ft.z -= fz;
2786
+ });
2787
+
2788
+ var baseRadius = threeOrbRadius(count);
2789
+ var maxVelocity = 0;
2790
+ entries.forEach(function (entry) {
2791
+ var p = entry.mesh.position;
2792
+ var entity = entry.node.entity;
2793
+ var len = Math.max(1, Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z));
2794
+ var desiredRadius = baseRadius * threeOrbScale(entity);
2795
+ var radial = (desiredRadius - len) * 0.055;
2796
+ var f = forces.get(entry.node.id);
2797
+ f.x += (p.x / len) * radial;
2798
+ f.y += (p.y / len) * radial;
2799
+ f.z += (p.z / len) * radial;
2800
+ var depthTarget = desiredRadius * threeKindDepthBias(entity);
2801
+ f.z += (depthTarget - p.z) * 0.002;
2802
+ entry.vx = clamp((entry.vx + f.x) * 0.80, -4.2, 4.2);
2803
+ entry.vy = clamp((entry.vy + f.y) * 0.80, -4.2, 4.2);
2804
+ entry.vz = clamp((entry.vz + f.z) * 0.80, -4.2, 4.2);
2805
+ p.x += entry.vx;
2806
+ p.y += entry.vy;
2807
+ p.z += entry.vz;
2808
+ var nextLen = Math.max(1, Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z));
2809
+ var correction = (desiredRadius - nextLen) * 0.08;
2810
+ p.x += (p.x / nextLen) * correction;
2811
+ p.y += (p.y / nextLen) * correction;
2812
+ p.z += (p.z / nextLen) * correction;
2813
+ maxVelocity = Math.max(maxVelocity, Math.abs(entry.vx), Math.abs(entry.vy), Math.abs(entry.vz));
2814
+ });
2815
+ state.three.physicsTick += 1;
2816
+ state.three.physicsIdle = maxVelocity < 0.030 ? state.three.physicsIdle + 1 : 0;
2817
+ if (state.three.physicsIdle > 32) state.three.physicsTick = 181;
2818
+ }
2819
+
2820
+ function startThreePointer(event) {
2821
+ if (activeRenderMode() !== "3d" || event.button !== 0) return;
2822
+ event.preventDefault();
2823
+ var picked = pickThreeNode(event);
2824
+ if (picked) {
2825
+ var entry = state.three.nodeById.get(picked.id);
2826
+ state.three.drag = {
2827
+ type: "node",
2828
+ x: event.clientX,
2829
+ y: event.clientY,
2830
+ moved: false,
2831
+ picked: picked,
2832
+ entry: entry,
2833
+ plane: createThreeDragPlane(entry && entry.mesh)
2834
+ };
2835
+ return;
2836
+ }
2837
+ state.three.drag = {
2838
+ type: "space",
2839
+ x: event.clientX,
2840
+ y: event.clientY,
2841
+ rotationX: state.three.rotationX,
2842
+ rotationY: state.three.rotationY,
2843
+ moved: false,
2844
+ picked: null
2845
+ };
2846
+ }
2847
+
2848
+ function moveThreePointer(event) {
2849
+ if (activeRenderMode() !== "3d") return;
2850
+ state.three.lastPointerEvent = event;
2851
+ if (state.three.drag) {
2852
+ var dx = event.clientX - state.three.drag.x;
2853
+ var dy = event.clientY - state.three.drag.y;
2854
+ if (Math.abs(dx) > 2 || Math.abs(dy) > 2) state.three.drag.moved = true;
2855
+ if (state.three.drag.type === "node") {
2856
+ moveThreeDraggedNode(event);
2857
+ renderThreeFrame();
2858
+ return;
2859
+ }
2860
+ state.three.rotationY = state.three.drag.rotationY + dx * 0.0065;
2861
+ state.three.rotationX = clamp(state.three.drag.rotationX + dy * 0.0048, -1.15, 0.45);
2862
+ if (state.three.root) {
2863
+ state.three.root.rotation.x = state.three.rotationX;
2864
+ state.three.root.rotation.y = state.three.rotationY;
2865
+ }
2866
+ renderThreeFrame();
2867
+ return;
2868
+ }
2869
+ state.three.hoverNode = pickThreeNode(event);
2870
+ updateThreeTooltip(event);
2871
+ }
2872
+
2873
+ function endThreePointer(event) {
2874
+ if (!state.three.drag) return;
2875
+ var picked = !state.three.drag.moved || state.three.drag.type === "node"
2876
+ ? (pickThreeNode(event) || state.three.drag.picked)
2877
+ : null;
2878
+ state.three.drag = null;
2879
+ if (picked) {
2880
+ state.selected = { kind: "entity", id: picked.id };
2881
+ render();
2882
+ }
2883
+ }
2884
+
2885
+ function leaveThreePointer() {
2886
+ state.three.hoverNode = null;
2887
+ state.three.drag = null;
2888
+ if (els.tooltip) els.tooltip.classList.remove("visible");
2889
+ }
2890
+
2891
+ function handleThreeWheel(event) {
2892
+ if (activeRenderMode() !== "3d") return;
2893
+ event.preventDefault();
2894
+ zoomThreeGraph(event.deltaY > 0 ? 0.88 : 1.14);
2895
+ renderThreeFrame();
2896
+ }
2897
+
2898
+ function handleThreeDoubleClick(event) {
2899
+ var picked = pickThreeNode(event);
2900
+ if (!picked) return;
2901
+ state.selected = { kind: "entity", id: picked.id };
2902
+ render();
2903
+ }
2904
+
2905
+ function pickThreeNode(event) {
2906
+ if (!state.three.raycaster || !state.three.camera || !state.three.pointer) return null;
2907
+ setThreePointerFromEvent(event);
2908
+ state.three.raycaster.setFromCamera(state.three.pointer, state.three.camera);
2909
+ var meshes = Array.from(state.three.nodeById.values()).map(function (entry) { return entry.mesh; });
2910
+ var hits = state.three.raycaster.intersectObjects(meshes, false);
2911
+ return hits.length && hits[0].object.userData ? hits[0].object.userData.node : null;
2912
+ }
2913
+
2914
+ function createThreeDragPlane(mesh) {
2915
+ if (!mesh || !state.three.camera || !state.three.THREE) return null;
2916
+ var THREE = state.three.THREE;
2917
+ state.three.root.updateMatrixWorld(true);
2918
+ state.three.camera.updateMatrixWorld(true);
2919
+ var normal = new THREE.Vector3();
2920
+ state.three.camera.getWorldDirection(normal);
2921
+ var point = new THREE.Vector3();
2922
+ mesh.getWorldPosition(point);
2923
+ return new THREE.Plane().setFromNormalAndCoplanarPoint(normal, point);
2924
+ }
2925
+
2926
+ function moveThreeDraggedNode(event) {
2927
+ var drag = state.three.drag;
2928
+ if (!drag || !drag.entry || !drag.entry.mesh || !drag.plane || !state.three.raycaster) return;
2929
+ var THREE = state.three.THREE;
2930
+ setThreePointerFromEvent(event);
2931
+ state.three.raycaster.setFromCamera(state.three.pointer, state.three.camera);
2932
+ var hit = new THREE.Vector3();
2933
+ if (!state.three.raycaster.ray.intersectPlane(drag.plane, hit)) return;
2934
+ var local = hit.clone();
2935
+ state.three.root.worldToLocal(local);
2936
+ drag.entry.mesh.position.copy(local);
2937
+ drag.entry.vx = 0;
2938
+ drag.entry.vy = 0;
2939
+ drag.entry.vz = 0;
2940
+ state.three.physicsTick = 0;
2941
+ state.three.physicsIdle = 0;
2942
+ updateThreeEdges();
2943
+ }
2944
+
2945
+ function updateThreeEdges() {
2946
+ if (!state.three.edgeRefs || !state.three.edgeRefs.length) return;
2947
+ state.three.edgeRefs.forEach(function (line) {
2948
+ var from = state.three.nodeById.get(line.userData.from);
2949
+ var to = state.three.nodeById.get(line.userData.to);
2950
+ if (!from || !to || !line.geometry) return;
2951
+ var attr = line.geometry.getAttribute("position");
2952
+ if (!attr) return;
2953
+ attr.setXYZ(0, from.mesh.position.x, from.mesh.position.y, from.mesh.position.z);
2954
+ attr.setXYZ(1, to.mesh.position.x, to.mesh.position.y, to.mesh.position.z);
2955
+ attr.needsUpdate = true;
2956
+ line.geometry.computeBoundingSphere();
2957
+ });
2958
+ }
2959
+
2960
+ function setThreePointerFromEvent(event) {
2961
+ var rect = els.threeGraph.getBoundingClientRect();
2962
+ state.three.pointer.x = ((event.clientX - rect.left) / Math.max(rect.width, 1)) * 2 - 1;
2963
+ state.three.pointer.y = -(((event.clientY - rect.top) / Math.max(rect.height, 1)) * 2 - 1);
2964
+ }
2965
+
2966
+ function updateThreeTooltip(event) {
2967
+ if (!els.tooltip) return;
2968
+ var node = state.three.hoverNode;
2969
+ if (!node || state.three.drag) {
2970
+ els.tooltip.classList.remove("visible");
2971
+ return;
2972
+ }
2973
+ var entity = node.entity;
2974
+ var relationCount = state.sim.edges.filter(function (edge) { return edge.from === node.id || edge.to === node.id; }).length;
2975
+ var color = nodeThemeColor(entity);
2976
+ els.tooltip.innerHTML = [
2977
+ "<div class=\"tt-name\"></div>",
2978
+ "<div class=\"tt-type\"></div>",
2979
+ "<div class=\"tt-summary\"></div>",
2980
+ "<div class=\"tt-conns\"></div>"
2981
+ ].join("");
2982
+ els.tooltip.querySelector(".tt-name").textContent = displayName(entity);
2983
+ els.tooltip.querySelector(".tt-type").textContent = nodeKindLabel(entity);
2984
+ els.tooltip.querySelector(".tt-type").style.color = color;
2985
+ els.tooltip.querySelector(".tt-summary").textContent = shortName(entity.summary || entity.id, 150);
2986
+ els.tooltip.querySelector(".tt-conns").textContent = relationCount + " relation" + (relationCount === 1 ? "" : "s");
2987
+ var rect = els.threeGraph.getBoundingClientRect();
2988
+ els.tooltip.style.left = (event.clientX - rect.left + 14) + "px";
2989
+ els.tooltip.style.top = (event.clientY - rect.top + 14) + "px";
2990
+ els.tooltip.classList.add("visible");
2991
+ }
2992
+
2993
+ function hashString(value) {
2994
+ var text = String(value || "");
2995
+ var hash = 0;
2996
+ for (var i = 0; i < text.length; i++) {
2997
+ hash = ((hash << 5) - hash + text.charCodeAt(i)) | 0;
2998
+ }
2999
+ return Math.abs(hash);
3000
+ }
3001
+
2320
3002
  function focusedCanvasNodeId() {
2321
3003
  if (state.sim.hoverNode) return state.sim.hoverNode.id;
2322
3004
  if (state.selected && state.selected.kind === "entity") return state.selected.id;
package/viewer/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Kage Memory Terminal</title>
7
- <link rel="stylesheet" href="./styles.css?v=15">
7
+ <link rel="stylesheet" href="./styles.css?v=17">
8
8
  </head>
9
9
  <body>
10
10
  <header class="app-header">
@@ -29,14 +29,15 @@
29
29
  <p id="graphSubhead">Combined repo memory and source graph.</p>
30
30
  </div>
31
31
  <div class="graph-actions" aria-label="Graph view controls">
32
- <span class="interaction-hint">drag canvas / wheel zoom / click node</span>
32
+ <span id="interactionHint" class="interaction-hint">drag canvas / wheel zoom / click node</span>
33
33
  <button id="zoomOut" type="button">-</button>
34
34
  <button id="fitView" type="button">Fit</button>
35
35
  <button id="zoomIn" type="button">+</button>
36
36
  </div>
37
37
  </div>
38
- <div class="graph-canvas-wrap">
38
+ <div id="graphCanvasWrap" class="graph-canvas-wrap">
39
39
  <canvas id="graphCanvas" aria-label="Interactive Kage memory and code graph"></canvas>
40
+ <div id="threeGraph" class="three-graph" aria-label="Interactive 3D Kage memory and code graph"></div>
40
41
  <div id="graphTooltip" class="graph-tooltip" role="status"></div>
41
42
  </div>
42
43
  <svg id="graphSvg" class="fallback-graph" viewBox="0 0 1000 660" role="img" aria-labelledby="graphTitle graphDescription">
@@ -74,6 +75,13 @@
74
75
  <option value="code">Code</option>
75
76
  </select>
76
77
  </label>
78
+ <label>
79
+ <span>Graph Mode</span>
80
+ <select id="renderMode">
81
+ <option value="2d" selected>2D Canvas</option>
82
+ <option value="3d">3D Space</option>
83
+ </select>
84
+ </label>
77
85
  <label>
78
86
  <span>Node Type</span>
79
87
  <select id="typeFilter">
@@ -149,6 +157,6 @@
149
157
  </section>
150
158
  </main>
151
159
 
152
- <script src="./app.js?v=15"></script>
160
+ <script src="./app.js?v=17"></script>
153
161
  </body>
154
162
  </html>
package/viewer/styles.css CHANGED
@@ -337,16 +337,49 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
337
337
  #020503;
338
338
  }
339
339
 
340
- #graphCanvas {
340
+ #graphCanvas, .three-graph {
341
341
  width: 100%;
342
342
  height: 100%;
343
343
  min-height: 600px;
344
344
  display: block;
345
+ }
346
+
347
+ #graphCanvas {
345
348
  cursor: grab;
346
349
  }
347
350
 
348
351
  #graphCanvas:active { cursor: grabbing; }
349
352
 
353
+ .three-graph {
354
+ position: absolute;
355
+ inset: 0;
356
+ display: none;
357
+ cursor: grab;
358
+ background:
359
+ linear-gradient(rgba(65, 255, 143, 0.035) 1px, transparent 1px),
360
+ linear-gradient(90deg, rgba(65, 255, 143, 0.035) 1px, transparent 1px),
361
+ radial-gradient(circle at 52% 46%, rgba(106, 215, 255, 0.10), transparent 46%),
362
+ #020503;
363
+ background-size: 34px 34px, 34px 34px, 100% 100%, 100% 100%;
364
+ }
365
+ .three-graph canvas {
366
+ display: block;
367
+ width: 100% !important;
368
+ height: 100% !important;
369
+ }
370
+ .three-graph:active { cursor: grabbing; }
371
+ .three-status {
372
+ position: absolute;
373
+ inset: 0;
374
+ display: grid;
375
+ place-items: center;
376
+ color: var(--terminal-dim);
377
+ font-weight: 780;
378
+ }
379
+ .graph-canvas-wrap.mode-3d #graphCanvas { display: none; }
380
+ .graph-canvas-wrap.mode-3d .three-graph { display: block; }
381
+ .graph-canvas-wrap.mode-2d .three-graph { display: none; }
382
+
350
383
  #graphSvg {
351
384
  width: 100%;
352
385
  height: 100%;
@@ -693,7 +726,7 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
693
726
  .control-panel .panel-heading, .metrics-grid, .legend { grid-column: 1 / -1; }
694
727
  .control-panel label, .control-panel button { margin-top: 0; }
695
728
  .graph-panel { min-height: 620px; }
696
- #graphCanvas, .graph-canvas-wrap, #graphSvg { min-height: 560px; }
729
+ #graphCanvas, .three-graph, .graph-canvas-wrap, #graphSvg { min-height: 560px; }
697
730
  .details-panel {
698
731
  height: auto;
699
732
  max-height: 70vh;
@@ -710,5 +743,5 @@ input:focus, select:focus, button:focus, .file-picker:focus-within {
710
743
  .graph-toolbar { align-items: flex-start; flex-direction: column; }
711
744
  .graph-actions { width: 100%; }
712
745
  .interaction-hint { flex: 1; white-space: normal; }
713
- #graphCanvas, .graph-canvas-wrap, #graphSvg { min-height: 450px; }
746
+ #graphCanvas, .three-graph, .graph-canvas-wrap, #graphSvg { min-height: 450px; }
714
747
  }