sdl-mcp 0.12.2 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/README.md +10 -0
  2. package/config/sdlmcp.config.example.json +53 -8
  3. package/config/sdlmcp.config.schema.json +530 -106
  4. package/dist/.tsbuildinfo +1 -1
  5. package/dist/benchmark/external-manifest.d.ts +162 -0
  6. package/dist/benchmark/external-manifest.d.ts.map +1 -0
  7. package/dist/benchmark/external-manifest.js +336 -0
  8. package/dist/benchmark/external-manifest.js.map +1 -0
  9. package/dist/benchmark/external-runner.d.ts +100 -0
  10. package/dist/benchmark/external-runner.d.ts.map +1 -0
  11. package/dist/benchmark/external-runner.js +1171 -0
  12. package/dist/benchmark/external-runner.js.map +1 -0
  13. package/dist/benchmark/output-file.d.ts +3 -0
  14. package/dist/benchmark/output-file.d.ts.map +1 -0
  15. package/dist/benchmark/output-file.js +8 -0
  16. package/dist/benchmark/output-file.js.map +1 -0
  17. package/dist/cli/argParsing.d.ts.map +1 -1
  18. package/dist/cli/argParsing.js +6 -0
  19. package/dist/cli/argParsing.js.map +1 -1
  20. package/dist/cli/commands/benchmark.d.ts.map +1 -1
  21. package/dist/cli/commands/benchmark.js +3 -2
  22. package/dist/cli/commands/benchmark.js.map +1 -1
  23. package/dist/cli/commands/index.d.ts +1 -0
  24. package/dist/cli/commands/index.d.ts.map +1 -1
  25. package/dist/cli/commands/index.js +13 -7
  26. package/dist/cli/commands/index.js.map +1 -1
  27. package/dist/cli/commands/init.d.ts +5 -1
  28. package/dist/cli/commands/init.d.ts.map +1 -1
  29. package/dist/cli/commands/init.js +73 -3
  30. package/dist/cli/commands/init.js.map +1 -1
  31. package/dist/cli/commands/serve.d.ts.map +1 -1
  32. package/dist/cli/commands/serve.js +2 -0
  33. package/dist/cli/commands/serve.js.map +1 -1
  34. package/dist/cli/index.js +2 -0
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/cli/transport/http.d.ts +1 -35
  37. package/dist/cli/transport/http.d.ts.map +1 -1
  38. package/dist/cli/transport/http.js +151 -175
  39. package/dist/cli/transport/http.js.map +1 -1
  40. package/dist/cli/types.d.ts +1 -0
  41. package/dist/cli/types.d.ts.map +1 -1
  42. package/dist/config/constants.d.ts +30 -0
  43. package/dist/config/constants.d.ts.map +1 -1
  44. package/dist/config/constants.js +30 -0
  45. package/dist/config/constants.js.map +1 -1
  46. package/dist/config/types.d.ts +53 -1
  47. package/dist/config/types.d.ts.map +1 -1
  48. package/dist/config/types.js +62 -14
  49. package/dist/config/types.js.map +1 -1
  50. package/dist/db/ladybug-batching.d.ts +1 -0
  51. package/dist/db/ladybug-batching.d.ts.map +1 -1
  52. package/dist/db/ladybug-batching.js +1 -0
  53. package/dist/db/ladybug-batching.js.map +1 -1
  54. package/dist/db/ladybug-core.d.ts +3 -0
  55. package/dist/db/ladybug-core.d.ts.map +1 -1
  56. package/dist/db/ladybug-core.js +72 -2
  57. package/dist/db/ladybug-core.js.map +1 -1
  58. package/dist/db/ladybug-embeddings.d.ts +6 -1
  59. package/dist/db/ladybug-embeddings.d.ts.map +1 -1
  60. package/dist/db/ladybug-embeddings.js +6 -1
  61. package/dist/db/ladybug-embeddings.js.map +1 -1
  62. package/dist/db/ladybug-file-summaries.d.ts.map +1 -1
  63. package/dist/db/ladybug-file-summaries.js +23 -0
  64. package/dist/db/ladybug-file-summaries.js.map +1 -1
  65. package/dist/db/ladybug-queries.js +1 -1
  66. package/dist/db/ladybug-queries.js.map +1 -1
  67. package/dist/db/migrations/index.d.ts.map +1 -1
  68. package/dist/db/migrations/index.js +2 -0
  69. package/dist/db/migrations/index.js.map +1 -1
  70. package/dist/db/migrations/m007-copy-embeddings-to-symbol.d.ts +5 -9
  71. package/dist/db/migrations/m007-copy-embeddings-to-symbol.d.ts.map +1 -1
  72. package/dist/db/migrations/m007-copy-embeddings-to-symbol.js +24 -126
  73. package/dist/db/migrations/m007-copy-embeddings-to-symbol.js.map +1 -1
  74. package/dist/db/migrations/m021-remediate-symbol-embeddings.d.ts +5 -0
  75. package/dist/db/migrations/m021-remediate-symbol-embeddings.d.ts.map +1 -0
  76. package/dist/db/migrations/m021-remediate-symbol-embeddings.js +7 -0
  77. package/dist/db/migrations/m021-remediate-symbol-embeddings.js.map +1 -0
  78. package/dist/db/migrations/symbol-embedding-remediation.d.ts +57 -0
  79. package/dist/db/migrations/symbol-embedding-remediation.d.ts.map +1 -0
  80. package/dist/db/migrations/symbol-embedding-remediation.js +335 -0
  81. package/dist/db/migrations/symbol-embedding-remediation.js.map +1 -0
  82. package/dist/graph/layout/force-layout.d.ts +4 -0
  83. package/dist/graph/layout/force-layout.d.ts.map +1 -0
  84. package/dist/graph/layout/force-layout.js +127 -0
  85. package/dist/graph/layout/force-layout.js.map +1 -0
  86. package/dist/graph/layout/layout-service.d.ts +37 -0
  87. package/dist/graph/layout/layout-service.d.ts.map +1 -0
  88. package/dist/graph/layout/layout-service.js +172 -0
  89. package/dist/graph/layout/layout-service.js.map +1 -0
  90. package/dist/graph/layout/native-engine.d.ts +9 -0
  91. package/dist/graph/layout/native-engine.d.ts.map +1 -0
  92. package/dist/graph/layout/native-engine.js +59 -0
  93. package/dist/graph/layout/native-engine.js.map +1 -0
  94. package/dist/graph/layout/prng.d.ts +3 -0
  95. package/dist/graph/layout/prng.d.ts.map +1 -0
  96. package/dist/graph/layout/prng.js +19 -0
  97. package/dist/graph/layout/prng.js.map +1 -0
  98. package/dist/graph/layout/serializer.d.ts +3 -0
  99. package/dist/graph/layout/serializer.d.ts.map +1 -0
  100. package/dist/graph/layout/serializer.js +20 -0
  101. package/dist/graph/layout/serializer.js.map +1 -0
  102. package/dist/graph/layout/types.d.ts +30 -0
  103. package/dist/graph/layout/types.d.ts.map +1 -0
  104. package/dist/graph/layout/types.js +2 -0
  105. package/dist/graph/layout/types.js.map +1 -0
  106. package/dist/indexer/embeddings.d.ts +7 -0
  107. package/dist/indexer/embeddings.d.ts.map +1 -1
  108. package/dist/indexer/embeddings.js +44 -4
  109. package/dist/indexer/embeddings.js.map +1 -1
  110. package/dist/indexer/file-summary-embeddings.d.ts +12 -0
  111. package/dist/indexer/file-summary-embeddings.d.ts.map +1 -1
  112. package/dist/indexer/file-summary-embeddings.js +83 -21
  113. package/dist/indexer/file-summary-embeddings.js.map +1 -1
  114. package/dist/indexer/fileWalker.d.ts.map +1 -1
  115. package/dist/indexer/fileWalker.js +2 -1
  116. package/dist/indexer/fileWalker.js.map +1 -1
  117. package/dist/indexer/metrics-updater.d.ts.map +1 -1
  118. package/dist/indexer/metrics-updater.js +27 -4
  119. package/dist/indexer/metrics-updater.js.map +1 -1
  120. package/dist/indexer/watcher.d.ts.map +1 -1
  121. package/dist/indexer/watcher.js +2 -1
  122. package/dist/indexer/watcher.js.map +1 -1
  123. package/dist/mcp/context-response-projection.d.ts.map +1 -1
  124. package/dist/mcp/context-response-projection.js +49 -13
  125. package/dist/mcp/context-response-projection.js.map +1 -1
  126. package/dist/mcp/tools/search-edit/planner.d.ts.map +1 -1
  127. package/dist/mcp/tools/search-edit/planner.js +10 -3
  128. package/dist/mcp/tools/search-edit/planner.js.map +1 -1
  129. package/dist/observability/event-tap.d.ts +2 -1
  130. package/dist/observability/event-tap.d.ts.map +1 -1
  131. package/dist/observability/event-tap.js.map +1 -1
  132. package/dist/observability/service.d.ts +11 -4
  133. package/dist/observability/service.d.ts.map +1 -1
  134. package/dist/observability/service.js +83 -13
  135. package/dist/observability/service.js.map +1 -1
  136. package/dist/observability/types.d.ts +48 -2
  137. package/dist/observability/types.d.ts.map +1 -1
  138. package/dist/ui/.tsbuildinfo +1 -0
  139. package/dist/ui/vendor/fflate.js +2692 -0
  140. package/dist/ui/vendor/jsm/controls/ArcballControls.js +3543 -0
  141. package/dist/ui/vendor/jsm/controls/DragControls.js +452 -0
  142. package/dist/ui/vendor/jsm/controls/FirstPersonControls.js +488 -0
  143. package/dist/ui/vendor/jsm/controls/FlyControls.js +384 -0
  144. package/dist/ui/vendor/jsm/controls/MapControls.js +116 -0
  145. package/dist/ui/vendor/jsm/controls/OrbitControls.js +1963 -0
  146. package/dist/ui/vendor/jsm/controls/PointerLockControls.js +264 -0
  147. package/dist/ui/vendor/jsm/controls/TrackballControls.js +1001 -0
  148. package/dist/ui/vendor/jsm/controls/TransformControls.js +2003 -0
  149. package/dist/ui/vendor/jsm/loaders/3DMLoader.js +1836 -0
  150. package/dist/ui/vendor/jsm/loaders/3MFLoader.js +1621 -0
  151. package/dist/ui/vendor/jsm/loaders/AMFLoader.js +541 -0
  152. package/dist/ui/vendor/jsm/loaders/BVHLoader.js +484 -0
  153. package/dist/ui/vendor/jsm/loaders/ColladaLoader.js +153 -0
  154. package/dist/ui/vendor/jsm/loaders/DDSLoader.js +385 -0
  155. package/dist/ui/vendor/jsm/loaders/DRACOLoader.js +772 -0
  156. package/dist/ui/vendor/jsm/loaders/EXRLoader.js +3395 -0
  157. package/dist/ui/vendor/jsm/loaders/FBXLoader.js +4580 -0
  158. package/dist/ui/vendor/jsm/loaders/FontLoader.js +260 -0
  159. package/dist/ui/vendor/jsm/loaders/GCodeLoader.js +318 -0
  160. package/dist/ui/vendor/jsm/loaders/GLTFLoader.js +4860 -0
  161. package/dist/ui/vendor/jsm/loaders/HDRCubeTextureLoader.js +164 -0
  162. package/dist/ui/vendor/jsm/loaders/HDRLoader.js +463 -0
  163. package/dist/ui/vendor/jsm/loaders/IESLoader.js +379 -0
  164. package/dist/ui/vendor/jsm/loaders/KMZLoader.js +163 -0
  165. package/dist/ui/vendor/jsm/loaders/KTX2Loader.js +1276 -0
  166. package/dist/ui/vendor/jsm/loaders/KTXLoader.js +197 -0
  167. package/dist/ui/vendor/jsm/loaders/LDrawLoader.js +2510 -0
  168. package/dist/ui/vendor/jsm/loaders/LUT3dlLoader.js +205 -0
  169. package/dist/ui/vendor/jsm/loaders/LUTCubeLoader.js +190 -0
  170. package/dist/ui/vendor/jsm/loaders/LUTImageLoader.js +190 -0
  171. package/dist/ui/vendor/jsm/loaders/LWOLoader.js +1051 -0
  172. package/dist/ui/vendor/jsm/loaders/LottieLoader.js +130 -0
  173. package/dist/ui/vendor/jsm/loaders/MD2Loader.js +435 -0
  174. package/dist/ui/vendor/jsm/loaders/MDDLoader.js +147 -0
  175. package/dist/ui/vendor/jsm/loaders/MTLLoader.js +593 -0
  176. package/dist/ui/vendor/jsm/loaders/MaterialXLoader.js +1126 -0
  177. package/dist/ui/vendor/jsm/loaders/NRRDLoader.js +718 -0
  178. package/dist/ui/vendor/jsm/loaders/OBJLoader.js +955 -0
  179. package/dist/ui/vendor/jsm/loaders/PCDLoader.js +620 -0
  180. package/dist/ui/vendor/jsm/loaders/PDBLoader.js +272 -0
  181. package/dist/ui/vendor/jsm/loaders/PLYLoader.js +968 -0
  182. package/dist/ui/vendor/jsm/loaders/PVRLoader.js +270 -0
  183. package/dist/ui/vendor/jsm/loaders/RGBELoader.js +18 -0
  184. package/dist/ui/vendor/jsm/loaders/STLLoader.js +421 -0
  185. package/dist/ui/vendor/jsm/loaders/SVGLoader.js +3319 -0
  186. package/dist/ui/vendor/jsm/loaders/TDSLoader.js +1142 -0
  187. package/dist/ui/vendor/jsm/loaders/TGALoader.js +538 -0
  188. package/dist/ui/vendor/jsm/loaders/TIFFLoader.js +59 -0
  189. package/dist/ui/vendor/jsm/loaders/TTFLoader.js +261 -0
  190. package/dist/ui/vendor/jsm/loaders/USDLoader.js +303 -0
  191. package/dist/ui/vendor/jsm/loaders/USDZLoader.js +16 -0
  192. package/dist/ui/vendor/jsm/loaders/UltraHDRLoader.js +755 -0
  193. package/dist/ui/vendor/jsm/loaders/VOXLoader.js +919 -0
  194. package/dist/ui/vendor/jsm/loaders/VRMLLoader.js +3646 -0
  195. package/dist/ui/vendor/jsm/loaders/VTKLoader.js +1293 -0
  196. package/dist/ui/vendor/jsm/loaders/XYZLoader.js +143 -0
  197. package/dist/ui/vendor/jsm/loaders/collada/ColladaComposer.js +3044 -0
  198. package/dist/ui/vendor/jsm/loaders/collada/ColladaParser.js +1977 -0
  199. package/dist/ui/vendor/jsm/loaders/lwo/IFFParser.js +1217 -0
  200. package/dist/ui/vendor/jsm/loaders/lwo/LWO2Parser.js +414 -0
  201. package/dist/ui/vendor/jsm/loaders/lwo/LWO3Parser.js +373 -0
  202. package/dist/ui/vendor/jsm/loaders/usd/USDAParser.js +901 -0
  203. package/dist/ui/vendor/jsm/loaders/usd/USDCParser.js +1878 -0
  204. package/dist/ui/vendor/jsm/loaders/usd/USDComposer.js +4627 -0
  205. package/dist/ui/vendor/jsm/utils/BufferGeometryUtils.js +1501 -0
  206. package/dist/ui/vendor/jsm/utils/CameraUtils.js +82 -0
  207. package/dist/ui/vendor/jsm/utils/ColorUtils.js +76 -0
  208. package/dist/ui/vendor/jsm/utils/GeometryCompressionUtils.js +547 -0
  209. package/dist/ui/vendor/jsm/utils/GeometryUtils.js +226 -0
  210. package/dist/ui/vendor/jsm/utils/LDrawUtils.js +211 -0
  211. package/dist/ui/vendor/jsm/utils/SceneOptimizer.js +458 -0
  212. package/dist/ui/vendor/jsm/utils/SceneUtils.js +363 -0
  213. package/dist/ui/vendor/jsm/utils/ShadowMapViewer.js +244 -0
  214. package/dist/ui/vendor/jsm/utils/ShadowMapViewerGPU.js +233 -0
  215. package/dist/ui/vendor/jsm/utils/SkeletonUtils.js +496 -0
  216. package/dist/ui/vendor/jsm/utils/SortUtils.js +175 -0
  217. package/dist/ui/vendor/jsm/utils/UVsDebug.js +173 -0
  218. package/dist/ui/vendor/jsm/utils/WebGLTextureUtils.js +115 -0
  219. package/dist/ui/vendor/jsm/utils/WebGPUTextureUtils.js +81 -0
  220. package/dist/ui/vendor/jsm/utils/WorkerPool.js +167 -0
  221. package/dist/ui/vendor/three.core.min.js +6 -0
  222. package/dist/ui/vendor/three.module.min.js +6 -0
  223. package/dist/ui/viewer/ambient.js +39 -0
  224. package/dist/ui/viewer/api.js +93 -0
  225. package/dist/ui/viewer/chrome.js +36 -0
  226. package/dist/ui/viewer/edges.js +33 -0
  227. package/dist/ui/viewer/index.html +32 -0
  228. package/dist/ui/viewer/inspector.js +23 -0
  229. package/dist/ui/viewer/lenses/activity.js +24 -0
  230. package/dist/ui/viewer/lenses/community.js +5 -0
  231. package/dist/ui/viewer/lenses/edges.js +4 -0
  232. package/dist/ui/viewer/lenses/impact.js +6 -0
  233. package/dist/ui/viewer/lenses/search.js +69 -0
  234. package/dist/ui/viewer/lod.js +100 -0
  235. package/dist/ui/viewer/main.js +92 -0
  236. package/dist/ui/viewer/picking.js +45 -0
  237. package/dist/ui/viewer/scene.js +85 -0
  238. package/dist/ui/viewer/skins/default-skin.js +15 -0
  239. package/dist/ui/viewer/skins/loader.js +44 -0
  240. package/dist/ui/viewer/skins/manifest-schema.js +30 -0
  241. package/dist/ui/viewer/state.js +44 -0
  242. package/dist/ui/viewer/theme.js +23 -0
  243. package/dist/ui/viewer/universe.js +151 -0
  244. package/dist/ui/viewer/viewer.css +85 -0
  245. package/dist/util/safeRegex.d.ts.map +1 -1
  246. package/dist/util/safeRegex.js +221 -68
  247. package/dist/util/safeRegex.js.map +1 -1
  248. package/dist/viewer/legacy-graph.d.ts +40 -0
  249. package/dist/viewer/legacy-graph.d.ts.map +1 -0
  250. package/dist/viewer/legacy-graph.js +152 -0
  251. package/dist/viewer/legacy-graph.js.map +1 -0
  252. package/dist/viewer/routes.d.ts +10 -0
  253. package/dist/viewer/routes.d.ts.map +1 -0
  254. package/dist/viewer/routes.js +221 -0
  255. package/dist/viewer/routes.js.map +1 -0
  256. package/dist/viewer/service.d.ts +44 -0
  257. package/dist/viewer/service.d.ts.map +1 -0
  258. package/dist/viewer/service.js +160 -0
  259. package/dist/viewer/service.js.map +1 -0
  260. package/dist/viewer/types.d.ts +63 -0
  261. package/dist/viewer/types.d.ts.map +1 -0
  262. package/dist/viewer/types.js +2 -0
  263. package/dist/viewer/types.js.map +1 -0
  264. package/dist/viewer/viewer-config.d.ts +12 -0
  265. package/dist/viewer/viewer-config.d.ts.map +1 -0
  266. package/dist/viewer/viewer-config.js +36 -0
  267. package/dist/viewer/viewer-config.js.map +1 -0
  268. package/package.json +18 -9
  269. package/templates/SDL.md +19 -0
  270. package/templates/claude-code.json +8 -43
  271. package/dist/ui/graph.css +0 -108
  272. package/dist/ui/graph.d.ts +0 -2
  273. package/dist/ui/graph.d.ts.map +0 -1
  274. package/dist/ui/graph.html +0 -52
  275. package/dist/ui/graph.js +0 -259
  276. package/dist/ui/graph.js.map +0 -1
