@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,1483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express app factory for the editor backend.
|
|
3
|
+
*
|
|
4
|
+
* All /__editor/* API routes extracted from vite-plugin-asset-browser.ts.
|
|
5
|
+
* Works in both dev (alongside Vite middleware) and production (standalone).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
11
|
+
import type { Server as HttpServer } from 'node:http';
|
|
12
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path';
|
|
13
|
+
import type { EditorServerCompatibility } from '@volter/editor-sdk/session/editor-compatibility';
|
|
14
|
+
import { isContainedRelativePath } from '@volter/editor-sdk/session/relative-path-guard';
|
|
15
|
+
import chokidar, { type FSWatcher } from 'chokidar';
|
|
16
|
+
import type { Request, Response, Router } from 'express';
|
|
17
|
+
import express from 'express';
|
|
18
|
+
import { MANIFEST_FILENAME } from '@volter/editor-project/manifest/filename';
|
|
19
|
+
import { resolveManifestPath } from '@volter/editor-project/manifest/locate';
|
|
20
|
+
import { GAME_MANIFEST_VERSION } from '@volter/editor-project/manifest/schema';
|
|
21
|
+
import type { TripwireGate } from './support/project/build-discipline';
|
|
22
|
+
import {
|
|
23
|
+
openSessionJournal,
|
|
24
|
+
type SessionJournal,
|
|
25
|
+
type SessionJournalEvent,
|
|
26
|
+
} from './support/project/session-journal';
|
|
27
|
+
import type { HarnessChatSnapshot } from '../src/harness-chat-types';
|
|
28
|
+
import { EditorAccountService } from './account-service';
|
|
29
|
+
import { createAssetLibraryRouter } from './asset-library-routes';
|
|
30
|
+
import { canonicalProjectRoot } from './canonical-path';
|
|
31
|
+
import {
|
|
32
|
+
checkoutWorkspaceIdentityError,
|
|
33
|
+
createWorkspaceIdentityRestartMonitor,
|
|
34
|
+
type WorkspaceIdentityRestartMonitor,
|
|
35
|
+
} from './checkout-workspace-preflight';
|
|
36
|
+
// The pieces that used to sit above `createEditorServer` in this file, and the
|
|
37
|
+
// public names other modules import FROM this path. The re-exports are
|
|
38
|
+
// deliberate: `editor-server.ts` stays the one import site for the server's
|
|
39
|
+
// public surface even though the implementations now live beside it.
|
|
40
|
+
import {
|
|
41
|
+
type AgentAuthorLease,
|
|
42
|
+
agentAuthorLease,
|
|
43
|
+
filesystemMutationAuthor,
|
|
44
|
+
localCollaborationRole,
|
|
45
|
+
} from './collaboration-attribution';
|
|
46
|
+
import {
|
|
47
|
+
CollaborationConflictError,
|
|
48
|
+
type CollaborationRole,
|
|
49
|
+
collaborationSession,
|
|
50
|
+
} from './collaboration-session';
|
|
51
|
+
import { createConsoleLedger } from './console-ledger';
|
|
52
|
+
import type { EditorServerOptions } from './editor-server-options';
|
|
53
|
+
import { broadcast, sendToAllClients, sendToClient } from './editor-sse';
|
|
54
|
+
import { engineProvenance } from './engine-provenance';
|
|
55
|
+
import {
|
|
56
|
+
createEngineSourceRestartGate,
|
|
57
|
+
type EngineSourceRestartGate,
|
|
58
|
+
} from './engine-source-restart';
|
|
59
|
+
import { reconcileGenerationJobs } from './generation-reconciler';
|
|
60
|
+
import { harnessChatCallerSessionFromEnv } from './harness-chat-caller';
|
|
61
|
+
import { type FrontendHandoffResult, HarnessChatService } from './harness-chat-service';
|
|
62
|
+
import { LAUNCHER_SETTINGS_PATH } from './launcher-settings';
|
|
63
|
+
import { openBrowserUrl, shouldOpenTabInBackground } from './open-browser';
|
|
64
|
+
import type { PlayPhaseRecord } from './play-stall';
|
|
65
|
+
import { withDependencyChangeInvalidation } from './project-dependency-invalidation';
|
|
66
|
+
import {
|
|
67
|
+
hashAndSize,
|
|
68
|
+
headerValue,
|
|
69
|
+
readRunningEngineVersion,
|
|
70
|
+
unlinkIfPresent,
|
|
71
|
+
} from './project-file-scan';
|
|
72
|
+
import { setKindModuleLoader } from './project-kinds';
|
|
73
|
+
import { createProjectWatch } from './project-watch';
|
|
74
|
+
import { ProjectWorkCoordinator } from './project-work-coordinator';
|
|
75
|
+
import { addToRecentProjects, RECENT_PROJECTS_PATH } from './recent-projects-store';
|
|
76
|
+
import { registerAccountRoutes } from './routes/account';
|
|
77
|
+
import { registerAgentRoutes } from './routes/agents';
|
|
78
|
+
import { registerAssetRoutes } from './routes/assets';
|
|
79
|
+
import { registerBuildRoutes } from './routes/build';
|
|
80
|
+
import { registerCollaborationRoutes } from './routes/collaboration';
|
|
81
|
+
import { registerConfigurationRoutes, stopAllConfigurations } from './routes/configurations';
|
|
82
|
+
import type { RouteContext, SourceValidationState, TrustedShareIdentity } from './routes/context';
|
|
83
|
+
import { createControlPlane } from './routes/control-plane';
|
|
84
|
+
import { registerLogRoutes } from './routes/logs';
|
|
85
|
+
import {
|
|
86
|
+
registerProjectAttributionRoute,
|
|
87
|
+
registerProjectIdentityRoutes,
|
|
88
|
+
} from './routes/project-identity';
|
|
89
|
+
import { registerProjectOpenRoutes } from './routes/project-open';
|
|
90
|
+
import {
|
|
91
|
+
registerModuleTransportNotFound,
|
|
92
|
+
registerProjectSourceRoutes,
|
|
93
|
+
} from './routes/project-source';
|
|
94
|
+
import { registerProjectStateRoutes } from './routes/project-state';
|
|
95
|
+
import { registerRelayRoutes } from './routes/relay';
|
|
96
|
+
import { registerServedModuleRoutes } from './routes/served-modules';
|
|
97
|
+
import { registerSessionTabRoutes } from './routes/session-tabs';
|
|
98
|
+
import { registerSettingsRoutes } from './routes/settings';
|
|
99
|
+
import { registerShareControlRoutes } from './routes/share-control';
|
|
100
|
+
import { registerThemeRoutes } from './routes/themes';
|
|
101
|
+
import { registerToolRoutes } from './routes/tools';
|
|
102
|
+
import { registerWorktreeRoutes } from './routes/worktrees';
|
|
103
|
+
import {
|
|
104
|
+
isAllowedEditorOrigin,
|
|
105
|
+
isCanonicalPathInside,
|
|
106
|
+
isCanonicalWritePathInside,
|
|
107
|
+
isPathInside,
|
|
108
|
+
} from './server-utils';
|
|
109
|
+
import { processSessionId, sessionIdentity, writesRecentProjects } from './session-registry';
|
|
110
|
+
import { verifyShareClaimHeaders } from './share-claims';
|
|
111
|
+
import { ShareHost } from './share-host';
|
|
112
|
+
import { createTabHeartbeatServer } from './tab-heartbeat';
|
|
113
|
+
import { createTabLifecycle, type TabLifecycleController } from './tab-lifecycle';
|
|
114
|
+
import { TeamAgentMirror } from './team-agent-mirror';
|
|
115
|
+
|
|
116
|
+
export {
|
|
117
|
+
AGENT_AUTHOR_LEASE_MS,
|
|
118
|
+
type AgentAuthorLease,
|
|
119
|
+
agentAuthorLease,
|
|
120
|
+
filesystemMutationAuthor,
|
|
121
|
+
localCollaborationRole,
|
|
122
|
+
setGatewayParticipantRole,
|
|
123
|
+
} from './collaboration-attribution';
|
|
124
|
+
export {
|
|
125
|
+
type EditorServerOptions,
|
|
126
|
+
noEditorConnectedMessage,
|
|
127
|
+
} from './editor-server-options';
|
|
128
|
+
export {
|
|
129
|
+
headerValue,
|
|
130
|
+
installProjectDependencies,
|
|
131
|
+
} from './project-file-scan';
|
|
132
|
+
export {
|
|
133
|
+
loadRecentProjects,
|
|
134
|
+
playLogFilename,
|
|
135
|
+
playRunSlug,
|
|
136
|
+
saveRecentProjects,
|
|
137
|
+
slugify,
|
|
138
|
+
} from './recent-projects-store';
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Factory
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
export type EditorServerRouter = Router & {
|
|
145
|
+
close(): Promise<void>;
|
|
146
|
+
/**
|
|
147
|
+
* Serve the duplex control socket on this HTTP server (`upgrade` for
|
|
148
|
+
* `/__editor/events`). Every host that calls `listen` must call this, or
|
|
149
|
+
* local tabs fall back to nothing — the browser's WebSocket handshake gets
|
|
150
|
+
* no answer. Routes by pathname and leaves every other upgrade (Vite's HMR
|
|
151
|
+
* socket) to its own listener.
|
|
152
|
+
*/
|
|
153
|
+
attachControlSocket(server: HttpServer): void;
|
|
154
|
+
/**
|
|
155
|
+
* Tab bijection, session end: best-effort `tab-close` to EVERY connected
|
|
156
|
+
* tab. Hosts MUST run this before tearing the HTTP server down (the write
|
|
157
|
+
* needs a live connection); a no-op when bijection is off or a restart
|
|
158
|
+
* handoff was announced (`POST /__editor/tab/expect-restart`).
|
|
159
|
+
*/
|
|
160
|
+
notifyTabSessionEnded(): Promise<void>;
|
|
161
|
+
/**
|
|
162
|
+
* Absolute path of the project-local session journal this server is
|
|
163
|
+
* appending to (`<project>/logs/editor-*.jsonl`), or `null` when no project
|
|
164
|
+
* is open / the project is unwritable. Hosts print it at boot so the one
|
|
165
|
+
* file an agent should read is named where the URL is named.
|
|
166
|
+
*/
|
|
167
|
+
sessionJournalPath(): string | null;
|
|
168
|
+
/**
|
|
169
|
+
* RECORD A SHUTDOWN TASK THAT DID NOT FINISH. The host owns the shutdown
|
|
170
|
+
* list (`process-shutdown.ts`) and this server owns the journal, so the host
|
|
171
|
+
* hands the record through this door rather than opening a second writer on
|
|
172
|
+
* the same file. Named-task-only on purpose: a shutdown that WORKS is already
|
|
173
|
+
* recorded by `session-shutdown`, and the line worth having is the one that
|
|
174
|
+
* says which resource is still alive.
|
|
175
|
+
*/
|
|
176
|
+
journalShutdownTask(record: {
|
|
177
|
+
readonly task: string;
|
|
178
|
+
readonly outcome: 'timeout' | 'failed';
|
|
179
|
+
readonly ms: number;
|
|
180
|
+
readonly detail?: string;
|
|
181
|
+
}): void;
|
|
182
|
+
/**
|
|
183
|
+
* THE CHAT VIEW'S RUNTIME, as the environment the REH must be SPAWNED with
|
|
184
|
+
* (`frontend-handoff.ts`). The host calls this once, immediately before
|
|
185
|
+
* `startFrameWorkbench`, because an extension host inherits its server's
|
|
186
|
+
* environment and that environment is fixed at spawn. Never throws: a box with
|
|
187
|
+
* no signed-in harness answers with a `refusal` sentence and an empty `env`,
|
|
188
|
+
* and the session opens anyway.
|
|
189
|
+
*/
|
|
190
|
+
frontendHandoff(): Promise<FrontendHandoffResult>;
|
|
191
|
+
/** The standing reason the Chat view has no agent, or `null` — re-raised per page load
|
|
192
|
+
* because the console ledger's clearing rule (a) is page-scoped and this condition is not. */
|
|
193
|
+
frontendRefusal(): string | null;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export function createEditorServer(options: EditorServerOptions): EditorServerRouter {
|
|
197
|
+
const { engineRoot } = options;
|
|
198
|
+
const recentProjectsPath = options.recentProjectsPath ?? RECENT_PROJECTS_PATH;
|
|
199
|
+
const recordsRecentProjects = options.recordsRecentProjects ?? writesRecentProjects();
|
|
200
|
+
/** `addToRecentProjects`, unless this session is not somebody's launcher. */
|
|
201
|
+
const recordRecentProject = async (name: string, projectDir: string): Promise<void> => {
|
|
202
|
+
if (!recordsRecentProjects) return;
|
|
203
|
+
await addToRecentProjects(name, projectDir, recentProjectsPath);
|
|
204
|
+
};
|
|
205
|
+
const launcherSettingsPath = options.launcherSettingsPath ?? LAUNCHER_SETTINGS_PATH;
|
|
206
|
+
const serverStartedAt = new Date().toISOString();
|
|
207
|
+
const runningEngineVersion = readRunningEngineVersion(engineRoot);
|
|
208
|
+
// WHICH engine source, not just which version — see engine-provenance.ts.
|
|
209
|
+
// Primed here (not awaited) so the first `/__editor/project` answer already
|
|
210
|
+
// has it and no boot path ever waits on git.
|
|
211
|
+
const enginePromise = engineProvenance(engineRoot);
|
|
212
|
+
let staleSource: { changedPath: string; changedAt: string } | null = null;
|
|
213
|
+
let engineSourceWatcher: FSWatcher | null = null;
|
|
214
|
+
let engineSourceRestartGate: EngineSourceRestartGate | null = null;
|
|
215
|
+
let workspaceIdentityRestartMonitor: WorkspaceIdentityRestartMonitor | null = null;
|
|
216
|
+
// Mutable project state — updated by open-project endpoint. Declared before
|
|
217
|
+
// the source monitors because workspace-link identity follows the active
|
|
218
|
+
// project dynamically.
|
|
219
|
+
let projectRoot = options.projectPath ?? engineRoot;
|
|
220
|
+
|
|
221
|
+
const markSourceStale = (change: { changedPath: string; changedAt: string }): void => {
|
|
222
|
+
if (staleSource) return;
|
|
223
|
+
staleSource = change;
|
|
224
|
+
options.onEngineSourceStale?.(staleSource);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if (options.watchEngineSource) {
|
|
228
|
+
// ONLY the Node process's own source belongs here: the restart-required
|
|
229
|
+
// banner BLOCKS the whole editor, so it must fire only when the running
|
|
230
|
+
// process genuinely holds outdated code. package.json / package-lock.json
|
|
231
|
+
// are deliberately NOT watched (owner, 2026-08-07): a manifest or lockfile
|
|
232
|
+
// edit changes nothing the running process has already loaded — deps take
|
|
233
|
+
// effect per-use through the project dependency fingerprint
|
|
234
|
+
// (`project-dependency-invalidation.ts`), and a truly reinstalled
|
|
235
|
+
// node_modules surfaces as real module errors, not silently. Measured
|
|
236
|
+
// cost of watching them: a sibling PR's lockfile entries (#1278) reached
|
|
237
|
+
// a serving worktree via an ordinary git refresh and locked a live
|
|
238
|
+
// authoring session behind the banner, on a change with zero effect on
|
|
239
|
+
// the running server.
|
|
240
|
+
const watchedPaths = [
|
|
241
|
+
join(engineRoot, 'packages', 'editor', 'server'),
|
|
242
|
+
join(engineRoot, 'packages', 'engine', 'src'),
|
|
243
|
+
join(engineRoot, 'packages', 'create-vgai-project', 'src'),
|
|
244
|
+
join(engineRoot, 'vite.config.ts'),
|
|
245
|
+
join(engineRoot, 'tsconfig.json'),
|
|
246
|
+
join(engineRoot, 'tsconfig.server.json'),
|
|
247
|
+
].filter(existsSync);
|
|
248
|
+
// Debounce + content-aware suppression (diagnosis cause 4): raw chokidar
|
|
249
|
+
// events restarted the dev host on every mtime change — content-identical
|
|
250
|
+
// touches and multi-file bursts (git branch switch, editor "save all")
|
|
251
|
+
// produced back-to-back restart storms. The gate baselines each watched
|
|
252
|
+
// file's content hash during the initial scan (ignoreInitial: false —
|
|
253
|
+
// pre-`ready` add events ARE the scan), ignores events whose bytes are
|
|
254
|
+
// unchanged, and coalesces a burst into one restart after a quiet window.
|
|
255
|
+
engineSourceRestartGate = createEngineSourceRestartGate({
|
|
256
|
+
onRestart: (change) => {
|
|
257
|
+
markSourceStale({
|
|
258
|
+
changedPath: relative(engineRoot, resolve(change.changedPath)).split(sep).join('/'),
|
|
259
|
+
changedAt: change.changedAt,
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
engineSourceWatcher = chokidar.watch(watchedPaths, {
|
|
264
|
+
ignoreInitial: false,
|
|
265
|
+
ignored: (watchedPath) => {
|
|
266
|
+
const name = basename(watchedPath);
|
|
267
|
+
return name.startsWith('.') || name.endsWith('~') || /\.(?:swp|swo|tmp|temp)$/i.test(name);
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
let engineSourceScanDone = false;
|
|
271
|
+
engineSourceWatcher.once('ready', () => {
|
|
272
|
+
engineSourceScanDone = true;
|
|
273
|
+
});
|
|
274
|
+
engineSourceWatcher.on('all', (event, changedPath: string) => {
|
|
275
|
+
if (!engineSourceScanDone) {
|
|
276
|
+
engineSourceRestartGate?.recordBaseline(changedPath);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
engineSourceRestartGate?.handleEvent(event, changedPath);
|
|
280
|
+
});
|
|
281
|
+
workspaceIdentityRestartMonitor = createWorkspaceIdentityRestartMonitor({
|
|
282
|
+
engineRoot,
|
|
283
|
+
projectPath: () => (projectRoot === engineRoot ? undefined : projectRoot),
|
|
284
|
+
onStale: markSourceStale,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let publicRoot = resolve(projectRoot, 'public');
|
|
289
|
+
// Repository/worktree identity belongs to the opened SESSION, not to each
|
|
290
|
+
// HTTP request. Resolving it shells out synchronously to Git several times;
|
|
291
|
+
// doing that from `/__editor/project`, presence, SSE and worktree chrome
|
|
292
|
+
// during the same cold boot blocked Vite's single Node event loop while the
|
|
293
|
+
// browser was waiting for CSS, shaders and source modules. Cache it for the
|
|
294
|
+
// current project. A project switch changes the key and recomputes; the
|
|
295
|
+
// worktree picker still performs its own explicit fresh repository scan.
|
|
296
|
+
let sessionIdentityCache: {
|
|
297
|
+
projectRoot: string;
|
|
298
|
+
identity: ReturnType<typeof sessionIdentity>;
|
|
299
|
+
} | null = null;
|
|
300
|
+
const currentSessionIdentity = () => {
|
|
301
|
+
if (projectRoot === engineRoot) return null;
|
|
302
|
+
if (sessionIdentityCache?.projectRoot === projectRoot) return sessionIdentityCache.identity;
|
|
303
|
+
const identity = sessionIdentity(projectRoot);
|
|
304
|
+
sessionIdentityCache = { projectRoot, identity };
|
|
305
|
+
return identity;
|
|
306
|
+
};
|
|
307
|
+
// Pay the one identity resolution before the server advertises readiness,
|
|
308
|
+
// never in competition with the browser's cold module graph.
|
|
309
|
+
currentSessionIdentity();
|
|
310
|
+
// When this server started serving THIS project — the clock behind the
|
|
311
|
+
// "never once played" tripwire below. Reset by the open-project endpoint,
|
|
312
|
+
// because a switch starts a new build on a new project, not minute 40 of the
|
|
313
|
+
// previous one.
|
|
314
|
+
let servingProjectSince = Date.now();
|
|
315
|
+
/**
|
|
316
|
+
* The page's last word on its play boot — which of play's eight steps it was
|
|
317
|
+
* ENTERING. `null` until a page reports one. See `play-stall.ts` for the
|
|
318
|
+
* three-silent-timeouts measurement this exists for, and
|
|
319
|
+
* `src/play-boot-phase.ts` for the page's half.
|
|
320
|
+
*/
|
|
321
|
+
let livePlayPhase: PlayPhaseRecord | null = null;
|
|
322
|
+
// The session journal (`<project>/logs/editor-*.jsonl`) — the DURABLE half of
|
|
323
|
+
// everything this server says. Every emit point below journals first and
|
|
324
|
+
// renders second, so nothing can print without leaving a readable line
|
|
325
|
+
// behind. Opened per PROJECT, because the file lives inside the project; a
|
|
326
|
+
// live switch closes one and opens the next (see the open-project route).
|
|
327
|
+
let journal: SessionJournal | null =
|
|
328
|
+
projectRoot === engineRoot ? null : openSessionJournal(projectRoot);
|
|
329
|
+
/** The ONE call every renderer makes before it prints. */
|
|
330
|
+
function journalEvent(event: SessionJournalEvent): void {
|
|
331
|
+
journal?.append(event);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** The open project's parsed `vgai.project.json`, or `null` when there is no
|
|
335
|
+
* project (engine-repo mode) or the file is missing/unparseable. Read fresh
|
|
336
|
+
* each call and never cached: the manifest is editable while the server
|
|
337
|
+
* runs, and every caller here is answering a question about the CURRENT
|
|
338
|
+
* project shape. `null` degrades to "the project's own `src/`", never to an
|
|
339
|
+
* error — nothing that consults this may fail because a manifest is
|
|
340
|
+
* mid-edit. */
|
|
341
|
+
function readProjectManifest(): unknown {
|
|
342
|
+
if (projectRoot === engineRoot) return null;
|
|
343
|
+
try {
|
|
344
|
+
return JSON.parse(readFileSync(resolveManifestPath(projectRoot), 'utf8')) as unknown;
|
|
345
|
+
} catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
journalEvent({ kind: 'session-started', project: projectRoot, pid: process.pid });
|
|
350
|
+
/**
|
|
351
|
+
* The 8-character id every transport journal line carries. Long enough to
|
|
352
|
+
* correlate `command-relayed` with the `client-connected` that owns it,
|
|
353
|
+
* short enough to read a column of them.
|
|
354
|
+
*/
|
|
355
|
+
function short(id: string | null | undefined): string {
|
|
356
|
+
return typeof id === 'string' ? id.slice(0, 8) : '';
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Which TAB owns a control connection.
|
|
360
|
+
*
|
|
361
|
+
* A control socket is a per-page-load thing; a TAB outlives its reloads
|
|
362
|
+
* (tab-presence.ts). The page declares its `tabId` on the events URL, so
|
|
363
|
+
* the relay can say which tab it addressed even after the connection that
|
|
364
|
+
* carried the command is gone.
|
|
365
|
+
*/
|
|
366
|
+
const clientTabIds = new Map<string, string>();
|
|
367
|
+
/**
|
|
368
|
+
* Pre-listener page errors, keyed by client id — which IS the page-load id,
|
|
369
|
+
* so a reload starts from nothing rather than inheriting the dead page's
|
|
370
|
+
* errors. Same lifetime and same teardown path as `clientTabIds` beside it.
|
|
371
|
+
*/
|
|
372
|
+
const pageErrorsByClient = new Map<string, string[]>();
|
|
373
|
+
/**
|
|
374
|
+
* THE UNRESOLVED CONSOLE SET — errors and warnings that outlive the tab that
|
|
375
|
+
* reported them (`console-ledger.ts`). Distinct from `pageErrorsByClient`
|
|
376
|
+
* beside it in every way that matters: that map is a per-page-load, first-8,
|
|
377
|
+
* bootstrap-only buffer that is DELETED when its connection closes, and this
|
|
378
|
+
* ledger is the session's durable record, accumulated across reloads and tab
|
|
379
|
+
* deaths, cleared only by a reload that does not reproduce the condition or
|
|
380
|
+
* by a named acknowledgment. Every CLI response envelope carries its counts.
|
|
381
|
+
*/
|
|
382
|
+
const consoleLedger = createConsoleLedger({
|
|
383
|
+
journal: (event) => journalEvent(event),
|
|
384
|
+
});
|
|
385
|
+
function tabIdForClient(clientId: string | null): string | null {
|
|
386
|
+
return clientId === null ? null : (clientTabIds.get(clientId) ?? null);
|
|
387
|
+
}
|
|
388
|
+
/** Every live control connection this TAB currently holds. A tab has more
|
|
389
|
+
* than one only across a reload's overlap, and none at all mid-reload —
|
|
390
|
+
* which is precisely why the relay waits instead of refusing. */
|
|
391
|
+
function clientIdsForTab(tabId: string): string[] {
|
|
392
|
+
return [...clientTabIds].filter(([, id]) => id === tabId).map(([clientId]) => clientId);
|
|
393
|
+
}
|
|
394
|
+
/** Lifecycle events address a TAB; delivery finds its current channel(s). */
|
|
395
|
+
function sendToTab(tabId: string, event: string, data: unknown): boolean {
|
|
396
|
+
let delivered = false;
|
|
397
|
+
for (const clientId of clientIdsForTab(tabId)) {
|
|
398
|
+
if (sendToClient(clientId, event, data)) delivered = true;
|
|
399
|
+
}
|
|
400
|
+
return delivered;
|
|
401
|
+
}
|
|
402
|
+
let collaborationUnsubscribe: (() => void) | null = null;
|
|
403
|
+
const participantConnections = new Map<string, number>();
|
|
404
|
+
const participantLeaveTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
405
|
+
const localParticipantIds = new Set<string>();
|
|
406
|
+
let localShareHost: {
|
|
407
|
+
participantId: string;
|
|
408
|
+
account: { id: string; email: string; name?: string };
|
|
409
|
+
} | null = null;
|
|
410
|
+
let shareHost: ShareHost | null = null;
|
|
411
|
+
const currentCollaboration = () =>
|
|
412
|
+
projectRoot === engineRoot ? null : collaborationSession(canonicalProjectRoot(projectRoot));
|
|
413
|
+
const currentCollaborationLocation = () => {
|
|
414
|
+
if (projectRoot === engineRoot) return undefined;
|
|
415
|
+
const identity = currentSessionIdentity();
|
|
416
|
+
return identity
|
|
417
|
+
? {
|
|
418
|
+
repositoryId: identity.repositoryId,
|
|
419
|
+
worktreeId: identity.worktreeId,
|
|
420
|
+
sessionId: processSessionId(),
|
|
421
|
+
}
|
|
422
|
+
: undefined;
|
|
423
|
+
};
|
|
424
|
+
const bindCollaboration = () => {
|
|
425
|
+
currentCollaboration()?.flush();
|
|
426
|
+
if (shareHost?.status().active) void shareHost.stop();
|
|
427
|
+
localShareHost = null;
|
|
428
|
+
collaborationUnsubscribe?.();
|
|
429
|
+
collaborationUnsubscribe = null;
|
|
430
|
+
const collaboration = currentCollaboration();
|
|
431
|
+
if (collaboration) {
|
|
432
|
+
collaborationUnsubscribe = collaboration.subscribeEvents((event) =>
|
|
433
|
+
broadcast(`collaboration-${event.channel}`, event),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
// Every Node-side project-module load goes through this one wrapper, so a
|
|
438
|
+
// dependency installed under a live session is picked up by tool discovery,
|
|
439
|
+
// tool execution and generation reconciliation alike.
|
|
440
|
+
const loadProjectModule = withDependencyChangeInvalidation(options.loadProjectModule, {
|
|
441
|
+
getProjectRoot: () => projectRoot,
|
|
442
|
+
invalidateModules: options.invalidateProjectModules,
|
|
443
|
+
});
|
|
444
|
+
setKindModuleLoader(loadProjectModule ?? null);
|
|
445
|
+
const initialHarnessCaller = harnessChatCallerSessionFromEnv();
|
|
446
|
+
const collaborationAgents = new Map<string, { participantId: string; projectRoot: string }>();
|
|
447
|
+
let recentAgentAuthor: AgentAuthorLease | null = null;
|
|
448
|
+
const markAgentTurnRunning = (participantId: string): void => {
|
|
449
|
+
recentAgentAuthor = agentAuthorLease(participantId);
|
|
450
|
+
};
|
|
451
|
+
const markAgentTurnEnded = (participantId: string): void => {
|
|
452
|
+
if (recentAgentAuthor?.participantId !== participantId) return;
|
|
453
|
+
recentAgentAuthor = { participantId, until: Date.now() + 2_000, running: false };
|
|
454
|
+
};
|
|
455
|
+
const teamMirror = new TeamAgentMirror();
|
|
456
|
+
/** Post whatever the room is owed for this snapshot. Called from the harness
|
|
457
|
+
* subscription AND straight after an `@agent` dispatch, because a fast turn
|
|
458
|
+
* can settle before the subscription's next snapshot arrives. */
|
|
459
|
+
const postMirroredTeamMessages = (snapshot: HarnessChatSnapshot): void => {
|
|
460
|
+
const collaboration = currentCollaboration();
|
|
461
|
+
if (!collaboration) return;
|
|
462
|
+
const authorId = `agent:${snapshot.activeSessionId}`;
|
|
463
|
+
// Drain only once the author is in the room: a drained answer is consumed,
|
|
464
|
+
// so posting it at a participant that does not exist yet would lose it.
|
|
465
|
+
// The next snapshot re-offers it, by which time the session has joined.
|
|
466
|
+
if (!collaboration.snapshot().participants.some((entry) => entry.participantId === authorId)) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
for (const text of teamMirror.drain(snapshot)) {
|
|
470
|
+
try {
|
|
471
|
+
collaboration.postMessage({ authorId, text, references: [] });
|
|
472
|
+
} catch {
|
|
473
|
+
// The participant can disappear between the harness snapshot and this projection.
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
const syncHarnessParticipant = (snapshot: HarnessChatSnapshot): void => {
|
|
478
|
+
const active = snapshot.sessions.find((session) => session.id === snapshot.activeSessionId);
|
|
479
|
+
const root = canonicalProjectRoot(projectRoot);
|
|
480
|
+
const relevant =
|
|
481
|
+
projectRoot === engineRoot
|
|
482
|
+
? []
|
|
483
|
+
: snapshot.sessions.filter((session) => {
|
|
484
|
+
if (session.id === snapshot.activeSessionId && snapshot.mode !== 'none') return true;
|
|
485
|
+
if (!session.live || !session.cwd) return false;
|
|
486
|
+
return canonicalProjectRoot(session.cwd) === root;
|
|
487
|
+
});
|
|
488
|
+
const desired = new Set(relevant.map((session) => `agent:${session.id}`));
|
|
489
|
+
for (const [sessionId, agent] of collaborationAgents) {
|
|
490
|
+
if (agent.projectRoot === root && desired.has(agent.participantId)) continue;
|
|
491
|
+
if (recentAgentAuthor?.participantId === agent.participantId) {
|
|
492
|
+
markAgentTurnEnded(agent.participantId);
|
|
493
|
+
}
|
|
494
|
+
collaborationSession(agent.projectRoot).leave(agent.participantId);
|
|
495
|
+
collaborationAgents.delete(sessionId);
|
|
496
|
+
teamMirror.forget(sessionId);
|
|
497
|
+
}
|
|
498
|
+
if (relevant.length === 0) return;
|
|
499
|
+
const collaboration = currentCollaboration();
|
|
500
|
+
if (!collaboration) return;
|
|
501
|
+
for (const session of relevant) {
|
|
502
|
+
const participantId = `agent:${session.id}`;
|
|
503
|
+
const isActive = session.id === active?.id;
|
|
504
|
+
const status = isActive
|
|
505
|
+
? snapshot.requests.some((request) => request.status === 'pending') || snapshot.error
|
|
506
|
+
? 'needs-input'
|
|
507
|
+
: snapshot.turn.state !== 'idle'
|
|
508
|
+
? 'active'
|
|
509
|
+
: snapshot.taskPlan.items.length > 0 &&
|
|
510
|
+
snapshot.taskPlan.items.every((item) =>
|
|
511
|
+
['completed', 'cancelled'].includes(item.status),
|
|
512
|
+
)
|
|
513
|
+
? 'done'
|
|
514
|
+
: 'idle'
|
|
515
|
+
: 'idle';
|
|
516
|
+
if (!collaborationAgents.has(session.id)) {
|
|
517
|
+
collaboration.join({
|
|
518
|
+
participantId,
|
|
519
|
+
displayName: session.title || session.harness || 'Coding agent',
|
|
520
|
+
kind: 'agent',
|
|
521
|
+
role: collaboration.roleFor(participantId) ?? 'terminal',
|
|
522
|
+
location: currentCollaborationLocation(),
|
|
523
|
+
status,
|
|
524
|
+
agent: { harness: session.harness, conversationId: session.identity },
|
|
525
|
+
});
|
|
526
|
+
collaborationAgents.set(session.id, { participantId, projectRoot: root });
|
|
527
|
+
} else {
|
|
528
|
+
collaboration.updateParticipant(participantId, {
|
|
529
|
+
displayName: session.title || session.harness || 'Coding agent',
|
|
530
|
+
location: currentCollaborationLocation(),
|
|
531
|
+
status,
|
|
532
|
+
agent: { harness: session.harness, conversationId: session.identity },
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
collaboration.updatePresence(participantId, {
|
|
536
|
+
document: `agent:${session.id}`,
|
|
537
|
+
file: null,
|
|
538
|
+
oid: null,
|
|
539
|
+
selection: [],
|
|
540
|
+
camera: null,
|
|
541
|
+
gesture: {
|
|
542
|
+
harness: session.harness,
|
|
543
|
+
turn: session.id === active?.id ? snapshot.turn.state : 'available',
|
|
544
|
+
operation: session.id === active?.id ? snapshot.operation : null,
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (!active || snapshot.mode === 'none') return;
|
|
549
|
+
const nextId = `agent:${active.id}`;
|
|
550
|
+
if (snapshot.turn.state === 'running') {
|
|
551
|
+
// Every fresh running snapshot RENEWS the lease; a harness that stops
|
|
552
|
+
// producing snapshots stops renewing, and attribution falls back to the
|
|
553
|
+
// host within AGENT_AUTHOR_LEASE_MS.
|
|
554
|
+
markAgentTurnRunning(nextId);
|
|
555
|
+
} else if (recentAgentAuthor?.participantId === nextId && recentAgentAuthor.running) {
|
|
556
|
+
markAgentTurnEnded(nextId);
|
|
557
|
+
}
|
|
558
|
+
postMirroredTeamMessages(snapshot);
|
|
559
|
+
};
|
|
560
|
+
const account = new EditorAccountService();
|
|
561
|
+
const harnessChat = new HarnessChatService({
|
|
562
|
+
engineRoot,
|
|
563
|
+
getProjectRoot: () => projectRoot,
|
|
564
|
+
resolveCodingInference: (workspace) => account.resolvedCodingInference(workspace),
|
|
565
|
+
onChange: (snapshot) => {
|
|
566
|
+
broadcast('harness-chat', snapshot);
|
|
567
|
+
syncHarnessParticipant(snapshot);
|
|
568
|
+
},
|
|
569
|
+
...(initialHarnessCaller ? { callerSessions: [initialHarnessCaller] } : {}),
|
|
570
|
+
});
|
|
571
|
+
const projectWork = new ProjectWorkCoordinator({
|
|
572
|
+
getProjectRoot: () => projectRoot,
|
|
573
|
+
harnessChat,
|
|
574
|
+
onChange: (snapshot) => broadcast('project-work', snapshot),
|
|
575
|
+
});
|
|
576
|
+
void projectWork.initialize();
|
|
577
|
+
const shareGatewaySecret = options.shareGatewaySecret ?? randomBytes(32).toString('base64url');
|
|
578
|
+
/** The three fields share-claim verification actually reads. Structural so
|
|
579
|
+
* a raw WebSocket upgrade request can be checked by the same function an
|
|
580
|
+
* Express `Request` is. */
|
|
581
|
+
interface ShareClaimSource {
|
|
582
|
+
readonly method: string;
|
|
583
|
+
readonly originalUrl: string;
|
|
584
|
+
header(name: string): string | undefined;
|
|
585
|
+
}
|
|
586
|
+
const trustedShareIdentity = (req: ShareClaimSource) => {
|
|
587
|
+
const claims = verifyShareClaimHeaders(
|
|
588
|
+
shareGatewaySecret,
|
|
589
|
+
req.method,
|
|
590
|
+
req.originalUrl,
|
|
591
|
+
(name) => req.header(name),
|
|
592
|
+
);
|
|
593
|
+
return claims
|
|
594
|
+
? {
|
|
595
|
+
participantId: claims.participantId,
|
|
596
|
+
invitationId: claims.invitationId,
|
|
597
|
+
credentialId: claims.credentialId,
|
|
598
|
+
role: claims.role as CollaborationRole,
|
|
599
|
+
account: claims.account,
|
|
600
|
+
}
|
|
601
|
+
: null;
|
|
602
|
+
};
|
|
603
|
+
const browserShareControlSecret = randomBytes(32).toString('base64url');
|
|
604
|
+
if (options.collaborationPort) {
|
|
605
|
+
shareHost = new ShareHost(
|
|
606
|
+
options.collaborationPort,
|
|
607
|
+
processSessionId(),
|
|
608
|
+
account,
|
|
609
|
+
() => canonicalProjectRoot(projectRoot),
|
|
610
|
+
shareGatewaySecret,
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
let generationReconcileInFlight = false;
|
|
614
|
+
const reconcileGenerations = async () => {
|
|
615
|
+
if (generationReconcileInFlight || projectRoot === engineRoot) return;
|
|
616
|
+
generationReconcileInFlight = true;
|
|
617
|
+
try {
|
|
618
|
+
// No credential is read here, and none is read by a tick that finds
|
|
619
|
+
// nothing to reconcile: `executeProjectTool` reads the ONE provider a
|
|
620
|
+
// pending job names, at the moment it runs it. See
|
|
621
|
+
// `provider-credentials.ts`'s header — this five-second timer used to
|
|
622
|
+
// sweep every provider's Keychain item on the first tick of every
|
|
623
|
+
// session, which is a stack of unlock prompts per `vgai edit`.
|
|
624
|
+
await reconcileGenerationJobs(projectRoot, loadProjectModule, 3, account);
|
|
625
|
+
} catch (error) {
|
|
626
|
+
broadcast('server-log', {
|
|
627
|
+
level: 'error',
|
|
628
|
+
message: `Generation reconciliation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
629
|
+
});
|
|
630
|
+
} finally {
|
|
631
|
+
generationReconcileInFlight = false;
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
const generationReconcileTimer = setInterval(() => void reconcileGenerations(), 5_000);
|
|
635
|
+
void reconcileGenerations();
|
|
636
|
+
|
|
637
|
+
const compatibilityIdentity = (): EditorServerCompatibility => {
|
|
638
|
+
// Unlike source bytes, project-local npm links can change without a file
|
|
639
|
+
// event the engine watcher observes. Re-evaluate them on the reuse
|
|
640
|
+
// handshake so `vgai edit` restarts a server from checkout A after the
|
|
641
|
+
// project is relinked to checkout B, instead of composing both identities.
|
|
642
|
+
const workspaceIdentityError = checkoutWorkspaceIdentityError(
|
|
643
|
+
engineRoot,
|
|
644
|
+
projectRoot === engineRoot ? undefined : projectRoot,
|
|
645
|
+
);
|
|
646
|
+
if (workspaceIdentityError && !staleSource) {
|
|
647
|
+
markSourceStale({
|
|
648
|
+
changedPath: 'node_modules workspace package links',
|
|
649
|
+
changedAt: new Date().toISOString(),
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
apiVersion: 1,
|
|
654
|
+
engineVersion: runningEngineVersion,
|
|
655
|
+
manifestVersion: GAME_MANIFEST_VERSION,
|
|
656
|
+
startedAt: serverStartedAt,
|
|
657
|
+
source: staleSource ? { state: 'restart-required', ...staleSource } : { state: 'current' },
|
|
658
|
+
};
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// #run2-cold-vite: last time this server served the editor's own INDEX
|
|
662
|
+
// page (as opposed to an API/SSE/asset request) to a browser. Detected by
|
|
663
|
+
// response Content-Type rather than by route/path, because the index page
|
|
664
|
+
// is served by two different mechanisms depending on mode (Vite's dev
|
|
665
|
+
// middleware at `/` in dev.ts, the packaged server's own
|
|
666
|
+
// `express.static` + SPA fallback) and none of those routes
|
|
667
|
+
// are owned by this file. Exposed on `/__editor/state` so the CLI's
|
|
668
|
+
// verified-open polling (`waitForVerifiedEditorOpen` in
|
|
669
|
+
// packages/vgai-cli/src/editor-sessions.ts) can tell "the browser tab
|
|
670
|
+
// arrived and is loading" apart from "the auto-open never reached a
|
|
671
|
+
// browser at all" — the distinction a cold Vite dep-optimize on drvfs
|
|
672
|
+
// needs, since that alone can blow past the old flat 15s timeout on a
|
|
673
|
+
// project's very first page load.
|
|
674
|
+
let lastIndexRequestAt: number | null = null;
|
|
675
|
+
|
|
676
|
+
// The TAB TABLE, and the bijection that reads it (tab-presence.ts /
|
|
677
|
+
// tab-lifecycle.ts): one browser tab per edited game, chosen from tabs that
|
|
678
|
+
// prove themselves by heartbeat.
|
|
679
|
+
//
|
|
680
|
+
// The table exists for EVERY session, including a headless one. Presence is
|
|
681
|
+
// not a maintenance feature — it is how the relay and `vgai status` know
|
|
682
|
+
// whether anybody is home — and a `--no-open` session that a person later
|
|
683
|
+
// opens the URL on must see that tab and take its commands. What the flag
|
|
684
|
+
// turns off is `maintain`: this session enforces one tab and never OPENS
|
|
685
|
+
// one.
|
|
686
|
+
const tabBijectionOptions = options.tabBijection?.enabled ? options.tabBijection : null;
|
|
687
|
+
/** Who wants to know a tab's page reloaded (the control plane, to settle
|
|
688
|
+
* the commands the old page-load was holding). Every lifecycle reports
|
|
689
|
+
* through here, host and participant alike. */
|
|
690
|
+
const tabReloadListeners = new Set<(tabId: string, epochCount: number) => void>();
|
|
691
|
+
const makeTabLifecycle = (
|
|
692
|
+
participantId: string | null,
|
|
693
|
+
maintain: boolean,
|
|
694
|
+
): TabLifecycleController =>
|
|
695
|
+
createTabLifecycle({
|
|
696
|
+
editorUrl: tabBijectionOptions?.editorUrl ?? '',
|
|
697
|
+
onTabReloaded: (tabId, epochCount) => {
|
|
698
|
+
for (const listener of tabReloadListeners) listener(tabId, epochCount);
|
|
699
|
+
},
|
|
700
|
+
// Placement is decided per OPEN, not once at boot: `projectRoot` is
|
|
701
|
+
// reassignable (`/__editor/open-project`), so a session that switches
|
|
702
|
+
// from a human checkout into an agent worktree switches with it.
|
|
703
|
+
openUrl:
|
|
704
|
+
tabBijectionOptions?.openUrl ??
|
|
705
|
+
((url: string) =>
|
|
706
|
+
openBrowserUrl(url, {
|
|
707
|
+
background: shouldOpenTabInBackground({
|
|
708
|
+
projectRoot: canonicalProjectRoot(projectRoot),
|
|
709
|
+
env: process.env,
|
|
710
|
+
}),
|
|
711
|
+
})),
|
|
712
|
+
journal: journalEvent,
|
|
713
|
+
probeTabs: (tabIds) => heartbeat.probe(tabIds),
|
|
714
|
+
sendToTab: (tabId, event, data) =>
|
|
715
|
+
sendToTab(tabId, event, {
|
|
716
|
+
...(typeof data === 'object' && data !== null ? data : {}),
|
|
717
|
+
participantId,
|
|
718
|
+
}),
|
|
719
|
+
// The HOST lifecycle owns no participant, so its session-end
|
|
720
|
+
// broadcast is genuinely everyone; a per-participant lifecycle
|
|
721
|
+
// stays inside its own participant's connections.
|
|
722
|
+
broadcastToClients: (event, data) =>
|
|
723
|
+
sendToAllClients(
|
|
724
|
+
event,
|
|
725
|
+
{ ...(typeof data === 'object' && data !== null ? data : {}), participantId },
|
|
726
|
+
participantId ?? undefined,
|
|
727
|
+
),
|
|
728
|
+
lastIndexRequestAt: () => lastIndexRequestAt,
|
|
729
|
+
maintain: maintain && tabBijectionOptions !== null,
|
|
730
|
+
});
|
|
731
|
+
const hostTabLifecycle = makeTabLifecycle(null, true);
|
|
732
|
+
/**
|
|
733
|
+
* The tab table's transport. It exists even for a `--no-open` session (no
|
|
734
|
+
* bijection controller at all): presence is not a maintenance feature, it
|
|
735
|
+
* is how `vgai status` and the relay know whether anybody is home, and a
|
|
736
|
+
* headless session that a person later opens the URL on must still see
|
|
737
|
+
* that tab. Beats with no controller to feed simply have nowhere to land.
|
|
738
|
+
*/
|
|
739
|
+
const heartbeat = createTabHeartbeatServer({
|
|
740
|
+
authorize: (request) =>
|
|
741
|
+
isAllowedEditorOrigin(
|
|
742
|
+
headerValue(request, 'origin'),
|
|
743
|
+
process.env['VGAI_EDITOR_HOST'] ? [process.env['VGAI_EDITOR_HOST']] : [],
|
|
744
|
+
)
|
|
745
|
+
? null
|
|
746
|
+
: 'Cross-origin request rejected.',
|
|
747
|
+
beat: (beat) => {
|
|
748
|
+
// The phase the page's main thread is inside, carried on the beat
|
|
749
|
+
// because the page's own socket cannot send once it is blocked. The
|
|
750
|
+
// stall diagnosis reads livePlayPhase; this is its second road.
|
|
751
|
+
if (beat.phase !== undefined) {
|
|
752
|
+
// Two roads carry the same announcement; journal a CHANGE, not both
|
|
753
|
+
// copies (a session's builds wrote 121 lines in eight minutes).
|
|
754
|
+
const changed = livePlayPhase?.phase !== beat.phase;
|
|
755
|
+
livePlayPhase = { phase: beat.phase, at: Date.now(), run: 0, receivedAt: Date.now() };
|
|
756
|
+
if (changed) journalEvent({ kind: 'page-phase', phase: beat.phase });
|
|
757
|
+
}
|
|
758
|
+
return hostTabLifecycle.onBeat(beat);
|
|
759
|
+
},
|
|
760
|
+
hint: () => hostTabLifecycle.tick(),
|
|
761
|
+
});
|
|
762
|
+
let hostParticipantId: string | null = null;
|
|
763
|
+
const participantTabLifecycles = new Map<string, TabLifecycleController>();
|
|
764
|
+
const clientTabLifecycles = new Map<string, TabLifecycleController>();
|
|
765
|
+
const tabLifecycleForParticipant = (
|
|
766
|
+
participantId: string,
|
|
767
|
+
hostEligible = true,
|
|
768
|
+
): TabLifecycleController => {
|
|
769
|
+
const existing = participantTabLifecycles.get(participantId);
|
|
770
|
+
if (existing) return existing;
|
|
771
|
+
if (hostEligible) {
|
|
772
|
+
// EVERY local page shares the host's tab table. A participant id is
|
|
773
|
+
// localStorage per browser PROFILE, and `open <url>` lands in whichever
|
|
774
|
+
// profile window the owner last touched — so a tab opened by a later
|
|
775
|
+
// `vgai edit` routinely carries a participant id the first tab never
|
|
776
|
+
// had. Giving that id its own lifecycle split one tab's facts across
|
|
777
|
+
// two tables: its heartbeat reached the host table (beats carry no
|
|
778
|
+
// participant), its control channel reached the new table, and the
|
|
779
|
+
// host table read a beating tab with no channel — the zombie rule —
|
|
780
|
+
// and told it to yield 30 s after it appeared. Measured on the blind
|
|
781
|
+
// modeling bench (2026-09-05): every replacement tab of a session died
|
|
782
|
+
// that way, 8 of 8, until the agent gave up. The host PARTICIPANT ID
|
|
783
|
+
// (control ownership) stays the first local one; only the table is
|
|
784
|
+
// shared, which is what "one tab per edited game" meant all along.
|
|
785
|
+
if (hostParticipantId === null) hostParticipantId = participantId;
|
|
786
|
+
participantTabLifecycles.set(participantId, hostTabLifecycle);
|
|
787
|
+
return hostTabLifecycle;
|
|
788
|
+
}
|
|
789
|
+
const lifecycle = makeTabLifecycle(participantId, false);
|
|
790
|
+
participantTabLifecycles.set(participantId, lifecycle);
|
|
791
|
+
return lifecycle;
|
|
792
|
+
};
|
|
793
|
+
// The filesystem half of the session. Constructed BEFORE the route context,
|
|
794
|
+
// because the context reads several of its outputs; it takes live root
|
|
795
|
+
// getters rather than values so a project switch moves it too.
|
|
796
|
+
const watch = createProjectWatch({
|
|
797
|
+
engineRoot,
|
|
798
|
+
consoleLedger,
|
|
799
|
+
projectRoot: () => projectRoot,
|
|
800
|
+
publicRoot: () => publicRoot,
|
|
801
|
+
journalEvent,
|
|
802
|
+
servingProjectSince: () => servingProjectSince,
|
|
803
|
+
currentCollaboration,
|
|
804
|
+
bindCollaboration,
|
|
805
|
+
recentAgentAuthor: () => recentAgentAuthor,
|
|
806
|
+
readProjectManifest,
|
|
807
|
+
});
|
|
808
|
+
const startWatcher = watch.start;
|
|
809
|
+
// ---- Editor command relay + state ----
|
|
810
|
+
let editorState: Record<string, unknown> = {};
|
|
811
|
+
const editorStatesByClient = new Map<
|
|
812
|
+
string,
|
|
813
|
+
{ state: Record<string, unknown>; updatedAt: number }
|
|
814
|
+
>();
|
|
815
|
+
// When `editorState` was last POSTed by a browser tab (epoch ms), so a
|
|
816
|
+
// caller reading a cached snapshot (no live SSE client, `connected: false`
|
|
817
|
+
// below) can say HOW STALE it is instead of just THAT it's stale. `null`
|
|
818
|
+
// until the first POST of this server's lifetime (or after a project
|
|
819
|
+
// switch clears the cache).
|
|
820
|
+
let editorStateUpdatedAt: number | null = null;
|
|
821
|
+
|
|
822
|
+
const collaborationOr400 = (res: Response) => {
|
|
823
|
+
const collaboration = currentCollaboration();
|
|
824
|
+
if (!collaboration) res.status(400).json({ error: 'No project is open.' });
|
|
825
|
+
return collaboration;
|
|
826
|
+
};
|
|
827
|
+
const defaultCollaborationRole = (
|
|
828
|
+
participantId: string,
|
|
829
|
+
invitedRole?: CollaborationRole,
|
|
830
|
+
): CollaborationRole => {
|
|
831
|
+
const collaboration = currentCollaboration();
|
|
832
|
+
if (!collaboration) return 'viewer';
|
|
833
|
+
if (invitedRole) return invitedRole;
|
|
834
|
+
return localCollaborationRole(
|
|
835
|
+
collaboration.hasMaintainer(),
|
|
836
|
+
collaboration.roleFor(participantId),
|
|
837
|
+
);
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
const localOwnerRequest = (req: Request): boolean => {
|
|
841
|
+
const address = req.socket.remoteAddress ?? '';
|
|
842
|
+
const loopback = address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';
|
|
843
|
+
return loopback && trustedShareIdentity(req) === null;
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const requireLocalOwner = (req: Request, res: Response): boolean => {
|
|
847
|
+
if (localOwnerRequest(req)) return true;
|
|
848
|
+
res.status(403).json({ error: 'Worktree controls are local to the editor owner.' });
|
|
849
|
+
return false;
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
/** The play-run log file being appended to. Owned by the log family's
|
|
853
|
+
* session routes; the relay's recording route names it in its manifest. */
|
|
854
|
+
let activeLogFile: string | null = null;
|
|
855
|
+
|
|
856
|
+
const mutationTails = new Map<string, Promise<void>>();
|
|
857
|
+
const sourceVersions = new Map<string, Array<{ revision: number; content: string | null }>>();
|
|
858
|
+
const sourceConflictTickets = new Map<string, { paths: Set<string>; expiresAt: number }>();
|
|
859
|
+
/**
|
|
860
|
+
* Read a path's remembered versions and mark it as the most recently USED.
|
|
861
|
+
*
|
|
862
|
+
* A `Map` iterates in insertion order, so evicting `keys().next()` drops the
|
|
863
|
+
* oldest-INSERTED path — the file everyone keeps editing was still first out,
|
|
864
|
+
* which is the opposite of what the cache is for. Re-inserting on every
|
|
865
|
+
* access is what makes that eviction a true LRU.
|
|
866
|
+
*/
|
|
867
|
+
const touchSourceVersions = (
|
|
868
|
+
path: string,
|
|
869
|
+
): Array<{ revision: number; content: string | null }> | null => {
|
|
870
|
+
const versions = sourceVersions.get(path);
|
|
871
|
+
if (!versions) return null;
|
|
872
|
+
sourceVersions.delete(path);
|
|
873
|
+
sourceVersions.set(path, versions);
|
|
874
|
+
return versions;
|
|
875
|
+
};
|
|
876
|
+
const rememberSourceVersion = async (
|
|
877
|
+
path: string,
|
|
878
|
+
absolute: string,
|
|
879
|
+
revision: number,
|
|
880
|
+
): Promise<void> => {
|
|
881
|
+
const versions = touchSourceVersions(path) ?? [];
|
|
882
|
+
if (versions.some((entry) => entry.revision === revision)) return;
|
|
883
|
+
let content: string | null;
|
|
884
|
+
try {
|
|
885
|
+
const info = await stat(absolute);
|
|
886
|
+
if (!info.isFile() || info.size > 2 * 1024 * 1024) return;
|
|
887
|
+
content = await readFile(absolute, 'utf8');
|
|
888
|
+
if (content.includes('\0')) return;
|
|
889
|
+
} catch (error) {
|
|
890
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') return;
|
|
891
|
+
content = null;
|
|
892
|
+
}
|
|
893
|
+
versions.push({ revision, content });
|
|
894
|
+
sourceVersions.delete(path);
|
|
895
|
+
sourceVersions.set(path, versions.slice(-4));
|
|
896
|
+
if (sourceVersions.size > 100) {
|
|
897
|
+
const oldest = sourceVersions.keys().next().value;
|
|
898
|
+
if (oldest) sourceVersions.delete(oldest);
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
const withResourceMutationLock = async <T>(
|
|
902
|
+
paths: readonly string[],
|
|
903
|
+
task: () => Promise<T>,
|
|
904
|
+
): Promise<T> => {
|
|
905
|
+
const ordered = [...new Set(paths)].sort();
|
|
906
|
+
const predecessors = ordered.map((path) => mutationTails.get(path) ?? Promise.resolve());
|
|
907
|
+
let release = () => {};
|
|
908
|
+
const gate = new Promise<void>((resolveGate) => {
|
|
909
|
+
release = resolveGate;
|
|
910
|
+
});
|
|
911
|
+
for (const path of ordered) mutationTails.set(path, gate);
|
|
912
|
+
await Promise.all(predecessors);
|
|
913
|
+
try {
|
|
914
|
+
return await task();
|
|
915
|
+
} finally {
|
|
916
|
+
release();
|
|
917
|
+
for (const path of ordered) {
|
|
918
|
+
if (mutationTails.get(path) === gate) mutationTails.delete(path);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
const commitProjectMutation = async (
|
|
924
|
+
req: Request,
|
|
925
|
+
resources: readonly { path: string; content: string | Buffer | null }[],
|
|
926
|
+
) => {
|
|
927
|
+
const trusted = trustedShareIdentity(req);
|
|
928
|
+
const normalized = await Promise.all(
|
|
929
|
+
resources.map(async (resource) => {
|
|
930
|
+
const path = resource.path.split('\\').join('/');
|
|
931
|
+
const absolute = resolve(projectRoot, path);
|
|
932
|
+
if (!isContainedRelativePath(path) || !isPathInside(projectRoot, absolute)) {
|
|
933
|
+
throw new Error(`Project mutation path is outside the project: ${resource.path}`);
|
|
934
|
+
}
|
|
935
|
+
if (trusted && !(await isCanonicalWritePathInside(projectRoot, absolute))) {
|
|
936
|
+
throw new Error(`Shared project mutation path leaves the project: ${resource.path}`);
|
|
937
|
+
}
|
|
938
|
+
return { ...resource, path, absolute };
|
|
939
|
+
}),
|
|
940
|
+
);
|
|
941
|
+
return withResourceMutationLock(
|
|
942
|
+
normalized.map((resource) => resource.path),
|
|
943
|
+
async () => {
|
|
944
|
+
const collaboration = currentCollaboration();
|
|
945
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
946
|
+
const localClaim =
|
|
947
|
+
typeof body['participantId'] === 'string' &&
|
|
948
|
+
localParticipantIds.has(body['participantId'])
|
|
949
|
+
? body['participantId']
|
|
950
|
+
: null;
|
|
951
|
+
const actorId =
|
|
952
|
+
trusted?.participantId ?? localClaim ?? filesystemMutationAuthor(recentAgentAuthor);
|
|
953
|
+
if (collaboration) {
|
|
954
|
+
const states = new Map(
|
|
955
|
+
collaboration
|
|
956
|
+
.resourceStates(normalized.map((resource) => resource.path))
|
|
957
|
+
.map((state) => [state.path, state.revision]),
|
|
958
|
+
);
|
|
959
|
+
await Promise.all(
|
|
960
|
+
normalized.map((resource) =>
|
|
961
|
+
rememberSourceVersion(
|
|
962
|
+
resource.path,
|
|
963
|
+
resource.absolute,
|
|
964
|
+
states.get(resource.path) ?? collaboration.snapshot().revision,
|
|
965
|
+
),
|
|
966
|
+
),
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
if (collaboration && trusted) {
|
|
970
|
+
if (!Number.isInteger(body['expectedRevision'])) {
|
|
971
|
+
throw new Error('Remote project mutations require expectedRevision.');
|
|
972
|
+
}
|
|
973
|
+
collaboration.assertSourceMutation(
|
|
974
|
+
actorId,
|
|
975
|
+
body['expectedRevision'] as number,
|
|
976
|
+
normalized.map((resource) => resource.path),
|
|
977
|
+
);
|
|
978
|
+
} else if (collaboration && localClaim && Number.isInteger(body['expectedRevision'])) {
|
|
979
|
+
collaboration.assertSourceMutation(
|
|
980
|
+
actorId,
|
|
981
|
+
body['expectedRevision'] as number,
|
|
982
|
+
normalized.map((resource) => resource.path),
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
for (const resource of normalized) {
|
|
986
|
+
const sha =
|
|
987
|
+
resource.content === null
|
|
988
|
+
? null
|
|
989
|
+
: createHash('sha256').update(resource.content).digest('hex').slice(0, 16);
|
|
990
|
+
// Atomic renames can produce both add and change events. Retain the
|
|
991
|
+
// digest briefly so every duplicate event for this one write is
|
|
992
|
+
// ignored; a different digest invalidates it immediately.
|
|
993
|
+
watch.expectedEditorMutations.set(resource.path, { sha, expiresAt: Date.now() + 5_000 });
|
|
994
|
+
}
|
|
995
|
+
while (watch.expectedEditorMutations.size > 200) {
|
|
996
|
+
const oldest = watch.expectedEditorMutations.keys().next().value;
|
|
997
|
+
if (!oldest) break;
|
|
998
|
+
watch.expectedEditorMutations.delete(oldest);
|
|
999
|
+
}
|
|
1000
|
+
for (const resource of normalized) {
|
|
1001
|
+
if (resource.content === null) {
|
|
1002
|
+
await unlinkIfPresent(resource.absolute);
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
await mkdir(dirname(resource.absolute), { recursive: true });
|
|
1006
|
+
const temporary = join(
|
|
1007
|
+
dirname(resource.absolute),
|
|
1008
|
+
`.${basename(resource.absolute)}.vgai-${randomUUID()}.tmp`,
|
|
1009
|
+
);
|
|
1010
|
+
try {
|
|
1011
|
+
await writeFile(temporary, resource.content);
|
|
1012
|
+
await rename(temporary, resource.absolute);
|
|
1013
|
+
} finally {
|
|
1014
|
+
await unlinkIfPresent(temporary);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const committed = await Promise.all(
|
|
1018
|
+
normalized.map(async (resource) => ({
|
|
1019
|
+
path: resource.path,
|
|
1020
|
+
sha: resource.content === null ? null : (await hashAndSize(resource.absolute)).hash,
|
|
1021
|
+
})),
|
|
1022
|
+
);
|
|
1023
|
+
const recorded = collaboration?.recordSourceMutation({
|
|
1024
|
+
authorId: actorId,
|
|
1025
|
+
source: 'editor',
|
|
1026
|
+
resources: committed,
|
|
1027
|
+
});
|
|
1028
|
+
if (recorded || !collaboration) return recorded;
|
|
1029
|
+
// A filesystem observer may have won the event-loop race after the
|
|
1030
|
+
// atomic rename. If it already recorded these exact resulting bytes,
|
|
1031
|
+
// return that real revision rather than reporting a null mutation.
|
|
1032
|
+
const states = collaboration.resourceStates(committed.map((resource) => resource.path));
|
|
1033
|
+
const matchingRevision = Math.max(
|
|
1034
|
+
0,
|
|
1035
|
+
...states
|
|
1036
|
+
.filter((state, index) => state.sha === committed[index]?.sha)
|
|
1037
|
+
.map((state) => state.revision),
|
|
1038
|
+
);
|
|
1039
|
+
return (
|
|
1040
|
+
collaboration
|
|
1041
|
+
.snapshot()
|
|
1042
|
+
.revisions.find((revision) => revision.revision === matchingRevision) ?? null
|
|
1043
|
+
);
|
|
1044
|
+
},
|
|
1045
|
+
);
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
const projectMutationError = (res: Response, error: unknown): void => {
|
|
1049
|
+
if (error instanceof CollaborationConflictError) {
|
|
1050
|
+
const collaboration = currentCollaboration();
|
|
1051
|
+
const conflictId = randomUUID();
|
|
1052
|
+
for (const [id, ticket] of sourceConflictTickets) {
|
|
1053
|
+
if (ticket.expiresAt < Date.now()) sourceConflictTickets.delete(id);
|
|
1054
|
+
}
|
|
1055
|
+
if (sourceConflictTickets.size >= 100) {
|
|
1056
|
+
const oldest = sourceConflictTickets.keys().next().value;
|
|
1057
|
+
if (oldest) sourceConflictTickets.delete(oldest);
|
|
1058
|
+
}
|
|
1059
|
+
sourceConflictTickets.set(conflictId, {
|
|
1060
|
+
paths: new Set(error.resources),
|
|
1061
|
+
expiresAt: Date.now() + 10 * 60 * 1_000,
|
|
1062
|
+
});
|
|
1063
|
+
res.status(409).json({
|
|
1064
|
+
code: 'SOURCE_REVISION_CONFLICT',
|
|
1065
|
+
conflictId,
|
|
1066
|
+
expectedRevision: error.expectedRevision,
|
|
1067
|
+
currentRevision: error.currentRevision,
|
|
1068
|
+
conflicts: collaboration?.resourceStates(error.resources) ?? [],
|
|
1069
|
+
bases: Object.fromEntries(
|
|
1070
|
+
error.resources.flatMap((path) => {
|
|
1071
|
+
const version = touchSourceVersions(path)
|
|
1072
|
+
?.filter((candidate) => candidate.revision <= error.expectedRevision)
|
|
1073
|
+
.at(-1);
|
|
1074
|
+
return version ? [[path, version.content]] : [];
|
|
1075
|
+
}),
|
|
1076
|
+
),
|
|
1077
|
+
});
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
// ---- The closure, named once ----
|
|
1084
|
+
// Everything above this line is the editor session's state. Everything below
|
|
1085
|
+
// it is routing. `ctx` is the one hand-off between the two: see
|
|
1086
|
+
// `routes/context.ts` for why mutable session state crosses as accessors
|
|
1087
|
+
// rather than as values.
|
|
1088
|
+
const ctx: RouteContext = {
|
|
1089
|
+
engineRoot,
|
|
1090
|
+
get projectRoot() {
|
|
1091
|
+
return projectRoot;
|
|
1092
|
+
},
|
|
1093
|
+
set projectRoot(next: string) {
|
|
1094
|
+
projectRoot = next;
|
|
1095
|
+
},
|
|
1096
|
+
get publicRoot() {
|
|
1097
|
+
return publicRoot;
|
|
1098
|
+
},
|
|
1099
|
+
set publicRoot(next: string) {
|
|
1100
|
+
publicRoot = next;
|
|
1101
|
+
},
|
|
1102
|
+
account,
|
|
1103
|
+
commitProjectMutation,
|
|
1104
|
+
projectMutationError,
|
|
1105
|
+
sourceConflictTickets,
|
|
1106
|
+
readProjectManifest,
|
|
1107
|
+
loadProjectModule,
|
|
1108
|
+
currentProjectComponents: watch.currentProjectComponents,
|
|
1109
|
+
currentSessionIdentity,
|
|
1110
|
+
localOwnerRequest,
|
|
1111
|
+
requireLocalOwner,
|
|
1112
|
+
launcherSettingsPath,
|
|
1113
|
+
recentProjectsPath,
|
|
1114
|
+
recordRecentProject,
|
|
1115
|
+
startWatcher,
|
|
1116
|
+
reconcileGenerations,
|
|
1117
|
+
compatibilityIdentity,
|
|
1118
|
+
enginePromise,
|
|
1119
|
+
editorStatesByClient,
|
|
1120
|
+
consoleLedger,
|
|
1121
|
+
projectValidation: watch.projectValidation,
|
|
1122
|
+
projectWarnings: watch.projectWarnings,
|
|
1123
|
+
currentValidationLogEntries: watch.currentValidationLogEntries,
|
|
1124
|
+
clientIdsForTab,
|
|
1125
|
+
participantConnections,
|
|
1126
|
+
tabIdForClient,
|
|
1127
|
+
clientTabIds,
|
|
1128
|
+
hostTabLifecycle,
|
|
1129
|
+
participantTabLifecycles,
|
|
1130
|
+
clientTabLifecycles,
|
|
1131
|
+
tabLifecycleForParticipant,
|
|
1132
|
+
onTabReloaded: (listener) => {
|
|
1133
|
+
tabReloadListeners.add(listener);
|
|
1134
|
+
},
|
|
1135
|
+
tabBijectionOptions,
|
|
1136
|
+
heartbeat,
|
|
1137
|
+
pageErrorsByClient,
|
|
1138
|
+
participantLeaveTimers,
|
|
1139
|
+
short,
|
|
1140
|
+
get livePlayPhase() {
|
|
1141
|
+
return livePlayPhase;
|
|
1142
|
+
},
|
|
1143
|
+
set livePlayPhase(next: PlayPhaseRecord | null) {
|
|
1144
|
+
livePlayPhase = next;
|
|
1145
|
+
},
|
|
1146
|
+
get hostParticipantId() {
|
|
1147
|
+
return hostParticipantId;
|
|
1148
|
+
},
|
|
1149
|
+
set hostParticipantId(next: string | null) {
|
|
1150
|
+
hostParticipantId = next;
|
|
1151
|
+
},
|
|
1152
|
+
get publicAssetsLastChangedAt() {
|
|
1153
|
+
return watch.publicAssetsLastChangedAt;
|
|
1154
|
+
},
|
|
1155
|
+
set publicAssetsLastChangedAt(next: number | null) {
|
|
1156
|
+
watch.publicAssetsLastChangedAt = next;
|
|
1157
|
+
},
|
|
1158
|
+
get publicAssetsLastPath() {
|
|
1159
|
+
return watch.publicAssetsLastPath;
|
|
1160
|
+
},
|
|
1161
|
+
set publicAssetsLastPath(next: string | null) {
|
|
1162
|
+
watch.publicAssetsLastPath = next;
|
|
1163
|
+
},
|
|
1164
|
+
get publicAssetsChangedCount() {
|
|
1165
|
+
return watch.publicAssetsChangedCount;
|
|
1166
|
+
},
|
|
1167
|
+
set publicAssetsChangedCount(next: number) {
|
|
1168
|
+
watch.publicAssetsChangedCount = next;
|
|
1169
|
+
},
|
|
1170
|
+
get lastIndexRequestAt() {
|
|
1171
|
+
return lastIndexRequestAt;
|
|
1172
|
+
},
|
|
1173
|
+
set lastIndexRequestAt(next: number | null) {
|
|
1174
|
+
lastIndexRequestAt = next;
|
|
1175
|
+
},
|
|
1176
|
+
get sourceValidation() {
|
|
1177
|
+
return watch.sourceValidation;
|
|
1178
|
+
},
|
|
1179
|
+
set sourceValidation(next: SourceValidationState) {
|
|
1180
|
+
watch.sourceValidation = next;
|
|
1181
|
+
},
|
|
1182
|
+
get servingProjectSince() {
|
|
1183
|
+
return servingProjectSince;
|
|
1184
|
+
},
|
|
1185
|
+
set servingProjectSince(next: number) {
|
|
1186
|
+
servingProjectSince = next;
|
|
1187
|
+
},
|
|
1188
|
+
get journal() {
|
|
1189
|
+
return journal;
|
|
1190
|
+
},
|
|
1191
|
+
set journal(next: SessionJournal | null) {
|
|
1192
|
+
journal = next;
|
|
1193
|
+
},
|
|
1194
|
+
get cadenceGate() {
|
|
1195
|
+
return watch.cadenceGate;
|
|
1196
|
+
},
|
|
1197
|
+
set cadenceGate(next: TripwireGate) {
|
|
1198
|
+
watch.cadenceGate = next;
|
|
1199
|
+
},
|
|
1200
|
+
get unplayedGate() {
|
|
1201
|
+
return watch.unplayedGate;
|
|
1202
|
+
},
|
|
1203
|
+
set unplayedGate(next: TripwireGate) {
|
|
1204
|
+
watch.unplayedGate = next;
|
|
1205
|
+
},
|
|
1206
|
+
get editorState() {
|
|
1207
|
+
return editorState;
|
|
1208
|
+
},
|
|
1209
|
+
set editorState(next: Record<string, unknown>) {
|
|
1210
|
+
editorState = next;
|
|
1211
|
+
},
|
|
1212
|
+
get editorStateUpdatedAt() {
|
|
1213
|
+
return editorStateUpdatedAt;
|
|
1214
|
+
},
|
|
1215
|
+
set editorStateUpdatedAt(next: number | null) {
|
|
1216
|
+
editorStateUpdatedAt = next;
|
|
1217
|
+
},
|
|
1218
|
+
harnessChat,
|
|
1219
|
+
frontendRefusal: () => harnessChat.frontendRefusal(),
|
|
1220
|
+
projectWork,
|
|
1221
|
+
journalEvent,
|
|
1222
|
+
announceBuildDisciplineTripwires: watch.announceBuildDisciplineTripwires,
|
|
1223
|
+
get activeLogFile() {
|
|
1224
|
+
return activeLogFile;
|
|
1225
|
+
},
|
|
1226
|
+
set activeLogFile(next: string | null) {
|
|
1227
|
+
activeLogFile = next;
|
|
1228
|
+
},
|
|
1229
|
+
options,
|
|
1230
|
+
currentCollaboration,
|
|
1231
|
+
currentCollaborationLocation,
|
|
1232
|
+
collaborationOr400,
|
|
1233
|
+
defaultCollaborationRole,
|
|
1234
|
+
markAgentTurnRunning,
|
|
1235
|
+
markAgentTurnEnded,
|
|
1236
|
+
teamMirror,
|
|
1237
|
+
postMirroredTeamMessages,
|
|
1238
|
+
localParticipantIds,
|
|
1239
|
+
trustedShareIdentity,
|
|
1240
|
+
browserShareControlSecret,
|
|
1241
|
+
get shareHost() {
|
|
1242
|
+
return shareHost;
|
|
1243
|
+
},
|
|
1244
|
+
set shareHost(next: ShareHost | null) {
|
|
1245
|
+
shareHost = next;
|
|
1246
|
+
},
|
|
1247
|
+
get localShareHost() {
|
|
1248
|
+
return localShareHost;
|
|
1249
|
+
},
|
|
1250
|
+
set localShareHost(next: {
|
|
1251
|
+
participantId: string;
|
|
1252
|
+
account: TrustedShareIdentity['account'];
|
|
1253
|
+
} | null) {
|
|
1254
|
+
localShareHost = next;
|
|
1255
|
+
},
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
// ---- Router ----
|
|
1259
|
+
const router = express.Router() as EditorServerRouter;
|
|
1260
|
+
router.use((req: Request, res: Response, next) => {
|
|
1261
|
+
try {
|
|
1262
|
+
verifyShareClaimHeaders(shareGatewaySecret, req.method, req.originalUrl, (name) =>
|
|
1263
|
+
req.header(name),
|
|
1264
|
+
);
|
|
1265
|
+
next();
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
res.status(403).json({ error: error instanceof Error ? error.message : String(error) });
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
router.use(express.json({ limit: '50mb' }));
|
|
1271
|
+
|
|
1272
|
+
// ---- CSRF / drive-by protection (S3) ----
|
|
1273
|
+
// This MUST be installed before every mutating /__editor route, including
|
|
1274
|
+
// global account and billing mutations. Express middleware is ordered; the
|
|
1275
|
+
// former placement below the account routes silently exempted exactly those
|
|
1276
|
+
// routes from the protection it claimed to provide.
|
|
1277
|
+
router.use((req: Request, res: Response, next) => {
|
|
1278
|
+
const method = req.method.toUpperCase();
|
|
1279
|
+
const isMutating =
|
|
1280
|
+
method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE';
|
|
1281
|
+
if (isMutating && req.path.startsWith('/__editor')) {
|
|
1282
|
+
const host = process.env['VGAI_EDITOR_HOST'];
|
|
1283
|
+
const extra = host ? [host] : [];
|
|
1284
|
+
if (!isAllowedEditorOrigin(req.headers.origin, extra)) {
|
|
1285
|
+
res.status(403).json({ error: 'Cross-origin request rejected.' });
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
next();
|
|
1290
|
+
});
|
|
1291
|
+
registerAccountRoutes(router, ctx);
|
|
1292
|
+
// ---- Cold-Vite verified-open signal ----
|
|
1293
|
+
// Stamp `lastIndexRequestAt` for any GET response that turns out to be
|
|
1294
|
+
// genuine HTML (status 2xx, `Content-Type: text/html…`) — that is, purely
|
|
1295
|
+
// observational, via `res.on('finish')`, so it applies no matter which
|
|
1296
|
+
// downstream middleware (Vite, express.static, the SPA fallback) actually
|
|
1297
|
+
// produced the response. A cached root document is the one exception:
|
|
1298
|
+
// browsers may receive a 304 with no Content-Type, but that request is still
|
|
1299
|
+
// positive proof that an editor tab arrived. Missing that reload signal
|
|
1300
|
+
// makes tab self-heal open replacements beneath the already-loading tab.
|
|
1301
|
+
// API responses are JSON, SSE is `text/event-stream`, and assets carry their
|
|
1302
|
+
// own MIME types — none of those match, so only real page loads stamp.
|
|
1303
|
+
router.use((req: Request, res: Response, next) => {
|
|
1304
|
+
if (req.method === 'GET') {
|
|
1305
|
+
res.on('finish', () => {
|
|
1306
|
+
const contentType = res.get('Content-Type');
|
|
1307
|
+
const servedHtml =
|
|
1308
|
+
res.statusCode >= 200 && res.statusCode < 300 && contentType?.includes('text/html');
|
|
1309
|
+
const servedCachedIndex = req.path === '/' && res.statusCode === 304;
|
|
1310
|
+
if (servedHtml || servedCachedIndex) {
|
|
1311
|
+
lastIndexRequestAt = Date.now();
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
next();
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
// ---- Asset Library (online asset browsing + download) ----
|
|
1319
|
+
router.use(
|
|
1320
|
+
'/__editor/asset-library',
|
|
1321
|
+
createAssetLibraryRouter(() => publicRoot),
|
|
1322
|
+
);
|
|
1323
|
+
|
|
1324
|
+
registerProjectAttributionRoute(router, ctx);
|
|
1325
|
+
|
|
1326
|
+
// ---- Project file serving middleware ----
|
|
1327
|
+
// Serves the v2 manifest from root and static files from project's public/
|
|
1328
|
+
router.use(async (req: Request, res: Response, next) => {
|
|
1329
|
+
// Only handle non-API requests when a project is open
|
|
1330
|
+
if (projectRoot === engineRoot) return next();
|
|
1331
|
+
const pathname = req.path;
|
|
1332
|
+
|
|
1333
|
+
// Skip editor API endpoints
|
|
1334
|
+
if (pathname.startsWith('/__')) return next();
|
|
1335
|
+
if (pathname.startsWith('/@')) return next();
|
|
1336
|
+
if (pathname.startsWith('/node_modules')) return next();
|
|
1337
|
+
|
|
1338
|
+
// Serve the RAW manifest (no view synthesis — the browser-side adapter
|
|
1339
|
+
// resolver parses it itself via the pure `@vgai/project/manifest/load` half;
|
|
1340
|
+
// T3.3 slice 2 part C) from the project root, not public/. 404s (falls
|
|
1341
|
+
// through to `next()`, same as any other missing project-root file) for
|
|
1342
|
+
// folders without a valid manifest.
|
|
1343
|
+
if (pathname === `/${MANIFEST_FILENAME}`) {
|
|
1344
|
+
const manifestPath = resolveManifestPath(projectRoot);
|
|
1345
|
+
if (trustedShareIdentity(req) && !(await isCanonicalPathInside(projectRoot, manifestPath))) {
|
|
1346
|
+
res.status(403).json({ error: 'Shared path leaves the project root.' });
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
try {
|
|
1350
|
+
const raw = await readFile(manifestPath, 'utf-8');
|
|
1351
|
+
res.setHeader('Content-Type', 'application/json');
|
|
1352
|
+
res.send(raw);
|
|
1353
|
+
} catch {
|
|
1354
|
+
next();
|
|
1355
|
+
}
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const filePath = resolve(publicRoot, pathname.slice(1));
|
|
1360
|
+
// `isPathInside`, not `startsWith`: a bare prefix test admits the SIBLING
|
|
1361
|
+
// `<root>/publicX/…` as if it were inside `<root>/public`.
|
|
1362
|
+
if (!isPathInside(publicRoot, filePath)) return next();
|
|
1363
|
+
|
|
1364
|
+
try {
|
|
1365
|
+
const s = await stat(filePath);
|
|
1366
|
+
if (!s.isFile()) return next();
|
|
1367
|
+
if (trustedShareIdentity(req) && !(await isCanonicalPathInside(publicRoot, filePath))) {
|
|
1368
|
+
res.status(403).json({ error: 'Shared path leaves the public project root.' });
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
const data = await readFile(filePath);
|
|
1372
|
+
// Public files include executable web assets, not only model/media formats.
|
|
1373
|
+
res.type(extname(filePath));
|
|
1374
|
+
res.setHeader('Content-Length', s.size);
|
|
1375
|
+
res.end(data);
|
|
1376
|
+
} catch {
|
|
1377
|
+
next();
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
registerProjectIdentityRoutes(router, ctx);
|
|
1382
|
+
|
|
1383
|
+
registerServedModuleRoutes(router, ctx);
|
|
1384
|
+
|
|
1385
|
+
registerWorktreeRoutes(router, ctx);
|
|
1386
|
+
|
|
1387
|
+
registerShareControlRoutes(router, ctx);
|
|
1388
|
+
|
|
1389
|
+
registerProjectSourceRoutes(router, ctx);
|
|
1390
|
+
|
|
1391
|
+
registerCollaborationRoutes(router, ctx);
|
|
1392
|
+
|
|
1393
|
+
const plane = createControlPlane(router, ctx);
|
|
1394
|
+
|
|
1395
|
+
registerSessionTabRoutes(router, ctx, plane);
|
|
1396
|
+
|
|
1397
|
+
registerAgentRoutes(router, ctx);
|
|
1398
|
+
|
|
1399
|
+
registerRelayRoutes(router, ctx, plane);
|
|
1400
|
+
|
|
1401
|
+
registerProjectStateRoutes(router, ctx, plane);
|
|
1402
|
+
|
|
1403
|
+
registerAssetRoutes(router, ctx);
|
|
1404
|
+
|
|
1405
|
+
registerBuildRoutes(router, ctx);
|
|
1406
|
+
registerConfigurationRoutes(router, ctx);
|
|
1407
|
+
registerSettingsRoutes(router, ctx);
|
|
1408
|
+
registerThemeRoutes(router, ctx);
|
|
1409
|
+
|
|
1410
|
+
registerProjectOpenRoutes(router, ctx);
|
|
1411
|
+
|
|
1412
|
+
registerToolRoutes(router, ctx);
|
|
1413
|
+
|
|
1414
|
+
registerModuleTransportNotFound(router, ctx);
|
|
1415
|
+
|
|
1416
|
+
registerLogRoutes(router, ctx);
|
|
1417
|
+
|
|
1418
|
+
router.notifyTabSessionEnded = async () => {
|
|
1419
|
+
await Promise.all(
|
|
1420
|
+
[...new Set([hostTabLifecycle, ...participantTabLifecycles.values()])].map((lifecycle) =>
|
|
1421
|
+
lifecycle?.notifySessionEnded(),
|
|
1422
|
+
),
|
|
1423
|
+
);
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
router.sessionJournalPath = () => journal?.path ?? null;
|
|
1427
|
+
router.frontendRefusal = () => harnessChat.frontendRefusal();
|
|
1428
|
+
router.frontendHandoff = async () => {
|
|
1429
|
+
const handoff = await harnessChat.frontendHandoff();
|
|
1430
|
+
// A Chat view with no runtime behind it is REMAINING WORK, not log noise:
|
|
1431
|
+
// it rides the unresolved-console ledger, so every `vgai` command reprints
|
|
1432
|
+
// it and exits non-zero until the harness is signed in or the condition is
|
|
1433
|
+
// acked with a reason. The extension's own empty state says the same thing
|
|
1434
|
+
// in the panel; this is the half a terminal can read.
|
|
1435
|
+
if (handoff.refusal) {
|
|
1436
|
+
consoleLedger.observe({
|
|
1437
|
+
severity: 'warn',
|
|
1438
|
+
message: `The Chat view has no agent runtime: ${handoff.refusal}`,
|
|
1439
|
+
source: 'session/frontend-handoff',
|
|
1440
|
+
loadId: 'session',
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
return handoff;
|
|
1444
|
+
};
|
|
1445
|
+
|
|
1446
|
+
router.journalShutdownTask = (record) => {
|
|
1447
|
+
journalEvent({ kind: 'session-shutdown-task', ...record });
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
router.close = async () => {
|
|
1451
|
+
// Best-effort: a SIGKILL logs nothing, and the ABSENCE of this line is
|
|
1452
|
+
// what identifies that case (same reasoning as dev.ts's last-gasp line).
|
|
1453
|
+
journalEvent({ kind: 'session-shutdown' });
|
|
1454
|
+
currentCollaboration()?.flush();
|
|
1455
|
+
plane.close();
|
|
1456
|
+
heartbeat.close();
|
|
1457
|
+
for (const lifecycle of new Set([hostTabLifecycle, ...participantTabLifecycles.values()])) {
|
|
1458
|
+
lifecycle?.stop();
|
|
1459
|
+
}
|
|
1460
|
+
for (const agent of collaborationAgents.values()) {
|
|
1461
|
+
collaborationSession(agent.projectRoot).leave(agent.participantId);
|
|
1462
|
+
}
|
|
1463
|
+
collaborationAgents.clear();
|
|
1464
|
+
for (const timer of participantLeaveTimers.values()) clearTimeout(timer);
|
|
1465
|
+
participantLeaveTimers.clear();
|
|
1466
|
+
collaborationUnsubscribe?.();
|
|
1467
|
+
collaborationUnsubscribe = null;
|
|
1468
|
+
clearInterval(generationReconcileTimer);
|
|
1469
|
+
engineSourceRestartGate?.dispose();
|
|
1470
|
+
workspaceIdentityRestartMonitor?.dispose();
|
|
1471
|
+
await Promise.all([
|
|
1472
|
+
watch.close(),
|
|
1473
|
+
engineSourceWatcher?.close(),
|
|
1474
|
+
harnessChat.close(),
|
|
1475
|
+
// Declared processes die with the session (`routes/run.ts`).
|
|
1476
|
+
stopAllConfigurations(),
|
|
1477
|
+
shareHost?.stop(),
|
|
1478
|
+
projectWork.close(),
|
|
1479
|
+
]);
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1482
|
+
return router;
|
|
1483
|
+
}
|