@kage-core/kage-graph-mcp 1.1.0 → 1.1.1

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/viewer/app.js CHANGED
@@ -15,6 +15,21 @@
15
15
  pendingPackets: [],
16
16
  reviewText: "",
17
17
  viewBox: { x: 0, y: 0, width: 1000, height: 660 },
18
+ lastVisibleSignature: "",
19
+ sim: {
20
+ nodes: [],
21
+ edges: [],
22
+ nodeById: new Map(),
23
+ panX: 0,
24
+ panY: 0,
25
+ zoom: 1,
26
+ dragNode: null,
27
+ panning: null,
28
+ hoverNode: null,
29
+ raf: null,
30
+ running: false,
31
+ lastSignature: ""
32
+ },
18
33
  pan: null
19
34
  };
20
35
 
@@ -47,10 +62,15 @@
47
62
  viewMode: document.getElementById("viewMode"),
48
63
  typeFilter: document.getElementById("typeFilter"),
49
64
  relationFilter: document.getElementById("relationFilter"),
65
+ scopeFilter: document.getElementById("scopeFilter"),
66
+ maxNodes: document.getElementById("maxNodes"),
67
+ showDependencies: document.getElementById("showDependencies"),
50
68
  resetView: document.getElementById("resetView"),
51
69
  zoomOut: document.getElementById("zoomOut"),
52
70
  zoomIn: document.getElementById("zoomIn"),
53
71
  fitView: document.getElementById("fitView"),
72
+ canvas: document.getElementById("graphCanvas"),
73
+ tooltip: document.getElementById("graphTooltip"),
54
74
  svg: document.getElementById("graphSvg"),
55
75
  nodeLayer: document.getElementById("nodeLayer"),
56
76
  edgeLayer: document.getElementById("edgeLayer"),
@@ -72,21 +92,38 @@
72
92
  els.viewMode.addEventListener("change", render);
73
93
  els.typeFilter.addEventListener("change", render);
74
94
  els.relationFilter.addEventListener("change", render);
75
- els.zoomOut.addEventListener("click", function () { zoomView(1.18); });
76
- els.zoomIn.addEventListener("click", function () { zoomView(0.84); });
77
- els.fitView.addEventListener("click", function () { fitView(); renderSvg(); });
95
+ els.scopeFilter.addEventListener("change", render);
96
+ els.maxNodes.addEventListener("change", render);
97
+ els.showDependencies.addEventListener("change", render);
98
+ els.zoomOut.addEventListener("click", function () { zoomCanvas(0.82); });
99
+ els.zoomIn.addEventListener("click", function () { zoomCanvas(1.22); });
100
+ els.fitView.addEventListener("click", function () { fitCanvas(); drawCanvasGraph(); });
101
+ els.canvas.addEventListener("mousedown", startCanvasPointer);
102
+ els.canvas.addEventListener("mousemove", moveCanvasPointer);
103
+ els.canvas.addEventListener("mouseup", endCanvasPointer);
104
+ els.canvas.addEventListener("mouseleave", leaveCanvasPointer);
105
+ els.canvas.addEventListener("wheel", handleCanvasWheel, { passive: false });
106
+ els.canvas.addEventListener("dblclick", handleCanvasDoubleClick);
78
107
  els.svg.addEventListener("mousedown", startPan);
79
108
  els.svg.addEventListener("click", handleSvgClick);
80
109
  els.svg.addEventListener("wheel", handleWheelZoom, { passive: false });
81
110
  window.addEventListener("mousemove", continuePan);
82
111
  window.addEventListener("mouseup", endPan);
112
+ window.addEventListener("resize", function () {
113
+ resizeCanvas();
114
+ fitCanvas();
115
+ drawCanvasGraph();
116
+ });
83
117
  els.resetView.addEventListener("click", function () {
84
118
  els.searchInput.value = "";
85
119
  els.viewMode.value = "combined";
86
120
  els.typeFilter.value = "";
87
121
  els.relationFilter.value = "";
122
+ els.scopeFilter.value = "signal";
123
+ els.maxNodes.value = "90";
124
+ els.showDependencies.checked = false;
88
125
  state.selected = null;
89
- fitView();
126
+ state.lastVisibleSignature = "";
90
127
  render();
91
128
  });
92
129
  loadFromUrlParams();
@@ -134,10 +171,9 @@
134
171
  return [episode.id, episode];
135
172
  }));
136
173
  state.selected = null;
174
+ state.lastVisibleSignature = "";
137
175
 
138
176
  populateFilters();
139
- layoutGraph();
140
- fitView();
141
177
  els.emptyState.classList.add("hidden");
142
178
  els.graphSummary.textContent = fileName + " loaded: " + entities.length + " nodes, " + edges.length + " relations.";
143
179
  render();
@@ -381,27 +417,32 @@
381
417
  });
382
418
  }
383
419
 