@@ -0,0 +1,39 @@
1
+ import * as THREE from "three";
2
+ import { setAmbient, state } from "./state.js";
3
+ export class AmbientController {
4
+ viewer;
5
+ universe;
6
+ caption;
7
+ lastInput = performance.now();
8
+ captionTimer = 0;
9
+ constructor(viewer, universe, caption) {
10
+ this.viewer = viewer;
11
+ this.universe = universe;
12
+ this.caption = caption;
13
+ for (const type of ["pointermove", "keydown", "wheel", "pointerdown"])
14
+ window.addEventListener(type, this.onInput, { passive: true });
15
+ viewer.onFrame((delta) => this.frame(delta));
16
+ }
17
+ setCaption(text) {
18
+ this.caption.textContent = text;
19
+ this.caption.classList.add("visible");
20
+ window.clearTimeout(this.captionTimer);
21
+ this.captionTimer = window.setTimeout(() => this.caption.classList.remove("visible"), 3500);
22
+ }
23
+ onInput = () => { this.lastInput = performance.now(); if (state.ambient)
24
+ setAmbient(false); };
25
+ frame(delta) {
26
+ const settings = state.settings;
27
+ if (!settings?.ambient.enabled)
28
+ return;
29
+ if (!state.ambient && performance.now() - this.lastInput > settings.ambient.idleSeconds * 1000)
30
+ setAmbient(true);
31
+ if (!state.ambient)
32
+ return;
33
+ this.viewer.setFpsCap(settings.ambient.fps);
34
+ const angle = (delta / 1000) * 0.02;
35
+ this.viewer.camera.position.applyAxisAngle(new THREE.Vector3(0, 1, 0), angle);
36
+ this.viewer.controls.target.lerp(new THREE.Vector3(0, 0, 0), 0.02);
37
+ void this.universe;
38
+ }
39
+ }
@@ -0,0 +1,93 @@
1
+ const TOKEN_STORAGE_KEY = "sdl-mcp-observability-token";
2
+ export function readAuthTokenFromPage() {
3
+ const params = new URLSearchParams(window.location.hash.replace(/^#/, ""));
4
+ const hashToken = params.get("token");
5
+ if (hashToken) {
6
+ localStorage.setItem(TOKEN_STORAGE_KEY, hashToken);
7
+ history.replaceState(null, "", window.location.pathname + window.location.search);
8
+ return hashToken;
9
+ }
10
+ return localStorage.getItem(TOKEN_STORAGE_KEY) ?? "";
11
+ }
12
+ export class ViewerApi {
13
+ token = readAuthTokenFromPage();
14
+ setToken(token) {
15
+ this.token = token;
16
+ if (token)
17
+ localStorage.setItem(TOKEN_STORAGE_KEY, token);
18
+ else
19
+ localStorage.removeItem(TOKEN_STORAGE_KEY);
20
+ }
21
+ getToken() {
22
+ return this.token;
23
+ }
24
+ headers(accept = "application/json") {
25
+ const headers = { Accept: accept };
26
+ if (this.token)
27
+ headers.Authorization = "Bearer " + this.token;
28
+ return headers;
29
+ }
30
+ async json(url) {
31
+ const response = await fetch(url, { headers: this.headers() });
32
+ if (!response.ok)
33
+ throw new Error("HTTP " + response.status + " for " + url);
34
+ return response.json();
35
+ }
36
+ async universe() { return this.json("/api/graph/universe"); }
37
+ async clusters(repoId) { return this.json("/api/graph/repo/" + encodeURIComponent(repoId) + "/clusters"); }
38
+ async clusterEdges(repoId) { return this.json("/api/graph/repo/" + encodeURIComponent(repoId) + "/edges?scope=clusters"); }
39
+ async layout(repoId, lod, clusterId) {
40
+ const params = new URLSearchParams({ lod });
41
+ if (clusterId)
42
+ params.set("clusterId", clusterId);
43
+ return this.json("/api/graph/repo/" + encodeURIComponent(repoId) + "/layout?" + params.toString());
44
+ }
45
+ async symbolEdges(repoId, clusterId, filters) {
46
+ const params = new URLSearchParams({ clusterId });
47
+ if (filters?.minConfidence !== undefined)
48
+ params.set("minConfidence", String(filters.minConfidence));
49
+ if (filters?.exactOnly)
50
+ params.set("exactOnly", "1");
51
+ if (filters?.kinds?.length)
52
+ params.set("kinds", filters.kinds.join(","));
53
+ return this.json("/api/graph/repo/" + encodeURIComponent(repoId) + "/edges?" + params.toString());
54
+ }
55
+ async card(repoId, symbolId) { return this.json("/api/graph/repo/" + encodeURIComponent(repoId) + "/symbol/" + encodeURIComponent(symbolId) + "/card"); }
56
+ async search(repoId, query) { return this.json("/api/graph/repo/" + encodeURIComponent(repoId) + "/search?q=" + encodeURIComponent(query)); }
57
+ async impact(repoId, fromVersion, toVersion) {
58
+ return this.json("/api/graph/repo/" + encodeURIComponent(repoId) + "/impact?fromVersion=" + encodeURIComponent(fromVersion) + "&toVersion=" + encodeURIComponent(toVersion));
59
+ }
60
+ async skins() { return this.json("/api/graph/skins"); }
61
+ async skinBytes(id) {
62
+ const response = await fetch("/api/graph/skins/" + encodeURIComponent(id), { headers: this.headers("application/zip") });
63
+ if (!response.ok)
64
+ throw new Error("HTTP " + response.status + " for skin " + id);
65
+ return response.arrayBuffer();
66
+ }
67
+ async graphStream(onEvent, signal) {
68
+ const response = await fetch("/api/observability/stream?types=graph", { headers: this.headers("text/event-stream"), signal });
69
+ if (!response.ok || !response.body)
70
+ throw new Error("SSE failed: HTTP " + response.status);
71
+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
72
+ let buffer = "";
73
+ while (!signal.aborted) {
74
+ const chunk = await reader.read();
75
+ if (chunk.done)
76
+ break;
77
+ buffer += chunk.value;
78
+ const events = buffer.split("\n\n");
79
+ buffer = events.pop() ?? "";
80
+ for (const event of events) {
81
+ const data = event.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("\n");
82
+ if (!data)
83
+ continue;
84
+ try {
85
+ onEvent(JSON.parse(data));
86
+ }
87
+ catch (error) {
88
+ console.warn("[viewer] bad SSE event", error);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,36 @@
1
+ import { setActiveLens, setVisibleRepos, subscribe } from "./state.js";
2
+ export class ViewerChrome {
3
+ searchInput;
4
+ lensSelect;
5
+ skinSelect;
6
+ fpsSelect;
7
+ ambientButton;
8
+ status;
9
+ repoHost;
10
+ constructor(host) {
11
+ host.innerHTML = '<div class="brand"><span class="mark"></span><div><h1>SDL Galaxy</h1><p>graph viewer</p></div></div>' +
12
+ '<div class="toolbar"><div class="repo-filter"></div><input class="search" type="search" placeholder="Search symbols" aria-label="Search symbols" />' +
13
+ '<select class="lens" aria-label="Lens"><option value="none">Normal</option><option value="community">Community</option><option value="impact">Impact</option><option value="edges">Edges</option></select>' +
14
+ '<select class="skin" aria-label="Skin"><option value="default">Default</option></select>' +
15
+ '<select class="fps" aria-label="FPS"><option>15</option><option>30</option><option selected>60</option><option>90</option><option>120</option></select>' +
16
+ '<button class="ambient" type="button" aria-pressed="false">Ambient</button><span class="status">booting</span></div>';
17
+ this.searchInput = host.querySelector(".search");
18
+ this.lensSelect = host.querySelector(".lens");
19
+ this.skinSelect = host.querySelector(".skin");
20
+ this.fpsSelect = host.querySelector(".fps");
21
+ this.ambientButton = host.querySelector(".ambient");
22
+ this.status = host.querySelector(".status");
23
+ this.repoHost = host.querySelector(".repo-filter");
24
+ this.lensSelect.addEventListener("change", () => setActiveLens(this.lensSelect.value));
25
+ subscribe((snapshot) => { this.ambientButton.setAttribute("aria-pressed", String(snapshot.ambient)); });
26
+ }
27
+ setRepos(repos) {
28
+ this.repoHost.innerHTML = repos.map((repo) => '<label><input type="checkbox" value="' + repo.repoId + '" checked />' + repo.repoId + '</label>').join("");
29
+ this.repoHost.addEventListener("change", () => {
30
+ setVisibleRepos([...this.repoHost.querySelectorAll('input:checked')].map((input) => input.value));
31
+ });
32
+ }
33
+ setSettings(settings) { this.fpsSelect.value = String(settings.fps); }
34
+ setSkins(skins) { this.skinSelect.innerHTML = '<option value="default">Default</option>' + skins.map((skin) => '<option value="' + skin.id + '">' + skin.id + '</option>').join(""); }
35
+ setStatus(value) { this.status.textContent = value; }
36
+ }
@@ -0,0 +1,33 @@
1
+ import * as THREE from "three";
2
+ import { KIND_COLORS } from "./theme.js";
3
+ export function createSymbolEdges(edges, positions, minConfidence = 0, exactOnly = false, kinds = new Set()) {
4
+ const points = [];
5
+ const colors = [];
6
+ for (const edge of edges) {
7
+ const confidence = edge.confidence ?? 1;
8
+ const kind = edge.kind ?? "call";
9
+ if (confidence < minConfidence)
10
+ continue;
11
+ if (exactOnly && edge.resolution && edge.resolution !== "exact")
12
+ continue;
13
+ if (kinds.size > 0 && !kinds.has(kind))
14
+ continue;
15
+ const from = positions.get(edge.from);
16
+ const to = positions.get(edge.to);
17
+ if (!from || !to)
18
+ continue;
19
+ const color = new THREE.Color(KIND_COLORS[kind] ?? "#93a7c8").multiplyScalar(Math.max(0.65, confidence));
20
+ points.push(from.x, from.y, from.z, to.x, to.y, to.z);
21
+ colors.push(color.r, color.g, color.b, color.r, color.g, color.b);
22
+ }
23
+ const geometry = new THREE.BufferGeometry();
24
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(points, 3));
25
+ geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
26
+ return new THREE.LineSegments(geometry, new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.85, blending: THREE.AdditiveBlending, depthWrite: false }));
27
+ }
28
+ export function edgeCounts(edges) {
29
+ const counts = new Map();
30
+ for (const edge of edges)
31
+ counts.set(edge.kind ?? "call", (counts.get(edge.kind ?? "call") ?? 0) + 1);
32
+ return counts;
33
+ }
@@ -0,0 +1,32 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>SDL Galaxy</title>
7
+ <link rel="stylesheet" href="/ui/observability.css" />
8
+ <link rel="stylesheet" href="/ui/viewer/viewer.css" />
9
+ <script type="importmap">
10
+ {
11
+ "imports": {
12
+ "three": "/ui/vendor/three.module.min.js",
13
+ "three/addons/": "/ui/vendor/jsm/",
14
+ "fflate": "/ui/vendor/fflate.js"
15
+ }
16
+ }
17
+ </script>
18
+ </head>
19
+ <body class="viewer-body">
20
+ <header id="viewerChrome" class="viewer-chrome"></header>
21
+ <main class="viewer-shell">
22
+ <section id="viewerCanvas" class="viewer-canvas" aria-label="SDL Galaxy graph canvas"></section>
23
+ <aside id="viewerInspector" class="panel viewer-inspector" aria-label="Inspector"></aside>
24
+ <section class="panel viewer-results" aria-label="Search and lens results">
25
+ <header class="panel-head"><h2>RESULTS</h2><span class="panel-sub">live</span></header>
26
+ <div id="viewerResults" class="result-list"></div>
27
+ </section>
28
+ </main>
29
+ <div id="viewerCaption" class="viewer-caption" role="status" aria-live="polite"></div>
30
+ <script type="module" src="/ui/viewer/main.js"></script>
31
+ </body>
32
+ </html>
@@ -0,0 +1,23 @@
1
+ import { subscribe } from "./state.js";
2
+ function text(value) { return value === undefined || value === null || value === "" ? "-" : String(value); }
3
+ export class InspectorPanel {
4
+ host;
5
+ constructor(host) {
6
+ this.host = host;
7
+ subscribe((state) => this.render(state.selection?.card ?? null, state.selection?.clusterId));
8
+ }
9
+ render(card, clusterId) {
10
+ if (!card && !clusterId) {
11
+ this.host.innerHTML = '<header class="panel-head"><h2>INSPECTOR</h2><span class="panel-sub">no selection</span></header><div class="empty">Select a cluster or symbol.</div>';
12
+ return;
13
+ }
14
+ if (!card) {
15
+ this.host.innerHTML = '<header class="panel-head"><h2>CLUSTER</h2><span class="panel-sub">' + text(clusterId) + '</span></header><dl class="inspector-dl"><dt>Cluster ID</dt><dd>' + text(clusterId) + '</dd></dl>';
16
+ return;
17
+ }
18
+ const deps = [...(card.deps?.out ?? []), ...(card.deps?.in ?? [])].slice(0, 8).map((dep) => '<button type="button" data-symbol="' + text(dep.symbolId) + '">' + text(dep.name ?? dep.symbolId) + '</button>').join("");
19
+ this.host.innerHTML = '<header class="panel-head"><h2>' + text(card.name ?? card.symbolId) + '</h2><span class="panel-sub">' + text(card.kind) + '</span></header>' +
20
+ '<dl class="inspector-dl"><dt>Signature</dt><dd><code>' + text(typeof card.signature === "string" ? card.signature : JSON.stringify(card.signature ?? "")) + '</code></dd><dt>Summary</dt><dd>' + text(card.summary) + '</dd></dl>' +
21
+ '<div class="dep-list">' + deps + '</div>';
22
+ }
23
+ }
@@ -0,0 +1,24 @@
1
+ import { pushGraphEvent } from "../state.js";
2
+ export class ActivityLens {
3
+ api;
4
+ universe;
5
+ setCaption;
6
+ abortController = null;
7
+ constructor(api, universe, setCaption) {
8
+ this.api = api;
9
+ this.universe = universe;
10
+ this.setCaption = setCaption;
11
+ }
12
+ start() {
13
+ this.abortController?.abort();
14
+ this.abortController = new AbortController();
15
+ void this.api.graphStream((event) => this.handle(event), this.abortController.signal).catch((error) => console.warn("[viewer] graph stream", error));
16
+ }
17
+ stop() { this.abortController?.abort(); }
18
+ handle(event) {
19
+ pushGraphEvent(event);
20
+ if (event.repoId && event.clusterId)
21
+ this.universe.pulseCluster(event.repoId, event.clusterId);
22
+ this.setCaption(event.type + (event.count ? ": " + event.count : ""));
23
+ }
24
+ }
@@ -0,0 +1,5 @@
1
+ import { setActiveLens } from "../state.js";
2
+ export function applyCommunityLens(universe) {
3
+ setActiveLens("community");
4
+ universe.applyLens("community");
5
+ }
@@ -0,0 +1,4 @@
1
+ import { edgeCounts } from "../edges.js";
2
+ export function renderEdgeCounts(host, edges) {
3
+ host.innerHTML = [...edgeCounts(edges)].map(([kind, count]) => '<label><input type="checkbox" value="' + kind + '" checked />' + kind + ' <span>' + count + '</span></label>').join("");
4
+ }
@@ -0,0 +1,6 @@
1
+ export async function runImpactLens(api, repoId, fromVersion, toVersion, renderList) {
2
+ const result = await api.impact(repoId, fromVersion, toVersion);
3
+ const changed = (result.changed ?? []).map((symbolId) => symbolId);
4
+ const blast = (result.blastRadius ?? []).map((item) => item.name ?? item.symbolId);
5
+ renderList([...changed, ...blast]);
6
+ }
@@ -0,0 +1,69 @@
1
+ import { setSearchResults } from "../state.js";
2
+ function escapeHtml(value) {
3
+ return String(value ?? "")
4
+ .replaceAll("&", "&amp;")
5
+ .replaceAll("<", "&lt;")
6
+ .replaceAll(">", "&gt;")
7
+ .replaceAll('"', "&quot;");
8
+ }
9
+ export function renderSearchResultsHtml(results) {
10
+ if (results.length === 0)
11
+ return '<div class="empty">No results</div>';
12
+ return results.map((result, index) => {
13
+ const score = typeof result.score === "number" ? " · " + result.score.toFixed(3) : "";
14
+ const path = result.relPath ? '<span class="result-path">' + escapeHtml(result.relPath) + '</span>' : "";
15
+ return '<button type="button" class="result-item" data-index="' + String(index) + '" data-symbol-id="' + escapeHtml(result.symbolId) + '" data-cluster-id="' + escapeHtml(result.clusterId ?? "") + '">' +
16
+ '<span class="result-name">' + escapeHtml(result.name || result.symbolId) + '</span>' +
17
+ '<span class="result-meta">' + escapeHtml(result.kind) + score + '</span>' +
18
+ path +
19
+ '</button>';
20
+ }).join("");
21
+ }
22
+ export function attachSearch(input, resultsHost, api, getRepoId, onSelect) {
23
+ let timer = 0;
24
+ let currentResults = [];
25
+ const clear = () => {
26
+ currentResults = [];
27
+ setSearchResults([]);
28
+ resultsHost.innerHTML = "";
29
+ };
30
+ const choose = async (result) => {
31
+ if (!result)
32
+ return;
33
+ await onSelect(result);
34
+ };
35
+ input.addEventListener("keydown", (event) => {
36
+ if (event.key === "Escape") {
37
+ input.value = "";
38
+ clear();
39
+ }
40
+ if (event.key === "Enter") {
41
+ event.preventDefault();
42
+ void choose(currentResults[0]);
43
+ }
44
+ });
45
+ window.addEventListener("keydown", (event) => { if (event.key === "/" && document.activeElement !== input) {
46
+ event.preventDefault();
47
+ input.focus();
48
+ } });
49
+ resultsHost.addEventListener("click", (event) => {
50
+ const button = event.target?.closest("button[data-index]");
51
+ if (!button)
52
+ return;
53
+ void choose(currentResults[Number(button.dataset.index)]);
54
+ });
55
+ input.addEventListener("input", () => {
56
+ window.clearTimeout(timer);
57
+ timer = window.setTimeout(async () => {
58
+ const query = input.value.trim();
59
+ if (!query) {
60
+ clear();
61
+ return;
62
+ }
63
+ const response = await api.search(getRepoId(), query);
64
+ currentResults = response.results ?? [];
65
+ setSearchResults(currentResults);
66
+ resultsHost.innerHTML = renderSearchResultsHtml(currentResults);
67
+ }, 180);
68
+ });
69
+ }
@@ -0,0 +1,100 @@
1
+ import * as THREE from "three";
2
+ import { createSymbolEdges } from "./edges.js";
3
+ import { touchExpandedCluster, removeExpandedCluster, state } from "./state.js";
4
+ import { KIND_COLORS, hashColor } from "./theme.js";
5
+ export class LodController {
6
+ api;
7
+ viewer;
8
+ universe;
9
+ expanded = new Map();
10
+ constructor(api, viewer, universe) {
11
+ this.api = api;
12
+ this.viewer = viewer;
13
+ this.universe = universe;
14
+ }
15
+ async maybeExpandNearest() {
16
+ let nearest = null;
17
+ for (const cluster of this.universe.getClusters()) {
18
+ const distance = cluster.position.distanceTo(this.viewer.camera.position);
19
+ if (distance < 520 && (!nearest || distance < nearest.distance))
20
+ nearest = { key: cluster.repoId + ":" + cluster.clusterId, distance };
21
+ }
22
+ if (nearest && !this.expanded.has(nearest.key))
23
+ await this.expand(nearest.key);
24
+ for (const [key, expanded] of this.expanded) {
25
+ const cluster = this.universe.getCluster(key);
26
+ if (!cluster || cluster.position.distanceTo(this.viewer.camera.position) > 1150 || !state.expandedClusters.has(key))
27
+ this.collapse(key, expanded);
28
+ }
29
+ }
30
+ async expand(key) {
31
+ const cluster = this.universe.getCluster(key);
32
+ if (!cluster)
33
+ return;
34
+ touchExpandedCluster(key);
35
+ const limit = state.settings?.layout.maxSymbolsPerClusterExpand ?? 5000;
36
+ if (cluster.symbolCount > limit)
37
+ return;
38
+ const [layout, edgeResponse] = await Promise.all([
39
+ this.api.layout(cluster.repoId, "symbol", cluster.clusterId),
40
+ this.api.symbolEdges(cluster.repoId, cluster.clusterId),
41
+ ]);
42
+ const nodes = edgeResponse.nodes ?? layout.positions.map((position) => ({ id: position.id }));
43
+ const geometry = new THREE.SphereGeometry(3.5, 10, 8);
44
+ const material = new THREE.MeshBasicMaterial();
45
+ const mesh = new THREE.InstancedMesh(geometry, material, Math.max(nodes.length, 1));
46
+ mesh.name = "symbols:" + key;
47
+ const matrix = new THREE.Matrix4();
48
+ const color = new THREE.Color();
49
+ const positions = new Map();
50
+ const layoutPositions = new Map(layout.positions.map((position) => [position.id, position]));
51
+ nodes.forEach((node, index) => {
52
+ const p = layoutPositions.get(node.id);
53
+ const position = new THREE.Vector3(p?.x ?? 0, p?.y ?? 0, p?.z ?? 0).multiplyScalar(0.45).add(cluster.position);
54
+ positions.set(node.id, position);
55
+ const scale = 0.5 + Math.log2(1 + (node.fanIn ?? 0)) * 0.25;
56
+ matrix.compose(position, new THREE.Quaternion(), new THREE.Vector3(scale, scale, scale));
57
+ mesh.setMatrixAt(index, matrix);
58
+ mesh.setColorAt(index, this.nodeColor(node, cluster.clusterId, color));
59
+ });
60
+ mesh.instanceMatrix.needsUpdate = true;
61
+ if (mesh.instanceColor)
62
+ mesh.instanceColor.needsUpdate = true;
63
+ const lines = createSymbolEdges(edgeResponse.edges, positions);
64
+ const group = new THREE.Group();
65
+ group.add(mesh);
66
+ group.add(lines);
67
+ this.viewer.root.add(group);
68
+ this.expanded.set(key, { key, clusterId: cluster.clusterId, group, positions, nodes, mesh, lines });
69
+ }
70
+ collapse(key, expanded = this.expanded.get(key)) {
71
+ if (!expanded)
72
+ return;
73
+ this.viewer.disposeObject(expanded.group);
74
+ this.expanded.delete(key);
75
+ removeExpandedCluster(key);
76
+ }
77
+ applyLens() {
78
+ const color = new THREE.Color();
79
+ for (const expanded of this.expanded.values()) {
80
+ expanded.nodes.forEach((node, index) => {
81
+ expanded.mesh.setColorAt(index, this.nodeColor(node, expanded.clusterId, color));
82
+ });
83
+ if (expanded.mesh.instanceColor)
84
+ expanded.mesh.instanceColor.needsUpdate = true;
85
+ expanded.lines.material.opacity = state.activeLens === "edges" ? 1 : 0.85;
86
+ }
87
+ }
88
+ nodeColor(node, clusterId, target) {
89
+ const lens = state.activeLens;
90
+ if (lens === "community")
91
+ return target.set(hashColor(clusterId));
92
+ if (lens === "impact") {
93
+ const heat = Math.min(1, Math.log2(1 + (node.fanIn ?? 0)) / 8);
94
+ return target.set("#38bdf8").lerp(new THREE.Color("#fb7185"), heat);
95
+ }
96
+ target.set(KIND_COLORS[node.kind ?? "function"] ?? "#e5e7eb");
97
+ return lens === "edges" ? target.multiplyScalar(0.35) : target;
98
+ }
99
+ getExpanded() { return [...this.expanded.values()]; }
100
+ }
@@ -0,0 +1,92 @@
1
+ import * as THREE from "three";
2
+ import { ViewerApi } from "./api.js";
3
+ import { AmbientController } from "./ambient.js";
4
+ import { ViewerChrome } from "./chrome.js";
5
+ import { InspectorPanel } from "./inspector.js";
6
+ import { ActivityLens } from "./lenses/activity.js";
7
+ import { attachSearch } from "./lenses/search.js";
8
+ import { LodController } from "./lod.js";
9
+ import { PickingController } from "./picking.js";
10
+ import { ViewerScene } from "./scene.js";
11
+ import { setSettings, loadVisibleRepos, state, subscribe, setAmbient, setSelection } from "./state.js";
12
+ import { loadSkinZip, applySkinCssVars } from "./skins/loader.js";
13
+ import { UniverseRenderer } from "./universe.js";
14
+ const canvasHost = document.getElementById("viewerCanvas");
15
+ const chromeHost = document.getElementById("viewerChrome");
16
+ const inspectorHost = document.getElementById("viewerInspector");
17
+ const captionHost = document.getElementById("viewerCaption");
18
+ if (!canvasHost || !chromeHost || !inspectorHost || !captionHost)
19
+ throw new Error("viewer shell missing required elements");
20
+ const api = new ViewerApi();
21
+ const scene = new ViewerScene(canvasHost);
22
+ const universeRenderer = new UniverseRenderer(scene);
23
+ const chrome = new ViewerChrome(chromeHost);
24
+ new InspectorPanel(inspectorHost);
25
+ new PickingController(api, scene, universeRenderer);
26
+ const lod = new LodController(api, scene, universeRenderer);
27
+ const ambient = new AmbientController(scene, universeRenderer, captionHost);
28
+ const activity = new ActivityLens(api, universeRenderer, (text) => ambient.setCaption(text));
29
+ function firstVisibleRepo() { return [...state.visibleRepos][0] ?? "sdl-mcp"; }
30
+ const resultsHost = document.getElementById("viewerResults");
31
+ attachSearch(chrome.searchInput, resultsHost, api, firstVisibleRepo, async (result) => {
32
+ const repoId = firstVisibleRepo();
33
+ const cluster = result.clusterId
34
+ ? universeRenderer.getClusters().find((item) => item.repoId === repoId && item.clusterId === result.clusterId)
35
+ : undefined;
36
+ if (cluster)
37
+ scene.flyTo(cluster.position);
38
+ const card = await api.card(repoId, result.symbolId);
39
+ setSelection({ repoId, clusterId: result.clusterId ?? undefined, symbolId: result.symbolId, card });
40
+ });
41
+ chrome.ambientButton.addEventListener("click", () => setAmbient(!state.ambient));
42
+ chrome.fpsSelect.addEventListener("change", () => scene.setFpsCap(Number(chrome.fpsSelect.value)));
43
+ chrome.skinSelect.addEventListener("change", async () => {
44
+ if (chrome.skinSelect.value === "default")
45
+ return;
46
+ const bytes = await api.skinBytes(chrome.skinSelect.value);
47
+ const caps = state.settings?.skins ?? { maxZipBytes: 52_428_800, maxEntries: 500, maxDecompressedBytes: 209_715_200 };
48
+ const skin = loadSkinZip(new Uint8Array(bytes), caps);
49
+ applySkinCssVars(canvasHost, skin.manifest);
50
+ });
51
+ let lastLens = state.activeLens;
52
+ subscribe((snapshot) => {
53
+ chrome.lensSelect.value = snapshot.activeLens;
54
+ if (snapshot.settings)
55
+ scene.setFpsCap(snapshot.ambient ? snapshot.settings.ambient.fps : Number(chrome.fpsSelect.value));
56
+ if (snapshot.activeLens !== lastLens) {
57
+ lastLens = snapshot.activeLens;
58
+ universeRenderer.applyLens(snapshot.activeLens);
59
+ lod.applyLens();
60
+ }
61
+ });
62
+ async function boot() {
63
+ const universe = await api.universe();
64
+ setSettings(universe.settings);
65
+ chrome.setSettings(universe.settings);
66
+ chrome.setRepos(universe.repos);
67
+ loadVisibleRepos(universe.repos.map((repo) => repo.repoId));
68
+ const skins = await api.skins().catch(() => ({ skins: [] }));
69
+ chrome.setSkins(skins.skins);
70
+ const rendered = [];
71
+ for (const repo of universe.repos.filter((item) => state.visibleRepos.has(item.repoId))) {
72
+ try {
73
+ const [clustersResponse, layout, edgeResponse] = await Promise.all([api.clusters(repo.repoId), api.layout(repo.repoId, "cluster"), api.clusterEdges(repo.repoId)]);
74
+ const clusters = clustersResponse.clusters ?? [];
75
+ universeRenderer.renderRepo(repo, clusters, layout);
76
+ universeRenderer.renderClusterEdges(repo.repoId, edgeResponse.edges ?? []);
77
+ rendered.push(repo);
78
+ }
79
+ catch (error) {
80
+ console.warn(`[viewer] skipping repo ${repo.repoId}`, error);
81
+ }
82
+ }
83
+ const home = rendered.reduce((best, repo) => (!best || repo.symbolCount > best.symbolCount ? repo : best), undefined);
84
+ if (home)
85
+ scene.flyTo(new THREE.Vector3(home.galaxy.position[0], home.galaxy.position[1], home.galaxy.position[2]), Math.max(420, home.galaxy.radius * 1.6));
86
+ else
87
+ scene.flyTo(new THREE.Vector3(0, 0, 0));
88
+ scene.onFrame(() => { void lod.maybeExpandNearest(); });
89
+ activity.start();
90
+ chrome.setStatus("live");
91
+ }
92
+ boot().catch((error) => { console.error("[viewer] boot failed", error); chrome.setStatus("error"); });
@@ -0,0 +1,45 @@
1
+ import * as THREE from "three";
2
+ import { setHovered, setSelection } from "./state.js";
3
+ export class PickingController {
4
+ viewer;
5
+ universe;
6
+ raycaster = new THREE.Raycaster();
7
+ pointer = new THREE.Vector2();
8
+ constructor(_api, viewer, universe) {
9
+ this.viewer = viewer;
10
+ this.universe = universe;
11
+ viewer.renderer.domElement.addEventListener("pointermove", this.onMove);
12
+ viewer.renderer.domElement.addEventListener("click", this.onClick);
13
+ }
14
+ pick(event) {
15
+ const rect = this.viewer.renderer.domElement.getBoundingClientRect();
16
+ this.pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
17
+ this.pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
18
+ this.raycaster.setFromCamera(this.pointer, this.viewer.camera);
19
+ return this.raycaster.intersectObjects(this.viewer.root.children, true)[0] ?? null;
20
+ }
21
+ onMove = (event) => {
22
+ const hit = this.pick(event);
23
+ setHovered(hit?.object.name ?? null);
24
+ };
25
+ onClick = async (event) => {
26
+ const hit = this.pick(event);
27
+ if (!hit) {
28
+ setSelection(null);
29
+ return;
30
+ }
31
+ if (hit.object.name.startsWith("clusters:")) {
32
+ const repoId = hit.object.name.slice("clusters:".length);
33
+ const instanceId = hit.instanceId ?? 0;
34
+ const cluster = this.universe.getClusters().filter((item) => item.repoId === repoId)[instanceId];
35
+ if (cluster) {
36
+ this.viewer.flyTo(cluster.position);
37
+ setSelection({ repoId, clusterId: cluster.clusterId });
38
+ }
39
+ }
40
+ };
41
+ dispose() {
42
+ this.viewer.renderer.domElement.removeEventListener("pointermove", this.onMove);
43
+ this.viewer.renderer.domElement.removeEventListener("click", this.onClick);
44
+ }
45
+ }