@volter/editor-core 0.5.57

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 (719) hide show
  1. package/BUNDLED_NOTICES +1527 -0
  2. package/LICENSE +686 -0
  3. package/LICENSE-APACHE +202 -0
  4. package/NOTICE +18 -0
  5. package/README.md +24 -0
  6. package/dist/build/chunk-JS4D4XZ3.js +175 -0
  7. package/dist/build/chunk-JS4D4XZ3.js.map +7 -0
  8. package/dist/build/vite-plugin-product-contributions.js +100 -0
  9. package/dist/build/vite-plugin-product-contributions.js.map +7 -0
  10. package/dist/build/vite-plugin-shared-react.js +25 -0
  11. package/dist/build/vite-plugin-shared-react.js.map +7 -0
  12. package/dist/build/vite-plugin-shared-three.js +86 -0
  13. package/dist/build/vite-plugin-shared-three.js.map +7 -0
  14. package/dist/server/chunk-KMJC7AGO.js +98 -0
  15. package/dist/server/chunk-KMJC7AGO.js.map +7 -0
  16. package/dist/server/chunk-PSILUMGS.js +46 -0
  17. package/dist/server/chunk-PSILUMGS.js.map +7 -0
  18. package/dist/server/frame-proxy.js +255 -0
  19. package/dist/server/frame-proxy.js.map +7 -0
  20. package/dist/server/open-browser.js +53 -0
  21. package/dist/server/open-browser.js.map +7 -0
  22. package/dist/server/process-shutdown.js +127 -0
  23. package/dist/server/process-shutdown.js.map +7 -0
  24. package/dist/server/session-registry.js +141 -0
  25. package/dist/server/session-registry.js.map +7 -0
  26. package/dist/server/spawn-opener.js +11 -0
  27. package/dist/server/spawn-opener.js.map +7 -0
  28. package/dist/server/worktree-identity.js +11 -0
  29. package/dist/server/worktree-identity.js.map +7 -0
  30. package/dist-server/packaged.mjs +88816 -0
  31. package/package.json +118 -0
  32. package/scripts/build-plugins.mjs +14 -0
  33. package/scripts/build-server.mjs +26 -0
  34. package/scripts/build-session.mjs +9 -0
  35. package/server/account-credentials.ts +60 -0
  36. package/server/account-service.ts +1434 -0
  37. package/server/adapter-region-includes.ts +47 -0
  38. package/server/asset-catalog-v2.ts +175 -0
  39. package/server/asset-history-snapshots.ts +196 -0
  40. package/server/asset-ledger-store.ts +195 -0
  41. package/server/asset-library-routes.ts +1595 -0
  42. package/server/blender-wasm-artifact.ts +430 -0
  43. package/server/boot-timings.ts +106 -0
  44. package/server/canonical-path.ts +50 -0
  45. package/server/checkout-workspace-preflight.ts +349 -0
  46. package/server/cloud-asset-catalog.ts +158 -0
  47. package/server/coding-inference-launch.ts +237 -0
  48. package/server/collaboration-account-client.ts +202 -0
  49. package/server/collaboration-attribution.ts +122 -0
  50. package/server/collaboration-session.ts +934 -0
  51. package/server/comfyui-bridge.ts +248 -0
  52. package/server/console-ledger.ts +499 -0
  53. package/server/creation-site-transform.ts +304 -0
  54. package/server/creation-site-write.ts +648 -0
  55. package/server/data-file-serialize.ts +158 -0
  56. package/server/editor-brand-html.ts +72 -0
  57. package/server/editor-control-socket.ts +366 -0
  58. package/server/editor-server-options.ts +190 -0
  59. package/server/editor-server.ts +1483 -0
  60. package/server/editor-sse.ts +352 -0
  61. package/server/engine-provenance.ts +169 -0
  62. package/server/engine-source-restart.ts +117 -0
  63. package/server/frame-bridge.ts +115 -0
  64. package/server/frame-proxy.ts +490 -0
  65. package/server/frame-workbench.ts +259 -0
  66. package/server/frontend-handoff.ts +229 -0
  67. package/server/game-globals-shadow.ts +113 -0
  68. package/server/gameplay-session-retention.ts +149 -0
  69. package/server/gameplay-sessions.ts +123 -0
  70. package/server/generation-jobs.ts +201 -0
  71. package/server/generation-reconciler.ts +137 -0
  72. package/server/generative-execution-context.ts +59 -0
  73. package/server/git-workflow.ts +612 -0
  74. package/server/harness-chat-caller.ts +36 -0
  75. package/server/harness-chat-service.ts +1681 -0
  76. package/server/idle-shutdown.ts +170 -0
  77. package/server/js-profiling-policy.ts +52 -0
  78. package/server/launcher-settings.ts +84 -0
  79. package/server/local-asset-catalog.ts +410 -0
  80. package/server/managed-account-defaults.ts +22 -0
  81. package/server/mock-control-plane.ts +540 -0
  82. package/server/model-import-conversion.ts +345 -0
  83. package/server/native-credential-store.ts +162 -0
  84. package/server/open-browser.ts +118 -0
  85. package/server/packaged.ts +1192 -0
  86. package/server/play-stall.ts +126 -0
  87. package/server/process-shutdown.ts +218 -0
  88. package/server/product-presets.ts +55 -0
  89. package/server/project-build-artifact.ts +95 -0
  90. package/server/project-components.ts +100 -0
  91. package/server/project-dependency-invalidation.ts +114 -0
  92. package/server/project-file-scan.ts +235 -0
  93. package/server/project-hmr-files.ts +215 -0
  94. package/server/project-install-roots.ts +210 -0
  95. package/server/project-kinds.ts +71 -0
  96. package/server/project-mcp-servers.ts +82 -0
  97. package/server/project-module-freshness.ts +109 -0
  98. package/server/project-module-instance.ts +375 -0
  99. package/server/project-optimize-deps-entries.ts +827 -0
  100. package/server/project-output-writer.ts +679 -0
  101. package/server/project-package-origin.ts +63 -0
  102. package/server/project-root-surface.ts +831 -0
  103. package/server/project-scratch-path.ts +47 -0
  104. package/server/project-script-hmr.ts +619 -0
  105. package/server/project-serving-plugins.ts +236 -0
  106. package/server/project-tools.ts +653 -0
  107. package/server/project-validation.ts +250 -0
  108. package/server/project-verbs.ts +127 -0
  109. package/server/project-view.ts +52 -0
  110. package/server/project-watch.ts +1134 -0
  111. package/server/project-work-coordinator.ts +288 -0
  112. package/server/provider-credentials.ts +331 -0
  113. package/server/recent-projects-store.ts +98 -0
  114. package/server/redact-secrets.ts +71 -0
  115. package/server/repository-presence.ts +267 -0
  116. package/server/routes/account.ts +357 -0
  117. package/server/routes/agents.ts +60 -0
  118. package/server/routes/assets.ts +154 -0
  119. package/server/routes/build.ts +13 -0
  120. package/server/routes/collaboration.ts +595 -0
  121. package/server/routes/configurations.ts +419 -0
  122. package/server/routes/context.ts +264 -0
  123. package/server/routes/control-plane.ts +1811 -0
  124. package/server/routes/logs.ts +272 -0
  125. package/server/routes/project-identity.ts +302 -0
  126. package/server/routes/project-open.ts +429 -0
  127. package/server/routes/project-source.ts +686 -0
  128. package/server/routes/project-state.ts +383 -0
  129. package/server/routes/relay.ts +1029 -0
  130. package/server/routes/route-helpers.ts +29 -0
  131. package/server/routes/served-modules.ts +138 -0
  132. package/server/routes/session-tabs.ts +196 -0
  133. package/server/routes/settings.ts +121 -0
  134. package/server/routes/share-control.ts +486 -0
  135. package/server/routes/themes.ts +116 -0
  136. package/server/routes/tools.ts +230 -0
  137. package/server/routes/worktrees.ts +595 -0
  138. package/server/scoped-game-css.ts +461 -0
  139. package/server/server-utils.ts +1194 -0
  140. package/server/session-product.ts +112 -0
  141. package/server/session-registry.ts +230 -0
  142. package/server/share-claims.ts +138 -0
  143. package/server/share-host.ts +526 -0
  144. package/server/share-session-gateway.ts +1362 -0
  145. package/server/share-tunnel.ts +123 -0
  146. package/server/spawn-opener.ts +74 -0
  147. package/server/support/play/log-format.ts +76 -0
  148. package/server/support/play/session-record.ts +199 -0
  149. package/server/support/project/build-discipline.ts +718 -0
  150. package/server/support/project/inspection-node.ts +119 -0
  151. package/server/support/project/log-naming.ts +54 -0
  152. package/server/support/project/provenance.ts +290 -0
  153. package/server/support/project/run-name.ts +18 -0
  154. package/server/support/project/session-journal.ts +830 -0
  155. package/server/support/project/shared.ts +327 -0
  156. package/server/tab-bootstrap.ts +62 -0
  157. package/server/tab-heartbeat.ts +477 -0
  158. package/server/tab-lifecycle.ts +550 -0
  159. package/server/tab-presence.ts +1508 -0
  160. package/server/team-agent-mirror.ts +95 -0
  161. package/server/twin-auth.ts +138 -0
  162. package/server/vendored-lock-recorder.ts +968 -0
  163. package/server/worktree-identity.ts +121 -0
  164. package/server/worktree-management.ts +398 -0
  165. package/server/worktree-retention.d.mts +13 -0
  166. package/server/worktree-retention.mjs +205 -0
  167. package/src/EditorContext.tsx +169 -0
  168. package/src/account.ts +333 -0
  169. package/src/action-registry.ts +532 -0
  170. package/src/active-product.ts +74 -0
  171. package/src/active-project.ts +155 -0
  172. package/src/adapter-editor-config.ts +25 -0
  173. package/src/adapter-observation.ts +49 -0
  174. package/src/animation/stage-transport.ts +379 -0
  175. package/src/animation/three-clips-subject.ts +172 -0
  176. package/src/api/asset-library-wire.ts +45 -0
  177. package/src/api/assets.ts +365 -0
  178. package/src/api/base.ts +10 -0
  179. package/src/api/build.ts +99 -0
  180. package/src/api/git-wire.ts +56 -0
  181. package/src/api/logs.ts +92 -0
  182. package/src/api/project-identity.ts +74 -0
  183. package/src/api/project-open.ts +355 -0
  184. package/src/api/project-source.ts +162 -0
  185. package/src/api/project-state.ts +107 -0
  186. package/src/api/relay.ts +268 -0
  187. package/src/api/settings.ts +36 -0
  188. package/src/api/themes.ts +45 -0
  189. package/src/api/worktrees.ts +257 -0
  190. package/src/asset-compare-core.ts +171 -0
  191. package/src/asset-compare.ts +294 -0
  192. package/src/asset-editor-context.tsx +100 -0
  193. package/src/asset-events.ts +97 -0
  194. package/src/asset-inspector-actions.ts +87 -0
  195. package/src/asset-preview-framing.ts +357 -0
  196. package/src/asset-preview.ts +2802 -0
  197. package/src/asset-selection-viewer-registry.ts +113 -0
  198. package/src/asset-selection.ts +133 -0
  199. package/src/asset-workflow/asset-capabilities.ts +344 -0
  200. package/src/asset-workflow/asset-import-jobs.ts +106 -0
  201. package/src/asset-workflow/asset-ledger-backend.ts +126 -0
  202. package/src/asset-workflow/asset-ledger.ts +156 -0
  203. package/src/asset-workflow/asset-materialization-report.ts +140 -0
  204. package/src/asset-workflow/asset-pack-manifest.ts +320 -0
  205. package/src/asset-workflow/asset-types.ts +142 -0
  206. package/src/asset-workflow/asset-workflow-quality.ts +61 -0
  207. package/src/asset-workflow/audio-preview-player.ts +193 -0
  208. package/src/asset-workflow/audio-waveform.ts +22 -0
  209. package/src/asset-workflow/cloud-asset-client.ts +263 -0
  210. package/src/asset-workflow/folder-preview.ts +345 -0
  211. package/src/asset-workflow/hosted-asset-materialization.ts +236 -0
  212. package/src/asset-workflow/image-view-scale.ts +32 -0
  213. package/src/asset-workflow/import-contract.ts +124 -0
  214. package/src/asset-workflow/ledger-write-lock.ts +244 -0
  215. package/src/asset-workflow/model-inspection.ts +818 -0
  216. package/src/asset-workflow/pixi-spritesheet.ts +197 -0
  217. package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
  218. package/src/asset-workflow/project-asset-commands.ts +23 -0
  219. package/src/asset-workflow/project-asset-health.ts +139 -0
  220. package/src/asset-workflow/project-asset-operations.ts +154 -0
  221. package/src/asset-workflow/project-asset-roots.ts +68 -0
  222. package/src/asset-workflow/project-content.ts +288 -0
  223. package/src/asset-workflow/project-source-index.ts +550 -0
  224. package/src/asset-workflow/thumbnail-system.ts +256 -0
  225. package/src/authoring/active-adapter.ts +199 -0
  226. package/src/authoring/active-systems.ts +422 -0
  227. package/src/authoring/adapter-key.ts +18 -0
  228. package/src/authoring/authoring-asset-url.ts +27 -0
  229. package/src/authoring/bootstrap-state.ts +49 -0
  230. package/src/authoring/boundary-authoring-adapter.ts +184 -0
  231. package/src/authoring/breakpoint-state.ts +43 -0
  232. package/src/authoring/canvas-scene-guides.ts +76 -0
  233. package/src/authoring/component-instance-root.ts +171 -0
  234. package/src/authoring/composite-authoring-adapter.ts +2110 -0
  235. package/src/authoring/consumer-actions.ts +520 -0
  236. package/src/authoring/css-numeric-style.ts +97 -0
  237. package/src/authoring/design-time-layers.ts +849 -0
  238. package/src/authoring/design-time-mount-registry.ts +235 -0
  239. package/src/authoring/design-time-settle.ts +343 -0
  240. package/src/authoring/edit-mode-authoring.ts +619 -0
  241. package/src/authoring/eyedropper-session.ts +60 -0
  242. package/src/authoring/instance-source-menu-register.ts +13 -0
  243. package/src/authoring/instance-source-menu.ts +135 -0
  244. package/src/authoring/layered-pick.ts +183 -0
  245. package/src/authoring/live-gesture-lock.ts +50 -0
  246. package/src/authoring/live-object-transform.ts +62 -0
  247. package/src/authoring/mount-failure-report.ts +154 -0
  248. package/src/authoring/mounted-root-subjects.ts +144 -0
  249. package/src/authoring/no-authoring-adapter.ts +55 -0
  250. package/src/authoring/null-inspection-subjects.tsx +75 -0
  251. package/src/authoring/object3d-document-persistence.ts +106 -0
  252. package/src/authoring/object3d-document-session-registry.ts +120 -0
  253. package/src/authoring/object3d-document-session.ts +1326 -0
  254. package/src/authoring/object3d-gesture-controller.ts +113 -0
  255. package/src/authoring/panel-authoring.ts +121 -0
  256. package/src/authoring/prefab-instance-inspector-section.tsx +237 -0
  257. package/src/authoring/prefab-instance-section-model.ts +55 -0
  258. package/src/authoring/project-authoring-session.ts +105 -0
  259. package/src/authoring/provenance.ts +99 -0
  260. package/src/authoring/quarks-particle-systems.ts +19 -0
  261. package/src/authoring/react-canvas-navigation.ts +255 -0
  262. package/src/authoring/react-design-canvas-style.ts +20 -0
  263. package/src/authoring/react-story-board.ts +937 -0
  264. package/src/authoring/selection-scope.ts +195 -0
  265. package/src/authoring/shell-document-ops.ts +169 -0
  266. package/src/authoring/shell-object3d-document-write-policy.ts +108 -0
  267. package/src/authoring/shell-viewport-policy.ts +48 -0
  268. package/src/authoring/source-object3d-authoring-adapter.ts +526 -0
  269. package/src/authoring/stories-scope.ts +35 -0
  270. package/src/authoring/story-board-chrome-fit.ts +107 -0
  271. package/src/authoring/story-board-presentation.ts +111 -0
  272. package/src/authoring/three-projection-core.ts +226 -0
  273. package/src/authoring/viewport-pick-context.ts +40 -0
  274. package/src/authoring/viewport-raycast.ts +240 -0
  275. package/src/authoring/viewport-tool-context.ts +73 -0
  276. package/src/authoring/world-canvas-viewport-state.ts +35 -0
  277. package/src/authoring/world-hidden-viewport.ts +152 -0
  278. package/src/authoring/world-pan-state.ts +198 -0
  279. package/src/authoring/world-session-state.ts +101 -0
  280. package/src/authoring/write-pipe.ts +173 -0
  281. package/src/availability-tick.ts +66 -0
  282. package/src/bitmap-label.ts +120 -0
  283. package/src/blender-tab-metrics.ts +161 -0
  284. package/src/board-open-actions.ts +20 -0
  285. package/src/boot-routing.ts +391 -0
  286. package/src/build-identity.ts +15 -0
  287. package/src/bytes-codec.ts +62 -0
  288. package/src/camera-authoring.ts +175 -0
  289. package/src/cancellation-reason.ts +58 -0
  290. package/src/canvas-preview-frames.ts +482 -0
  291. package/src/capture-camera-pose.ts +77 -0
  292. package/src/chrome-registry.ts +159 -0
  293. package/src/chrome-slot-registry.ts +91 -0
  294. package/src/collaboration-client.ts +264 -0
  295. package/src/collaboration-presence.ts +41 -0
  296. package/src/command-dispatch.ts +19 -0
  297. package/src/command-listener.ts +2743 -0
  298. package/src/command-registry.ts +70 -0
  299. package/src/component-board-registry.ts +205 -0
  300. package/src/component-states-registry.ts +200 -0
  301. package/src/components/AgentPresentationNotice.tsx +75 -0
  302. package/src/components/AlignToolbar.tsx +204 -0
  303. package/src/components/AppRoot.tsx +394 -0
  304. package/src/components/ApplicationMenus.tsx +415 -0
  305. package/src/components/AssetBrowser.tsx +2843 -0
  306. package/src/components/AssetEditorShell.tsx +216 -0
  307. package/src/components/AssetInspectorToolSection.tsx +124 -0
  308. package/src/components/BoardRulers.tsx +354 -0
  309. package/src/components/CameraInfo.tsx +81 -0
  310. package/src/components/CanvasSceneViewport.tsx +556 -0
  311. package/src/components/CapabilityCoverageSection.tsx +185 -0
  312. package/src/components/CenterDocuments.tsx +22 -0
  313. package/src/components/ChromeSlot.tsx +20 -0
  314. package/src/components/CodeView.tsx +470 -0
  315. package/src/components/CollaborationHeaderControl.css +74 -0
  316. package/src/components/CompactInspectorCard.tsx +190 -0
  317. package/src/components/CompactInspectorShell.tsx +39 -0
  318. package/src/components/ConsolePanel.css +55 -0
  319. package/src/components/ConsolePanel.tsx +270 -0
  320. package/src/components/DefaultEditorLayout.tsx +106 -0
  321. package/src/components/DocumentHeaderStrip.tsx +96 -0
  322. package/src/components/DocumentShelfRail.tsx +45 -0
  323. package/src/components/DocumentThumbnail.tsx +345 -0
  324. package/src/components/EditorLeaseGuard.tsx +372 -0
  325. package/src/components/ErrorBanner.tsx +123 -0
  326. package/src/components/FolderPreviewTile.tsx +159 -0
  327. package/src/components/GameHierarchy.tsx +3842 -0
  328. package/src/components/GameplaySessionTimeline.tsx +295 -0
  329. package/src/components/InspectionProjection.tsx +968 -0
  330. package/src/components/Inspector.tsx +233 -0
  331. package/src/components/InspectorCanvasPreview.tsx +35 -0
  332. package/src/components/InspectorFieldsSection.tsx +290 -0
  333. package/src/components/InspectorObjectPreview.tsx +57 -0
  334. package/src/components/InspectorStoriesSection.tsx +92 -0
  335. package/src/components/InspectorToolSection.tsx +96 -0
  336. package/src/components/InspectorTransformSection.tsx +245 -0
  337. package/src/components/LightExplorerPanel.tsx +433 -0
  338. package/src/components/MediaProperties.tsx +145 -0
  339. package/src/components/NonThreeAuthoringBootstrap.tsx +19 -0
  340. package/src/components/Object3DDocumentToolbar.css +100 -0
  341. package/src/components/Object3DDocumentToolbar.tsx +454 -0
  342. package/src/components/Object3DDocumentViewport.tsx +58 -0
  343. package/src/components/ProjectHeader.css +617 -0
  344. package/src/components/ProjectHeader.tsx +328 -0
  345. package/src/components/ProjectLayout.tsx +249 -0
  346. package/src/components/ReactCanvasControls.tsx +284 -0
  347. package/src/components/RootSelectionOverlay.tsx +3162 -0
  348. package/src/components/RootTextEditor.tsx +79 -0
  349. package/src/components/SaveStatus.tsx +70 -0
  350. package/src/components/StageHost.tsx +1954 -0
  351. package/src/components/StageOverlays.tsx +21 -0
  352. package/src/components/StartupErrorScreen.tsx +76 -0
  353. package/src/components/StartupLoadingScreen.tsx +65 -0
  354. package/src/components/StatsOverlay.tsx +78 -0
  355. package/src/components/SurfaceStateOverlay.tsx +24 -0
  356. package/src/components/ToolContributionSurfaces.tsx +65 -0
  357. package/src/components/ToolHost.tsx +370 -0
  358. package/src/components/ToolObject3DPreview.tsx +39 -0
  359. package/src/components/Toolbar.tsx +595 -0
  360. package/src/components/TransientHint.tsx +44 -0
  361. package/src/components/TransportStrip.tsx +174 -0
  362. package/src/components/VersionControlSection.tsx +470 -0
  363. package/src/components/VgaiLogo.css +83 -0
  364. package/src/components/VgaiLogo.tsx +35 -0
  365. package/src/components/ViewportControlsHint.tsx +60 -0
  366. package/src/components/ViewportFurniture.tsx +401 -0
  367. package/src/components/ViewportOverlay.tsx +145 -0
  368. package/src/components/ViewportOverlaysMenu.tsx +153 -0
  369. package/src/components/ViewportShadingMenu.tsx +300 -0
  370. package/src/components/ViewportViewMenu.tsx +101 -0
  371. package/src/components/WorkspaceDocumentSurface.tsx +204 -0
  372. package/src/components/WorkspaceUtilitySurface.tsx +22 -0
  373. package/src/components/WorktreeSwitcher.css +239 -0
  374. package/src/components/WorktreeSwitcher.tsx +890 -0
  375. package/src/components/account-documents.tsx +1162 -0
  376. package/src/components/asset-documents.tsx +812 -0
  377. package/src/components/asset-editor-persistence.ts +216 -0
  378. package/src/components/asset-selection-section.tsx +535 -0
  379. package/src/components/asset-thumbnails.tsx +312 -0
  380. package/src/components/asset-viewers/AudioViewer.tsx +201 -0
  381. package/src/components/asset-viewers/EntityModelDocument.tsx +122 -0
  382. package/src/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
  383. package/src/components/asset-viewers/GenericJsonViewer.tsx +102 -0
  384. package/src/components/asset-viewers/ImageViewer.tsx +300 -0
  385. package/src/components/asset-viewers/JsonAssetDocument.tsx +93 -0
  386. package/src/components/asset-viewers/LiveModuleDocument.tsx +417 -0
  387. package/src/components/asset-viewers/LutAssetDocument.tsx +444 -0
  388. package/src/components/asset-viewers/ModelAssetDocument.tsx +105 -0
  389. package/src/components/asset-viewers/Object3DPreview.tsx +359 -0
  390. package/src/components/asset-viewers/OnlineAssetDetail.tsx +421 -0
  391. package/src/components/asset-viewers/PasteboardModuleDocument.tsx +67 -0
  392. package/src/components/asset-viewers/QuarksAssetDocument.tsx +526 -0
  393. package/src/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
  394. package/src/components/asset-viewers/SourceAssetViewer.tsx +281 -0
  395. package/src/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
  396. package/src/components/asset-viewers/VideoViewer.tsx +101 -0
  397. package/src/components/asset-viewers/shader-source.ts +144 -0
  398. package/src/components/asset-workflow.css +731 -0
  399. package/src/components/board-guides.ts +150 -0
  400. package/src/components/compact-inspector.css +539 -0
  401. package/src/components/core-utilities.tsx +90 -0
  402. package/src/components/editor-notifications.css +40 -0
  403. package/src/components/engine-workspace.css +274 -0
  404. package/src/components/inspector-preview-section.tsx +238 -0
  405. package/src/components/inspector-property-grouping.ts +64 -0
  406. package/src/components/inspector-revert-label.ts +20 -0
  407. package/src/components/inspector-selection.ts +42 -0
  408. package/src/components/inspector-stories-gating.ts +171 -0
  409. package/src/components/inspector-transform-subject.ts +11 -0
  410. package/src/components/inspector-transform.ts +75 -0
  411. package/src/components/kind-documents.tsx +500 -0
  412. package/src/components/palette-action-publisher.tsx +140 -0
  413. package/src/components/primitives/DraftColorInput.tsx +74 -0
  414. package/src/components/product-shell.css +1829 -0
  415. package/src/components/project-tool-documents.tsx +410 -0
  416. package/src/components/scene-documents.tsx +223 -0
  417. package/src/components/stage-keyboard.tsx +37 -0
  418. package/src/components/stage-overlay-set.tsx +107 -0
  419. package/src/components/stage-presence-markers.ts +478 -0
  420. package/src/components/standard-viewport-dressing.ts +417 -0
  421. package/src/components/status-contributions.tsx +403 -0
  422. package/src/components/tool-documents.tsx +309 -0
  423. package/src/components/tool-schema-form.tsx +262 -0
  424. package/src/components/use-after-paint.ts +41 -0
  425. package/src/components/use-project-image-assets.ts +86 -0
  426. package/src/components/viewport-header-controls.css +90 -0
  427. package/src/components/viewport-surface-status.tsx +55 -0
  428. package/src/components/workspace-history.ts +32 -0
  429. package/src/components/workspace-static-panel-registry.tsx +152 -0
  430. package/src/components/workspace-surfaces.css +621 -0
  431. package/src/components/world-documents.tsx +586 -0
  432. package/src/components/world-overlay-gestures.ts +1694 -0
  433. package/src/composite-screenshot.ts +1641 -0
  434. package/src/console-sync.ts +131 -0
  435. package/src/constraint-helper.ts +338 -0
  436. package/src/content-entry-source-registry.ts +184 -0
  437. package/src/coverage/authoring-seam-evidence.ts +300 -0
  438. package/src/coverage/canvas-reveal.ts +192 -0
  439. package/src/coverage/design-time-surfaces.ts +101 -0
  440. package/src/coverage/live-seam-evidence.ts +11 -0
  441. package/src/coverage/ontology-invariants.ts +466 -0
  442. package/src/coverage/session-vitals.ts +501 -0
  443. package/src/coverage/system-seam-evidence.ts +72 -0
  444. package/src/crash-null-boundary.ts +36 -0
  445. package/src/creation-site-edit.ts +1479 -0
  446. package/src/creation-site-registry.ts +160 -0
  447. package/src/delegate-harness-registry.ts +143 -0
  448. package/src/document-context-registry.ts +177 -0
  449. package/src/document-open-registry.ts +200 -0
  450. package/src/document-preview-source.ts +20 -0
  451. package/src/document-renderer-session.ts +138 -0
  452. package/src/editor-api.ts +46 -0
  453. package/src/editor-chrome-capture.ts +139 -0
  454. package/src/editor-commands.ts +164 -0
  455. package/src/editor-console.ts +563 -0
  456. package/src/editor-current-view.ts +82 -0
  457. package/src/editor-document-probe.ts +881 -0
  458. package/src/editor-git-client.ts +115 -0
  459. package/src/editor-host-door.ts +462 -0
  460. package/src/editor-hotkeys.ts +842 -0
  461. package/src/editor-lease-view.ts +39 -0
  462. package/src/editor-lease.ts +415 -0
  463. package/src/editor-mode.ts +19 -0
  464. package/src/editor-notifications.ts +140 -0
  465. package/src/editor-presence.ts +563 -0
  466. package/src/editor-presentation-activity.ts +50 -0
  467. package/src/editor-presentation-notice.ts +42 -0
  468. package/src/editor-runtime.tsx +145 -0
  469. package/src/editor-server-response.ts +86 -0
  470. package/src/editor-session-attribution.ts +75 -0
  471. package/src/editor-session-mode.ts +54 -0
  472. package/src/editor-shell-store.ts +1304 -0
  473. package/src/editor-state-facets.ts +74 -0
  474. package/src/editor-styles.css +33 -0
  475. package/src/editor-view-presentation.ts +810 -0
  476. package/src/editor-viewport.ts +5302 -0
  477. package/src/entity-lod.ts +31 -0
  478. package/src/entity-object.ts +91 -0
  479. package/src/files/file-provider.ts +62 -0
  480. package/src/files/project-files.ts +264 -0
  481. package/src/finders/index.ts +136 -0
  482. package/src/finders/scenes-from-entrypoint-selection.ts +387 -0
  483. package/src/frame/bridge.tsx +1408 -0
  484. package/src/frame/product.ts +77 -0
  485. package/src/gameplay-dom-recording.ts +319 -0
  486. package/src/gameplay-export-state.ts +14 -0
  487. package/src/gameplay-replay.ts +417 -0
  488. package/src/gameplay-session-time.ts +9 -0
  489. package/src/gameplay-sessions.ts +204 -0
  490. package/src/harness-chat-types.ts +253 -0
  491. package/src/hierarchy-component-marks.ts +298 -0
  492. package/src/hierarchy-drop.ts +91 -0
  493. package/src/hierarchy-expansion-state.ts +80 -0
  494. package/src/hierarchy-header-slot.ts +52 -0
  495. package/src/hierarchy-internals.ts +197 -0
  496. package/src/hierarchy-kind-icon.ts +217 -0
  497. package/src/hierarchy-mark-reader.ts +73 -0
  498. package/src/hierarchy-menu-registry.ts +67 -0
  499. package/src/hierarchy-node-rows.ts +307 -0
  500. package/src/hierarchy-panel-view.ts +280 -0
  501. package/src/hierarchy-projection.ts +76 -0
  502. package/src/hierarchy-row-cache.ts +243 -0
  503. package/src/hierarchy-row-model.ts +308 -0
  504. package/src/hierarchy-rows.ts +11 -0
  505. package/src/hierarchy-walk.ts +86 -0
  506. package/src/history/editor-session.ts +25 -0
  507. package/src/history/history-commands.ts +147 -0
  508. package/src/history/history-delegate.ts +163 -0
  509. package/src/history/history-limit-notices.ts +43 -0
  510. package/src/history/history-service.ts +1173 -0
  511. package/src/history/persistence-coordinator.ts +35 -0
  512. package/src/history/project-file-history.ts +386 -0
  513. package/src/history/project-root-history-backends.ts +139 -0
  514. package/src/history/resource-registry.ts +209 -0
  515. package/src/history/snapshot-store.ts +103 -0
  516. package/src/history/source-history-backend.ts +546 -0
  517. package/src/history/types.ts +124 -0
  518. package/src/hmr-registration-group.ts +67 -0
  519. package/src/hmr-stable-react-context.ts +23 -0
  520. package/src/hotkeys.ts +188 -0
  521. package/src/inference-diagnostics.ts +69 -0
  522. package/src/initial-project.ts +80 -0
  523. package/src/inspection/active-subject.ts +578 -0
  524. package/src/inspection/active-surface.ts +142 -0
  525. package/src/inspection/compose.ts +1064 -0
  526. package/src/inspection/display.ts +170 -0
  527. package/src/inspection/document-subject.ts +109 -0
  528. package/src/inspection/game-subject.ts +85 -0
  529. package/src/inspection/model.ts +542 -0
  530. package/src/inspection/null-subject.ts +115 -0
  531. package/src/inspection/serialize.ts +357 -0
  532. package/src/inspection/use-active-inspection.ts +180 -0
  533. package/src/inspector-presentation.ts +201 -0
  534. package/src/inspector-section-registry.ts +221 -0
  535. package/src/instance-source-actions.ts +163 -0
  536. package/src/instanced-presentation.ts +164 -0
  537. package/src/js-heap.ts +71 -0
  538. package/src/key-actions.ts +91 -0
  539. package/src/keymap-presets.ts +401 -0
  540. package/src/layout-policy.ts +31 -0
  541. package/src/learn-links.ts +73 -0
  542. package/src/light-explorer-model.ts +134 -0
  543. package/src/live-canvas-frame.ts +55 -0
  544. package/src/live-document.ts +279 -0
  545. package/src/live-module-source.ts +230 -0
  546. package/src/live-session-registry.ts +220 -0
  547. package/src/live-transition.ts +633 -0
  548. package/src/manifest-project.ts +107 -0
  549. package/src/model-thumbnail.ts +523 -0
  550. package/src/native-selection-style.ts +202 -0
  551. package/src/object3d-document-write-policy.ts +137 -0
  552. package/src/packaged-runtime.ts +108 -0
  553. package/src/palettes/maya.palette.json +57 -0
  554. package/src/palettes/substance.palette.json +57 -0
  555. package/src/pasteboard-module.ts +81 -0
  556. package/src/performance-profiler.ts +367 -0
  557. package/src/performance-sources.ts +69 -0
  558. package/src/play-boot-phase.ts +145 -0
  559. package/src/presentation-surface.ts +248 -0
  560. package/src/project-adapter.ts +1143 -0
  561. package/src/project-asset-refresh.ts +26 -0
  562. package/src/project-declaration-refresh.ts +31 -0
  563. package/src/project-local-state.ts +118 -0
  564. package/src/project-manager.ts +243 -0
  565. package/src/project-module-changes.ts +163 -0
  566. package/src/project-module-split.ts +266 -0
  567. package/src/project-provenance.ts +115 -0
  568. package/src/project-ready.ts +42 -0
  569. package/src/project-session-reset.ts +67 -0
  570. package/src/project-shape.ts +68 -0
  571. package/src/project-tool-discovery.ts +66 -0
  572. package/src/project-tools.ts +107 -0
  573. package/src/project-work-types.ts +149 -0
  574. package/src/projection/three.ts +898 -0
  575. package/src/projection/types.ts +44 -0
  576. package/src/readiness.ts +113 -0
  577. package/src/reflection-probe-helper.ts +142 -0
  578. package/src/reported-play-state.ts +90 -0
  579. package/src/resolve-relative-specifier.ts +33 -0
  580. package/src/scene-document-plan.ts +315 -0
  581. package/src/scene-framing.ts +315 -0
  582. package/src/scene-live-open.ts +210 -0
  583. package/src/scene-view-fog.ts +89 -0
  584. package/src/scoped-game-css.ts +152 -0
  585. package/src/session-orphan-record.ts +193 -0
  586. package/src/session-tombstone.ts +126 -0
  587. package/src/settings/settings-provider.ts +82 -0
  588. package/src/settings-store.ts +345 -0
  589. package/src/shared-view-restore.ts +42 -0
  590. package/src/shell-store-door.ts +45 -0
  591. package/src/source-conflict.ts +122 -0
  592. package/src/spatial-handle-visuals.ts +332 -0
  593. package/src/stage-context.ts +341 -0
  594. package/src/stage-store-registry.ts +63 -0
  595. package/src/stale-chunk-recovery.ts +34 -0
  596. package/src/state-report-deferral.ts +73 -0
  597. package/src/storage/host-files-storage.ts +97 -0
  598. package/src/storage/http-storage.ts +174 -0
  599. package/src/storage/index.ts +75 -0
  600. package/src/storage/mem-storage.ts +149 -0
  601. package/src/storage/path-lock.ts +44 -0
  602. package/src/storage/paths.ts +26 -0
  603. package/src/storage/types.ts +116 -0
  604. package/src/stories/StoryComponentThumbnail.tsx +184 -0
  605. package/src/stories/StoryPreviewMount.tsx +306 -0
  606. package/src/stories/component-content-source.tsx +106 -0
  607. package/src/stories/component-states-source.ts +78 -0
  608. package/src/stories/compose-project-stories.ts +255 -0
  609. package/src/stories/pixi-story-model.ts +30 -0
  610. package/src/stories/prefabs-finder.ts +54 -0
  611. package/src/stories/prefabs-from-stories.ts +182 -0
  612. package/src/stories/project-story-discovery.ts +78 -0
  613. package/src/stories/project-story-regions.ts +24 -0
  614. package/src/stories/story-actions.ts +54 -0
  615. package/src/stories/story-capture-command.ts +124 -0
  616. package/src/stories/story-capture.ts +584 -0
  617. package/src/stories/story-declared-medium.ts +126 -0
  618. package/src/stories/story-discovery.ts +176 -0
  619. package/src/stories/story-document-openers.ts +36 -0
  620. package/src/stories/story-dom-runtime.ts +78 -0
  621. package/src/stories/story-grouping.ts +111 -0
  622. package/src/stories/story-lane.ts +88 -0
  623. package/src/stories/story-mount-turn.ts +27 -0
  624. package/src/stories/story-opener.ts +260 -0
  625. package/src/stories/story-pixi-preview.ts +408 -0
  626. package/src/stories/story-presentation.ts +215 -0
  627. package/src/stories/story-registry.ts +517 -0
  628. package/src/stories/story-three-preview.ts +807 -0
  629. package/src/stories/three-story-model.ts +96 -0
  630. package/src/story-three-preview-runtime.ts +56 -0
  631. package/src/surface-keyboard.ts +112 -0
  632. package/src/surface-state.ts +124 -0
  633. package/src/tab-bootstrap.js +628 -0
  634. package/src/tab-census.ts +198 -0
  635. package/src/tab-lifecycle-client.ts +196 -0
  636. package/src/theme-library.ts +844 -0
  637. package/src/theme-preference.ts +378 -0
  638. package/src/theme.css +3437 -0
  639. package/src/three-viewport/bone-selection-highlight.ts +119 -0
  640. package/src/three-viewport/camera-fit.ts +41 -0
  641. package/src/three-viewport/interactive-renderer.ts +132 -0
  642. package/src/three-viewport/selection-brackets.ts +310 -0
  643. package/src/three-viewport/selection-outline.ts +183 -0
  644. package/src/three-viewport/skeleton-helper.ts +61 -0
  645. package/src/three-viewport/source-color.ts +167 -0
  646. package/src/three-viewport/studio-environment.ts +96 -0
  647. package/src/three-viewport-presentation.ts +23 -0
  648. package/src/tool-contribution-play.ts +74 -0
  649. package/src/tool-loader.ts +1798 -0
  650. package/src/transform-mode-request.ts +57 -0
  651. package/src/transient-hint.ts +78 -0
  652. package/src/trigger-volume-helper.ts +116 -0
  653. package/src/ui-source/adapter-region-includes.ts +241 -0
  654. package/src/ui-source/ensure-import.ts +132 -0
  655. package/src/ui-source/file-region-resolver.ts +302 -0
  656. package/src/ui-source/inspect.ts +775 -0
  657. package/src/ui-source/oid-transform.ts +1845 -0
  658. package/src/ui-source/plan-csf-story.ts +196 -0
  659. package/src/ui-source/plan-extract-component.ts +421 -0
  660. package/src/ui-source/plan-fork-component.ts +621 -0
  661. package/src/ui-source/plan-named-style.ts +95 -0
  662. package/src/ui-source/plan-source-edit.ts +353 -0
  663. package/src/ui-source/r3f-contract-resolver.ts +327 -0
  664. package/src/ui-source/r3f-diagnostic-index.ts +165 -0
  665. package/src/ui-source/r3f-environment-binding.ts +133 -0
  666. package/src/ui-source/r3f-joint-binding.ts +251 -0
  667. package/src/ui-source/r3f-lod-binding.ts +91 -0
  668. package/src/ui-source/r3f-particle-binding.ts +235 -0
  669. package/src/ui-source/r3f-physics-binding.ts +483 -0
  670. package/src/ui-source/r3f-project-contracts.ts +444 -0
  671. package/src/ui-source/relative-import-specifier.ts +34 -0
  672. package/src/ui-source/reparent-guard.ts +544 -0
  673. package/src/ui-source/source-edit-request.ts +97 -0
  674. package/src/ui-source/source-write-backend.ts +618 -0
  675. package/src/ui-source/syntactic-prop-specs.ts +296 -0
  676. package/src/ui-source/tier-source-write-backend.ts +279 -0
  677. package/src/ui-source/ts-ast.ts +171 -0
  678. package/src/ui-source/utility-class-support.ts +169 -0
  679. package/src/ui-source/write-component-default.ts +133 -0
  680. package/src/ui-source/writer.ts +1989 -0
  681. package/src/viewport-activation-timings.ts +840 -0
  682. package/src/viewport-authoring-policy.ts +186 -0
  683. package/src/viewport-controls-hint.ts +57 -0
  684. package/src/viewport-door.ts +237 -0
  685. package/src/viewport-shading-boundary.ts +12 -0
  686. package/src/vite-error-surface.ts +141 -0
  687. package/src/wait-until.ts +37 -0
  688. package/src/workspace-areas.ts +156 -0
  689. package/src/workspace-aux-commands.ts +11 -0
  690. package/src/workspace-available-documents.ts +127 -0
  691. package/src/workspace-core-utilities.ts +31 -0
  692. package/src/workspace-document-ids.ts +59 -0
  693. package/src/workspace-document-registry.ts +612 -0
  694. package/src/workspace-document-restore.ts +145 -0
  695. package/src/workspace-host-commands.ts +141 -0
  696. package/src/workspace-persistence-gate.ts +40 -0
  697. package/src/workspace-play-utilities.ts +44 -0
  698. package/src/workspace-presets.ts +425 -0
  699. package/src/workspace-regions.ts +294 -0
  700. package/src/workspace-state-persistence.ts +536 -0
  701. package/src/workspace-static-panels.ts +73 -0
  702. package/src/workspace-status-registry.ts +121 -0
  703. package/src/workspace-style.ts +239 -0
  704. package/src/workspace-utility-commands.ts +74 -0
  705. package/src/workspace-utility-registry.ts +263 -0
  706. package/src/workspace-viewport-rect.ts +97 -0
  707. package/src/world-adoption.ts +115 -0
  708. package/src/world-document-routing.ts +104 -0
  709. package/vite-plugin-creation-site-write.ts +184 -0
  710. package/vite-plugin-creation-site.ts +80 -0
  711. package/vite-plugin-game-static.ts +303 -0
  712. package/vite-plugin-module-doorways.ts +336 -0
  713. package/vite-plugin-product-contributions.ts +197 -0
  714. package/vite-plugin-project-game-static.ts +125 -0
  715. package/vite-plugin-project-jsx-js.ts +65 -0
  716. package/vite-plugin-project-root-absolute-assets.ts +359 -0
  717. package/vite-plugin-shared-react.ts +420 -0
  718. package/vite-plugin-shared-three.ts +203 -0
  719. package/vite-plugin-ui-oid.ts +2106 -0
