@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.
- package/BUNDLED_NOTICES +1527 -0
- package/LICENSE +686 -0
- package/LICENSE-APACHE +202 -0
- package/NOTICE +18 -0
- package/README.md +24 -0
- package/dist/build/chunk-JS4D4XZ3.js +175 -0
- package/dist/build/chunk-JS4D4XZ3.js.map +7 -0
- package/dist/build/vite-plugin-product-contributions.js +100 -0
- package/dist/build/vite-plugin-product-contributions.js.map +7 -0
- package/dist/build/vite-plugin-shared-react.js +25 -0
- package/dist/build/vite-plugin-shared-react.js.map +7 -0
- package/dist/build/vite-plugin-shared-three.js +86 -0
- package/dist/build/vite-plugin-shared-three.js.map +7 -0
- package/dist/server/chunk-KMJC7AGO.js +98 -0
- package/dist/server/chunk-KMJC7AGO.js.map +7 -0
- package/dist/server/chunk-PSILUMGS.js +46 -0
- package/dist/server/chunk-PSILUMGS.js.map +7 -0
- package/dist/server/frame-proxy.js +255 -0
- package/dist/server/frame-proxy.js.map +7 -0
- package/dist/server/open-browser.js +53 -0
- package/dist/server/open-browser.js.map +7 -0
- package/dist/server/process-shutdown.js +127 -0
- package/dist/server/process-shutdown.js.map +7 -0
- package/dist/server/session-registry.js +141 -0
- package/dist/server/session-registry.js.map +7 -0
- package/dist/server/spawn-opener.js +11 -0
- package/dist/server/spawn-opener.js.map +7 -0
- package/dist/server/worktree-identity.js +11 -0
- package/dist/server/worktree-identity.js.map +7 -0
- package/dist-server/packaged.mjs +88816 -0
- package/package.json +118 -0
- package/scripts/build-plugins.mjs +14 -0
- package/scripts/build-server.mjs +26 -0
- package/scripts/build-session.mjs +9 -0
- package/server/account-credentials.ts +60 -0
- package/server/account-service.ts +1434 -0
- package/server/adapter-region-includes.ts +47 -0
- package/server/asset-catalog-v2.ts +175 -0
- package/server/asset-history-snapshots.ts +196 -0
- package/server/asset-ledger-store.ts +195 -0
- package/server/asset-library-routes.ts +1595 -0
- package/server/blender-wasm-artifact.ts +430 -0
- package/server/boot-timings.ts +106 -0
- package/server/canonical-path.ts +50 -0
- package/server/checkout-workspace-preflight.ts +349 -0
- package/server/cloud-asset-catalog.ts +158 -0
- package/server/coding-inference-launch.ts +237 -0
- package/server/collaboration-account-client.ts +202 -0
- package/server/collaboration-attribution.ts +122 -0
- package/server/collaboration-session.ts +934 -0
- package/server/comfyui-bridge.ts +248 -0
- package/server/console-ledger.ts +499 -0
- package/server/creation-site-transform.ts +304 -0
- package/server/creation-site-write.ts +648 -0
- package/server/data-file-serialize.ts +158 -0
- package/server/editor-brand-html.ts +72 -0
- package/server/editor-control-socket.ts +366 -0
- package/server/editor-server-options.ts +190 -0
- package/server/editor-server.ts +1483 -0
- package/server/editor-sse.ts +352 -0
- package/server/engine-provenance.ts +169 -0
- package/server/engine-source-restart.ts +117 -0
- package/server/frame-bridge.ts +115 -0
- package/server/frame-proxy.ts +490 -0
- package/server/frame-workbench.ts +259 -0
- package/server/frontend-handoff.ts +229 -0
- package/server/game-globals-shadow.ts +113 -0
- package/server/gameplay-session-retention.ts +149 -0
- package/server/gameplay-sessions.ts +123 -0
- package/server/generation-jobs.ts +201 -0
- package/server/generation-reconciler.ts +137 -0
- package/server/generative-execution-context.ts +59 -0
- package/server/git-workflow.ts +612 -0
- package/server/harness-chat-caller.ts +36 -0
- package/server/harness-chat-service.ts +1681 -0
- package/server/idle-shutdown.ts +170 -0
- package/server/js-profiling-policy.ts +52 -0
- package/server/launcher-settings.ts +84 -0
- package/server/local-asset-catalog.ts +410 -0
- package/server/managed-account-defaults.ts +22 -0
- package/server/mock-control-plane.ts +540 -0
- package/server/model-import-conversion.ts +345 -0
- package/server/native-credential-store.ts +162 -0
- package/server/open-browser.ts +118 -0
- package/server/packaged.ts +1192 -0
- package/server/play-stall.ts +126 -0
- package/server/process-shutdown.ts +218 -0
- package/server/product-presets.ts +55 -0
- package/server/project-build-artifact.ts +95 -0
- package/server/project-components.ts +100 -0
- package/server/project-dependency-invalidation.ts +114 -0
- package/server/project-file-scan.ts +235 -0
- package/server/project-hmr-files.ts +215 -0
- package/server/project-install-roots.ts +210 -0
- package/server/project-kinds.ts +71 -0
- package/server/project-mcp-servers.ts +82 -0
- package/server/project-module-freshness.ts +109 -0
- package/server/project-module-instance.ts +375 -0
- package/server/project-optimize-deps-entries.ts +827 -0
- package/server/project-output-writer.ts +679 -0
- package/server/project-package-origin.ts +63 -0
- package/server/project-root-surface.ts +831 -0
- package/server/project-scratch-path.ts +47 -0
- package/server/project-script-hmr.ts +619 -0
- package/server/project-serving-plugins.ts +236 -0
- package/server/project-tools.ts +653 -0
- package/server/project-validation.ts +250 -0
- package/server/project-verbs.ts +127 -0
- package/server/project-view.ts +52 -0
- package/server/project-watch.ts +1134 -0
- package/server/project-work-coordinator.ts +288 -0
- package/server/provider-credentials.ts +331 -0
- package/server/recent-projects-store.ts +98 -0
- package/server/redact-secrets.ts +71 -0
- package/server/repository-presence.ts +267 -0
- package/server/routes/account.ts +357 -0
- package/server/routes/agents.ts +60 -0
- package/server/routes/assets.ts +154 -0
- package/server/routes/build.ts +13 -0
- package/server/routes/collaboration.ts +595 -0
- package/server/routes/configurations.ts +419 -0
- package/server/routes/context.ts +264 -0
- package/server/routes/control-plane.ts +1811 -0
- package/server/routes/logs.ts +272 -0
- package/server/routes/project-identity.ts +302 -0
- package/server/routes/project-open.ts +429 -0
- package/server/routes/project-source.ts +686 -0
- package/server/routes/project-state.ts +383 -0
- package/server/routes/relay.ts +1029 -0
- package/server/routes/route-helpers.ts +29 -0
- package/server/routes/served-modules.ts +138 -0
- package/server/routes/session-tabs.ts +196 -0
- package/server/routes/settings.ts +121 -0
- package/server/routes/share-control.ts +486 -0
- package/server/routes/themes.ts +116 -0
- package/server/routes/tools.ts +230 -0
- package/server/routes/worktrees.ts +595 -0
- package/server/scoped-game-css.ts +461 -0
- package/server/server-utils.ts +1194 -0
- package/server/session-product.ts +112 -0
- package/server/session-registry.ts +230 -0
- package/server/share-claims.ts +138 -0
- package/server/share-host.ts +526 -0
- package/server/share-session-gateway.ts +1362 -0
- package/server/share-tunnel.ts +123 -0
- package/server/spawn-opener.ts +74 -0
- package/server/support/play/log-format.ts +76 -0
- package/server/support/play/session-record.ts +199 -0
- package/server/support/project/build-discipline.ts +718 -0
- package/server/support/project/inspection-node.ts +119 -0
- package/server/support/project/log-naming.ts +54 -0
- package/server/support/project/provenance.ts +290 -0
- package/server/support/project/run-name.ts +18 -0
- package/server/support/project/session-journal.ts +830 -0
- package/server/support/project/shared.ts +327 -0
- package/server/tab-bootstrap.ts +62 -0
- package/server/tab-heartbeat.ts +477 -0
- package/server/tab-lifecycle.ts +550 -0
- package/server/tab-presence.ts +1508 -0
- package/server/team-agent-mirror.ts +95 -0
- package/server/twin-auth.ts +138 -0
- package/server/vendored-lock-recorder.ts +968 -0
- package/server/worktree-identity.ts +121 -0
- package/server/worktree-management.ts +398 -0
- package/server/worktree-retention.d.mts +13 -0
- package/server/worktree-retention.mjs +205 -0
- package/src/EditorContext.tsx +169 -0
- package/src/account.ts +333 -0
- package/src/action-registry.ts +532 -0
- package/src/active-product.ts +74 -0
- package/src/active-project.ts +155 -0
- package/src/adapter-editor-config.ts +25 -0
- package/src/adapter-observation.ts +49 -0
- package/src/animation/stage-transport.ts +379 -0
- package/src/animation/three-clips-subject.ts +172 -0
- package/src/api/asset-library-wire.ts +45 -0
- package/src/api/assets.ts +365 -0
- package/src/api/base.ts +10 -0
- package/src/api/build.ts +99 -0
- package/src/api/git-wire.ts +56 -0
- package/src/api/logs.ts +92 -0
- package/src/api/project-identity.ts +74 -0
- package/src/api/project-open.ts +355 -0
- package/src/api/project-source.ts +162 -0
- package/src/api/project-state.ts +107 -0
- package/src/api/relay.ts +268 -0
- package/src/api/settings.ts +36 -0
- package/src/api/themes.ts +45 -0
- package/src/api/worktrees.ts +257 -0
- package/src/asset-compare-core.ts +171 -0
- package/src/asset-compare.ts +294 -0
- package/src/asset-editor-context.tsx +100 -0
- package/src/asset-events.ts +97 -0
- package/src/asset-inspector-actions.ts +87 -0
- package/src/asset-preview-framing.ts +357 -0
- package/src/asset-preview.ts +2802 -0
- package/src/asset-selection-viewer-registry.ts +113 -0
- package/src/asset-selection.ts +133 -0
- package/src/asset-workflow/asset-capabilities.ts +344 -0
- package/src/asset-workflow/asset-import-jobs.ts +106 -0
- package/src/asset-workflow/asset-ledger-backend.ts +126 -0
- package/src/asset-workflow/asset-ledger.ts +156 -0
- package/src/asset-workflow/asset-materialization-report.ts +140 -0
- package/src/asset-workflow/asset-pack-manifest.ts +320 -0
- package/src/asset-workflow/asset-types.ts +142 -0
- package/src/asset-workflow/asset-workflow-quality.ts +61 -0
- package/src/asset-workflow/audio-preview-player.ts +193 -0
- package/src/asset-workflow/audio-waveform.ts +22 -0
- package/src/asset-workflow/cloud-asset-client.ts +263 -0
- package/src/asset-workflow/folder-preview.ts +345 -0
- package/src/asset-workflow/hosted-asset-materialization.ts +236 -0
- package/src/asset-workflow/image-view-scale.ts +32 -0
- package/src/asset-workflow/import-contract.ts +124 -0
- package/src/asset-workflow/ledger-write-lock.ts +244 -0
- package/src/asset-workflow/model-inspection.ts +818 -0
- package/src/asset-workflow/pixi-spritesheet.ts +197 -0
- package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +139 -0
- package/src/asset-workflow/project-asset-operations.ts +154 -0
- package/src/asset-workflow/project-asset-roots.ts +68 -0
- package/src/asset-workflow/project-content.ts +288 -0
- package/src/asset-workflow/project-source-index.ts +550 -0
- package/src/asset-workflow/thumbnail-system.ts +256 -0
- package/src/authoring/active-adapter.ts +199 -0
- package/src/authoring/active-systems.ts +422 -0
- package/src/authoring/adapter-key.ts +18 -0
- package/src/authoring/authoring-asset-url.ts +27 -0
- package/src/authoring/bootstrap-state.ts +49 -0
- package/src/authoring/boundary-authoring-adapter.ts +184 -0
- package/src/authoring/breakpoint-state.ts +43 -0
- package/src/authoring/canvas-scene-guides.ts +76 -0
- package/src/authoring/component-instance-root.ts +171 -0
- package/src/authoring/composite-authoring-adapter.ts +2110 -0
- package/src/authoring/consumer-actions.ts +520 -0
- package/src/authoring/css-numeric-style.ts +97 -0
- package/src/authoring/design-time-layers.ts +849 -0
- package/src/authoring/design-time-mount-registry.ts +235 -0
- package/src/authoring/design-time-settle.ts +343 -0
- package/src/authoring/edit-mode-authoring.ts +619 -0
- package/src/authoring/eyedropper-session.ts +60 -0
- package/src/authoring/instance-source-menu-register.ts +13 -0
- package/src/authoring/instance-source-menu.ts +135 -0
- package/src/authoring/layered-pick.ts +183 -0
- package/src/authoring/live-gesture-lock.ts +50 -0
- package/src/authoring/live-object-transform.ts +62 -0
- package/src/authoring/mount-failure-report.ts +154 -0
- package/src/authoring/mounted-root-subjects.ts +144 -0
- package/src/authoring/no-authoring-adapter.ts +55 -0
- package/src/authoring/null-inspection-subjects.tsx +75 -0
- package/src/authoring/object3d-document-persistence.ts +106 -0
- package/src/authoring/object3d-document-session-registry.ts +120 -0
- package/src/authoring/object3d-document-session.ts +1326 -0
- package/src/authoring/object3d-gesture-controller.ts +113 -0
- package/src/authoring/panel-authoring.ts +121 -0
- package/src/authoring/prefab-instance-inspector-section.tsx +237 -0
- package/src/authoring/prefab-instance-section-model.ts +55 -0
- package/src/authoring/project-authoring-session.ts +105 -0
- package/src/authoring/provenance.ts +99 -0
- package/src/authoring/quarks-particle-systems.ts +19 -0
- package/src/authoring/react-canvas-navigation.ts +255 -0
- package/src/authoring/react-design-canvas-style.ts +20 -0
- package/src/authoring/react-story-board.ts +937 -0
- package/src/authoring/selection-scope.ts +195 -0
- package/src/authoring/shell-document-ops.ts +169 -0
- package/src/authoring/shell-object3d-document-write-policy.ts +108 -0
- package/src/authoring/shell-viewport-policy.ts +48 -0
- package/src/authoring/source-object3d-authoring-adapter.ts +526 -0
- package/src/authoring/stories-scope.ts +35 -0
- package/src/authoring/story-board-chrome-fit.ts +107 -0
- package/src/authoring/story-board-presentation.ts +111 -0
- package/src/authoring/three-projection-core.ts +226 -0
- package/src/authoring/viewport-pick-context.ts +40 -0
- package/src/authoring/viewport-raycast.ts +240 -0
- package/src/authoring/viewport-tool-context.ts +73 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +152 -0
- package/src/authoring/world-pan-state.ts +198 -0
- package/src/authoring/world-session-state.ts +101 -0
- package/src/authoring/write-pipe.ts +173 -0
- package/src/availability-tick.ts +66 -0
- package/src/bitmap-label.ts +120 -0
- package/src/blender-tab-metrics.ts +161 -0
- package/src/board-open-actions.ts +20 -0
- package/src/boot-routing.ts +391 -0
- package/src/build-identity.ts +15 -0
- package/src/bytes-codec.ts +62 -0
- package/src/camera-authoring.ts +175 -0
- package/src/cancellation-reason.ts +58 -0
- package/src/canvas-preview-frames.ts +482 -0
- package/src/capture-camera-pose.ts +77 -0
- package/src/chrome-registry.ts +159 -0
- package/src/chrome-slot-registry.ts +91 -0
- package/src/collaboration-client.ts +264 -0
- package/src/collaboration-presence.ts +41 -0
- package/src/command-dispatch.ts +19 -0
- package/src/command-listener.ts +2743 -0
- package/src/command-registry.ts +70 -0
- package/src/component-board-registry.ts +205 -0
- package/src/component-states-registry.ts +200 -0
- package/src/components/AgentPresentationNotice.tsx +75 -0
- package/src/components/AlignToolbar.tsx +204 -0
- package/src/components/AppRoot.tsx +394 -0
- package/src/components/ApplicationMenus.tsx +415 -0
- package/src/components/AssetBrowser.tsx +2843 -0
- package/src/components/AssetEditorShell.tsx +216 -0
- package/src/components/AssetInspectorToolSection.tsx +124 -0
- package/src/components/BoardRulers.tsx +354 -0
- package/src/components/CameraInfo.tsx +81 -0
- package/src/components/CanvasSceneViewport.tsx +556 -0
- package/src/components/CapabilityCoverageSection.tsx +185 -0
- package/src/components/CenterDocuments.tsx +22 -0
- package/src/components/ChromeSlot.tsx +20 -0
- package/src/components/CodeView.tsx +470 -0
- package/src/components/CollaborationHeaderControl.css +74 -0
- package/src/components/CompactInspectorCard.tsx +190 -0
- package/src/components/CompactInspectorShell.tsx +39 -0
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +270 -0
- package/src/components/DefaultEditorLayout.tsx +106 -0
- package/src/components/DocumentHeaderStrip.tsx +96 -0
- package/src/components/DocumentShelfRail.tsx +45 -0
- package/src/components/DocumentThumbnail.tsx +345 -0
- package/src/components/EditorLeaseGuard.tsx +372 -0
- package/src/components/ErrorBanner.tsx +123 -0
- package/src/components/FolderPreviewTile.tsx +159 -0
- package/src/components/GameHierarchy.tsx +3842 -0
- package/src/components/GameplaySessionTimeline.tsx +295 -0
- package/src/components/InspectionProjection.tsx +968 -0
- package/src/components/Inspector.tsx +233 -0
- package/src/components/InspectorCanvasPreview.tsx +35 -0
- package/src/components/InspectorFieldsSection.tsx +290 -0
- package/src/components/InspectorObjectPreview.tsx +57 -0
- package/src/components/InspectorStoriesSection.tsx +92 -0
- package/src/components/InspectorToolSection.tsx +96 -0
- package/src/components/InspectorTransformSection.tsx +245 -0
- package/src/components/LightExplorerPanel.tsx +433 -0
- package/src/components/MediaProperties.tsx +145 -0
- package/src/components/NonThreeAuthoringBootstrap.tsx +19 -0
- package/src/components/Object3DDocumentToolbar.css +100 -0
- package/src/components/Object3DDocumentToolbar.tsx +454 -0
- package/src/components/Object3DDocumentViewport.tsx +58 -0
- package/src/components/ProjectHeader.css +617 -0
- package/src/components/ProjectHeader.tsx +328 -0
- package/src/components/ProjectLayout.tsx +249 -0
- package/src/components/ReactCanvasControls.tsx +284 -0
- package/src/components/RootSelectionOverlay.tsx +3162 -0
- package/src/components/RootTextEditor.tsx +79 -0
- package/src/components/SaveStatus.tsx +70 -0
- package/src/components/StageHost.tsx +1954 -0
- package/src/components/StageOverlays.tsx +21 -0
- package/src/components/StartupErrorScreen.tsx +76 -0
- package/src/components/StartupLoadingScreen.tsx +65 -0
- package/src/components/StatsOverlay.tsx +78 -0
- package/src/components/SurfaceStateOverlay.tsx +24 -0
- package/src/components/ToolContributionSurfaces.tsx +65 -0
- package/src/components/ToolHost.tsx +370 -0
- package/src/components/ToolObject3DPreview.tsx +39 -0
- package/src/components/Toolbar.tsx +595 -0
- package/src/components/TransientHint.tsx +44 -0
- package/src/components/TransportStrip.tsx +174 -0
- package/src/components/VersionControlSection.tsx +470 -0
- package/src/components/VgaiLogo.css +83 -0
- package/src/components/VgaiLogo.tsx +35 -0
- package/src/components/ViewportControlsHint.tsx +60 -0
- package/src/components/ViewportFurniture.tsx +401 -0
- package/src/components/ViewportOverlay.tsx +145 -0
- package/src/components/ViewportOverlaysMenu.tsx +153 -0
- package/src/components/ViewportShadingMenu.tsx +300 -0
- package/src/components/ViewportViewMenu.tsx +101 -0
- package/src/components/WorkspaceDocumentSurface.tsx +204 -0
- package/src/components/WorkspaceUtilitySurface.tsx +22 -0
- package/src/components/WorktreeSwitcher.css +239 -0
- package/src/components/WorktreeSwitcher.tsx +890 -0
- package/src/components/account-documents.tsx +1162 -0
- package/src/components/asset-documents.tsx +812 -0
- package/src/components/asset-editor-persistence.ts +216 -0
- package/src/components/asset-selection-section.tsx +535 -0
- package/src/components/asset-thumbnails.tsx +312 -0
- package/src/components/asset-viewers/AudioViewer.tsx +201 -0
- package/src/components/asset-viewers/EntityModelDocument.tsx +122 -0
- package/src/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
- package/src/components/asset-viewers/GenericJsonViewer.tsx +102 -0
- package/src/components/asset-viewers/ImageViewer.tsx +300 -0
- package/src/components/asset-viewers/JsonAssetDocument.tsx +93 -0
- package/src/components/asset-viewers/LiveModuleDocument.tsx +417 -0
- package/src/components/asset-viewers/LutAssetDocument.tsx +444 -0
- package/src/components/asset-viewers/ModelAssetDocument.tsx +105 -0
- package/src/components/asset-viewers/Object3DPreview.tsx +359 -0
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +421 -0
- package/src/components/asset-viewers/PasteboardModuleDocument.tsx +67 -0
- package/src/components/asset-viewers/QuarksAssetDocument.tsx +526 -0
- package/src/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
- package/src/components/asset-viewers/SourceAssetViewer.tsx +281 -0
- package/src/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
- package/src/components/asset-viewers/VideoViewer.tsx +101 -0
- package/src/components/asset-viewers/shader-source.ts +144 -0
- package/src/components/asset-workflow.css +731 -0
- package/src/components/board-guides.ts +150 -0
- package/src/components/compact-inspector.css +539 -0
- package/src/components/core-utilities.tsx +90 -0
- package/src/components/editor-notifications.css +40 -0
- package/src/components/engine-workspace.css +274 -0
- package/src/components/inspector-preview-section.tsx +238 -0
- package/src/components/inspector-property-grouping.ts +64 -0
- package/src/components/inspector-revert-label.ts +20 -0
- package/src/components/inspector-selection.ts +42 -0
- package/src/components/inspector-stories-gating.ts +171 -0
- package/src/components/inspector-transform-subject.ts +11 -0
- package/src/components/inspector-transform.ts +75 -0
- package/src/components/kind-documents.tsx +500 -0
- package/src/components/palette-action-publisher.tsx +140 -0
- package/src/components/primitives/DraftColorInput.tsx +74 -0
- package/src/components/product-shell.css +1829 -0
- package/src/components/project-tool-documents.tsx +410 -0
- package/src/components/scene-documents.tsx +223 -0
- package/src/components/stage-keyboard.tsx +37 -0
- package/src/components/stage-overlay-set.tsx +107 -0
- package/src/components/stage-presence-markers.ts +478 -0
- package/src/components/standard-viewport-dressing.ts +417 -0
- package/src/components/status-contributions.tsx +403 -0
- package/src/components/tool-documents.tsx +309 -0
- package/src/components/tool-schema-form.tsx +262 -0
- package/src/components/use-after-paint.ts +41 -0
- package/src/components/use-project-image-assets.ts +86 -0
- package/src/components/viewport-header-controls.css +90 -0
- package/src/components/viewport-surface-status.tsx +55 -0
- package/src/components/workspace-history.ts +32 -0
- package/src/components/workspace-static-panel-registry.tsx +152 -0
- package/src/components/workspace-surfaces.css +621 -0
- package/src/components/world-documents.tsx +586 -0
- package/src/components/world-overlay-gestures.ts +1694 -0
- package/src/composite-screenshot.ts +1641 -0
- package/src/console-sync.ts +131 -0
- package/src/constraint-helper.ts +338 -0
- package/src/content-entry-source-registry.ts +184 -0
- package/src/coverage/authoring-seam-evidence.ts +300 -0
- package/src/coverage/canvas-reveal.ts +192 -0
- package/src/coverage/design-time-surfaces.ts +101 -0
- package/src/coverage/live-seam-evidence.ts +11 -0
- package/src/coverage/ontology-invariants.ts +466 -0
- package/src/coverage/session-vitals.ts +501 -0
- package/src/coverage/system-seam-evidence.ts +72 -0
- package/src/crash-null-boundary.ts +36 -0
- package/src/creation-site-edit.ts +1479 -0
- package/src/creation-site-registry.ts +160 -0
- package/src/delegate-harness-registry.ts +143 -0
- package/src/document-context-registry.ts +177 -0
- package/src/document-open-registry.ts +200 -0
- package/src/document-preview-source.ts +20 -0
- package/src/document-renderer-session.ts +138 -0
- package/src/editor-api.ts +46 -0
- package/src/editor-chrome-capture.ts +139 -0
- package/src/editor-commands.ts +164 -0
- package/src/editor-console.ts +563 -0
- package/src/editor-current-view.ts +82 -0
- package/src/editor-document-probe.ts +881 -0
- package/src/editor-git-client.ts +115 -0
- package/src/editor-host-door.ts +462 -0
- package/src/editor-hotkeys.ts +842 -0
- package/src/editor-lease-view.ts +39 -0
- package/src/editor-lease.ts +415 -0
- package/src/editor-mode.ts +19 -0
- package/src/editor-notifications.ts +140 -0
- package/src/editor-presence.ts +563 -0
- package/src/editor-presentation-activity.ts +50 -0
- package/src/editor-presentation-notice.ts +42 -0
- package/src/editor-runtime.tsx +145 -0
- package/src/editor-server-response.ts +86 -0
- package/src/editor-session-attribution.ts +75 -0
- package/src/editor-session-mode.ts +54 -0
- package/src/editor-shell-store.ts +1304 -0
- package/src/editor-state-facets.ts +74 -0
- package/src/editor-styles.css +33 -0
- package/src/editor-view-presentation.ts +810 -0
- package/src/editor-viewport.ts +5302 -0
- package/src/entity-lod.ts +31 -0
- package/src/entity-object.ts +91 -0
- package/src/files/file-provider.ts +62 -0
- package/src/files/project-files.ts +264 -0
- package/src/finders/index.ts +136 -0
- package/src/finders/scenes-from-entrypoint-selection.ts +387 -0
- package/src/frame/bridge.tsx +1408 -0
- package/src/frame/product.ts +77 -0
- package/src/gameplay-dom-recording.ts +319 -0
- package/src/gameplay-export-state.ts +14 -0
- package/src/gameplay-replay.ts +417 -0
- package/src/gameplay-session-time.ts +9 -0
- package/src/gameplay-sessions.ts +204 -0
- package/src/harness-chat-types.ts +253 -0
- package/src/hierarchy-component-marks.ts +298 -0
- package/src/hierarchy-drop.ts +91 -0
- package/src/hierarchy-expansion-state.ts +80 -0
- package/src/hierarchy-header-slot.ts +52 -0
- package/src/hierarchy-internals.ts +197 -0
- package/src/hierarchy-kind-icon.ts +217 -0
- package/src/hierarchy-mark-reader.ts +73 -0
- package/src/hierarchy-menu-registry.ts +67 -0
- package/src/hierarchy-node-rows.ts +307 -0
- package/src/hierarchy-panel-view.ts +280 -0
- package/src/hierarchy-projection.ts +76 -0
- package/src/hierarchy-row-cache.ts +243 -0
- package/src/hierarchy-row-model.ts +308 -0
- package/src/hierarchy-rows.ts +11 -0
- package/src/hierarchy-walk.ts +86 -0
- package/src/history/editor-session.ts +25 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-delegate.ts +163 -0
- package/src/history/history-limit-notices.ts +43 -0
- package/src/history/history-service.ts +1173 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +386 -0
- package/src/history/project-root-history-backends.ts +139 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +103 -0
- package/src/history/source-history-backend.ts +546 -0
- package/src/history/types.ts +124 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hotkeys.ts +188 -0
- package/src/inference-diagnostics.ts +69 -0
- package/src/initial-project.ts +80 -0
- package/src/inspection/active-subject.ts +578 -0
- package/src/inspection/active-surface.ts +142 -0
- package/src/inspection/compose.ts +1064 -0
- package/src/inspection/display.ts +170 -0
- package/src/inspection/document-subject.ts +109 -0
- package/src/inspection/game-subject.ts +85 -0
- package/src/inspection/model.ts +542 -0
- package/src/inspection/null-subject.ts +115 -0
- package/src/inspection/serialize.ts +357 -0
- package/src/inspection/use-active-inspection.ts +180 -0
- package/src/inspector-presentation.ts +201 -0
- package/src/inspector-section-registry.ts +221 -0
- package/src/instance-source-actions.ts +163 -0
- package/src/instanced-presentation.ts +164 -0
- package/src/js-heap.ts +71 -0
- package/src/key-actions.ts +91 -0
- package/src/keymap-presets.ts +401 -0
- package/src/layout-policy.ts +31 -0
- package/src/learn-links.ts +73 -0
- package/src/light-explorer-model.ts +134 -0
- package/src/live-canvas-frame.ts +55 -0
- package/src/live-document.ts +279 -0
- package/src/live-module-source.ts +230 -0
- package/src/live-session-registry.ts +220 -0
- package/src/live-transition.ts +633 -0
- package/src/manifest-project.ts +107 -0
- package/src/model-thumbnail.ts +523 -0
- package/src/native-selection-style.ts +202 -0
- package/src/object3d-document-write-policy.ts +137 -0
- package/src/packaged-runtime.ts +108 -0
- package/src/palettes/maya.palette.json +57 -0
- package/src/palettes/substance.palette.json +57 -0
- package/src/pasteboard-module.ts +81 -0
- package/src/performance-profiler.ts +367 -0
- package/src/performance-sources.ts +69 -0
- package/src/play-boot-phase.ts +145 -0
- package/src/presentation-surface.ts +248 -0
- package/src/project-adapter.ts +1143 -0
- package/src/project-asset-refresh.ts +26 -0
- package/src/project-declaration-refresh.ts +31 -0
- package/src/project-local-state.ts +118 -0
- package/src/project-manager.ts +243 -0
- package/src/project-module-changes.ts +163 -0
- package/src/project-module-split.ts +266 -0
- package/src/project-provenance.ts +115 -0
- package/src/project-ready.ts +42 -0
- package/src/project-session-reset.ts +67 -0
- package/src/project-shape.ts +68 -0
- package/src/project-tool-discovery.ts +66 -0
- package/src/project-tools.ts +107 -0
- package/src/project-work-types.ts +149 -0
- package/src/projection/three.ts +898 -0
- package/src/projection/types.ts +44 -0
- package/src/readiness.ts +113 -0
- package/src/reflection-probe-helper.ts +142 -0
- package/src/reported-play-state.ts +90 -0
- package/src/resolve-relative-specifier.ts +33 -0
- package/src/scene-document-plan.ts +315 -0
- package/src/scene-framing.ts +315 -0
- package/src/scene-live-open.ts +210 -0
- package/src/scene-view-fog.ts +89 -0
- package/src/scoped-game-css.ts +152 -0
- package/src/session-orphan-record.ts +193 -0
- package/src/session-tombstone.ts +126 -0
- package/src/settings/settings-provider.ts +82 -0
- package/src/settings-store.ts +345 -0
- package/src/shared-view-restore.ts +42 -0
- package/src/shell-store-door.ts +45 -0
- package/src/source-conflict.ts +122 -0
- package/src/spatial-handle-visuals.ts +332 -0
- package/src/stage-context.ts +341 -0
- package/src/stage-store-registry.ts +63 -0
- package/src/stale-chunk-recovery.ts +34 -0
- package/src/state-report-deferral.ts +73 -0
- package/src/storage/host-files-storage.ts +97 -0
- package/src/storage/http-storage.ts +174 -0
- package/src/storage/index.ts +75 -0
- package/src/storage/mem-storage.ts +149 -0
- package/src/storage/path-lock.ts +44 -0
- package/src/storage/paths.ts +26 -0
- package/src/storage/types.ts +116 -0
- package/src/stories/StoryComponentThumbnail.tsx +184 -0
- package/src/stories/StoryPreviewMount.tsx +306 -0
- package/src/stories/component-content-source.tsx +106 -0
- package/src/stories/component-states-source.ts +78 -0
- package/src/stories/compose-project-stories.ts +255 -0
- package/src/stories/pixi-story-model.ts +30 -0
- package/src/stories/prefabs-finder.ts +54 -0
- package/src/stories/prefabs-from-stories.ts +182 -0
- package/src/stories/project-story-discovery.ts +78 -0
- package/src/stories/project-story-regions.ts +24 -0
- package/src/stories/story-actions.ts +54 -0
- package/src/stories/story-capture-command.ts +124 -0
- package/src/stories/story-capture.ts +584 -0
- package/src/stories/story-declared-medium.ts +126 -0
- package/src/stories/story-discovery.ts +176 -0
- package/src/stories/story-document-openers.ts +36 -0
- package/src/stories/story-dom-runtime.ts +78 -0
- package/src/stories/story-grouping.ts +111 -0
- package/src/stories/story-lane.ts +88 -0
- package/src/stories/story-mount-turn.ts +27 -0
- package/src/stories/story-opener.ts +260 -0
- package/src/stories/story-pixi-preview.ts +408 -0
- package/src/stories/story-presentation.ts +215 -0
- package/src/stories/story-registry.ts +517 -0
- package/src/stories/story-three-preview.ts +807 -0
- package/src/stories/three-story-model.ts +96 -0
- package/src/story-three-preview-runtime.ts +56 -0
- package/src/surface-keyboard.ts +112 -0
- package/src/surface-state.ts +124 -0
- package/src/tab-bootstrap.js +628 -0
- package/src/tab-census.ts +198 -0
- package/src/tab-lifecycle-client.ts +196 -0
- package/src/theme-library.ts +844 -0
- package/src/theme-preference.ts +378 -0
- package/src/theme.css +3437 -0
- package/src/three-viewport/bone-selection-highlight.ts +119 -0
- package/src/three-viewport/camera-fit.ts +41 -0
- package/src/three-viewport/interactive-renderer.ts +132 -0
- package/src/three-viewport/selection-brackets.ts +310 -0
- package/src/three-viewport/selection-outline.ts +183 -0
- package/src/three-viewport/skeleton-helper.ts +61 -0
- package/src/three-viewport/source-color.ts +167 -0
- package/src/three-viewport/studio-environment.ts +96 -0
- package/src/three-viewport-presentation.ts +23 -0
- package/src/tool-contribution-play.ts +74 -0
- package/src/tool-loader.ts +1798 -0
- package/src/transform-mode-request.ts +57 -0
- package/src/transient-hint.ts +78 -0
- package/src/trigger-volume-helper.ts +116 -0
- package/src/ui-source/adapter-region-includes.ts +241 -0
- package/src/ui-source/ensure-import.ts +132 -0
- package/src/ui-source/file-region-resolver.ts +302 -0
- package/src/ui-source/inspect.ts +775 -0
- package/src/ui-source/oid-transform.ts +1845 -0
- package/src/ui-source/plan-csf-story.ts +196 -0
- package/src/ui-source/plan-extract-component.ts +421 -0
- package/src/ui-source/plan-fork-component.ts +621 -0
- package/src/ui-source/plan-named-style.ts +95 -0
- package/src/ui-source/plan-source-edit.ts +353 -0
- package/src/ui-source/r3f-contract-resolver.ts +327 -0
- package/src/ui-source/r3f-diagnostic-index.ts +165 -0
- package/src/ui-source/r3f-environment-binding.ts +133 -0
- package/src/ui-source/r3f-joint-binding.ts +251 -0
- package/src/ui-source/r3f-lod-binding.ts +91 -0
- package/src/ui-source/r3f-particle-binding.ts +235 -0
- package/src/ui-source/r3f-physics-binding.ts +483 -0
- package/src/ui-source/r3f-project-contracts.ts +444 -0
- package/src/ui-source/relative-import-specifier.ts +34 -0
- package/src/ui-source/reparent-guard.ts +544 -0
- package/src/ui-source/source-edit-request.ts +97 -0
- package/src/ui-source/source-write-backend.ts +618 -0
- package/src/ui-source/syntactic-prop-specs.ts +296 -0
- package/src/ui-source/tier-source-write-backend.ts +279 -0
- package/src/ui-source/ts-ast.ts +171 -0
- package/src/ui-source/utility-class-support.ts +169 -0
- package/src/ui-source/write-component-default.ts +133 -0
- package/src/ui-source/writer.ts +1989 -0
- package/src/viewport-activation-timings.ts +840 -0
- package/src/viewport-authoring-policy.ts +186 -0
- package/src/viewport-controls-hint.ts +57 -0
- package/src/viewport-door.ts +237 -0
- package/src/viewport-shading-boundary.ts +12 -0
- package/src/vite-error-surface.ts +141 -0
- package/src/wait-until.ts +37 -0
- package/src/workspace-areas.ts +156 -0
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-available-documents.ts +127 -0
- package/src/workspace-core-utilities.ts +31 -0
- package/src/workspace-document-ids.ts +59 -0
- package/src/workspace-document-registry.ts +612 -0
- package/src/workspace-document-restore.ts +145 -0
- package/src/workspace-host-commands.ts +141 -0
- package/src/workspace-persistence-gate.ts +40 -0
- package/src/workspace-play-utilities.ts +44 -0
- package/src/workspace-presets.ts +425 -0
- package/src/workspace-regions.ts +294 -0
- package/src/workspace-state-persistence.ts +536 -0
- package/src/workspace-static-panels.ts +73 -0
- package/src/workspace-status-registry.ts +121 -0
- package/src/workspace-style.ts +239 -0
- package/src/workspace-utility-commands.ts +74 -0
- package/src/workspace-utility-registry.ts +263 -0
- package/src/workspace-viewport-rect.ts +97 -0
- package/src/world-adoption.ts +115 -0
- package/src/world-document-routing.ts +104 -0
- package/vite-plugin-creation-site-write.ts +184 -0
- package/vite-plugin-creation-site.ts +80 -0
- package/vite-plugin-game-static.ts +303 -0
- package/vite-plugin-module-doorways.ts +336 -0
- package/vite-plugin-product-contributions.ts +197 -0
- package/vite-plugin-project-game-static.ts +125 -0
- package/vite-plugin-project-jsx-js.ts +65 -0
- package/vite-plugin-project-root-absolute-assets.ts +359 -0
- package/vite-plugin-shared-react.ts +420 -0
- package/vite-plugin-shared-three.ts +203 -0
- 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
|
+
}
|