384
- function layoutGraph() {
420
+ function layoutGraph(visibleIds) {
385
421
  state.positions = new Map();
422
+ var candidates = state.entities.filter(function (entity) {
423
+ return !visibleIds || visibleIds.has(entity.id);
424
+ });
386
425
  var lanes = [
387
- { name: "memory", x: 190, y: 86, step: 62, match: function (entity) { return entity.graph_kind === "memory"; } },
388
- { name: "files", x: 480, y: 78, step: 60, match: function (entity) { return entity.graph_kind === "code" && entity.type === "file"; } },
389
- { name: "flow", x: 700, y: 88, step: 58, match: function (entity) { return entity.graph_kind === "code" && ["symbol", "route", "test"].indexOf(entity.type) !== -1; } },
390
- { name: "external", x: 895, y: 110, step: 64, match: function (entity) { return entity.graph_kind === "code" && ["external", "script"].indexOf(entity.type) !== -1; } },
391
- { name: "other", x: 700, y: 570, step: 58, match: function (entity) { return ["memory", "code"].indexOf(entity.graph_kind) === -1 || (entity.graph_kind === "code" && ["file", "symbol", "route", "test", "external", "script"].indexOf(entity.type) === -1); } }
426
+ { name: "memory", x: 170, y: 96, step: 82, columns: 10, match: function (entity) { return entity.graph_kind === "memory"; } },
427
+ { name: "files", x: 470, y: 86, step: 78, columns: 10, match: function (entity) { return entity.graph_kind === "code" && entity.type === "file"; } },
428
+ { name: "flow", x: 760, y: 96, step: 76, columns: 10, match: function (entity) { return entity.graph_kind === "code" && ["symbol", "route", "test"].indexOf(entity.type) !== -1; } },
429
+ { name: "external", x: 1040, y: 122, step: 78, columns: 8, match: function (entity) { return isDependencyEntity(entity) || (entity.graph_kind === "code" && ["external", "script"].indexOf(entity.type) !== -1); } },
430
+ { name: "other", x: 760, y: 560, step: 78, columns: 8, match: function (entity) { return ["memory", "code"].indexOf(entity.graph_kind) === -1 || (entity.graph_kind === "code" && ["file", "symbol", "route", "test", "external", "script"].indexOf(entity.type) === -1); } }
392
431
  ];
432
+ var placed = new Set();
393
433
  lanes.forEach(function (lane) {
394
- var bucket = state.entities.filter(function (entity) {
395
- return lane.match(entity);
434
+ var bucket = candidates.filter(function (entity) {
435
+ return !placed.has(entity.id) && lane.match(entity);
396
436
  }).sort(function (a, b) {
397
- return degreeOf(b.id) - degreeOf(a.id) || displayName(a).localeCompare(displayName(b));
437
+ return entityImportance(b) - entityImportance(a) || displayName(a).localeCompare(displayName(b));
398
438
  });
399
439
  if (!bucket.length) return;
400
440
  bucket.forEach(function (entity, index) {
401
- var column = Math.floor(index / 8);
402
- var row = index % 8;
403
- var xOffset = column * 168;
404
- var yJitter = column % 2 ? 18 : 0;
441
+ placed.add(entity.id);
442
+ var column = Math.floor(index / lane.columns);
443
+ var row = index % lane.columns;
444
+ var xOffset = column * 228;
445
+ var yJitter = column % 2 ? 26 : 0;
405
446
  state.positions.set(entity.id, {
406
447
  x: lane.x + xOffset,
407
448
  y: lane.y + row * lane.step + yJitter
@@ -447,10 +488,23 @@
447
488
  matchedEdgeIds = new Set(state.edges.filter(function (edge) { return mode === "combined" || edge.graph_kind === mode; }).map(function (edge) { return edge.id; }));
448
489
  }
449
490
 
450
- state.visibleEntityIds = matchedEntityIds;
451
- state.visibleEdgeIds = matchedEdgeIds;
491
+ var visible = refineVisibleGraph(matchedEntityIds, matchedEdgeIds, {
492
+ query: query,
493
+ type: type,
494
+ relation: relation,
495
+ scope: els.scopeFilter.value,
496
+ maxNodes: Number(els.maxNodes.value || 90),
497
+ showDependencies: els.showDependencies.checked
498
+ });
452
499
 
453
- renderSvg();
500
+ state.visibleEntityIds = visible.entities;
501
+ state.visibleEdgeIds = visible.edges;
502
+ layoutGraph(state.visibleEntityIds);
503
+ var nextSignature = visibleSignature(state.visibleEntityIds, state.visibleEdgeIds);
504
+ var graphChanged = nextSignature !== state.lastVisibleSignature;
505
+ state.lastVisibleSignature = nextSignature;
506
+
507
+ renderCanvasGraph(graphChanged);
454
508
  renderLists();
455
509
  renderDetails();
456
510
  renderMetrics();
@@ -458,6 +512,413 @@
458
512
  renderProof();
459
513
  }
460
514
 
515
+ function refineVisibleGraph(entityIds, edgeIds, options) {
516
+ var entities = new Set(entityIds);
517
+ var edges = new Set(edgeIds);
518
+
519
+ if (!options.showDependencies) {
520
+ Array.from(entities).forEach(function (id) {
521
+ var entity = state.entityById.get(id);
522
+ if (!entity) return;
523
+ if (isDependencyEntity(entity) && !(options.query && searchableText(entity).indexOf(options.query) !== -1)) {
524
+ entities.delete(id);
525
+ }
526
+ });
527
+ edges = edgesWithVisibleEndpoints(edges, entities);
528
+ }
529
+
530
+ if (options.scope === "focus" && state.selected) {
531
+ var focused = focusSelection(entities, edges, state.selected);
532
+ entities = focused.entities;
533
+ edges = focused.edges;
534
+ } else if (options.scope === "signal" && entities.size > options.maxNodes) {
535
+ var ranked = Array.from(entities).sort(function (a, b) {
536
+ return entityImportance(state.entityById.get(b)) - entityImportance(state.entityById.get(a)) ||
537
+ displayName(state.entityById.get(a)).localeCompare(displayName(state.entityById.get(b)));
538
+ });
539
+ entities = new Set(ranked.slice(0, options.maxNodes));
540
+ if (state.selected && state.selected.kind === "entity") entities.add(state.selected.id);
541
+ edges = edgesWithVisibleEndpoints(edges, entities);
542
+ }
543
+
544
+ if (state.selected && state.selected.kind === "edge" && edges.has(state.selected.id)) {
545
+ var selectedEdge = state.edges.find(function (edge) { return edge.id === state.selected.id; });
546
+ if (selectedEdge) {
547
+ entities.add(selectedEdge.from);
548
+ entities.add(selectedEdge.to);
549
+ }
550
+ }
551
+
552
+ return { entities: entities, edges: edgesWithVisibleEndpoints(edges, entities) };
553
+ }
554
+
555
+ function focusSelection(entityIds, edgeIds, selection) {
556
+ var entities = new Set();
557
+ var edges = new Set();
558
+ if (selection.kind === "entity") {
559
+ entities.add(selection.id);
560
+ state.edges.forEach(function (edge) {
561
+ if (!edgeIds.has(edge.id)) return;
562
+ if (edge.from === selection.id || edge.to === selection.id) {
563
+ edges.add(edge.id);
564
+ entities.add(edge.from);
565
+ entities.add(edge.to);
566
+ }
567
+ });
568
+ } else {
569
+ var selectedEdge = state.edges.find(function (edge) { return edge.id === selection.id; });
570
+ if (selectedEdge && edgeIds.has(selectedEdge.id)) {
571
+ edges.add(selectedEdge.id);
572
+ entities.add(selectedEdge.from);
573
+ entities.add(selectedEdge.to);
574
+ }
575
+ }
576
+ entityIds.forEach(function (id) {
577
+ if (entities.has(id)) return;
578
+ var entity = state.entityById.get(id);
579
+ if (entity && entity.graph_kind === "memory" && entities.size < 16) entities.add(id);
580
+ });
581
+ return { entities: entities.size ? entities : entityIds, edges: edges.size ? edges : edgeIds };
582
+ }
583
+
584
+ function edgesWithVisibleEndpoints(edgeIds, entityIds) {
585
+ return new Set(Array.from(edgeIds).filter(function (id) {
586
+ var edge = state.edges.find(function (candidate) { return candidate.id === id; });
587
+ return edge && entityIds.has(edge.from) && entityIds.has(edge.to);
588
+ }));
589
+ }
590
+
591
+ function renderCanvasGraph(graphChanged) {
592
+ resizeCanvas();
593
+ syncSimulationGraph(graphChanged);
594
+ if (graphChanged) fitCanvas();
595
+ startSimulation();
596
+ drawCanvasGraph();
597
+ }
598
+
599
+ function resizeCanvas() {
600
+ var canvas = els.canvas;
601
+ var ctx = canvas.getContext("2d");
602
+ var rect = canvas.parentElement.getBoundingClientRect();
603
+ var dpr = Math.max(1, window.devicePixelRatio || 1);
604
+ var width = Math.max(320, rect.width);
605
+ var height = Math.max(360, rect.height);
606
+ if (canvas.width !== Math.round(width * dpr) || canvas.height !== Math.round(height * dpr)) {
607
+ canvas.width = Math.round(width * dpr);
608
+ canvas.height = Math.round(height * dpr);
609
+ canvas.style.width = width + "px";
610
+ canvas.style.height = height + "px";
611
+ }
612
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
613
+ }
614
+
615
+ function syncSimulationGraph(forceReset) {
616
+ var existing = state.sim.nodeById;
617
+ var visibleEntities = state.entities.filter(function (entity) { return state.visibleEntityIds.has(entity.id); });
618
+ var nextNodes = visibleEntities.map(function (entity, index) {
619
+ var current = existing.get(entity.id);
620
+ var lanePos = state.positions.get(entity.id);
621
+ var seed = seededPosition(index, visibleEntities.length);
622
+ var degree = degreeOf(entity.id);
623
+ if (!current || forceReset) {
624
+ current = {
625
+ id: entity.id,
626
+ entity: entity,
627
+ x: lanePos ? lanePos.x : seed.x,
628
+ y: lanePos ? lanePos.y : seed.y,
629
+ vx: 0,
630
+ vy: 0,
631
+ r: clamp(9 + degree * 1.7, 10, entity.graph_kind === "memory" ? 25 : 22)
632
+ };
633
+ }
634
+ current.entity = entity;
635
+ current.r = clamp(9 + degree * 1.7, 10, entity.graph_kind === "memory" ? 25 : 22);
636
+ return current;
637
+ });
638
+ state.sim.nodes = nextNodes;
639
+ state.sim.nodeById = new Map(nextNodes.map(function (node) { return [node.id, node]; }));
640
+ state.sim.edges = state.edges.filter(function (edge) {
641
+ return state.visibleEdgeIds.has(edge.id) && state.sim.nodeById.has(edge.from) && state.sim.nodeById.has(edge.to);
642
+ });
643
+ }
644
+
645
+ function seededPosition(index, total) {
646
+ var angle = (Math.PI * 2 * index) / Math.max(1, total);
647
+ var radius = 220 + Math.min(180, total * 2);
648
+ return {
649
+ x: Math.cos(angle) * radius + 520,
650
+ y: Math.sin(angle) * radius + 340
651
+ };
652
+ }
653
+
654
+ function startSimulation() {
655
+ if (state.sim.running) return;
656
+ state.sim.running = true;
657
+ state.sim.raf = window.requestAnimationFrame(simulationTick);
658
+ }
659
+
660
+ function simulationTick() {
661
+ if (!state.sim.running) return;
662
+ stepSimulation();
663
+ drawCanvasGraph();
664
+ state.sim.raf = window.requestAnimationFrame(simulationTick);
665
+ }
666
+
667
+ function stepSimulation() {
668
+ var nodes = state.sim.nodes;
669
+ if (!nodes.length) return;
670
+ var nodeMap = state.sim.nodeById;
671
+ var count = nodes.length;
672
+ var repulsion = count > 120 ? 3600 : count > 70 ? 2500 : 1700;
673
+ var attraction = count > 100 ? 0.006 : 0.010;
674
+ var centerGravity = count > 100 ? 0.006 : 0.011;
675
+ var laneGravity = 0.012;
676
+
677
+ nodes.forEach(function (node, index) {
678
+ if (state.sim.dragNode === node) return;
679
+ var fx = 0;
680
+ var fy = 0;
681
+ for (var j = 0; j < nodes.length; j++) {
682
+ if (index === j) continue;
683
+ var other = nodes[j];
684
+ var dx = node.x - other.x;
685
+ var dy = node.y - other.y;
686
+ var dist = Math.max(18, Math.sqrt(dx * dx + dy * dy));
687
+ var force = repulsion / (dist * dist);
688
+ fx += (dx / dist) * force;
689
+ fy += (dy / dist) * force;
690
+ }
691
+ var lane = state.positions.get(node.id);
692
+ if (lane) {
693
+ fx += (lane.x - node.x) * laneGravity;
694
+ fy += (lane.y - node.y) * laneGravity;
695
+ }
696
+ fx += (620 - node.x) * centerGravity * 0.15;
697
+ fy += (360 - node.y) * centerGravity * 0.15;
698
+ node.vx = (node.vx + fx) * 0.82;
699
+ node.vy = (node.vy + fy) * 0.82;
700
+ });
701
+
702
+ state.sim.edges.forEach(function (edge) {
703
+ var from = nodeMap.get(edge.from);
704
+ var to = nodeMap.get(edge.to);
705
+ if (!from || !to) return;
706
+ var dx = to.x - from.x;
707
+ var dy = to.y - from.y;
708
+ var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
709
+ var target = edge.graph_kind === "memory" ? 145 : 120;
710
+ var force = (dist - target) * attraction * (edge.confidence == null ? 1 : clamp(edge.confidence, 0.35, 1.2));
711
+ var fx = (dx / dist) * force;
712
+ var fy = (dy / dist) * force;
713
+ if (state.sim.dragNode !== from) {
714
+ from.vx += fx;
715
+ from.vy += fy;
716
+ }
717
+ if (state.sim.dragNode !== to) {
718
+ to.vx -= fx;
719
+ to.vy -= fy;
720
+ }
721
+ });
722
+
723
+ nodes.forEach(function (node) {
724
+ if (state.sim.dragNode === node) return;
725
+ node.x += clamp(node.vx, -8, 8);
726
+ node.y += clamp(node.vy, -8, 8);
727
+ });
728
+ }
729
+
730
+ function drawCanvasGraph() {
731
+ var canvas = els.canvas;
732
+ var ctx = canvas.getContext("2d");
733
+ var width = canvas.width / Math.max(1, window.devicePixelRatio || 1);
734
+ var height = canvas.height / Math.max(1, window.devicePixelRatio || 1);
735
+ ctx.clearRect(0, 0, width, height);
736
+ drawCanvasGrid(ctx, width, height);
737
+ ctx.save();
738
+ ctx.translate(state.sim.panX, state.sim.panY);
739
+ ctx.scale(state.sim.zoom, state.sim.zoom);
740
+ drawCanvasEdges(ctx);
741
+ drawCanvasNodes(ctx);
742
+ ctx.restore();
743
+
744
+ if (!state.sim.nodes.length) {
745
+ ctx.fillStyle = "#6ea77d";
746
+ ctx.font = "13px ui-monospace, Menlo, monospace";
747
+ ctx.textAlign = "center";
748
+ ctx.fillText("No graph data visible.", width / 2, height / 2);
749
+ }
750
+ }
751
+
752
+ function drawCanvasGrid(ctx, width, height) {
753
+ ctx.save();
754
+ ctx.fillStyle = "#020503";
755
+ ctx.fillRect(0, 0, width, height);
756
+ ctx.strokeStyle = "rgba(65,255,143,0.045)";
757
+ ctx.lineWidth = 1;
758
+ var grid = 28;
759
+ for (var x = 0; x < width; x += grid) {
760
+ ctx.beginPath();
761
+ ctx.moveTo(x, 0);
762
+ ctx.lineTo(x, height);
763
+ ctx.stroke();
764
+ }
765
+ for (var y = 0; y < height; y += grid) {
766
+ ctx.beginPath();
767
+ ctx.moveTo(0, y);
768
+ ctx.lineTo(width, y);
769
+ ctx.stroke();
770
+ }
771
+ ctx.restore();
772
+ }
773
+
774
+ function drawCanvasEdges(ctx) {
775
+ var nodeMap = state.sim.nodeById;
776
+ var focusId = focusedCanvasNodeId();
777
+ var query = normalize(els.searchInput.value);
778
+ var dense = state.sim.nodes.length > 55;
779
+ state.sim.edges.forEach(function (edge) {
780
+ var from = nodeMap.get(edge.from);
781
+ var to = nodeMap.get(edge.to);
782
+ if (!from || !to) return;
783
+ var connected = focusId && (edge.from === focusId || edge.to === focusId);
784
+ var matches = !query || searchableText(edge).indexOf(query) !== -1 || searchableText(from.entity).indexOf(query) !== -1 || searchableText(to.entity).indexOf(query) !== -1;
785
+ var alpha = !matches ? 0.04 : focusId ? (connected ? 0.78 : 0.07) : (dense ? 0.18 : 0.34);
786
+ var color = hexToRgb(palette[from.entity.type] || palette[from.entity.graph_kind] || palette.default);
787
+ var dx = to.x - from.x;
788
+ var dy = to.y - from.y;
789
+ var dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
790
+ var offset = dense ? 10 : 16;
791
+ var cx = (from.x + to.x) / 2 + (-dy / dist) * offset;
792
+ var cy = (from.y + to.y) / 2 + (dx / dist) * offset;
793
+ ctx.beginPath();
794
+ ctx.moveTo(from.x, from.y);
795
+ ctx.quadraticCurveTo(cx, cy, to.x, to.y);
796
+ ctx.strokeStyle = "rgba(" + color.r + "," + color.g + "," + color.b + "," + alpha + ")";
797
+ ctx.lineWidth = connected ? 2.8 : 1.2;
798
+ ctx.stroke();
799
+ if (connected || (!dense && state.sim.zoom > 1.25)) drawArrow(ctx, from, to, cx, cy, color, alpha);
800
+ if (connected && state.sim.zoom > 0.62) drawEdgeLabel(ctx, edge, cx, cy);
801
+ });
802
+ }
803
+
804
+ function drawCanvasNodes(ctx) {
805
+ var focusId = focusedCanvasNodeId();
806
+ var query = normalize(els.searchInput.value);
807
+ var dense = state.sim.nodes.length > 55;
808
+ state.sim.nodes.forEach(function (node) {
809
+ var entity = node.entity;
810
+ var selected = state.selected && state.selected.kind === "entity" && state.selected.id === node.id;
811
+ var hovered = state.sim.hoverNode && state.sim.hoverNode.id === node.id;
812
+ var connected = focusId && (node.id === focusId || state.sim.edges.some(function (edge) {
813
+ return (edge.from === focusId && edge.to === node.id) || (edge.to === focusId && edge.from === node.id);
814
+ }));
815
+ var matches = !query || searchableText(entity).indexOf(query) !== -1;
816
+ var alpha = !matches ? 0.12 : focusId && !connected ? 0.20 : 1;
817
+ var color = palette[entity.type] || palette[entity.graph_kind] || palette.default;
818
+ ctx.save();
819
+ ctx.globalAlpha = alpha;
820
+ if (selected || hovered || (!focusId && !dense)) {
821
+ ctx.shadowColor = color;
822
+ ctx.shadowBlur = selected ? 20 : hovered ? 16 : 5;
823
+ }
824
+ drawNodeShape(ctx, node.x, node.y, node.r, entity);
825
+ var grad = ctx.createRadialGradient(node.x - node.r * 0.3, node.y - node.r * 0.3, 1, node.x, node.y, node.r * 1.35);
826
+ grad.addColorStop(0, brighten(color, 54));
827
+ grad.addColorStop(1, color);
828
+ ctx.fillStyle = grad;
829
+ ctx.fill();
830
+ ctx.restore();
831
+
832
+ if (selected || hovered) {
833
+ ctx.save();
834
+ drawNodeShape(ctx, node.x, node.y, node.r + 4, entity);
835
+ ctx.strokeStyle = color;
836
+ ctx.lineWidth = selected ? 3 : 2;
837
+ ctx.shadowColor = color;
838
+ ctx.shadowBlur = 12;
839
+ ctx.stroke();
840
+ ctx.restore();
841
+ }
842
+
843
+ var shouldLabel = matches && (selected || hovered || (query && matches) || (!dense && state.sim.zoom > 0.75) || (dense && state.sim.zoom > 1.55 && node.r > 13));
844
+ if (shouldLabel) drawNodeLabel(ctx, node, selected || hovered);
845
+ });
846
+ }
847
+
848
+ function drawArrow(ctx, from, to, cx, cy, color, alpha) {
849
+ var angle = Math.atan2(to.y - cy, to.x - cx);
850
+ var tipX = to.x - to.r * Math.cos(angle);
851
+ var tipY = to.y - to.r * Math.sin(angle);
852
+ ctx.beginPath();
853
+ ctx.moveTo(tipX, tipY);
854
+ ctx.lineTo(tipX - 8 * Math.cos(angle - 0.35), tipY - 8 * Math.sin(angle - 0.35));
855
+ ctx.lineTo(tipX - 8 * Math.cos(angle + 0.35), tipY - 8 * Math.sin(angle + 0.35));
856
+ ctx.closePath();
857
+ ctx.fillStyle = "rgba(" + color.r + "," + color.g + "," + color.b + "," + Math.min(0.85, alpha + 0.10) + ")";
858
+ ctx.fill();
859
+ }
860
+
861
+ function drawEdgeLabel(ctx, edge, x, y) {
862
+ var inv = 1 / state.sim.zoom;
863
+ ctx.save();
864
+ ctx.font = "700 " + (10 * inv).toFixed(1) + "px ui-monospace, Menlo, monospace";
865
+ ctx.textAlign = "center";
866
+ ctx.fillStyle = "rgba(215,249,223,0.82)";
867
+ ctx.fillText(shortName(edge.relation || "related", 22), x, y - 5 * inv);
868
+ ctx.restore();
869
+ }
870
+
871
+ function drawNodeLabel(ctx, node, strong) {
872
+ var inv = 1 / state.sim.zoom;
873
+ var label = shortName(displayName(node.entity), strong ? 30 : 20);
874
+ ctx.save();
875
+ ctx.font = (strong ? "800 " : "700 ") + (12 * inv).toFixed(1) + "px ui-monospace, Menlo, monospace";
876
+ var width = ctx.measureText(label).width + 16 * inv;
877
+ var height = 20 * inv;
878
+ var x = node.x - width / 2;
879
+ var y = node.y + node.r + 8 * inv;
880
+ ctx.fillStyle = "rgba(3,6,4,0.88)";
881
+ roundedRect(ctx, x, y, width, height, 4 * inv);
882
+ ctx.fill();
883
+ ctx.strokeStyle = strong ? "rgba(65,255,143,0.55)" : "rgba(65,255,143,0.18)";
884
+ ctx.stroke();
885
+ ctx.fillStyle = strong ? "#d7f9df" : "#9be7c0";
886
+ ctx.textAlign = "center";
887
+ ctx.fillText(label, node.x, y + 14 * inv);
888
+ ctx.restore();
889
+ }
890
+
891
+ function drawNodeShape(ctx, x, y, r, entity) {
892
+ var type = entity.type || "";
893
+ if (type === "file" || type === "repo" || type === "command" || type === "script") {
894
+ roundedRect(ctx, x - r * 1.25, y - r * 0.75, r * 2.5, r * 1.5, 4);
895
+ return;
896
+ }
897
+ if (type === "decision" || type === "bug_fix" || type === "test") {
898
+ ctx.beginPath();
899
+ ctx.moveTo(x, y - r);
900
+ ctx.lineTo(x + r, y);
901
+ ctx.lineTo(x, y + r);
902
+ ctx.lineTo(x - r, y);
903
+ ctx.closePath();
904
+ return;
905
+ }
906
+ if (type === "route" || type === "external" || isDependencyEntity(entity)) {
907
+ ctx.beginPath();
908
+ for (var i = 0; i < 6; i++) {
909
+ var angle = Math.PI / 3 * i - Math.PI / 2;
910
+ var px = x + r * Math.cos(angle);
911
+ var py = y + r * Math.sin(angle);
912
+ if (i === 0) ctx.moveTo(px, py);
913
+ else ctx.lineTo(px, py);
914
+ }
915
+ ctx.closePath();
916
+ return;
917
+ }
918
+ ctx.beginPath();
919
+ ctx.arc(x, y, r, 0, Math.PI * 2);
920
+ }
921
+
461
922
  function renderSvg() {
462
923
  els.edgeLayer.textContent = "";
463
924
  els.nodeLayer.textContent = "";
@@ -467,17 +928,19 @@
467
928
  var selectedEdgeId = state.selected && state.selected.kind === "edge" ? state.selected.id : null;
468
929
  var connectedIds = connectedEntityIds(selectedEntityId, selectedEdgeId);
469
930
 
931
+ renderLaneLabels();
932
+
470
933
  state.edges.forEach(function (edge) {
934
+ if (!state.visibleEdgeIds.has(edge.id)) return;
471
935
  var from = state.positions.get(edge.from);
472
936
  var to = state.positions.get(edge.to);
473
937
  if (!from || !to) return;
474
938
 
475
939
  var group = svgEl("g");
476
- var visible = state.visibleEdgeIds.has(edge.id);
477
940
  var connected = selectedEdgeId === edge.id || connectedIds.edges.has(edge.id);
478
941
  var line = svgEl("path", {
479
942
  d: edgePath(from, to),
480
- class: classNames("edge-line", "review-" + reviewStatus(edge).replace(/\s+/g, "-"), !visible && "filtered", connected && "connected", selectedEdgeId === edge.id && "selected")
943
+ class: classNames("edge-line", "review-" + reviewStatus(edge).replace(/\s+/g, "-"), connected && "connected", selectedEdgeId === edge.id && "selected")
481
944
  });
482
945
  var hit = svgEl("path", {
483
946
  d: edgePath(from, to),
@@ -496,14 +959,14 @@
496
959
  });
497
960
 
498
961
  state.entities.forEach(function (entity) {
962
+ if (!state.visibleEntityIds.has(entity.id)) return;
499
963
  var pos = state.positions.get(entity.id);
500
964
  if (!pos) return;
501
965
 
502
- var visible = state.visibleEntityIds.has(entity.id);
503
966
  var selected = selectedEntityId === entity.id;
504
967
  var connected = connectedIds.entities.has(entity.id);
505
968
  var group = svgEl("g", {
506
- class: classNames("node", "graph-" + (entity.graph_kind || "unknown"), !visible && "filtered", selected && "selected", connected && "connected"),
969
+ class: classNames("node", "graph-" + (entity.graph_kind || "unknown"), "type-" + safeCssName(entity.type || "unknown"), isDependencyEntity(entity) && "dependency-node", selected && "selected", connected && "connected"),
507
970
  transform: "translate(" + pos.x + " " + pos.y + ")"
508
971
  });
509
972
  var dims = nodeDimensions(entity);
@@ -551,6 +1014,21 @@
551
1014
  });
552
1015
  }
553
1016
 
1017
+ function renderLaneLabels() {
1018
+ var visible = state.entities.filter(function (entity) { return state.visibleEntityIds.has(entity.id); });
1019
+ [
1020
+ ["MEMORY", 92, visible.some(function (entity) { return entity.graph_kind === "memory"; })],
1021
+ ["FILES", 392, visible.some(function (entity) { return entity.graph_kind === "code" && entity.type === "file"; })],
1022
+ ["FLOW", 682, visible.some(function (entity) { return entity.graph_kind === "code" && ["symbol", "route", "test"].indexOf(entity.type) !== -1; })],
1023
+ ["DEPS", 962, visible.some(function (entity) { return isDependencyEntity(entity); })]
1024
+ ].forEach(function (lane) {
1025
+ if (!lane[2]) return;
1026
+ var label = svgEl("text", { x: lane[1], y: 34, class: "lane-label" });
1027
+ label.textContent = lane[0];
1028
+ els.edgeLayer.appendChild(label);
1029
+ });
1030
+ }
1031
+
554
1032
  function renderLists() {
555
1033
  var visibleEntities = state.entities.filter(function (entity) {
556
1034
  return state.visibleEntityIds.has(entity.id);
@@ -688,6 +1166,9 @@
688
1166
  if (!els.metricsSummary) return;
689
1167
  var visibleEdges = state.edges.filter(function (edge) { return state.visibleEdgeIds.has(edge.id); });
690
1168
  var visibleEntities = state.entities.filter(function (entity) { return state.visibleEntityIds.has(entity.id); });
1169
+ var hiddenDependencies = state.entities.filter(function (entity) {
1170
+ return isDependencyEntity(entity) && !state.visibleEntityIds.has(entity.id);
1171
+ }).length;
691
1172
  var evidenceEdges = visibleEdges.filter(function (edge) { return Array.isArray(edge.evidence) && edge.evidence.length > 0; }).length;
692
1173
  var official = state.metrics;
693
1174
  var metrics = official ? [
@@ -716,7 +1197,8 @@
716
1197
  });
717
1198
  renderStatusStrip(visibleEntities, visibleEdges, official);
718
1199
  els.workspaceMode.textContent = (els.viewMode.value || "combined").replace(/^./, function (letter) { return letter.toUpperCase(); });
719
- els.graphSubhead.textContent = visibleEntities.length + " visible nodes and " + visibleEdges.length + " visible relations.";
1200
+ els.graphSubhead.textContent = visibleEntities.length + " visible nodes and " + visibleEdges.length + " visible relations" +
1201
+ (hiddenDependencies && !els.showDependencies.checked ? " (" + hiddenDependencies + " dependency/noise nodes hidden)." : ".");
720
1202
  }
721
1203
 
722
1204
  function renderReviewQueue() {
@@ -839,6 +1321,212 @@
839
1321
  }, 0);
840
1322
  }
841
1323
 
1324
+ function entityImportance(entity) {
1325
+ if (!entity) return -1000;
1326
+ var score = degreeOf(entity.id) * 8;
1327
+ if (entity.graph_kind === "memory") score += 80;
1328
+ if (entity.graph_kind === "code" && entity.type === "file") score += 42;
1329
+ if (["route", "test"].indexOf(entity.type) !== -1) score += 46;
1330
+ if (entity.type === "symbol") score += 34;
1331
+ if (entity.type === "script") score += 24;
1332
+ if (["runbook", "bug_fix", "decision", "workflow", "gotcha", "convention"].indexOf(entity.type) !== -1) score += 40;
1333
+ if (isDependencyEntity(entity)) score -= 90;
1334
+ if (isGeneratedEntity(entity)) score -= 48;
1335
+ return score;
1336
+ }
1337
+
1338
+ function isDependencyEntity(entity) {
1339
+ var text = normalize([entity.id, entity.name, entity.path, entity.summary].filter(Boolean).join(" "));
1340
+ return entity.type === "external" ||
1341
+ text.indexOf("node_modules/") !== -1 ||
1342
+ text.indexOf("/node_modules") !== -1 ||
1343
+ text.indexOf("package-lock.json") !== -1 ||
1344
+ text.indexOf("pnpm-lock.yaml") !== -1 ||
1345
+ text.indexOf("yarn.lock") !== -1 ||
1346
+ text.indexOf("bun.lockb") !== -1 ||
1347
+ text.indexOf("dist/") !== -1 ||
1348
+ text.indexOf("build/") !== -1;
1349
+ }
1350
+
1351
+ function isGeneratedEntity(entity) {
1352
+ var text = normalize([entity.id, entity.name, entity.path, entity.summary].filter(Boolean).join(" "));
1353
+ return text.indexOf(".agent_memory/indexes/") !== -1 ||
1354
+ text.indexOf(".agent_memory/code_graph/") !== -1 ||
1355
+ text.indexOf(".agent_memory/graph/") !== -1 ||
1356
+ text.indexOf(".min.js") !== -1 ||
1357
+ text.indexOf("coverage/") !== -1;
1358
+ }
1359
+
1360
+ function startCanvasPointer(event) {
1361
+ if (event.button !== 0) return;
1362
+ var world = canvasToWorld(event);
1363
+ var node = findCanvasNode(world.x, world.y);
1364
+ if (node) {
1365
+ state.sim.dragNode = node;
1366
+ state.sim.panning = null;
1367
+ } else {
1368
+ state.sim.panning = {
1369
+ x: event.clientX,
1370
+ y: event.clientY,
1371
+ panX: state.sim.panX,
1372
+ panY: state.sim.panY,
1373
+ moved: false
1374
+ };
1375
+ }
1376
+ }
1377
+
1378
+ function moveCanvasPointer(event) {
1379
+ var world = canvasToWorld(event);
1380
+ if (state.sim.dragNode) {
1381
+ state.sim.dragNode.x = world.x;
1382
+ state.sim.dragNode.y = world.y;
1383
+ state.sim.dragNode.vx = 0;
1384
+ state.sim.dragNode.vy = 0;
1385
+ } else if (state.sim.panning) {
1386
+ var dx = event.clientX - state.sim.panning.x;
1387
+ var dy = event.clientY - state.sim.panning.y;
1388
+ if (Math.abs(dx) > 2 || Math.abs(dy) > 2) state.sim.panning.moved = true;
1389
+ state.sim.panX = state.sim.panning.panX + dx;
1390
+ state.sim.panY = state.sim.panning.panY + dy;
1391
+ }
1392
+ state.sim.hoverNode = findCanvasNode(world.x, world.y);
1393
+ updateCanvasTooltip(event);
1394
+ drawCanvasGraph();
1395
+ }
1396
+
1397
+ function endCanvasPointer() {
1398
+ if (state.sim.dragNode) {
1399
+ state.selected = { kind: "entity", id: state.sim.dragNode.id };
1400
+ state.sim.dragNode = null;
1401
+ render();
1402
+ } else if (state.sim.panning && !state.sim.panning.moved) {
1403
+ state.selected = null;
1404
+ render();
1405
+ }
1406
+ state.sim.panning = null;
1407
+ }
1408
+
1409
+ function leaveCanvasPointer() {
1410
+ state.sim.hoverNode = null;
1411
+ state.sim.dragNode = null;
1412
+ state.sim.panning = null;
1413
+ if (els.tooltip) els.tooltip.classList.remove("visible");
1414
+ drawCanvasGraph();
1415
+ }
1416
+
1417
+ function handleCanvasWheel(event) {
1418
+ event.preventDefault();
1419
+ var rect = els.canvas.getBoundingClientRect();
1420
+ var before = canvasToWorld(event);
1421
+ var factor = event.deltaY > 0 ? 0.88 : 1.14;
1422
+ state.sim.zoom = clamp(state.sim.zoom * factor, 0.14, 4.5);
1423
+ state.sim.panX = event.clientX - rect.left - before.x * state.sim.zoom;
1424
+ state.sim.panY = event.clientY - rect.top - before.y * state.sim.zoom;
1425
+ drawCanvasGraph();
1426
+ }
1427
+
1428
+ function handleCanvasDoubleClick(event) {
1429
+ var world = canvasToWorld(event);
1430
+ var node = findCanvasNode(world.x, world.y);
1431
+ if (!node) return;
1432
+ state.selected = { kind: "entity", id: node.id };
1433
+ els.scopeFilter.value = "focus";
1434
+ render();
1435
+ }
1436
+
1437
+ function canvasToWorld(event) {
1438
+ var rect = els.canvas.getBoundingClientRect();
1439
+ return {
1440
+ x: (event.clientX - rect.left - state.sim.panX) / state.sim.zoom,
1441
+ y: (event.clientY - rect.top - state.sim.panY) / state.sim.zoom
1442
+ };
1443
+ }
1444
+
1445
+ function findCanvasNode(x, y) {
1446
+ for (var i = state.sim.nodes.length - 1; i >= 0; i--) {
1447
+ var node = state.sim.nodes[i];
1448
+ var dx = node.x - x;
1449
+ var dy = node.y - y;
1450
+ if (dx * dx + dy * dy <= Math.pow(node.r + 7, 2)) return node;
1451
+ }
1452
+ return null;
1453
+ }
1454
+
1455
+ function updateCanvasTooltip(event) {
1456
+ if (!els.tooltip) return;
1457
+ var node = state.sim.hoverNode;
1458
+ if (!node || state.sim.dragNode || state.sim.panning) {
1459
+ els.tooltip.classList.remove("visible");
1460
+ return;
1461
+ }
1462
+ var entity = node.entity;
1463
+ var relationCount = state.sim.edges.filter(function (edge) { return edge.from === node.id || edge.to === node.id; }).length;
1464
+ var color = palette[entity.type] || palette[entity.graph_kind] || palette.default;
1465
+ els.tooltip.innerHTML = [
1466
+ "<div class=\"tt-name\"></div>",
1467
+ "<div class=\"tt-type\"></div>",
1468
+ "<div class=\"tt-summary\"></div>",
1469
+ "<div class=\"tt-conns\"></div>"
1470
+ ].join("");
1471
+ els.tooltip.querySelector(".tt-name").textContent = displayName(entity);
1472
+ els.tooltip.querySelector(".tt-type").textContent = nodeKindLabel(entity);
1473
+ els.tooltip.querySelector(".tt-type").style.color = color;
1474
+ els.tooltip.querySelector(".tt-summary").textContent = shortName(entity.summary || entity.id, 150);
1475
+ els.tooltip.querySelector(".tt-conns").textContent = relationCount + " relation" + (relationCount === 1 ? "" : "s");
1476
+ var rect = els.canvas.getBoundingClientRect();
1477
+ els.tooltip.style.left = (event.clientX - rect.left + 14) + "px";
1478
+ els.tooltip.style.top = (event.clientY - rect.top + 14) + "px";
1479
+ els.tooltip.classList.add("visible");
1480
+ }
1481
+
1482
+ function zoomCanvas(factor) {
1483
+ resizeCanvas();
1484
+ var rect = els.canvas.getBoundingClientRect();
1485
+ var cx = rect.width / 2;
1486
+ var cy = rect.height / 2;
1487
+ var before = {
1488
+ x: (cx - state.sim.panX) / state.sim.zoom,
1489
+ y: (cy - state.sim.panY) / state.sim.zoom
1490
+ };
1491
+ state.sim.zoom = clamp(state.sim.zoom * factor, 0.14, 4.5);
1492
+ state.sim.panX = cx - before.x * state.sim.zoom;
1493
+ state.sim.panY = cy - before.y * state.sim.zoom;
1494
+ drawCanvasGraph();
1495
+ }
1496
+
1497
+ function fitCanvas() {
1498
+ resizeCanvas();
1499
+ if (!state.sim.nodes.length) {
1500
+ state.sim.panX = 0;
1501
+ state.sim.panY = 0;
1502
+ state.sim.zoom = 1;
1503
+ return;
1504
+ }
1505
+ var xs = state.sim.nodes.map(function (node) { return node.x; });
1506
+ var ys = state.sim.nodes.map(function (node) { return node.y; });
1507
+ var minX = Math.min.apply(null, xs) - 90;
1508
+ var maxX = Math.max.apply(null, xs) + 90;
1509
+ var minY = Math.min.apply(null, ys) - 90;
1510
+ var maxY = Math.max.apply(null, ys) + 90;
1511
+ var width = els.canvas.width / Math.max(1, window.devicePixelRatio || 1);
1512
+ var height = els.canvas.height / Math.max(1, window.devicePixelRatio || 1);
1513
+ var graphWidth = Math.max(1, maxX - minX);
1514
+ var graphHeight = Math.max(1, maxY - minY);
1515
+ state.sim.zoom = clamp(Math.min(width / graphWidth, height / graphHeight), 0.22, 1.45);
1516
+ state.sim.panX = width / 2 - ((minX + maxX) / 2) * state.sim.zoom;
1517
+ state.sim.panY = height / 2 - ((minY + maxY) / 2) * state.sim.zoom;
1518
+ }
1519
+
1520
+ function focusedCanvasNodeId() {
1521
+ if (state.sim.hoverNode) return state.sim.hoverNode.id;
1522
+ if (state.selected && state.selected.kind === "entity") return state.selected.id;
1523
+ if (state.selected && state.selected.kind === "edge") {
1524
+ var selectedEdge = state.edges.find(function (edge) { return edge.id === state.selected.id; });
1525
+ return selectedEdge ? selectedEdge.from : null;
1526
+ }
1527
+ return null;
1528
+ }
1529
+
842
1530
  function fitView() {
843
1531
  var visible = state.entities.filter(function (entity) {
844
1532
  return state.visibleEntityIds.size === 0 || state.visibleEntityIds.has(entity.id);
@@ -855,10 +1543,10 @@
855
1543
  var minY = Math.min.apply(null, ys) - 82;
856
1544
  var maxY = Math.max.apply(null, ys) + 82;
857
1545
  state.viewBox = {
858
- x: clamp(minX, -160, 1000),
859
- y: clamp(minY, -120, 660),
860
- width: Math.max(520, Math.min(1120, maxX - minX)),
861
- height: Math.max(360, Math.min(700, maxY - minY))
1546
+ x: minX,
1547
+ y: minY,
1548
+ width: Math.max(620, maxX - minX),
1549
+ height: Math.max(400, maxY - minY)
862
1550
  };
863
1551
  }
864
1552
 
@@ -982,10 +1670,49 @@
982
1670
  return Math.max(min, Math.min(max, value));
983
1671
  }
984
1672
 
1673
+ function hexToRgb(value) {
1674
+ var hex = String(value || "#9be7c0").replace("#", "");
1675
+ if (hex.length === 3) hex = hex.split("").map(function (part) { return part + part; }).join("");
1676
+ var parsed = parseInt(hex, 16);
1677
+ return {
1678
+ r: (parsed >> 16) & 255,
1679
+ g: (parsed >> 8) & 255,
1680
+ b: parsed & 255
1681
+ };
1682
+ }
1683
+
1684
+ function brighten(value, amount) {
1685
+ var rgb = hexToRgb(value);
1686
+ return "rgb(" + Math.min(255, rgb.r + amount) + "," + Math.min(255, rgb.g + amount) + "," + Math.min(255, rgb.b + amount) + ")";
1687
+ }
1688
+
1689
+ function roundedRect(ctx, x, y, width, height, radius) {
1690
+ var r = Math.min(radius, Math.abs(width) / 2, Math.abs(height) / 2);
1691
+ ctx.beginPath();
1692
+ ctx.moveTo(x + r, y);
1693
+ ctx.lineTo(x + width - r, y);
1694
+ ctx.quadraticCurveTo(x + width, y, x + width, y + r);
1695
+ ctx.lineTo(x + width, y + height - r);
1696
+ ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
1697
+ ctx.lineTo(x + r, y + height);
1698
+ ctx.quadraticCurveTo(x, y + height, x, y + height - r);
1699
+ ctx.lineTo(x, y + r);
1700
+ ctx.quadraticCurveTo(x, y, x + r, y);
1701
+ ctx.closePath();
1702
+ }
1703
+
985
1704
  function classNames() {
986
1705
  return Array.prototype.slice.call(arguments).filter(Boolean).join(" ");
987
1706
  }
988
1707
 
1708
+ function visibleSignature(entityIds, edgeIds) {
1709
+ return Array.from(entityIds).sort().join("|") + "::" + Array.from(edgeIds).sort().join("|");
1710
+ }
1711
+
1712
+ function safeCssName(value) {
1713
+ return String(value || "unknown").toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
1714
+ }
1715
+
989
1716
  function svgEl(name, attrs) {
990
1717
  var element = document.createElementNS("http://www.w3.org/2000/svg", name);
991
1718
  Object.keys(attrs || {}).forEach(function (key) {