@@ -0,0 +1,1798 @@
1
+ /**
2
+ * Browser loader for editor contribution modules. Callable discovery/execution
3
+ * stays Node-side; this module imports the contribution modules the server
4
+ * FOUND BY SCANNING (`server/project-tools.ts`) and reads each one's own
5
+ * declaration — `point`, `title`/`presentations`, and the `tool` it drives.
6
+ *
7
+ * Nothing about a contribution is written down anywhere but the module.
8
+ */
9
+
10
+ import type { IconDefinition } from '@fortawesome/fontawesome-svg-core';
11
+ import { faScrewdriverWrench } from '@fortawesome/free-solid-svg-icons';
12
+ import { EditorClient } from '@volter/editor-sdk';
13
+ import type {
14
+ ToolAnalyticsContributionProps,
15
+ ToolAssetInspectorContributionMatch,
16
+ ToolAssetInspectorContributionProps,
17
+ ToolContributionNode,
18
+ ToolContributionProps,
19
+ ToolGenerationResultContributionMatch,
20
+ ToolGenerationResultContributionProps,
21
+ ToolInspectorContributionMatch,
22
+ ToolInspectorContributionProps,
23
+ ToolUtilityContributionProps,
24
+ } from '@volter/editor-sdk/contributions';
25
+ import { registerProjectModuleLoader } from '@volter/editor-sdk/contributions';
26
+ import {
27
+ chromeContributionKind,
28
+ isCommandContribution,
29
+ isConfigurationKindContribution,
30
+ isEditorLanePath,
31
+ isFinderContribution,
32
+ isServiceContribution,
33
+ lookContributionKind,
34
+ } from '@volter/editor-sdk/session/tool-contribution-convention';
35
+ import type { AuthoringAdapter, EditorNode } from '@volter/editor-project/adapter';
36
+ import type { ToolContributionPoint } from '@volter/editor-sdk/project-tool-catalog';
37
+ import { type ComponentType, createElement } from 'react';
38
+ import { getCurrentProject } from './active-project';
39
+ import {
40
+ isContributableMenu,
41
+ registerContributedActions,
42
+ registerContributedHeaderItem,
43
+ registerContributedMenu,
44
+ } from './chrome-registry';
45
+ import { contributedCommandRows, registerContributedCommands } from './command-registry';
46
+ import { InspectorToolSection } from './components/InspectorToolSection';
47
+ import { ToolHost } from './components/ToolHost';
48
+ import { editorServerJson } from './editor-server-response';
49
+ import { CONTRIBUTED_SECTION_ORDER } from './inspection/model';
50
+ import {
51
+ type InspectorSectionIcon,
52
+ type InspectorSectionMatchContext,
53
+ registerInspectorSections,
54
+ } from './inspector-section-registry';
55
+ import { registerContributedKeymap, reportUnavailableKeymap } from './keymap-presets';
56
+ import { projectMounts } from './project-shape';
57
+ import type { ProjectToolCatalog, ProjectToolCatalogEntry } from './project-tools';
58
+ import { publishPlayUtilitiesReady } from './workspace-play-utilities';
59
+ import { registerContributedWorkspace } from './workspace-presets';
60
+ import { registerWorkspaceStatus } from './workspace-status-registry';
61
+ import { registerContributedStyle } from './workspace-style';
62
+ import { registerWorkspaceUtility } from './workspace-utility-registry';
63
+
64
+ /**
65
+ * A SUBJECT-DEPENDENT GLYPH NAME (see {@link LoadedToolContributionBase.icon}).
66
+ * `adapter` is the active authoring adapter, deliberately `unknown` here for
67
+ * the same reason it is at the contribution point: a contribution reads its
68
+ * own adapter's native API and the host fabricates no projection of it.
69
+ */
70
+ export type ToolContributionIconResolver = (
71
+ node: ToolContributionNode | null,
72
+ adapter: unknown,
73
+ ) => string;
74
+
75
+ interface LoadedToolContributionBase {
76
+ /**
77
+ * Path-derived contribution id (`humanoid-builder.document`), or the
78
+ * presentation id for a many-mount module. It used to be prefixed with the
79
+ * owning tool's name because contributions were nested under one; they are
80
+ * not, so the id is simply its own.
81
+ */
82
+ id: string;
83
+ title: string;
84
+ file: string;
85
+ version: number;
86
+ /**
87
+ * `export const icon = 'properties-data'` — the NAME of the glyph that
88
+ * stands for this contribution wherever the host shows one. A name, so the
89
+ * active icon set paints it (and may tint it by category); the host's
90
+ * generic tool glyph when omitted. Read by `selection.inspector` today.
91
+ *
92
+ * IT MAY ALSO BE A FUNCTION OF THE SUBJECT, and that is Blender's own shape
93
+ * rather than a generalization: the Properties editor's Object Data tab
94
+ * draws a DIFFERENT mark per object type — `buttons_context_compute`
95
+ * (`space_buttons/buttons_context.cc:795-810`) sets `sbuts->dataicon` from
96
+ * `RNA_struct_ui_icon(ptr->type)`, so a mesh's tab is `MESH_DATA`, an
97
+ * armature's `ARMATURE_DATA` and a light's `OUTLINER_DATA_LIGHT`. A static
98
+ * export cannot say that. The resolver is handed the same `(node, adapter)`
99
+ * a `selection.inspector` matcher gets and answers a glyph NAME; the host
100
+ * reads either shape (`inspection/compose.ts`).
101
+ */
102
+ icon?: string | ToolContributionIconResolver;
103
+ /**
104
+ * THE PACKAGE (or project folder) THIS MODULE SHIPPED IN, derived from
105
+ * `file`. It is what makes "a document's package owns its Properties rail"
106
+ * answerable: the composer keeps the sections whose owner is the active
107
+ * document's own package and stands every other one down
108
+ * (`inspection/compose.ts`, `ComposeInspectionInput.rail`).
109
+ */
110
+ owner: string;
111
+ /**
112
+ * `export const railGroup = 'scene'` — which GROUP of the Properties rail
113
+ * this section belongs to. Blender's own rail is grouped, not a flat strip:
114
+ * `ED_buttons_tabs_list` (`space_buttons/space_buttons.cc:201-255`) calls
115
+ * `add_spacer()` between the tool tab, the scene group (Render, Output, View
116
+ * Layer, Scene, World), Collection, the object group (Object … Material) and
117
+ * Texture, and each spacer appends a `BCONTEXT_SEPARATOR` the rail draws as
118
+ * a gap. The name is free and compared for equality only; consecutive
119
+ * sections sharing one form a group, and the presentation rules between
120
+ * groups (`components/InspectionProjection.tsx`). Read by
121
+ * `selection.inspector` alone, like `order` and `icon`.
122
+ */
123
+ railGroup?: string;
124
+ /**
125
+ * `export const railDefault = true` — this is the tab the Properties rail
126
+ * OPENS ON when nothing has been chosen for this subject yet. Blender's
127
+ * Properties editor does not open on its first tab: the context is stored
128
+ * per screen in the startup file, and at factory settings
129
+ * `bpy.data.screens['Layout']` reads `OBJECT` while the rail's first tab is
130
+ * Render (measured against Blender 5.2 LTS, walk 5 parity row 3). Without a
131
+ * declaration the presentation falls back to the first tab, which is what
132
+ * every non-Blender rail wants. Read by `selection.inspector` alone, like
133
+ * `order`, `icon` and `railGroup`.
134
+ */
135
+ railDefault?: boolean;
136
+ /**
137
+ * `export const order = 2` — where this contribution sits among its
138
+ * siblings, low first, INSIDE the contributed band
139
+ * ({@link contributedSectionOrder}). Read where sibling sequence is a real
140
+ * question, which today is `selection.inspector` alone.
141
+ *
142
+ * OPTIONAL, and a module that declares none sits BEHIND every module that
143
+ * did, keeping its discovery position among the other silent ones:
144
+ * declaring is how a module claims a place, and staying silent is how it
145
+ * takes whatever is left. Range 0…{@link MAX_DECLARED_ORDER}, so no
146
+ * declaration can climb out of the band and over a built-in block.
147
+ *
148
+ * It exists because the discovery scan is alphabetical, which is an
149
+ * accident of filenames rather than a statement about the surface: the
150
+ * dev-tools capability's four facet sections are Stats, Cheats, Tuning,
151
+ * Autopilot in the ruling that created them, and would otherwise mount
152
+ * Autopilot → Cheats → Stats → Tuning. A module states its own position
153
+ * because it is the only thing that knows it.
154
+ */
155
+ order?: number;
156
+ }
157
+
158
+ /**
159
+ * The points that DRIVE a callable, and therefore must name one.
160
+ *
161
+ * `tool` lives here rather than on the base because the PRESENTING points do
162
+ * not: {@link UtilityToolContribution}, {@link InspectorToolContribution},
163
+ * and {@link GlobalToolContribution} (a document may be a pure view — a
164
+ * game's Data Book over its own src/data modules). Keeping it off the base
165
+ * is what makes the relaxation apply to exactly those, instead of quietly
166
+ * making every consumer of every other point handle `undefined`.
167
+ */
168
+ interface CallableToolContributionBase extends LoadedToolContributionBase {
169
+ /**
170
+ * The catalog entry the module NAMED (`export const tool = 'project.x.y'`),
171
+ * resolved here. The editor still needs the whole entry — generation role and
172
+ * provider group the generative documents, summary/description search them —
173
+ * so resolution happens once, at load, rather than at every consumer.
174
+ */
175
+ tool: ProjectToolCatalogEntry;
176
+ }
177
+
178
+ export interface GlobalToolContribution extends LoadedToolContributionBase {
179
+ point: 'workspace.document';
180
+ /** Present when the document DRIVES a callable; absent for a pure view. */
181
+ tool?: ProjectToolCatalogEntry;
182
+ Component: ComponentType<ToolContributionProps>;
183
+ /**
184
+ * `export const standing = true` — this document is a PLACE in the project,
185
+ * not an errand: the editor opens it with the project and it is not
186
+ * closeable, exactly like the `3D` / `UI` / `Dev` boards. The default is an
187
+ * action-opened document you reach from the command palette.
188
+ *
189
+ * A capability that owns a whole authoring surface (the `data-tables` tab is
190
+ * the shipped case) declares this; a one-off runner does not.
191
+ */
192
+ standing: boolean;
193
+ /** `export const documentKind = 'model'` — this document is THE EDITOR for
194
+ * every adapter-table document of that kind (`ToolDocumentEntry`): a
195
+ * Content entry of the kind opens as its own document mounting this
196
+ * contribution, never as its source. */
197
+ documentKind?: string;
198
+ /** `export const Toolbar` — the document's header, rendered in the host's
199
+ * own strip (`WorkspaceDocumentSurface`). See `ToolDocumentToolbar`. */
200
+ Toolbar?: ComponentType<ToolContributionProps>;
201
+ /** `export const Shelf` — the document's tool rail (`DocumentShelfRail`). */
202
+ Shelf?: ComponentType<ToolContributionProps>;
203
+ /**
204
+ * `export const inspectorRail = 'owned'` — A DOCUMENT'S PACKAGE OWNS ITS
205
+ * PROPERTIES RAIL (owner ruling, WORK.md §Blender in the tab is Blender,
206
+ * "Inspection parity", I2 decision 1).
207
+ *
208
+ * On a `model` document the rail is Blender's alone. Blender's Properties
209
+ * editor draws the tabs `ED_buttons_tabs_list` returns AND NOTHING ELSE, so
210
+ * a rail that also carries the host's Preview, Transform, Object, Geometry
211
+ * and Materials blocks beside them is not Blender's rail — it is Blender's
212
+ * rail plus another editor's. Declaring this stands the host's own sections
213
+ * down: the composer keeps only the sections contributed by THIS
214
+ * contribution's own package ({@link LoadedToolContributionBase.owner}),
215
+ * plus whatever built-ins {@link inspectorBuiltins} names by id.
216
+ *
217
+ * It is a DOCUMENT's declaration rather than a section's because the
218
+ * question is "what does this editor show", which no single section knows.
219
+ * A document that declares nothing is unchanged: every section composes.
220
+ */
221
+ inspectorRail?: 'owned';
222
+ /**
223
+ * `export const inspectorBuiltins = ['preview']` — the host built-ins this
224
+ * document keeps beside its package's own sections, BY ID
225
+ * (`inspection/model.ts`'s `PREVIEW_SECTION_ID`, `TRANSFORM_SECTION_ID`,
226
+ * `PROPERTIES_SECTION_ID`, `STORIES_SECTION_ID`, a `group:<id>`). Read only
227
+ * with `inspectorRail = 'owned'`; empty or absent means none.
228
+ */
229
+ inspectorBuiltins?: readonly string[];
230
+ }
231
+
232
+ /**
233
+ * A project contribution that mounts as a TAB IN THE BOTTOM DRAWER
234
+ * (`workspace-utility-registry.ts`), in that registry's `project` cluster.
235
+ *
236
+ * The split from {@link GlobalToolContribution} is the drawer/center split the
237
+ * registries already draw: a document is a substantial editor you work IN, a
238
+ * utility is a transient output/record surface you glance at while working
239
+ * somewhere else. So there is no `standing` here — a utility's tab is its own
240
+ * presence, and the drawer already owns whether it is revealed.
241
+ */
242
+ export interface UtilityToolContribution extends LoadedToolContributionBase {
243
+ point: 'workspace.utility';
244
+ Component: ComponentType<ToolUtilityContributionProps>;
245
+ /**
246
+ * `export const available = () => …` — session-scoped availability, the
247
+ * registry's own optional gate (`workspace-utility-registry.ts`): a tab
248
+ * over a live adapter is on offer only while that adapter is registered.
249
+ * Absent means always available.
250
+ */
251
+ available?: () => boolean;
252
+ /**
253
+ * OPTIONAL here, as it is on a {@link InspectorToolContribution}, and
254
+ * required at the three points that RUN (`workspace.document`,
255
+ * `asset.inspector`, `generation.result`): a record-reading panel drives no
256
+ * single callable, so demanding one bought only a false association.
257
+ * Declaring a real one is still allowed and still resolved.
258
+ */
259
+ tool?: ProjectToolCatalogEntry;
260
+ }
261
+
262
+ export interface AnalyticsToolContribution extends LoadedToolContributionBase {
263
+ point: 'workspace.analytics';
264
+ Component: ComponentType<ToolAnalyticsContributionProps>;
265
+ tool?: ProjectToolCatalogEntry;
266
+ }
267
+
268
+ /**
269
+ * A STATUS-BAR item (`workspace-status-registry.ts`): compact, passive,
270
+ * registered under `tool:<id>` like a drawer tab. `export const align` picks
271
+ * the bar's side (default left); `order` is the slot order within it. Never
272
+ * mounted through `ToolHost` — it is a bare component the status bar renders.
273
+ */
274
+ export interface StatusToolContribution extends LoadedToolContributionBase {
275
+ point: 'workspace.status';
276
+ Component: ComponentType;
277
+ align: 'left' | 'right';
278
+ }
279
+
280
+ /** The two points the editor mounts as a WHOLE React surface, and therefore
281
+ * the two `ToolHost` renders (`components/ToolHost.tsx`). */
282
+ export type SurfaceToolContribution =
283
+ | GlobalToolContribution
284
+ | UtilityToolContribution
285
+ | AnalyticsToolContribution;
286
+
287
+ type DrawerToolContribution = UtilityToolContribution | AnalyticsToolContribution;
288
+
289
+ export interface InspectorToolContribution extends LoadedToolContributionBase {
290
+ point: 'selection.inspector';
291
+ Component: ComponentType<ToolInspectorContributionProps>;
292
+ match: ToolInspectorContributionMatch;
293
+ /**
294
+ * OPTIONAL, like a utility's: a section may present state the editor already
295
+ * has — live readings, verbs a game registered as debug commands — and drive
296
+ * no single registered callable. Demanding one there bought only a false
297
+ * association. A section that DOES commit through a callable still declares
298
+ * it and still gets the resolved entry.
299
+ */
300
+ tool?: ProjectToolCatalogEntry;
301
+ }
302
+
303
+ export interface AssetInspectorToolContribution extends LoadedToolContributionBase {
304
+ point: 'asset.inspector';
305
+ Component: ComponentType<ToolAssetInspectorContributionProps>;
306
+ match: ToolAssetInspectorContributionMatch;
307
+ /** OPTIONAL, like a selection inspector's: a section presents what an ASSET
308
+ * is, and one that declares no callable is still a section. */
309
+ tool?: ProjectToolCatalogEntry;
310
+ }
311
+
312
+ export interface GenerationResultToolContribution extends CallableToolContributionBase {
313
+ point: 'generation.result';
314
+ Component: ComponentType<ToolGenerationResultContributionProps>;
315
+ match: ToolGenerationResultContributionMatch;
316
+ }
317
+
318
+ export type LoadedToolContribution =
319
+ | GlobalToolContribution
320
+ | UtilityToolContribution
321
+ | AnalyticsToolContribution
322
+ | InspectorToolContribution
323
+ | AssetInspectorToolContribution
324
+ | GenerationResultToolContribution
325
+ | StatusToolContribution;
326
+
327
+ const TEMPLATE_EXAMPLE = 'src/contributions/my-tool.document.tsx';
328
+
329
+ /**
330
+ * Every point a contribution module may declare — the whole vocabulary, and
331
+ * the teaching error's own list, so the two cannot disagree.
332
+ *
333
+ * It must stay equal to `@vgai/sdk`'s `ToolContributionPoint` union and to the
334
+ * server scan's `TOOL_CONTRIBUTION_SUFFIXES` (`server/server-utils.ts`): a
335
+ * point the scan finds and this loader rejects is a module that loads,
336
+ * teaching-errors, and mounts nowhere.
337
+ */
338
+ const CONTRIBUTION_POINTS: readonly ToolContributionPoint[] = [
339
+ 'workspace.document',
340
+ 'workspace.utility',
341
+ 'workspace.analytics',
342
+ 'workspace.status',
343
+ 'selection.inspector',
344
+ 'asset.inspector',
345
+ 'generation.result',
346
+ ];
347
+
348
+ /**
349
+ * One PRESENTATION of a contribution module.
350
+ *
351
+ * A module is usually mounted once and needs none of this. But one module may
352
+ * legitimately appear many times under different identities — fal's document is
353
+ * seven workspace documents ('generate-image', 'generate-audio', …) and switches
354
+ * on `contributionId` to pick its endpoint. That fan-out belongs to the module,
355
+ * next to the map it must already agree with, not to a manifest that would
356
+ * restate it.
357
+ */
358
+ export interface ToolPresentation {
359
+ id: string;
360
+ title: string;
361
+ }
362
+
363
+ /** A module's declared presentations, or `undefined` when it mounts once. */
364
+ export function readToolPresentations(mod: unknown): ToolPresentation[] | undefined {
365
+ const declared = (mod as Record<string, unknown> | null)?.['presentations'];
366
+ if (!Array.isArray(declared) || declared.length === 0) return undefined;
367
+ const presentations: ToolPresentation[] = [];
368
+ for (const entry of declared) {
369
+ const record = entry as Record<string, unknown>;
370
+ if (typeof record?.['id'] !== 'string' || typeof record['title'] !== 'string') return undefined;
371
+ presentations.push({ id: record['id'], title: record['title'] });
372
+ }
373
+ return presentations;
374
+ }
375
+
376
+ /** `src/contributions/humanoid-builder.document.tsx` -> `humanoid-builder.document`. */
377
+ function contributionIdFromPath(entryPath: string): string {
378
+ const base = entryPath.split(/[\\/]/).pop() ?? entryPath;
379
+ return base.replace(/\.[jt]sx?$/, '');
380
+ }
381
+
382
+ /**
383
+ * One contribution module that did not load, kept as STATE rather than only as
384
+ * a console line.
385
+ *
386
+ * The console line existed already and is captured (`editor-console.ts`'s
387
+ * session-lifetime wrapper puts it in `vgai status`'s `sessionErrors`). What
388
+ * did not exist was a RECORD any consumer could read, so every downstream
389
+ * refusal was blind: `editor.present` answered "Tool document is not
390
+ * registered: data-tables.document" while the real answer — its capability's
391
+ * npm dependency was never installed — sat in a console nobody was asked to
392
+ * read (measured live, 2026-08-14, data-tables + missing `react-data-grid`).
393
+ */
394
+ export interface ToolContributionLoadFailure {
395
+ /** The project-relative module the catalog scan found. */
396
+ entryPath: string;
397
+ /** The id that module WOULD have claimed, derived from its path. A module
398
+ * declaring `presentations` claims other ids too, so a lookup that misses
399
+ * here still reports the whole failure list rather than nothing. */
400
+ id: string;
401
+ /** The underlying import error, stringified. */
402
+ error: string;
403
+ }
404
+
405
+ /**
406
+ * The highest `order` a module may declare. The inspector's built-in blocks
407
+ * are spaced 1000 apart (`inspection/model.ts`), so the contributed band has
408
+ * exactly that much room: keeping every declaration inside it is what stops
409
+ * `export const order = 9000` from quietly outranking Transform.
410
+ */
411
+ const MAX_DECLARED_ORDER = 998;
412
+
413
+ /** Where a module that declared no `order` lands: the back of the band. */
414
+ const UNDECLARED_ORDER = 999;
415
+
416
+ /** The rejected value, as the teaching line names it. `JSON.stringify` turns
417
+ * both NaN and Infinity into `"null"`, which is the one thing a message about
418
+ * a bad number must not say. */
419
+ function describeOrder(value: unknown): string {
420
+ return typeof value === 'number' ? String(value) : JSON.stringify(value);
421
+ }
422
+
423
+ function isDeclarableOrder(value: unknown): value is number {
424
+ return (
425
+ typeof value === 'number' &&
426
+ Number.isInteger(value) &&
427
+ value >= 0 &&
428
+ value <= MAX_DECLARED_ORDER
429
+ );
430
+ }
431
+
432
+ /**
433
+ * A contributed section's absolute order — the contributed band's base plus
434
+ * what the module declared, or the back of the band when it declared nothing.
435
+ *
436
+ * Pure, and the ONE place the "declared first, silent last" contract is
437
+ * spelled, so the docblock on `order` above and the composer's sort cannot
438
+ * describe two different arrangements.
439
+ */
440
+ export function contributedSectionOrder(declared: number | undefined): number {
441
+ return CONTRIBUTED_SECTION_ORDER + (declared ?? UNDECLARED_ORDER);
442
+ }
443
+
444
+ /**
445
+ * Shape one failed contribution import into the record and the teaching line —
446
+ * the pure half of the loudness fix, so the wording is testable without a DOM.
447
+ */
448
+ export function describeContributionLoadFailure(
449
+ entryPath: string,
450
+ error: unknown,
451
+ ): { failure: ToolContributionLoadFailure; message: string } {
452
+ const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
453
+ return {
454
+ failure: { entryPath, id: contributionIdFromPath(entryPath), error: detail },
455
+ message:
456
+ `[tool contributions] failed to load ${entryPath}; fix the module, or rename it out of ` +
457
+ `the *.document/*.utility/*.inspector/*.result convention if it is not a contribution.\n${detail}`,
458
+ };
459
+ }
460
+
461
+ /**
462
+ * Name the failed loads in a refusal that would otherwise name nothing.
463
+ * `id` is the contribution the caller asked for; when a failure claims it, that
464
+ * one failure is the answer, and when none does the whole list is — a module
465
+ * that fans out over `presentations` fails before its ids are readable.
466
+ * Returns `''` when nothing failed, so the caller's own message stands alone.
467
+ */
468
+ export function contributionFailureHint(id: string): string {
469
+ const failures = getToolContributionLoadFailures();
470
+ if (failures.length === 0) return '';
471
+ const owner = failures.find((failure) => failure.id === id);
472
+ if (owner) return ` ${owner.entryPath} failed to load: ${owner.error}`;
473
+ return ` ${failures.length} contribution module(s) failed to load: ${failures
474
+ .map((failure) => `${failure.entryPath} (${failure.error})`)
475
+ .join('; ')}`;
476
+ }
477
+
478
+ /**
479
+ * Read a contribution module's OWN declaration.
480
+ *
481
+ * Nothing is reconciled here, because there is no second source to reconcile
482
+ * against: the module exports `point`, `title` (or `presentations`), and the
483
+ * NAME of the tool it drives, and the scan only says the file exists. The tool
484
+ * name is resolved against `tools` — a name no registered callable answers is a
485
+ * teaching error, the same class as a missing default export. The `match`
486
+ * requirement still varies by point, because a `match` export alone is
487
+ * ambiguous between the inspector and result points.
488
+ */
489
+ /**
490
+ * Resolve a contribution's declared `tool`, enforcing the point's contract.
491
+ *
492
+ * Three points PRESENT rather than RUN, so none has an honest name to put
493
+ * here: a `workspace.utility` reads a record (a log, a run timeline), a
494
+ * `selection.inspector` renders a facet of the inspected subject, and a
495
+ * `workspace.document` may be a pure view (a game's Data Book rendering its
496
+ * own src/data modules — the zero-magic collaboration surface). Requiring a
497
+ * callable at any of these bought nothing but a false association: a surface
498
+ * declaring a tool it never runs. The generation points drive exactly one
499
+ * callable, so the teaching error stands there.
500
+ */
501
+ function resolveDeclaredTool(
502
+ record: Record<string, unknown>,
503
+ point: ToolContributionPoint,
504
+ tools: readonly ProjectToolCatalogEntry[],
505
+ file: string,
506
+ ): { tool: ProjectToolCatalogEntry | undefined } | { error: string; note?: boolean } {
507
+ const declaredTool = record['tool'];
508
+ const toolIsOptional =
509
+ point === 'workspace.utility' ||
510
+ point === 'workspace.analytics' ||
511
+ point === 'workspace.status' ||
512
+ point === 'selection.inspector' ||
513
+ point === 'asset.inspector' ||
514
+ point === 'workspace.document';
515
+ if (typeof declaredTool !== 'string' || declaredTool.trim() === '') {
516
+ if (!toolIsOptional || declaredTool !== undefined) {
517
+ return {
518
+ error: toolIsOptional
519
+ ? `[tool contributions] ${file} exports a \`tool\` that is not a non-empty string. A ` +
520
+ `'${point}' may omit it entirely; it may not declare a broken one. Skipped.`
521
+ : `[tool contributions] ${file} must \`export const tool\` — the name of the registered ` +
522
+ `tool it drives, e.g. \`export const tool = 'project.tuning';\` ` +
523
+ `(see ${TEMPLATE_EXAMPLE}). Skipped.`,
524
+ };
525
+ }
526
+ }
527
+ const tool =
528
+ typeof declaredTool === 'string'
529
+ ? tools.find((entry) => entry.name === declaredTool)
530
+ : undefined;
531
+ if (typeof declaredTool === 'string' && !tool) {
532
+ return {
533
+ error:
534
+ `[tool contributions] ${file} drives ${JSON.stringify(declaredTool)}, which no ` +
535
+ 'registered tool declares. Register it in package.json#vgai.tools, or fix the name. ' +
536
+ 'Skipped.',
537
+ };
538
+ }
539
+ return { tool };
540
+ }
541
+
542
+ export function extractProjectToolContribution(
543
+ mod: unknown,
544
+ file: string,
545
+ version: number,
546
+ tools: readonly ProjectToolCatalogEntry[],
547
+ presentation?: ToolPresentation,
548
+ ): { contribution: LoadedToolContribution } | { error: string } {
549
+ const record = (mod ?? {}) as Record<string, unknown>;
550
+ const Component = record['default'];
551
+ if (typeof Component !== 'function') {
552
+ return {
553
+ error:
554
+ `[tool contributions] ${file} has no default-exported React component ` +
555
+ `(see ${TEMPLATE_EXAMPLE}). Skipped.`,
556
+ };
557
+ }
558
+ // POINT FIRST, because it decides whether a `tool` is required at all.
559
+ const point = record['point'];
560
+ if (typeof point !== 'string' || !CONTRIBUTION_POINTS.includes(point as ToolContributionPoint)) {
561
+ return {
562
+ error:
563
+ `[tool contributions] ${file} must \`export const point\` — one of ` +
564
+ `${CONTRIBUTION_POINTS.map((value) => `'${value}'`).join(', ')} ` +
565
+ `(see ${TEMPLATE_EXAMPLE}). Skipped.`,
566
+ };
567
+ }
568
+ const resolvedTool = resolveDeclaredTool(record, point as ToolContributionPoint, tools, file);
569
+ if ('error' in resolvedTool) return resolvedTool;
570
+ const tool = resolvedTool.tool;
571
+ const declaredTitle = presentation?.title ?? record['title'];
572
+ if (typeof declaredTitle !== 'string' || declaredTitle.trim() === '') {
573
+ return {
574
+ error:
575
+ `[tool contributions] ${file} must \`export const title\` (non-empty), or list its ` +
576
+ 'presentations in `export const presentations`. Skipped.',
577
+ };
578
+ }
579
+ const declaredOrder = record['order'];
580
+ // `typeof NaN === 'number'` and so does `-1`, and both are silent: `NaN`
581
+ // poisons the composer's `a.order - b.order` comparator, and a negative one
582
+ // lifts the contribution over built-in blocks it was never meant to
583
+ // outrank. A loader that teaches on every other malformed export teaches
584
+ // here too, rather than clamping something the module plainly meant.
585
+ if (declaredOrder !== undefined && !isDeclarableOrder(declaredOrder)) {
586
+ return {
587
+ error:
588
+ `[tool contributions] ${file} exports an \`order\` of ${describeOrder(declaredOrder)}. ` +
589
+ `It is an optional whole number from 0 to ${MAX_DECLARED_ORDER} — low first, e.g. ` +
590
+ '`export const order = 2;` — and a module that omits it sits behind every module that ' +
591
+ 'declared one. Skipped.',
592
+ };
593
+ }
594
+ const declaredIcon = record['icon'];
595
+ if (
596
+ declaredIcon !== undefined &&
597
+ typeof declaredIcon !== 'function' &&
598
+ (typeof declaredIcon !== 'string' || declaredIcon.trim() === '')
599
+ ) {
600
+ return {
601
+ error:
602
+ `[tool contributions] ${file} exports an \`icon\` that is not a glyph name. It is an ` +
603
+ 'optional non-empty string naming a glyph in the active icon set — e.g. ' +
604
+ "`export const icon = 'properties-data';` — or a FUNCTION of the inspected subject " +
605
+ "answering one, e.g. `export const icon = (node, adapter) => 'properties-data-mesh';`. " +
606
+ 'Skipped.',
607
+ };
608
+ }
609
+ /** A SECTION'S RAIL GROUP (`selection.inspector`): the Properties
610
+ * presentation draws a separator between groups, the way
611
+ * `ED_buttons_tabs_list` (`space_buttons/space_buttons.cc:201-255`) inserts
612
+ * `BCONTEXT_SEPARATOR` between its tab groups. A free name, compared for
613
+ * equality only; sections that declare none form one trailing group. */
614
+ const declaredRailGroup = record['railGroup'];
615
+ if (
616
+ declaredRailGroup !== undefined &&
617
+ (typeof declaredRailGroup !== 'string' || declaredRailGroup.trim() === '')
618
+ ) {
619
+ return {
620
+ error:
621
+ `[tool contributions] ${file} exports a \`railGroup\` that is not a non-empty string. ` +
622
+ 'It names the rail group this section belongs to — e.g. `export const railGroup = ' +
623
+ "'scene';` — and the presentation rules between groups. Skipped.",
624
+ };
625
+ }
626
+ /** A SECTION'S RAIL DEFAULT (`selection.inspector`): the tab the Properties
627
+ * presentation opens on before a person has chosen one — Blender's stored
628
+ * `SpaceProperties.context`, which at factory settings is OBJECT and not
629
+ * the rail's first tab. */
630
+ const declaredRailDefault = record['railDefault'];
631
+ if (declaredRailDefault !== undefined && typeof declaredRailDefault !== 'boolean') {
632
+ return {
633
+ error:
634
+ `[tool contributions] ${file} exports a \`railDefault\` that is not a boolean. ` +
635
+ 'It says this is the tab the Properties rail opens on — `export const railDefault = ' +
636
+ 'true;` — and nothing else takes a value. Skipped.',
637
+ };
638
+ }
639
+ const base = {
640
+ id: presentation?.id ?? contributionIdFromPath(file),
641
+ title: declaredTitle.trim(),
642
+ file,
643
+ // The package (or project folder) this module shipped in — see
644
+ // `LoadedToolContributionBase.owner`.
645
+ owner: packageNameOf(file),
646
+ version,
647
+ ...(declaredOrder === undefined ? {} : { order: declaredOrder }),
648
+ ...(declaredIcon === undefined
649
+ ? {}
650
+ : {
651
+ icon:
652
+ typeof declaredIcon === 'function'
653
+ ? (declaredIcon as ToolContributionIconResolver)
654
+ : declaredIcon.trim(),
655
+ }),
656
+ ...(typeof declaredRailGroup === 'string' ? { railGroup: declaredRailGroup.trim() } : {}),
657
+ ...(declaredRailDefault === true ? { railDefault: true } : {}),
658
+ };
659
+ // The two points with no required callable, returned before `tool` is
660
+ // narrowed below.
661
+ if (point === 'workspace.utility') {
662
+ const available = record['available'];
663
+ if (available !== undefined && typeof available !== 'function') {
664
+ return {
665
+ error:
666
+ `[tool contributions] ${file} exports an \`available\` that is not a function. It is ` +
667
+ 'optional: `export const available = () => …` gates the tab on session state. Skipped.',
668
+ };
669
+ }
670
+ return {
671
+ contribution: {
672
+ ...base,
673
+ point,
674
+ ...(tool ? { tool } : {}),
675
+ ...(available ? { available: available as () => boolean } : {}),
676
+ Component: Component as ComponentType<ToolUtilityContributionProps>,
677
+ },
678
+ };
679
+ }
680
+ if (point === 'workspace.status') {
681
+ const align = record['align'] ?? 'left';
682
+ if (align !== 'left' && align !== 'right') {
683
+ return {
684
+ error:
685
+ `[tool contributions] ${file} exports an \`align\` of ${JSON.stringify(align)}; a status ` +
686
+ "item's align is 'left' (default) or 'right'. Skipped.",
687
+ };
688
+ }
689
+ return {
690
+ contribution: {
691
+ ...base,
692
+ point,
693
+ align,
694
+ Component: Component as ComponentType,
695
+ },
696
+ };
697
+ }
698
+ if (point === 'workspace.analytics') {
699
+ return {
700
+ contribution: {
701
+ ...base,
702
+ point,
703
+ ...(tool ? { tool } : {}),
704
+ Component: Component as ComponentType<ToolAnalyticsContributionProps>,
705
+ },
706
+ };
707
+ }
708
+ if (point === 'selection.inspector') {
709
+ const match = record['match'];
710
+ if (typeof match !== 'function') {
711
+ return {
712
+ error:
713
+ `[tool contributions] ${file} declares 'selection.inspector' but does not export ` +
714
+ '`match(node, adapter)`. Skipped.',
715
+ };
716
+ }
717
+ return {
718
+ contribution: {
719
+ ...base,
720
+ point,
721
+ ...(tool ? { tool } : {}),
722
+ Component: Component as ComponentType<ToolInspectorContributionProps>,
723
+ match: match as ToolInspectorContributionMatch,
724
+ },
725
+ };
726
+ }
727
+ if (point === 'workspace.document') {
728
+ // The third presenting point: a pure-view document (a game's Data Book)
729
+ // carries no tool; one that DRIVES a callable still names it above.
730
+ const toolbar = record['Toolbar'];
731
+ if (toolbar !== undefined && typeof toolbar !== 'function') {
732
+ return {
733
+ error:
734
+ `[tool contributions] ${file} exports 'Toolbar' but it is not a component. ` +
735
+ 'A document header is `export const Toolbar = (props) => …`. Skipped.',
736
+ };
737
+ }
738
+ const documentKind = record['documentKind'];
739
+ if (documentKind !== undefined && (typeof documentKind !== 'string' || documentKind === '')) {
740
+ return {
741
+ error:
742
+ `[tool contributions] ${file} exports 'documentKind' but it is not a non-empty string. ` +
743
+ "The kind a document edits is `export const documentKind = 'model'`. Skipped.",
744
+ };
745
+ }
746
+ const shelf = record['Shelf'];
747
+ if (shelf !== undefined && typeof shelf !== 'function') {
748
+ return {
749
+ error:
750
+ `[tool contributions] ${file} exports 'Shelf' but it is not a component. ` +
751
+ 'A document shelf is `export const Shelf = (props) => …`. Skipped.',
752
+ };
753
+ }
754
+ const inspectorRail = record['inspectorRail'];
755
+ if (inspectorRail !== undefined && inspectorRail !== 'owned') {
756
+ return {
757
+ error:
758
+ `[tool contributions] ${file} exports an \`inspectorRail\` of ` +
759
+ `${JSON.stringify(inspectorRail)}. The one value is 'owned' — this document's package ` +
760
+ "owns its Properties rail (`export const inspectorRail = 'owned';`). Skipped.",
761
+ };
762
+ }
763
+ const inspectorBuiltins = record['inspectorBuiltins'];
764
+ if (
765
+ inspectorBuiltins !== undefined &&
766
+ (!Array.isArray(inspectorBuiltins) ||
767
+ inspectorBuiltins.some((id) => typeof id !== 'string' || id.trim() === ''))
768
+ ) {
769
+ return {
770
+ error:
771
+ `[tool contributions] ${file} exports an \`inspectorBuiltins\` that is not a list of ` +
772
+ "section ids — e.g. `export const inspectorBuiltins = ['preview'];`. Skipped.",
773
+ };
774
+ }
775
+ return {
776
+ contribution: {
777
+ ...base,
778
+ point,
779
+ ...(tool ? { tool } : {}),
780
+ Component: Component as ComponentType<ToolContributionProps>,
781
+ standing: record['standing'] === true,
782
+ ...(typeof documentKind === 'string' ? { documentKind } : {}),
783
+ ...(toolbar ? { Toolbar: toolbar as ComponentType<ToolContributionProps> } : {}),
784
+ ...(shelf ? { Shelf: shelf as ComponentType<ToolContributionProps> } : {}),
785
+ ...(inspectorRail === 'owned' ? { inspectorRail } : {}),
786
+ ...(Array.isArray(inspectorBuiltins)
787
+ ? { inspectorBuiltins: inspectorBuiltins as readonly string[] }
788
+ : {}),
789
+ },
790
+ };
791
+ }
792
+ if (point === 'asset.inspector') {
793
+ // Presents an ASSET, the way a selection inspector presents a node: the
794
+ // callable is optional.
795
+ const match = record['match'];
796
+ if (typeof match !== 'function') {
797
+ return {
798
+ error:
799
+ `[tool contributions] ${file} declares 'asset.inspector' but does not export ` +
800
+ '`match(asset)`. Skipped.',
801
+ };
802
+ }
803
+ return {
804
+ contribution: {
805
+ ...base,
806
+ point,
807
+ ...(tool ? { tool } : {}),
808
+ Component: Component as ComponentType<ToolAssetInspectorContributionProps>,
809
+ match: match as ToolAssetInspectorContributionMatch,
810
+ },
811
+ };
812
+ }
813
+ if (!tool) {
814
+ return {
815
+ error:
816
+ `[tool contributions] ${file} must \`export const tool\` — the name of the registered ` +
817
+ `tool it drives, e.g. \`export const tool = 'project.tuning';\` ` +
818
+ `(see ${TEMPLATE_EXAMPLE}). Skipped.`,
819
+ };
820
+ }
821
+ const callable = { ...base, tool };
822
+ if (point === 'generation.result') {
823
+ const match = record['match'];
824
+ if (typeof match !== 'function') {
825
+ return {
826
+ error:
827
+ `[tool contributions] ${file} declares 'generation.result' but does not export ` +
828
+ '`match(job, result)`. Skipped.',
829
+ };
830
+ }
831
+ return {
832
+ contribution: {
833
+ ...callable,
834
+ point,
835
+ Component: Component as ComponentType<ToolGenerationResultContributionProps>,
836
+ match: match as ToolGenerationResultContributionMatch,
837
+ },
838
+ };
839
+ }
840
+ return {
841
+ error: `[tool contributions] ${file} declares unhandled point '${point}'. Skipped.`,
842
+ };
843
+ }
844
+
845
+ /**
846
+ * THE BUNDLED PACKAGES — what the PRODUCT composed into this page, whatever the
847
+ * open project declares (ARCHITECTURE-CORE §The universal editor).
848
+ *
849
+ * The kit names none of them (rule 1). A product's entry is the only writer:
850
+ * `product({ packages: { '@volter/editor-blender': blender, … } })`
851
+ * (`frame/product.ts`) hands over the lists it imported as
852
+ * `vgai:contributions/<package>` modules, each row synthesized from that
853
+ * package's own `package.json#vgai.contributions`
854
+ * (`vite-plugin-product-contributions.ts`).
855
+ *
856
+ * The SESSION is what decides which of these actually mount: its catalog
857
+ * (`server/project-tools.ts`, `packageContributionModules`) lists the product's
858
+ * composed packages BY SPECIFIER — exactly the key this map is built on, which
859
+ * is how a bundled module is imported from the page instead of fetched a second
860
+ * time through `/@fs/` — and the open project's own declared packages by
861
+ * absolute path. A package on both lists is listed once, as the product's.
862
+ *
863
+ * Filled at the product entry's module scope, before its `mountVgai` can be
864
+ * called, so no refresh pass can see a half-composed page.
865
+ */
866
+ // The product registers once at module evaluation. A loader-only HMR update
867
+ // must retain that composition; otherwise package specifiers fall through to
868
+ // project-relative /@fs/ URLs and every bundled contribution fails to load.
869
+ const bundledPackageLoaders: Map<string, () => Promise<unknown>> =
870
+ import.meta.hot?.data['bundledPackageLoaders'] ?? new Map();
871
+ if (import.meta.hot) {
872
+ import.meta.hot.dispose((data) => {
873
+ data['bundledPackageLoaders'] = bundledPackageLoaders;
874
+ });
875
+ }
876
+
877
+ /** One contribution a product bundles. The shape
878
+ * `vite-plugin-product-contributions.ts` emits and this module consumes. */
879
+ export interface BundledPackageContribution {
880
+ readonly entryPath: string;
881
+ readonly load: () => Promise<unknown>;
882
+ }
883
+
884
+ /** The product's composition, handed over by `frame/product.ts`. */
885
+ export function setBundledPackageContributions(
886
+ contributions: readonly BundledPackageContribution[],
887
+ ): void {
888
+ bundledPackageLoaders.clear();
889
+ for (const entry of contributions) bundledPackageLoaders.set(entry.entryPath, entry.load);
890
+ }
891
+
892
+ let globalContributions: GlobalToolContribution[] = [];
893
+ let utilityContributions: DrawerToolContribution[] = [];
894
+ let surfaceContributions: SurfaceToolContribution[] = [];
895
+ let assetInspectorContributions: AssetInspectorToolContribution[] = [];
896
+ let generationResultContributions: GenerationResultToolContribution[] = [];
897
+ let contributionLoadFailures: ToolContributionLoadFailure[] = [];
898
+ const listeners = new Set<() => void>();
899
+ let contributionRefreshEpoch = 0;
900
+
901
+ /** The contribution modules the last discovery pass could not import. Read by
902
+ * the refusals that would otherwise name nothing, and by the Project Tools
903
+ * document, which lists them beside the server's own `loadErrors`. */
904
+ export function getToolContributionLoadFailures(): ToolContributionLoadFailure[] {
905
+ return contributionLoadFailures;
906
+ }
907
+
908
+ export function getGlobalToolContributions(): GlobalToolContribution[] {
909
+ return globalContributions;
910
+ }
911
+
912
+ /**
913
+ * Every contribution the editor mounts as a WHOLE React surface — the center
914
+ * documents and the drawer utilities together, documents first.
915
+ *
916
+ * It exists because a project utility's TAB has to have a door. A drawer
917
+ * registration is `visibleByDefault: false` (a project tab does not force
918
+ * itself into a clean layout), so nothing presents it until a command asks:
919
+ * this is the list the Tools menu enumerates, and `showWorkspaceUtility` is
920
+ * what it calls. Reading `getGlobalToolContributions` there instead — which is
921
+ * documents-only — is a contribution that loads, registers, and can never be
922
+ * opened by the human the drawer exists for.
923
+ *
924
+ * A STABLE array, rebuilt once per load pass when both halves have been
925
+ * written, because it is a `useSyncExternalStore` snapshot (`Object.is`; a
926
+ * fresh array per call loops).
927
+ */
928
+ export function getSurfaceToolContributions(): SurfaceToolContribution[] {
929
+ return surfaceContributions;
930
+ }
931
+
932
+ export function getDocumentToolContributions(): GlobalToolContribution[] {
933
+ return globalContributions.filter((item) => item.point === 'workspace.document');
934
+ }
935
+
936
+ /** The document contribution registered as the editor for table documents of
937
+ * `kind` (`export const documentKind`), or `undefined` when none is. */
938
+ export function documentContributionForKind(kind: string): GlobalToolContribution | undefined {
939
+ return globalContributions.find(
940
+ (item): item is GlobalToolContribution =>
941
+ item.point === 'workspace.document' && item.documentKind === kind,
942
+ );
943
+ }
944
+
945
+ export function getAssetInspectorToolContributions(): AssetInspectorToolContribution[] {
946
+ return assetInspectorContributions;
947
+ }
948
+
949
+ export function getGenerationResultContribution(
950
+ pollTool: string,
951
+ job: import('@volter/editor-sdk/generations').GenerationJob,
952
+ result: unknown,
953
+ ): GenerationResultToolContribution | undefined {
954
+ const matches: GenerationResultToolContribution[] = [];
955
+ for (const item of generationResultContributions) {
956
+ if (item.tool.name !== pollTool) continue;
957
+ try {
958
+ if (item.match(job, result)) matches.push(item);
959
+ } catch (error) {
960
+ teachingError(
961
+ `[tool contributions] ${item.file}'s generation result match() threw.\n${String(error)}`,
962
+ );
963
+ }
964
+ }
965
+ if (matches.length > 1) {
966
+ teachingError(
967
+ `[tool contributions] generation result for ${JSON.stringify(job.operation)} matched ` +
968
+ `${matches.length} renderers (${matches.map((item) => item.id).join(', ')}). ` +
969
+ 'Falling back to native JSON until the matchers are unambiguous.',
970
+ );
971
+ return undefined;
972
+ }
973
+ return matches[0];
974
+ }
975
+
976
+ export function subscribeToolContributions(fn: () => void): () => void {
977
+ listeners.add(fn);
978
+ return () => listeners.delete(fn);
979
+ }
980
+
981
+ let editorClient: EditorClient | null = null;
982
+
983
+ export function getToolContributionClient(): EditorClient {
984
+ if (!editorClient) {
985
+ const origin = globalThis.location?.origin;
986
+ editorClient = new EditorClient({ ...(origin ? { url: origin } : {}) });
987
+ }
988
+ return editorClient;
989
+ }
990
+
991
+ let unregisterInspectorContributions: Array<() => void> = [];
992
+
993
+ /** A declared glyph NAME as the icon registry wants it: the wrench drawing
994
+ * under that name, so a set carrying the name paints its own glyph and one
995
+ * that does not still draws something sensible. No name is the generic
996
+ * project-tool wrench. */
997
+ function glyph(name: string | undefined): IconDefinition {
998
+ return name
999
+ ? { ...faScrewdriverWrench, iconName: name as IconDefinition['iconName'] }
1000
+ : faScrewdriverWrench;
1001
+ }
1002
+
1003
+ function registrationForInspectorContribution(item: InspectorToolContribution): () => void {
1004
+ let warned = false;
1005
+ const teachOnce = (what: string, error: unknown): void => {
1006
+ if (warned) return;
1007
+ warned = true;
1008
+ teachingError(
1009
+ `[tool contributions] ${item.file}'s ${what} threw; ` +
1010
+ `treating it as absent until the file is fixed and saved.\n${String(error)}`,
1011
+ );
1012
+ };
1013
+ // A FUNCTION `icon` is resolved PER SUBJECT, so Blender's Object Data tab
1014
+ // can draw the mark its object type earns
1015
+ // (`LoadedToolContributionBase.icon`). It is wrapped the way `match` is:
1016
+ // project code that throws teaches once and falls back to the generic
1017
+ // glyph, rather than taking the whole composition down.
1018
+ const declaredIcon = item.icon;
1019
+ const icon: InspectorSectionIcon =
1020
+ typeof declaredIcon === 'function'
1021
+ ? (node, adapter) => {
1022
+ try {
1023
+ return glyph(declaredIcon(node, adapter));
1024
+ } catch (error) {
1025
+ teachOnce('icon()', error);
1026
+ return glyph(undefined);
1027
+ }
1028
+ }
1029
+ : glyph(declaredIcon);
1030
+ return registerInspectorSections({
1031
+ match: (
1032
+ node: EditorNode | null,
1033
+ adapter: AuthoringAdapter,
1034
+ context: InspectorSectionMatchContext,
1035
+ ) => {
1036
+ try {
1037
+ // The match context travels UNCHANGED to project code: it is how a
1038
+ // contribution scopes itself to one empty-state subject (the play
1039
+ // surface's Game) instead of every surface's.
1040
+ return item.match(node, adapter, context);
1041
+ } catch (error) {
1042
+ teachOnce('match()', error);
1043
+ return false;
1044
+ }
1045
+ },
1046
+ // A project tool's section id is namespaced by its contribution id, so two
1047
+ // tools never collide with each other or with a built-in block's id.
1048
+ id: `tool:${item.id}`,
1049
+ title: item.title,
1050
+ // The contribution's OWN glyph by name when it declared one — the icon
1051
+ // set paints it and may tint it by category — else a project TOOL's
1052
+ // generic glyph. Only the NAME changes: the drawing beneath stays the
1053
+ // wrench, so a set that lacks the name still draws something sensible.
1054
+ icon,
1055
+ // The package this section shipped in — what a document's OWNED rail
1056
+ // filters by (`inspection/compose.ts`, `ComposeInspectionInput.rail`).
1057
+ owner: item.owner,
1058
+ // Which group of the rail it belongs to, and so where the separators fall.
1059
+ ...(item.railGroup === undefined ? {} : { railGroup: item.railGroup }),
1060
+ // The tab the rail opens on when nothing has been chosen for this subject.
1061
+ ...(item.railDefault === undefined ? {} : { railDefault: item.railDefault }),
1062
+ // Contributions sit after every built-in block, and among THEMSELVES in
1063
+ // the order each module declared (`export const order`). Without it the
1064
+ // arrangement is the discovery scan's alphabetical accident — a facet set
1065
+ // whose sequence is part of its meaning cannot be left to filenames.
1066
+ order: contributedSectionOrder(item.order),
1067
+ Section: ({ adapter, nodeId }) =>
1068
+ createElement(InspectorToolSection, {
1069
+ id: item.id,
1070
+ title: item.title,
1071
+ file: item.file,
1072
+ ...(item.tool ? { tool: item.tool } : {}),
1073
+ Component: item.Component,
1074
+ adapter,
1075
+ nodeId,
1076
+ }),
1077
+ });
1078
+ }
1079
+
1080
+ function applyInspectorContributions(items: InspectorToolContribution[]): void {
1081
+ for (const unregister of unregisterInspectorContributions) unregister();
1082
+ unregisterInspectorContributions = items.map(registrationForInspectorContribution);
1083
+ }
1084
+
1085
+ let unregisterUtilityContributions: Array<() => void> = [];
1086
+ let unregisterStatusContributions: Array<() => void> = [];
1087
+
1088
+ /** A look contribution as loaded: which point, and the module's one export. */
1089
+ interface LookModule {
1090
+ readonly entryPath: string;
1091
+ readonly kind: 'layout' | 'keymap' | 'style';
1092
+ readonly module: unknown;
1093
+ }
1094
+ let unregisterLookContributions: Array<() => void> = [];
1095
+
1096
+ interface CommandModule {
1097
+ readonly entryPath: string;
1098
+ readonly module: unknown;
1099
+ }
1100
+
1101
+ interface ServiceModule {
1102
+ readonly entryPath: string;
1103
+ readonly module: unknown;
1104
+ }
1105
+ let stopServices: Array<() => void> = [];
1106
+
1107
+ /** Start the contributed SERVICES (`@volter/editor-sdk/services`) after a
1108
+ * pass; the previous pass's are stopped first. A service that throws on
1109
+ * start teaches and is skipped; one that throws on stop is reported and the
1110
+ * rest still stop. */
1111
+ function applyServiceContributions(items: readonly ServiceModule[]): void {
1112
+ for (const stop of stopServices) {
1113
+ try {
1114
+ stop();
1115
+ } catch (error) {
1116
+ teachingError(`[tool contributions] a service could not stop.\n${String(error)}`);
1117
+ }
1118
+ }
1119
+ stopServices = [];
1120
+ for (const item of items) {
1121
+ const record = (item.module ?? {}) as Record<string, unknown>;
1122
+ if (record['point'] !== 'workspace.service') {
1123
+ teachingError(
1124
+ `[tool contributions] ${item.entryPath} must \`export const point = 'workspace.service'\` ` +
1125
+ '(its filename names that point). Skipped.',
1126
+ );
1127
+ continue;
1128
+ }
1129
+ const start = record['start'];
1130
+ if (typeof start !== 'function') {
1131
+ teachingError(
1132
+ `[tool contributions] ${item.entryPath} must \`export function start()\` returning its stop ` +
1133
+ '(@volter/editor-sdk/services). Skipped.',
1134
+ );
1135
+ continue;
1136
+ }
1137
+ try {
1138
+ const stop = (start as () => unknown)();
1139
+ if (typeof stop === 'function') stopServices.push(stop as () => void);
1140
+ } catch (error) {
1141
+ teachingError(`[tool contributions] ${item.entryPath} could not start.\n${String(error)}`);
1142
+ }
1143
+ }
1144
+ }
1145
+
1146
+ interface ChromeModule {
1147
+ readonly entryPath: string;
1148
+ readonly kind: 'action' | 'menu' | 'header';
1149
+ readonly module: unknown;
1150
+ }
1151
+ let unregisterChromeContributions: Array<() => void> = [];
1152
+
1153
+ /** Republish the contributed CHROME (`@volter/editor-sdk/chrome`): palette
1154
+ * actions and menu items. Data, no component; the same
1155
+ * unregister-then-register discipline as every other point. */
1156
+ function applyChromeContributions(items: readonly ChromeModule[]): void {
1157
+ for (const unregister of unregisterChromeContributions) unregister();
1158
+ unregisterChromeContributions = [];
1159
+ for (const item of items) {
1160
+ const record = (item.module ?? {}) as Record<string, unknown>;
1161
+ const expectedPoint = `workspace.${item.kind}`;
1162
+ if (record['point'] !== expectedPoint) {
1163
+ teachingError(
1164
+ `[tool contributions] ${item.entryPath} must \`export const point = '${expectedPoint}'\` ` +
1165
+ '(its filename names that point). Skipped.',
1166
+ );
1167
+ continue;
1168
+ }
1169
+ try {
1170
+ if (item.kind === 'header') {
1171
+ const Component = record['default'];
1172
+ if (typeof Component !== 'function') {
1173
+ teachingError(
1174
+ `[tool contributions] ${item.entryPath} has no default-exported React component. Skipped.`,
1175
+ );
1176
+ continue;
1177
+ }
1178
+ const order = record['order'];
1179
+ const placement = record['placement'];
1180
+ unregisterChromeContributions.push(
1181
+ registerContributedHeaderItem({
1182
+ id: `tool:${contributionIdFromPath(item.entryPath)}`,
1183
+ order: typeof order === 'number' && Number.isFinite(order) ? order : 0,
1184
+ placement: placement === 'object3d-document' ? 'object3d-document' : 'transport',
1185
+ Component: Component as ComponentType<{ documentId: string }>,
1186
+ }),
1187
+ );
1188
+ continue;
1189
+ }
1190
+ if (item.kind === 'action') {
1191
+ const actions = record['actions'];
1192
+ if (!Array.isArray(actions) && typeof actions !== 'function') {
1193
+ teachingError(
1194
+ `[tool contributions] ${item.entryPath} must \`export const actions\` — an array of ` +
1195
+ '`ContributedAction`, or a function returning one for a live set ' +
1196
+ '(@volter/editor-sdk/chrome). Skipped.',
1197
+ );
1198
+ continue;
1199
+ }
1200
+ const subscribe = record['subscribe'];
1201
+ unregisterChromeContributions.push(
1202
+ registerContributedActions({
1203
+ actions,
1204
+ ...(typeof subscribe === 'function' ? { subscribe } : {}),
1205
+ } as Parameters<typeof registerContributedActions>[0]),
1206
+ );
1207
+ } else {
1208
+ const menu = record['menu'] as { menu?: unknown; items?: unknown } | undefined;
1209
+ if (!menu || !isContributableMenu(menu.menu) || !Array.isArray(menu.items)) {
1210
+ teachingError(
1211
+ `[tool contributions] ${item.entryPath} must \`export const menu\` — a ` +
1212
+ "`MenuContribution` naming one of 'view', 'window', 'debug', 'tools', 'help' and " +
1213
+ 'its items (@volter/editor-sdk/chrome). Skipped.',
1214
+ );
1215
+ continue;
1216
+ }
1217
+ unregisterChromeContributions.push(
1218
+ registerContributedMenu(menu as Parameters<typeof registerContributedMenu>[0]),
1219
+ );
1220
+ }
1221
+ } catch (error) {
1222
+ teachingError(`[tool contributions] ${item.entryPath} could not register.\n${String(error)}`);
1223
+ }
1224
+ }
1225
+ }
1226
+ let unregisterCommandContributions: Array<() => void> = [];
1227
+
1228
+ /**
1229
+ * Republish the contributed COMMANDS (`@volter/editor-sdk/commands`): each
1230
+ * module's `commands` table into the registry, then the registered rows to
1231
+ * the server, which sizes its relay wait per verb from them. The same
1232
+ * unregister-then-register discipline as every other point.
1233
+ */
1234
+ function applyCommandContributions(items: readonly CommandModule[]): void {
1235
+ for (const unregister of unregisterCommandContributions) unregister();
1236
+ unregisterCommandContributions = [];
1237
+ for (const item of items) {
1238
+ const record = (item.module ?? {}) as Record<string, unknown>;
1239
+ if (record['point'] !== 'workspace.command') {
1240
+ teachingError(
1241
+ `[tool contributions] ${item.entryPath} must \`export const point = 'workspace.command'\` ` +
1242
+ '(its filename names that point). Skipped.',
1243
+ );
1244
+ continue;
1245
+ }
1246
+ const commands = record['commands'];
1247
+ if (typeof commands !== 'object' || commands === null) {
1248
+ teachingError(
1249
+ `[tool contributions] ${item.entryPath} must \`export const commands\` — a table of ` +
1250
+ '`CommandSpec` by verb (@volter/editor-sdk/commands). Skipped.',
1251
+ );
1252
+ continue;
1253
+ }
1254
+ try {
1255
+ unregisterCommandContributions.push(
1256
+ registerContributedCommands(
1257
+ item.entryPath,
1258
+ commands as Parameters<typeof registerContributedCommands>[1],
1259
+ ),
1260
+ );
1261
+ } catch (error) {
1262
+ teachingError(`[tool contributions] ${item.entryPath} could not register.\n${String(error)}`);
1263
+ }
1264
+ }
1265
+ void reportContributedCommandRows();
1266
+ }
1267
+
1268
+ /** The server's relay reads the host's table for its per-verb wait; a
1269
+ * contributed verb's row reaches it here, or it would fall to the generic
1270
+ * budget — the exact failure `command-table.ts` records. */
1271
+ async function reportContributedCommandRows(): Promise<void> {
1272
+ try {
1273
+ const response = await fetch('/__editor/contributed-commands', {
1274
+ method: 'POST',
1275
+ headers: { 'content-type': 'application/json' },
1276
+ body: JSON.stringify({ commands: contributedCommandRows() }),
1277
+ });
1278
+ if (!response.ok)
1279
+ teachingError(
1280
+ `[tool contributions] the server refused the contributed command rows (${response.status}).`,
1281
+ );
1282
+ } catch (error) {
1283
+ teachingError(
1284
+ `[tool contributions] could not report contributed command rows: ${String(error)}`,
1285
+ );
1286
+ }
1287
+ }
1288
+
1289
+ /**
1290
+ * Republish the looks — workspaces, keymaps and style bundles a project's
1291
+ * packages contribute (`@volter/editor-sdk/looks`). Data, no component: each
1292
+ * module's one named export is handed to its registry, which is also where
1293
+ * a duplicate id or a bad palette document refuses by name. The same
1294
+ * unregister-then-register discipline as the utilities, so a reload never
1295
+ * doubles a workspace.
1296
+ */
1297
+ function applyLookContributions(items: readonly LookModule[]): void {
1298
+ for (const unregister of unregisterLookContributions) unregister();
1299
+ unregisterLookContributions = [];
1300
+ for (const item of items) {
1301
+ const record = (item.module ?? {}) as Record<string, unknown>;
1302
+ const expectedPoint = `workspace.${item.kind}`;
1303
+ if (record['point'] !== expectedPoint) {
1304
+ teachingError(
1305
+ `[tool contributions] ${item.entryPath} must \`export const point = '${expectedPoint}'\` ` +
1306
+ `(its filename names that point). Skipped.`,
1307
+ );
1308
+ continue;
1309
+ }
1310
+ const value = record[item.kind];
1311
+ if (
1312
+ typeof value !== 'object' ||
1313
+ value === null ||
1314
+ typeof (value as { id?: unknown }).id !== 'string'
1315
+ ) {
1316
+ teachingError(
1317
+ `[tool contributions] ${item.entryPath} must \`export const ${item.kind}\` — an object with a ` +
1318
+ `string \`id\` (see \`${item.kind === 'layout' ? 'WorkspaceLayoutContribution' : item.kind === 'keymap' ? 'KeymapContribution' : 'StyleContribution'}\` in @volter/editor-sdk/looks). Skipped.`,
1319
+ );
1320
+ continue;
1321
+ }
1322
+ try {
1323
+ unregisterLookContributions.push(
1324
+ item.kind === 'layout'
1325
+ ? registerContributedWorkspace(
1326
+ value as Parameters<typeof registerContributedWorkspace>[0],
1327
+ )
1328
+ : item.kind === 'keymap'
1329
+ ? registerContributedKeymap(value as Parameters<typeof registerContributedKeymap>[0])
1330
+ : registerContributedStyle(value as Parameters<typeof registerContributedStyle>[0]),
1331
+ );
1332
+ } catch (error) {
1333
+ teachingError(`[tool contributions] ${item.entryPath} could not register.\n${String(error)}`);
1334
+ }
1335
+ }
1336
+ }
1337
+ let statusContributions: StatusToolContribution[] = [];
1338
+
1339
+ /** Republish the status bar's contributed items — the same
1340
+ * unregister-then-register discipline as {@link applyUtilityContributions}. */
1341
+ function applyStatusContributions(items: StatusToolContribution[]): void {
1342
+ for (const unregister of unregisterStatusContributions) unregister();
1343
+ unregisterStatusContributions = [];
1344
+ const claimed: StatusToolContribution[] = [];
1345
+ for (const item of items) {
1346
+ try {
1347
+ unregisterStatusContributions.push(
1348
+ registerWorkspaceStatus({
1349
+ id: `tool:${item.id}`,
1350
+ // The contribution's own TITLE is what a person calls it, and the
1351
+ // status bar's `Hide <item>` menu is where they read it.
1352
+ name: item.title,
1353
+ align: item.align,
1354
+ order: contributedSectionOrder(item.order),
1355
+ Content: item.Component,
1356
+ }),
1357
+ );
1358
+ claimed.push(item);
1359
+ } catch (error) {
1360
+ teachingError(
1361
+ `[tool contributions] ${item.file} could not claim its status-bar slot.\n${String(error)}`,
1362
+ );
1363
+ }
1364
+ }
1365
+ statusContributions = claimed;
1366
+ }
1367
+
1368
+ /** The status items the last load pass claimed (diagnostics; the bar reads
1369
+ * the registry itself). */
1370
+ export function getStatusToolContributions(): readonly StatusToolContribution[] {
1371
+ return statusContributions;
1372
+ }
1373
+ /** A project's finder contribution, imported and waiting for the
1374
+ * document-table host to register it (ARCHITECTURE-CORE §The project model). */
1375
+ export interface ContributedFinderModule {
1376
+ readonly entryPath: string;
1377
+ readonly module: unknown;
1378
+ }
1379
+ let contributedFinders: readonly ContributedFinderModule[] = [];
1380
+
1381
+ /** The finder contributions the last pass imported; `subscribeToolContributions`
1382
+ * fires when the list changes. */
1383
+ export function contributedFinderModules(): readonly ContributedFinderModule[] {
1384
+ return contributedFinders;
1385
+ }
1386
+
1387
+ function registrationForUtilityContribution(item: DrawerToolContribution): () => void {
1388
+ return registerWorkspaceUtility({
1389
+ // Namespaced by contribution id exactly as the inspector sections are, so
1390
+ // a project tab can never collide with one of the editor's own utilities
1391
+ // (ids key drawer tabs and persisted layout — the registry throws).
1392
+ id: `tool:${item.id}`,
1393
+ title: item.title,
1394
+ // The GAME's plane, not the editor's instruments.
1395
+ section: 'project',
1396
+ Content: () => createElement(ToolHost, { contribution: item }),
1397
+ // A project tab is revealed by the Window menu / a command, like every
1398
+ // other on-demand utility; it does not force itself into a clean layout.
1399
+ visibleByDefault: false,
1400
+ closeable: true,
1401
+ // Analytics reads a RUNNING world; a project that mounts nothing has no
1402
+ // run to read (ARCHITECTURE-CORE §Roots), so the tab is not on offer. A
1403
+ // utility gates itself on whatever session state it declared.
1404
+ ...(item.point === 'workspace.analytics'
1405
+ ? { available: projectMounts }
1406
+ : item.available
1407
+ ? { available: item.available }
1408
+ : {}),
1409
+ });
1410
+ }
1411
+
1412
+ /**
1413
+ * Republish the drawer's project cluster — unregister the previous load's
1414
+ * tabs, then register this one's.
1415
+ *
1416
+ * Unregister-then-register (rather than diffing) is the same discipline
1417
+ * {@link applyInspectorContributions} uses, and it is what makes the id
1418
+ * registry's duplicate THROW survivable across an HMR save: the old
1419
+ * registration is always gone before the new one claims the id. A registration
1420
+ * that still throws (an id some other surface owns) teaches and is skipped,
1421
+ * because one bad contribution must not strand the rest of the load with no
1422
+ * tabs at all.
1423
+ */
1424
+ function applyUtilityContributions(items: DrawerToolContribution[]): void {
1425
+ for (const unregister of unregisterUtilityContributions) unregister();
1426
+ unregisterUtilityContributions = [];
1427
+ const claimed: DrawerToolContribution[] = [];
1428
+ for (const item of items) {
1429
+ try {
1430
+ unregisterUtilityContributions.push(registrationForUtilityContribution(item));
1431
+ claimed.push(item);
1432
+ } catch (error) {
1433
+ teachingError(
1434
+ `[tool contributions] ${item.file} could not claim its drawer tab.\n${String(error)}`,
1435
+ );
1436
+ }
1437
+ }
1438
+ // Only the ones that HOLD a tab: the menu entry's whole job is revealing that
1439
+ // tab, so listing a contribution whose registration threw would be a door
1440
+ // onto nothing.
1441
+ utilityContributions = claimed;
1442
+ }
1443
+
1444
+ /** Clear every contribution point this loader owns. Shared by the
1445
+ * no-project and no-host paths so neither leaves a previous load's tabs,
1446
+ * sections and documents standing over a project that cannot supply them. */
1447
+ function clearProjectToolContributions(): void {
1448
+ publishPlayUtilitiesReady(false);
1449
+ assetInspectorContributions = [];
1450
+ generationResultContributions = [];
1451
+ contributionLoadFailures = [];
1452
+ applyGlobalContributions([]);
1453
+ applyInspectorContributions([]);
1454
+ applyUtilityContributions([]);
1455
+ applyStatusContributions([]);
1456
+ applyLookContributions([]);
1457
+ applyCommandContributions([]);
1458
+ applyChromeContributions([]);
1459
+ applyServiceContributions([]);
1460
+ publishToolContributions();
1461
+ }
1462
+
1463
+ /** `@vgai/game/contributions/x.ts` → `@vgai/game`. */
1464
+ function packageNameOf(specifier: string): string {
1465
+ const parts = specifier.split('/');
1466
+ return specifier.startsWith('@') ? `${parts[0]}/${parts[1]}` : (parts[0] ?? specifier);
1467
+ }
1468
+
1469
+ /**
1470
+ * The newest in-flight pass. A pass that is superseded mid-flight installs
1471
+ * nothing (the epoch check at its end), so an awaiter that only waited for
1472
+ * ITS pass could resume with the registry still empty while the newer pass
1473
+ * is loading — measured live (2026-09-02, load average 60) as a shared-view
1474
+ * URL restore refusing "Tool document is not registered" for a contribution
1475
+ * that was on disk and opened fine seconds later. Every awaiter now returns
1476
+ * only once the newest pass has installed.
1477
+ */
1478
+ /**
1479
+ * The origin that served THIS module — the editor's own dev server (the `vgai
1480
+ * edit` session), which is also what serves the open project's modules.
1481
+ *
1482
+ * Everywhere but one shape it equals the page's origin. Inside the Code-OSS
1483
+ * DESKTOP frame it does not: the page is `vscode-file://vscode-app` and the
1484
+ * session is loopback http (docs/CODE-OSS.md §Desktop). Taken once, as a plain
1485
+ * string, because `new URL(<expression>, import.meta.url)` is Vite's asset-URL
1486
+ * pattern and is rewritten statically at transform time.
1487
+ */
1488
+ const MODULE_SERVING_ORIGIN = new URL(import.meta.url).origin;
1489
+
1490
+ let latestContributionRefresh: Promise<void> | null = null;
1491
+
1492
+ export async function refreshProjectToolContributions(): Promise<void> {
1493
+ const pass = runContributionRefresh();
1494
+ latestContributionRefresh = pass;
1495
+ await pass;
1496
+ while (latestContributionRefresh !== null && latestContributionRefresh !== pass) {
1497
+ const newer: Promise<void> = latestContributionRefresh;
1498
+ await newer;
1499
+ if (latestContributionRefresh === newer) break;
1500
+ }
1501
+ }
1502
+
1503
+ async function runContributionRefresh(): Promise<void> {
1504
+ publishPlayUtilitiesReady(false);
1505
+ const epoch = ++contributionRefreshEpoch;
1506
+ const project = getCurrentProject();
1507
+ if (!project) {
1508
+ clearProjectToolContributions();
1509
+ return;
1510
+ }
1511
+
1512
+ let catalog: ProjectToolCatalog;
1513
+ try {
1514
+ const response = await fetch('/__editor/project-tools');
1515
+ // Never `res.json()` on an unexamined content type — see
1516
+ // `editor-server-response.ts` for why `response.ok` cannot answer this.
1517
+ catalog = await editorServerJson<ProjectToolCatalog>(response, 'Catalog request failed');
1518
+ } catch (error) {
1519
+ // A load that cannot happen is reported, never swallowed: the panel reads
1520
+ // `getToolContributionLoadFailures()`, so this is what stands between a
1521
+ // real host failure and a blank "No project tools registered."
1522
+ clearProjectToolContributions();
1523
+ const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
1524
+ contributionLoadFailures = [
1525
+ {
1526
+ entryPath: '/__editor/project-tools',
1527
+ id: 'project-tool-catalog',
1528
+ error: detail,
1529
+ },
1530
+ ];
1531
+ teachingError(`[tool contributions] could not read the project tool catalog.\n${detail}`);
1532
+ publishToolContributions();
1533
+ return;
1534
+ }
1535
+
1536
+ const version = Date.now();
1537
+ const nextGlobal: GlobalToolContribution[] = [];
1538
+ const nextUtility: Array<UtilityToolContribution | AnalyticsToolContribution> = [];
1539
+ const nextStatus: StatusToolContribution[] = [];
1540
+ const nextInspector: InspectorToolContribution[] = [];
1541
+ const nextAssetInspector: AssetInspectorToolContribution[] = [];
1542
+ const nextGenerationResult: GenerationResultToolContribution[] = [];
1543
+ const nextFailures: ToolContributionLoadFailure[] = [];
1544
+ const absolute = (entryPath: string) =>
1545
+ entryPath.startsWith('/') || bundledPackageLoaders.has(entryPath)
1546
+ ? entryPath
1547
+ : `${project.rootPath}/${entryPath}`;
1548
+ // The host serves a contribution through its own Vite (`/@fs/`,
1549
+ // cache-busted per refresh); a package this build bundles is already in the
1550
+ // page and loads through its own registered loader.
1551
+ const importContribution = (entryPath: string) =>
1552
+ bundledPackageLoaders.get(entryPath)?.() ??
1553
+ import(/* @vite-ignore */ `/@fs/${absolute(entryPath)}?t=${version}`);
1554
+ // The same door, handed to the contributions themselves for the project
1555
+ // modules THEY need (`@volter/editor-sdk/contributions` `importProjectModule`).
1556
+ registerProjectModuleLoader({
1557
+ import: (path) =>
1558
+ import(/* @vite-ignore */ `/@fs/${absolute(path)}?t=${version}`) as Promise<
1559
+ Record<string, unknown>
1560
+ >,
1561
+ // Resolved against the ORIGIN THAT SERVED THIS MODULE, never the
1562
+ // page's. A project module is served by the session that served this
1563
+ // file, and those two are the same origin in every shape but one:
1564
+ // inside the Code-OSS DESKTOP frame the page is
1565
+ // `vscode-file://vscode-app` (Electron's app root) while the session
1566
+ // stays on loopback http, so a url built from the page sent the Model
1567
+ // document's save-time re-import at the app root, where it 404s
1568
+ // (docs/CODE-OSS.md §Desktop, measured 2026-09-19).
1569
+ //
1570
+ // NOTE THE SHAPE, and do not "simplify" it back:
1571
+ // `new URL(<expression>, import.meta.url)` is Vite's own ASSET-URL
1572
+ // pattern, which its `assetImportMetaUrl` plugin rewrites statically
1573
+ // at transform time — with a computed first argument it resolves to
1574
+ // `undefined` and this door silently hands out
1575
+ // `<origin>/packages/editor/src/undefined` (measured the same day).
1576
+ // `new URL(import.meta.url)` with ONE argument is not that pattern,
1577
+ // so the origin is taken first and used as a plain base string.
1578
+ url: (path) => new URL(`/@fs/${absolute(path)}`, MODULE_SERVING_ORIGIN),
1579
+ // The file's TEXT through Vite's own `?raw` door (the HTTP storage
1580
+ // backend is rooted at `public/`, and a plain fetch of a `.ts` URL
1581
+ // answers with the transformed module). A fresh query each time:
1582
+ // the module may have just been written.
1583
+ source: (path) =>
1584
+ (
1585
+ import(
1586
+ /* @vite-ignore */ `/@fs/${absolute(path)}?raw&vgai-source=${Date.now()}`
1587
+ ) as Promise<{ default?: unknown }>
1588
+ ).then((raw) => {
1589
+ if (typeof raw.default !== 'string') {
1590
+ throw new Error(`${path}: the dev server did not answer with the file's text`);
1591
+ }
1592
+ return raw.default;
1593
+ }),
1594
+ });
1595
+ const moduleImports = new Map<string, Promise<unknown>>();
1596
+ for (const { entryPath } of catalog.contributions ?? []) {
1597
+ // Kind contributions are the manifest hosts' (server, validate script);
1598
+ // the page mounts nothing from them.
1599
+ if (isConfigurationKindContribution(entryPath)) continue;
1600
+ const modulePath = absolute(entryPath);
1601
+ moduleImports.set(modulePath, moduleImports.get(modulePath) ?? importContribution(entryPath));
1602
+ }
1603
+ const nextFinderModules: ContributedFinderModule[] = [];
1604
+ const nextLooks: LookModule[] = [];
1605
+ const nextCommands: CommandModule[] = [];
1606
+ const nextChrome: ChromeModule[] = [];
1607
+ const nextServices: ServiceModule[] = [];
1608
+ const claimedIds = new Set<string>();
1609
+ for (const { entryPath } of catalog.contributions ?? []) {
1610
+ if (isConfigurationKindContribution(entryPath)) continue; // a kind is the manifest hosts'
1611
+ if (isServiceContribution(entryPath)) {
1612
+ try {
1613
+ const mod = await moduleImports.get(absolute(entryPath))!;
1614
+ nextServices.push({ entryPath, module: mod });
1615
+ } catch (error) {
1616
+ const { failure, message } = describeContributionLoadFailure(entryPath, error);
1617
+ nextFailures.push(failure);
1618
+ teachingError(message);
1619
+ }
1620
+ continue;
1621
+ }
1622
+ const chrome = chromeContributionKind(entryPath);
1623
+ if (chrome) {
1624
+ try {
1625
+ const mod = await moduleImports.get(absolute(entryPath))!;
1626
+ nextChrome.push({ entryPath, kind: chrome, module: mod });
1627
+ } catch (error) {
1628
+ const { failure, message } = describeContributionLoadFailure(entryPath, error);
1629
+ nextFailures.push(failure);
1630
+ teachingError(message);
1631
+ }
1632
+ continue;
1633
+ }
1634
+ if (isCommandContribution(entryPath)) {
1635
+ try {
1636
+ const mod = await moduleImports.get(absolute(entryPath))!;
1637
+ nextCommands.push({ entryPath, module: mod });
1638
+ } catch (error) {
1639
+ const { failure, message } = describeContributionLoadFailure(entryPath, error);
1640
+ nextFailures.push(failure);
1641
+ teachingError(message);
1642
+ }
1643
+ continue;
1644
+ }
1645
+ const look = lookContributionKind(entryPath);
1646
+ if (look) {
1647
+ // A look is DATA for a registry (`applyLookContributions`), never a
1648
+ // mounted component.
1649
+ try {
1650
+ const mod = await moduleImports.get(absolute(entryPath))!;
1651
+ nextLooks.push({ entryPath, kind: look, module: mod });
1652
+ } catch (error) {
1653
+ const { failure, message } = describeContributionLoadFailure(entryPath, error);
1654
+ nextFailures.push(failure);
1655
+ teachingError(message);
1656
+ }
1657
+ continue;
1658
+ }
1659
+ if (isFinderContribution(entryPath)) {
1660
+ // A finder is the document-table host's (`project-adapter.ts`), which
1661
+ // registers what is collected here after this pass publishes.
1662
+ try {
1663
+ const mod = await moduleImports.get(absolute(entryPath))!;
1664
+ nextFinderModules.push({ entryPath, module: mod });
1665
+ } catch (error) {
1666
+ const { failure, message } = describeContributionLoadFailure(entryPath, error);
1667
+ nextFailures.push(failure);
1668
+ teachingError(message);
1669
+ }
1670
+ continue;
1671
+ }
1672
+ let mod: unknown;
1673
+ try {
1674
+ mod = await moduleImports.get(absolute(entryPath))!;
1675
+ } catch (error) {
1676
+ const { failure, message } = describeContributionLoadFailure(entryPath, error);
1677
+ nextFailures.push(failure);
1678
+ teachingError(message);
1679
+ continue;
1680
+ }
1681
+ // A module declaring `presentations` mounts once PER presentation.
1682
+ for (const presentation of readToolPresentations(mod) ?? [undefined]) {
1683
+ const result = extractProjectToolContribution(
1684
+ mod,
1685
+ entryPath,
1686
+ version,
1687
+ catalog.tools,
1688
+ presentation,
1689
+ );
1690
+ if ('error' in result) {
1691
+ if ('note' in result && result.note) teachingNote(result.error);
1692
+ else teachingError(result.error);
1693
+ continue;
1694
+ }
1695
+ // Ids are the editor's mount keys and are no longer tool-qualified, so
1696
+ // two files with the same basename would silently shadow one another.
1697
+ if (claimedIds.has(result.contribution.id)) {
1698
+ // The project's own module registers first, so the loser is the
1699
+ // later one — a package's, when a project still carries the copy a
1700
+ // capability shipped before it became a package.
1701
+ const packaged = entryPath.startsWith('@vgai/') || entryPath.includes('/node_modules/');
1702
+ teachingError(
1703
+ `[tool contributions] ${entryPath} claims id ${JSON.stringify(result.contribution.id)}, ` +
1704
+ 'which another contribution already uses. ' +
1705
+ (packaged
1706
+ ? 'This one ships in a package the project declares; if the other is a copy under ' +
1707
+ 'src/contributions/ from before that capability became a package, delete the copy. '
1708
+ : 'Rename one of them. ') +
1709
+ 'Skipped.',
1710
+ );
1711
+ continue;
1712
+ }
1713
+ claimedIds.add(result.contribution.id);
1714
+ if (result.contribution.point === 'selection.inspector') {
1715
+ nextInspector.push(result.contribution);
1716
+ } else if (
1717
+ result.contribution.point === 'workspace.utility' ||
1718
+ result.contribution.point === 'workspace.analytics'
1719
+ ) {
1720
+ nextUtility.push(result.contribution);
1721
+ } else if (result.contribution.point === 'workspace.status') {
1722
+ nextStatus.push(result.contribution);
1723
+ } else if (result.contribution.point === 'asset.inspector') {
1724
+ nextAssetInspector.push(result.contribution);
1725
+ } else if (result.contribution.point === 'generation.result') {
1726
+ nextGenerationResult.push(result.contribution);
1727
+ } else {
1728
+ nextGlobal.push(result.contribution);
1729
+ }
1730
+ }
1731
+ }
1732
+ if (epoch !== contributionRefreshEpoch || getCurrentProject()?.rootPath !== project.rootPath) {
1733
+ return;
1734
+ }
1735
+ assetInspectorContributions = nextAssetInspector;
1736
+ generationResultContributions = nextGenerationResult;
1737
+ contributionLoadFailures = nextFailures;
1738
+ applyGlobalContributions(nextGlobal);
1739
+ applyInspectorContributions(nextInspector);
1740
+ applyUtilityContributions(nextUtility);
1741
+ applyStatusContributions(nextStatus);
1742
+ applyLookContributions(nextLooks);
1743
+ applyCommandContributions(nextCommands);
1744
+ applyChromeContributions(nextChrome);
1745
+ applyServiceContributions(nextServices);
1746
+ // Only now is "no package contributes it" a fact about this project.
1747
+ reportUnavailableKeymap();
1748
+ publishPlayUtilitiesReady(true);
1749
+ contributedFinders = nextFinderModules;
1750
+ publishToolContributions();
1751
+ }
1752
+
1753
+ function applyGlobalContributions(next: GlobalToolContribution[]): void {
1754
+ globalContributions = next;
1755
+ }
1756
+
1757
+ /**
1758
+ * The ONE publish of a load pass: rebuild the surface snapshot from both halves
1759
+ * and notify every subscriber exactly once.
1760
+ *
1761
+ * The `apply*` functions above only WRITE their half; none of them notifies.
1762
+ * That split is the point — a load pass writes five stores (documents,
1763
+ * utilities, inspector sections, asset inspectors, generation results), and a
1764
+ * notify from inside any one of them publishes a half-updated editor: the new
1765
+ * documents beside the previous load's utilities, in a `useSyncExternalStore`
1766
+ * snapshot subscribers then render and reconcile from. Notifying per half also
1767
+ * ran every subscriber's work twice per refresh (dock reconcile, menu rebuild).
1768
+ */
1769
+ function publishToolContributions(): void {
1770
+ surfaceContributions = [...globalContributions, ...utilityContributions];
1771
+ for (const fn of listeners) fn();
1772
+ }
1773
+
1774
+ export function __publishGlobalToolContributionsForTest(next: GlobalToolContribution[]): void {
1775
+ applyGlobalContributions(next);
1776
+ publishToolContributions();
1777
+ }
1778
+
1779
+ function teachingError(message: string): void {
1780
+ // biome-ignore lint/suspicious/noConsole: broken project contributions must be loud without crashing the editor
1781
+ console.error(message);
1782
+ }
1783
+
1784
+ /** A contribution that is fine but INAPPLICABLE HERE — nothing is broken and
1785
+ * nothing on this tier can fix it, so it must not spend the console's error
1786
+ * badge (the "unresolved console is remaining work" rule only holds while
1787
+ * every error is real work). */
1788
+ function teachingNote(message: string): void {
1789
+ // biome-ignore lint/suspicious/noConsole: the tier's own explanation, at warning weight
1790
+ console.warn(message);
1791
+ }
1792
+
1793
+ if (import.meta.hot) {
1794
+ import.meta.hot.on('vgai:script-update', (data: { file: string }) => {
1795
+ if (!isEditorLanePath(data.file)) return;
1796
+ void refreshProjectToolContributions();
1797
+ });
1798
+ }