@volter/editor-core 0.5.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (719) hide show
  1. package/BUNDLED_NOTICES +1527 -0
  2. package/LICENSE +686 -0
  3. package/LICENSE-APACHE +202 -0
  4. package/NOTICE +18 -0
  5. package/README.md +24 -0
  6. package/dist/build/chunk-JS4D4XZ3.js +175 -0
  7. package/dist/build/chunk-JS4D4XZ3.js.map +7 -0
  8. package/dist/build/vite-plugin-product-contributions.js +100 -0
  9. package/dist/build/vite-plugin-product-contributions.js.map +7 -0
  10. package/dist/build/vite-plugin-shared-react.js +25 -0
  11. package/dist/build/vite-plugin-shared-react.js.map +7 -0
  12. package/dist/build/vite-plugin-shared-three.js +86 -0
  13. package/dist/build/vite-plugin-shared-three.js.map +7 -0
  14. package/dist/server/chunk-KMJC7AGO.js +98 -0
  15. package/dist/server/chunk-KMJC7AGO.js.map +7 -0
  16. package/dist/server/chunk-PSILUMGS.js +46 -0
  17. package/dist/server/chunk-PSILUMGS.js.map +7 -0
  18. package/dist/server/frame-proxy.js +255 -0
  19. package/dist/server/frame-proxy.js.map +7 -0
  20. package/dist/server/open-browser.js +53 -0
  21. package/dist/server/open-browser.js.map +7 -0
  22. package/dist/server/process-shutdown.js +127 -0
  23. package/dist/server/process-shutdown.js.map +7 -0
  24. package/dist/server/session-registry.js +141 -0
  25. package/dist/server/session-registry.js.map +7 -0
  26. package/dist/server/spawn-opener.js +11 -0
  27. package/dist/server/spawn-opener.js.map +7 -0
  28. package/dist/server/worktree-identity.js +11 -0
  29. package/dist/server/worktree-identity.js.map +7 -0
  30. package/dist-server/packaged.mjs +88816 -0
  31. package/package.json +118 -0
  32. package/scripts/build-plugins.mjs +14 -0
  33. package/scripts/build-server.mjs +26 -0
  34. package/scripts/build-session.mjs +9 -0
  35. package/server/account-credentials.ts +60 -0
  36. package/server/account-service.ts +1434 -0
  37. package/server/adapter-region-includes.ts +47 -0
  38. package/server/asset-catalog-v2.ts +175 -0
  39. package/server/asset-history-snapshots.ts +196 -0
  40. package/server/asset-ledger-store.ts +195 -0
  41. package/server/asset-library-routes.ts +1595 -0
  42. package/server/blender-wasm-artifact.ts +430 -0
  43. package/server/boot-timings.ts +106 -0
  44. package/server/canonical-path.ts +50 -0
  45. package/server/checkout-workspace-preflight.ts +349 -0
  46. package/server/cloud-asset-catalog.ts +158 -0
  47. package/server/coding-inference-launch.ts +237 -0
  48. package/server/collaboration-account-client.ts +202 -0
  49. package/server/collaboration-attribution.ts +122 -0
  50. package/server/collaboration-session.ts +934 -0
  51. package/server/comfyui-bridge.ts +248 -0
  52. package/server/console-ledger.ts +499 -0
  53. package/server/creation-site-transform.ts +304 -0
  54. package/server/creation-site-write.ts +648 -0
  55. package/server/data-file-serialize.ts +158 -0
  56. package/server/editor-brand-html.ts +72 -0
  57. package/server/editor-control-socket.ts +366 -0
  58. package/server/editor-server-options.ts +190 -0
  59. package/server/editor-server.ts +1483 -0
  60. package/server/editor-sse.ts +352 -0
  61. package/server/engine-provenance.ts +169 -0
  62. package/server/engine-source-restart.ts +117 -0
  63. package/server/frame-bridge.ts +115 -0
  64. package/server/frame-proxy.ts +490 -0
  65. package/server/frame-workbench.ts +259 -0
  66. package/server/frontend-handoff.ts +229 -0
  67. package/server/game-globals-shadow.ts +113 -0
  68. package/server/gameplay-session-retention.ts +149 -0
  69. package/server/gameplay-sessions.ts +123 -0
  70. package/server/generation-jobs.ts +201 -0
  71. package/server/generation-reconciler.ts +137 -0
  72. package/server/generative-execution-context.ts +59 -0
  73. package/server/git-workflow.ts +612 -0
  74. package/server/harness-chat-caller.ts +36 -0
  75. package/server/harness-chat-service.ts +1681 -0
  76. package/server/idle-shutdown.ts +170 -0
  77. package/server/js-profiling-policy.ts +52 -0
  78. package/server/launcher-settings.ts +84 -0
  79. package/server/local-asset-catalog.ts +410 -0
  80. package/server/managed-account-defaults.ts +22 -0
  81. package/server/mock-control-plane.ts +540 -0
  82. package/server/model-import-conversion.ts +345 -0
  83. package/server/native-credential-store.ts +162 -0
  84. package/server/open-browser.ts +118 -0
  85. package/server/packaged.ts +1192 -0
  86. package/server/play-stall.ts +126 -0
  87. package/server/process-shutdown.ts +218 -0
  88. package/server/product-presets.ts +55 -0
  89. package/server/project-build-artifact.ts +95 -0
  90. package/server/project-components.ts +100 -0
  91. package/server/project-dependency-invalidation.ts +114 -0
  92. package/server/project-file-scan.ts +235 -0
  93. package/server/project-hmr-files.ts +215 -0
  94. package/server/project-install-roots.ts +210 -0
  95. package/server/project-kinds.ts +71 -0
  96. package/server/project-mcp-servers.ts +82 -0
  97. package/server/project-module-freshness.ts +109 -0
  98. package/server/project-module-instance.ts +375 -0
  99. package/server/project-optimize-deps-entries.ts +827 -0
  100. package/server/project-output-writer.ts +679 -0
  101. package/server/project-package-origin.ts +63 -0
  102. package/server/project-root-surface.ts +831 -0
  103. package/server/project-scratch-path.ts +47 -0
  104. package/server/project-script-hmr.ts +619 -0
  105. package/server/project-serving-plugins.ts +236 -0
  106. package/server/project-tools.ts +653 -0
  107. package/server/project-validation.ts +250 -0
  108. package/server/project-verbs.ts +127 -0
  109. package/server/project-view.ts +52 -0
  110. package/server/project-watch.ts +1134 -0
  111. package/server/project-work-coordinator.ts +288 -0
  112. package/server/provider-credentials.ts +331 -0
  113. package/server/recent-projects-store.ts +98 -0
  114. package/server/redact-secrets.ts +71 -0
  115. package/server/repository-presence.ts +267 -0
  116. package/server/routes/account.ts +357 -0
  117. package/server/routes/agents.ts +60 -0
  118. package/server/routes/assets.ts +154 -0
  119. package/server/routes/build.ts +13 -0
  120. package/server/routes/collaboration.ts +595 -0
  121. package/server/routes/configurations.ts +419 -0
  122. package/server/routes/context.ts +264 -0
  123. package/server/routes/control-plane.ts +1811 -0
  124. package/server/routes/logs.ts +272 -0
  125. package/server/routes/project-identity.ts +302 -0
  126. package/server/routes/project-open.ts +429 -0
  127. package/server/routes/project-source.ts +686 -0
  128. package/server/routes/project-state.ts +383 -0
  129. package/server/routes/relay.ts +1029 -0
  130. package/server/routes/route-helpers.ts +29 -0
  131. package/server/routes/served-modules.ts +138 -0
  132. package/server/routes/session-tabs.ts +196 -0
  133. package/server/routes/settings.ts +121 -0
  134. package/server/routes/share-control.ts +486 -0
  135. package/server/routes/themes.ts +116 -0
  136. package/server/routes/tools.ts +230 -0
  137. package/server/routes/worktrees.ts +595 -0
  138. package/server/scoped-game-css.ts +461 -0
  139. package/server/server-utils.ts +1194 -0
  140. package/server/session-product.ts +112 -0
  141. package/server/session-registry.ts +230 -0
  142. package/server/share-claims.ts +138 -0
  143. package/server/share-host.ts +526 -0
  144. package/server/share-session-gateway.ts +1362 -0
  145. package/server/share-tunnel.ts +123 -0
  146. package/server/spawn-opener.ts +74 -0
  147. package/server/support/play/log-format.ts +76 -0
  148. package/server/support/play/session-record.ts +199 -0
  149. package/server/support/project/build-discipline.ts +718 -0
  150. package/server/support/project/inspection-node.ts +119 -0
  151. package/server/support/project/log-naming.ts +54 -0
  152. package/server/support/project/provenance.ts +290 -0
  153. package/server/support/project/run-name.ts +18 -0
  154. package/server/support/project/session-journal.ts +830 -0
  155. package/server/support/project/shared.ts +327 -0
  156. package/server/tab-bootstrap.ts +62 -0
  157. package/server/tab-heartbeat.ts +477 -0
  158. package/server/tab-lifecycle.ts +550 -0
  159. package/server/tab-presence.ts +1508 -0
  160. package/server/team-agent-mirror.ts +95 -0
  161. package/server/twin-auth.ts +138 -0
  162. package/server/vendored-lock-recorder.ts +968 -0
  163. package/server/worktree-identity.ts +121 -0
  164. package/server/worktree-management.ts +398 -0
  165. package/server/worktree-retention.d.mts +13 -0
  166. package/server/worktree-retention.mjs +205 -0
  167. package/src/EditorContext.tsx +169 -0
  168. package/src/account.ts +333 -0
  169. package/src/action-registry.ts +532 -0
  170. package/src/active-product.ts +74 -0
  171. package/src/active-project.ts +155 -0
  172. package/src/adapter-editor-config.ts +25 -0
  173. package/src/adapter-observation.ts +49 -0
  174. package/src/animation/stage-transport.ts +379 -0
  175. package/src/animation/three-clips-subject.ts +172 -0
  176. package/src/api/asset-library-wire.ts +45 -0
  177. package/src/api/assets.ts +365 -0
  178. package/src/api/base.ts +10 -0
  179. package/src/api/build.ts +99 -0
  180. package/src/api/git-wire.ts +56 -0
  181. package/src/api/logs.ts +92 -0
  182. package/src/api/project-identity.ts +74 -0
  183. package/src/api/project-open.ts +355 -0
  184. package/src/api/project-source.ts +162 -0
  185. package/src/api/project-state.ts +107 -0
  186. package/src/api/relay.ts +268 -0
  187. package/src/api/settings.ts +36 -0
  188. package/src/api/themes.ts +45 -0
  189. package/src/api/worktrees.ts +257 -0
  190. package/src/asset-compare-core.ts +171 -0
  191. package/src/asset-compare.ts +294 -0
  192. package/src/asset-editor-context.tsx +100 -0
  193. package/src/asset-events.ts +97 -0
  194. package/src/asset-inspector-actions.ts +87 -0
  195. package/src/asset-preview-framing.ts +357 -0
  196. package/src/asset-preview.ts +2802 -0
  197. package/src/asset-selection-viewer-registry.ts +113 -0
  198. package/src/asset-selection.ts +133 -0
  199. package/src/asset-workflow/asset-capabilities.ts +344 -0
  200. package/src/asset-workflow/asset-import-jobs.ts +106 -0
  201. package/src/asset-workflow/asset-ledger-backend.ts +126 -0
  202. package/src/asset-workflow/asset-ledger.ts +156 -0
  203. package/src/asset-workflow/asset-materialization-report.ts +140 -0
  204. package/src/asset-workflow/asset-pack-manifest.ts +320 -0
  205. package/src/asset-workflow/asset-types.ts +142 -0
  206. package/src/asset-workflow/asset-workflow-quality.ts +61 -0
  207. package/src/asset-workflow/audio-preview-player.ts +193 -0
  208. package/src/asset-workflow/audio-waveform.ts +22 -0
  209. package/src/asset-workflow/cloud-asset-client.ts +263 -0
  210. package/src/asset-workflow/folder-preview.ts +345 -0
  211. package/src/asset-workflow/hosted-asset-materialization.ts +236 -0
  212. package/src/asset-workflow/image-view-scale.ts +32 -0
  213. package/src/asset-workflow/import-contract.ts +124 -0
  214. package/src/asset-workflow/ledger-write-lock.ts +244 -0
  215. package/src/asset-workflow/model-inspection.ts +818 -0
  216. package/src/asset-workflow/pixi-spritesheet.ts +197 -0
  217. package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
  218. package/src/asset-workflow/project-asset-commands.ts +23 -0
  219. package/src/asset-workflow/project-asset-health.ts +139 -0
  220. package/src/asset-workflow/project-asset-operations.ts +154 -0
  221. package/src/asset-workflow/project-asset-roots.ts +68 -0
  222. package/src/asset-workflow/project-content.ts +288 -0
  223. package/src/asset-workflow/project-source-index.ts +550 -0
  224. package/src/asset-workflow/thumbnail-system.ts +256 -0
  225. package/src/authoring/active-adapter.ts +199 -0
  226. package/src/authoring/active-systems.ts +422 -0
  227. package/src/authoring/adapter-key.ts +18 -0
  228. package/src/authoring/authoring-asset-url.ts +27 -0
  229. package/src/authoring/bootstrap-state.ts +49 -0
  230. package/src/authoring/boundary-authoring-adapter.ts +184 -0
  231. package/src/authoring/breakpoint-state.ts +43 -0
  232. package/src/authoring/canvas-scene-guides.ts +76 -0
  233. package/src/authoring/component-instance-root.ts +171 -0
  234. package/src/authoring/composite-authoring-adapter.ts +2110 -0
  235. package/src/authoring/consumer-actions.ts +520 -0
  236. package/src/authoring/css-numeric-style.ts +97 -0
  237. package/src/authoring/design-time-layers.ts +849 -0
  238. package/src/authoring/design-time-mount-registry.ts +235 -0
  239. package/src/authoring/design-time-settle.ts +343 -0
  240. package/src/authoring/edit-mode-authoring.ts +619 -0
  241. package/src/authoring/eyedropper-session.ts +60 -0
  242. package/src/authoring/instance-source-menu-register.ts +13 -0
  243. package/src/authoring/instance-source-menu.ts +135 -0
  244. package/src/authoring/layered-pick.ts +183 -0
  245. package/src/authoring/live-gesture-lock.ts +50 -0
  246. package/src/authoring/live-object-transform.ts +62 -0
  247. package/src/authoring/mount-failure-report.ts +154 -0
  248. package/src/authoring/mounted-root-subjects.ts +144 -0
  249. package/src/authoring/no-authoring-adapter.ts +55 -0
  250. package/src/authoring/null-inspection-subjects.tsx +75 -0
  251. package/src/authoring/object3d-document-persistence.ts +106 -0
  252. package/src/authoring/object3d-document-session-registry.ts +120 -0
  253. package/src/authoring/object3d-document-session.ts +1326 -0
  254. package/src/authoring/object3d-gesture-controller.ts +113 -0
  255. package/src/authoring/panel-authoring.ts +121 -0
  256. package/src/authoring/prefab-instance-inspector-section.tsx +237 -0
  257. package/src/authoring/prefab-instance-section-model.ts +55 -0
  258. package/src/authoring/project-authoring-session.ts +105 -0
  259. package/src/authoring/provenance.ts +99 -0
  260. package/src/authoring/quarks-particle-systems.ts +19 -0
  261. package/src/authoring/react-canvas-navigation.ts +255 -0
  262. package/src/authoring/react-design-canvas-style.ts +20 -0
  263. package/src/authoring/react-story-board.ts +937 -0
  264. package/src/authoring/selection-scope.ts +195 -0
  265. package/src/authoring/shell-document-ops.ts +169 -0
  266. package/src/authoring/shell-object3d-document-write-policy.ts +108 -0
  267. package/src/authoring/shell-viewport-policy.ts +48 -0
  268. package/src/authoring/source-object3d-authoring-adapter.ts +526 -0
  269. package/src/authoring/stories-scope.ts +35 -0
  270. package/src/authoring/story-board-chrome-fit.ts +107 -0
  271. package/src/authoring/story-board-presentation.ts +111 -0
  272. package/src/authoring/three-projection-core.ts +226 -0
  273. package/src/authoring/viewport-pick-context.ts +40 -0
  274. package/src/authoring/viewport-raycast.ts +240 -0
  275. package/src/authoring/viewport-tool-context.ts +73 -0
  276. package/src/authoring/world-canvas-viewport-state.ts +35 -0
  277. package/src/authoring/world-hidden-viewport.ts +152 -0
  278. package/src/authoring/world-pan-state.ts +198 -0
  279. package/src/authoring/world-session-state.ts +101 -0
  280. package/src/authoring/write-pipe.ts +173 -0
  281. package/src/availability-tick.ts +66 -0
  282. package/src/bitmap-label.ts +120 -0
  283. package/src/blender-tab-metrics.ts +161 -0
  284. package/src/board-open-actions.ts +20 -0
  285. package/src/boot-routing.ts +391 -0
  286. package/src/build-identity.ts +15 -0
  287. package/src/bytes-codec.ts +62 -0
  288. package/src/camera-authoring.ts +175 -0
  289. package/src/cancellation-reason.ts +58 -0
  290. package/src/canvas-preview-frames.ts +482 -0
  291. package/src/capture-camera-pose.ts +77 -0
  292. package/src/chrome-registry.ts +159 -0
  293. package/src/chrome-slot-registry.ts +91 -0
  294. package/src/collaboration-client.ts +264 -0
  295. package/src/collaboration-presence.ts +41 -0
  296. package/src/command-dispatch.ts +19 -0
  297. package/src/command-listener.ts +2743 -0
  298. package/src/command-registry.ts +70 -0
  299. package/src/component-board-registry.ts +205 -0
  300. package/src/component-states-registry.ts +200 -0
  301. package/src/components/AgentPresentationNotice.tsx +75 -0
  302. package/src/components/AlignToolbar.tsx +204 -0
  303. package/src/components/AppRoot.tsx +394 -0
  304. package/src/components/ApplicationMenus.tsx +415 -0
  305. package/src/components/AssetBrowser.tsx +2843 -0
  306. package/src/components/AssetEditorShell.tsx +216 -0
  307. package/src/components/AssetInspectorToolSection.tsx +124 -0
  308. package/src/components/BoardRulers.tsx +354 -0
  309. package/src/components/CameraInfo.tsx +81 -0
  310. package/src/components/CanvasSceneViewport.tsx +556 -0
  311. package/src/components/CapabilityCoverageSection.tsx +185 -0
  312. package/src/components/CenterDocuments.tsx +22 -0
  313. package/src/components/ChromeSlot.tsx +20 -0
  314. package/src/components/CodeView.tsx +470 -0
  315. package/src/components/CollaborationHeaderControl.css +74 -0
  316. package/src/components/CompactInspectorCard.tsx +190 -0
  317. package/src/components/CompactInspectorShell.tsx +39 -0
  318. package/src/components/ConsolePanel.css +55 -0
  319. package/src/components/ConsolePanel.tsx +270 -0
  320. package/src/components/DefaultEditorLayout.tsx +106 -0
  321. package/src/components/DocumentHeaderStrip.tsx +96 -0
  322. package/src/components/DocumentShelfRail.tsx +45 -0
  323. package/src/components/DocumentThumbnail.tsx +345 -0
  324. package/src/components/EditorLeaseGuard.tsx +372 -0
  325. package/src/components/ErrorBanner.tsx +123 -0
  326. package/src/components/FolderPreviewTile.tsx +159 -0
  327. package/src/components/GameHierarchy.tsx +3842 -0
  328. package/src/components/GameplaySessionTimeline.tsx +295 -0
  329. package/src/components/InspectionProjection.tsx +968 -0
  330. package/src/components/Inspector.tsx +233 -0
  331. package/src/components/InspectorCanvasPreview.tsx +35 -0
  332. package/src/components/InspectorFieldsSection.tsx +290 -0
  333. package/src/components/InspectorObjectPreview.tsx +57 -0
  334. package/src/components/InspectorStoriesSection.tsx +92 -0
  335. package/src/components/InspectorToolSection.tsx +96 -0
  336. package/src/components/InspectorTransformSection.tsx +245 -0
  337. package/src/components/LightExplorerPanel.tsx +433 -0
  338. package/src/components/MediaProperties.tsx +145 -0
  339. package/src/components/NonThreeAuthoringBootstrap.tsx +19 -0
  340. package/src/components/Object3DDocumentToolbar.css +100 -0
  341. package/src/components/Object3DDocumentToolbar.tsx +454 -0
  342. package/src/components/Object3DDocumentViewport.tsx +58 -0
  343. package/src/components/ProjectHeader.css +617 -0
  344. package/src/components/ProjectHeader.tsx +328 -0
  345. package/src/components/ProjectLayout.tsx +249 -0
  346. package/src/components/ReactCanvasControls.tsx +284 -0
  347. package/src/components/RootSelectionOverlay.tsx +3162 -0
  348. package/src/components/RootTextEditor.tsx +79 -0
  349. package/src/components/SaveStatus.tsx +70 -0
  350. package/src/components/StageHost.tsx +1954 -0
  351. package/src/components/StageOverlays.tsx +21 -0
  352. package/src/components/StartupErrorScreen.tsx +76 -0
  353. package/src/components/StartupLoadingScreen.tsx +65 -0
  354. package/src/components/StatsOverlay.tsx +78 -0
  355. package/src/components/SurfaceStateOverlay.tsx +24 -0
  356. package/src/components/ToolContributionSurfaces.tsx +65 -0
  357. package/src/components/ToolHost.tsx +370 -0
  358. package/src/components/ToolObject3DPreview.tsx +39 -0
  359. package/src/components/Toolbar.tsx +595 -0
  360. package/src/components/TransientHint.tsx +44 -0
  361. package/src/components/TransportStrip.tsx +174 -0
  362. package/src/components/VersionControlSection.tsx +470 -0
  363. package/src/components/VgaiLogo.css +83 -0
  364. package/src/components/VgaiLogo.tsx +35 -0
  365. package/src/components/ViewportControlsHint.tsx +60 -0
  366. package/src/components/ViewportFurniture.tsx +401 -0
  367. package/src/components/ViewportOverlay.tsx +145 -0
  368. package/src/components/ViewportOverlaysMenu.tsx +153 -0
  369. package/src/components/ViewportShadingMenu.tsx +300 -0
  370. package/src/components/ViewportViewMenu.tsx +101 -0
  371. package/src/components/WorkspaceDocumentSurface.tsx +204 -0
  372. package/src/components/WorkspaceUtilitySurface.tsx +22 -0
  373. package/src/components/WorktreeSwitcher.css +239 -0
  374. package/src/components/WorktreeSwitcher.tsx +890 -0
  375. package/src/components/account-documents.tsx +1162 -0
  376. package/src/components/asset-documents.tsx +812 -0
  377. package/src/components/asset-editor-persistence.ts +216 -0
  378. package/src/components/asset-selection-section.tsx +535 -0
  379. package/src/components/asset-thumbnails.tsx +312 -0
  380. package/src/components/asset-viewers/AudioViewer.tsx +201 -0
  381. package/src/components/asset-viewers/EntityModelDocument.tsx +122 -0
  382. package/src/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
  383. package/src/components/asset-viewers/GenericJsonViewer.tsx +102 -0
  384. package/src/components/asset-viewers/ImageViewer.tsx +300 -0
  385. package/src/components/asset-viewers/JsonAssetDocument.tsx +93 -0
  386. package/src/components/asset-viewers/LiveModuleDocument.tsx +417 -0
  387. package/src/components/asset-viewers/LutAssetDocument.tsx +444 -0
  388. package/src/components/asset-viewers/ModelAssetDocument.tsx +105 -0
  389. package/src/components/asset-viewers/Object3DPreview.tsx +359 -0
  390. package/src/components/asset-viewers/OnlineAssetDetail.tsx +421 -0
  391. package/src/components/asset-viewers/PasteboardModuleDocument.tsx +67 -0
  392. package/src/components/asset-viewers/QuarksAssetDocument.tsx +526 -0
  393. package/src/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
  394. package/src/components/asset-viewers/SourceAssetViewer.tsx +281 -0
  395. package/src/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
  396. package/src/components/asset-viewers/VideoViewer.tsx +101 -0
  397. package/src/components/asset-viewers/shader-source.ts +144 -0
  398. package/src/components/asset-workflow.css +731 -0
  399. package/src/components/board-guides.ts +150 -0
  400. package/src/components/compact-inspector.css +539 -0
  401. package/src/components/core-utilities.tsx +90 -0
  402. package/src/components/editor-notifications.css +40 -0
  403. package/src/components/engine-workspace.css +274 -0
  404. package/src/components/inspector-preview-section.tsx +238 -0
  405. package/src/components/inspector-property-grouping.ts +64 -0
  406. package/src/components/inspector-revert-label.ts +20 -0
  407. package/src/components/inspector-selection.ts +42 -0
  408. package/src/components/inspector-stories-gating.ts +171 -0
  409. package/src/components/inspector-transform-subject.ts +11 -0
  410. package/src/components/inspector-transform.ts +75 -0
  411. package/src/components/kind-documents.tsx +500 -0
  412. package/src/components/palette-action-publisher.tsx +140 -0
  413. package/src/components/primitives/DraftColorInput.tsx +74 -0
  414. package/src/components/product-shell.css +1829 -0
  415. package/src/components/project-tool-documents.tsx +410 -0
  416. package/src/components/scene-documents.tsx +223 -0
  417. package/src/components/stage-keyboard.tsx +37 -0
  418. package/src/components/stage-overlay-set.tsx +107 -0
  419. package/src/components/stage-presence-markers.ts +478 -0
  420. package/src/components/standard-viewport-dressing.ts +417 -0
  421. package/src/components/status-contributions.tsx +403 -0
  422. package/src/components/tool-documents.tsx +309 -0
  423. package/src/components/tool-schema-form.tsx +262 -0
  424. package/src/components/use-after-paint.ts +41 -0
  425. package/src/components/use-project-image-assets.ts +86 -0
  426. package/src/components/viewport-header-controls.css +90 -0
  427. package/src/components/viewport-surface-status.tsx +55 -0
  428. package/src/components/workspace-history.ts +32 -0
  429. package/src/components/workspace-static-panel-registry.tsx +152 -0
  430. package/src/components/workspace-surfaces.css +621 -0
  431. package/src/components/world-documents.tsx +586 -0
  432. package/src/components/world-overlay-gestures.ts +1694 -0
  433. package/src/composite-screenshot.ts +1641 -0
  434. package/src/console-sync.ts +131 -0
  435. package/src/constraint-helper.ts +338 -0
  436. package/src/content-entry-source-registry.ts +184 -0
  437. package/src/coverage/authoring-seam-evidence.ts +300 -0
  438. package/src/coverage/canvas-reveal.ts +192 -0
  439. package/src/coverage/design-time-surfaces.ts +101 -0
  440. package/src/coverage/live-seam-evidence.ts +11 -0
  441. package/src/coverage/ontology-invariants.ts +466 -0
  442. package/src/coverage/session-vitals.ts +501 -0
  443. package/src/coverage/system-seam-evidence.ts +72 -0
  444. package/src/crash-null-boundary.ts +36 -0
  445. package/src/creation-site-edit.ts +1479 -0
  446. package/src/creation-site-registry.ts +160 -0
  447. package/src/delegate-harness-registry.ts +143 -0
  448. package/src/document-context-registry.ts +177 -0
  449. package/src/document-open-registry.ts +200 -0
  450. package/src/document-preview-source.ts +20 -0
  451. package/src/document-renderer-session.ts +138 -0
  452. package/src/editor-api.ts +46 -0
  453. package/src/editor-chrome-capture.ts +139 -0
  454. package/src/editor-commands.ts +164 -0
  455. package/src/editor-console.ts +563 -0
  456. package/src/editor-current-view.ts +82 -0
  457. package/src/editor-document-probe.ts +881 -0
  458. package/src/editor-git-client.ts +115 -0
  459. package/src/editor-host-door.ts +462 -0
  460. package/src/editor-hotkeys.ts +842 -0
  461. package/src/editor-lease-view.ts +39 -0
  462. package/src/editor-lease.ts +415 -0
  463. package/src/editor-mode.ts +19 -0
  464. package/src/editor-notifications.ts +140 -0
  465. package/src/editor-presence.ts +563 -0
  466. package/src/editor-presentation-activity.ts +50 -0
  467. package/src/editor-presentation-notice.ts +42 -0
  468. package/src/editor-runtime.tsx +145 -0
  469. package/src/editor-server-response.ts +86 -0
  470. package/src/editor-session-attribution.ts +75 -0
  471. package/src/editor-session-mode.ts +54 -0
  472. package/src/editor-shell-store.ts +1304 -0
  473. package/src/editor-state-facets.ts +74 -0
  474. package/src/editor-styles.css +33 -0
  475. package/src/editor-view-presentation.ts +810 -0
  476. package/src/editor-viewport.ts +5302 -0
  477. package/src/entity-lod.ts +31 -0
  478. package/src/entity-object.ts +91 -0
  479. package/src/files/file-provider.ts +62 -0
  480. package/src/files/project-files.ts +264 -0
  481. package/src/finders/index.ts +136 -0
  482. package/src/finders/scenes-from-entrypoint-selection.ts +387 -0
  483. package/src/frame/bridge.tsx +1408 -0
  484. package/src/frame/product.ts +77 -0
  485. package/src/gameplay-dom-recording.ts +319 -0
  486. package/src/gameplay-export-state.ts +14 -0
  487. package/src/gameplay-replay.ts +417 -0
  488. package/src/gameplay-session-time.ts +9 -0
  489. package/src/gameplay-sessions.ts +204 -0
  490. package/src/harness-chat-types.ts +253 -0
  491. package/src/hierarchy-component-marks.ts +298 -0
  492. package/src/hierarchy-drop.ts +91 -0
  493. package/src/hierarchy-expansion-state.ts +80 -0
  494. package/src/hierarchy-header-slot.ts +52 -0
  495. package/src/hierarchy-internals.ts +197 -0
  496. package/src/hierarchy-kind-icon.ts +217 -0
  497. package/src/hierarchy-mark-reader.ts +73 -0
  498. package/src/hierarchy-menu-registry.ts +67 -0
  499. package/src/hierarchy-node-rows.ts +307 -0
  500. package/src/hierarchy-panel-view.ts +280 -0
  501. package/src/hierarchy-projection.ts +76 -0
  502. package/src/hierarchy-row-cache.ts +243 -0
  503. package/src/hierarchy-row-model.ts +308 -0
  504. package/src/hierarchy-rows.ts +11 -0
  505. package/src/hierarchy-walk.ts +86 -0
  506. package/src/history/editor-session.ts +25 -0
  507. package/src/history/history-commands.ts +147 -0
  508. package/src/history/history-delegate.ts +163 -0
  509. package/src/history/history-limit-notices.ts +43 -0
  510. package/src/history/history-service.ts +1173 -0
  511. package/src/history/persistence-coordinator.ts +35 -0
  512. package/src/history/project-file-history.ts +386 -0
  513. package/src/history/project-root-history-backends.ts +139 -0
  514. package/src/history/resource-registry.ts +209 -0
  515. package/src/history/snapshot-store.ts +103 -0
  516. package/src/history/source-history-backend.ts +546 -0
  517. package/src/history/types.ts +124 -0
  518. package/src/hmr-registration-group.ts +67 -0
  519. package/src/hmr-stable-react-context.ts +23 -0
  520. package/src/hotkeys.ts +188 -0
  521. package/src/inference-diagnostics.ts +69 -0
  522. package/src/initial-project.ts +80 -0
  523. package/src/inspection/active-subject.ts +578 -0
  524. package/src/inspection/active-surface.ts +142 -0
  525. package/src/inspection/compose.ts +1064 -0
  526. package/src/inspection/display.ts +170 -0
  527. package/src/inspection/document-subject.ts +109 -0
  528. package/src/inspection/game-subject.ts +85 -0
  529. package/src/inspection/model.ts +542 -0
  530. package/src/inspection/null-subject.ts +115 -0
  531. package/src/inspection/serialize.ts +357 -0
  532. package/src/inspection/use-active-inspection.ts +180 -0
  533. package/src/inspector-presentation.ts +201 -0
  534. package/src/inspector-section-registry.ts +221 -0
  535. package/src/instance-source-actions.ts +163 -0
  536. package/src/instanced-presentation.ts +164 -0
  537. package/src/js-heap.ts +71 -0
  538. package/src/key-actions.ts +91 -0
  539. package/src/keymap-presets.ts +401 -0
  540. package/src/layout-policy.ts +31 -0
  541. package/src/learn-links.ts +73 -0
  542. package/src/light-explorer-model.ts +134 -0
  543. package/src/live-canvas-frame.ts +55 -0
  544. package/src/live-document.ts +279 -0
  545. package/src/live-module-source.ts +230 -0
  546. package/src/live-session-registry.ts +220 -0
  547. package/src/live-transition.ts +633 -0
  548. package/src/manifest-project.ts +107 -0
  549. package/src/model-thumbnail.ts +523 -0
  550. package/src/native-selection-style.ts +202 -0
  551. package/src/object3d-document-write-policy.ts +137 -0
  552. package/src/packaged-runtime.ts +108 -0
  553. package/src/palettes/maya.palette.json +57 -0
  554. package/src/palettes/substance.palette.json +57 -0
  555. package/src/pasteboard-module.ts +81 -0
  556. package/src/performance-profiler.ts +367 -0
  557. package/src/performance-sources.ts +69 -0
  558. package/src/play-boot-phase.ts +145 -0
  559. package/src/presentation-surface.ts +248 -0
  560. package/src/project-adapter.ts +1143 -0
  561. package/src/project-asset-refresh.ts +26 -0
  562. package/src/project-declaration-refresh.ts +31 -0
  563. package/src/project-local-state.ts +118 -0
  564. package/src/project-manager.ts +243 -0
  565. package/src/project-module-changes.ts +163 -0
  566. package/src/project-module-split.ts +266 -0
  567. package/src/project-provenance.ts +115 -0
  568. package/src/project-ready.ts +42 -0
  569. package/src/project-session-reset.ts +67 -0
  570. package/src/project-shape.ts +68 -0
  571. package/src/project-tool-discovery.ts +66 -0
  572. package/src/project-tools.ts +107 -0
  573. package/src/project-work-types.ts +149 -0
  574. package/src/projection/three.ts +898 -0
  575. package/src/projection/types.ts +44 -0
  576. package/src/readiness.ts +113 -0
  577. package/src/reflection-probe-helper.ts +142 -0
  578. package/src/reported-play-state.ts +90 -0
  579. package/src/resolve-relative-specifier.ts +33 -0
  580. package/src/scene-document-plan.ts +315 -0
  581. package/src/scene-framing.ts +315 -0
  582. package/src/scene-live-open.ts +210 -0
  583. package/src/scene-view-fog.ts +89 -0
  584. package/src/scoped-game-css.ts +152 -0
  585. package/src/session-orphan-record.ts +193 -0
  586. package/src/session-tombstone.ts +126 -0
  587. package/src/settings/settings-provider.ts +82 -0
  588. package/src/settings-store.ts +345 -0
  589. package/src/shared-view-restore.ts +42 -0
  590. package/src/shell-store-door.ts +45 -0
  591. package/src/source-conflict.ts +122 -0
  592. package/src/spatial-handle-visuals.ts +332 -0
  593. package/src/stage-context.ts +341 -0
  594. package/src/stage-store-registry.ts +63 -0
  595. package/src/stale-chunk-recovery.ts +34 -0
  596. package/src/state-report-deferral.ts +73 -0
  597. package/src/storage/host-files-storage.ts +97 -0
  598. package/src/storage/http-storage.ts +174 -0
  599. package/src/storage/index.ts +75 -0
  600. package/src/storage/mem-storage.ts +149 -0
  601. package/src/storage/path-lock.ts +44 -0
  602. package/src/storage/paths.ts +26 -0
  603. package/src/storage/types.ts +116 -0
  604. package/src/stories/StoryComponentThumbnail.tsx +184 -0
  605. package/src/stories/StoryPreviewMount.tsx +306 -0
  606. package/src/stories/component-content-source.tsx +106 -0
  607. package/src/stories/component-states-source.ts +78 -0
  608. package/src/stories/compose-project-stories.ts +255 -0
  609. package/src/stories/pixi-story-model.ts +30 -0
  610. package/src/stories/prefabs-finder.ts +54 -0
  611. package/src/stories/prefabs-from-stories.ts +182 -0
  612. package/src/stories/project-story-discovery.ts +78 -0
  613. package/src/stories/project-story-regions.ts +24 -0
  614. package/src/stories/story-actions.ts +54 -0
  615. package/src/stories/story-capture-command.ts +124 -0
  616. package/src/stories/story-capture.ts +584 -0
  617. package/src/stories/story-declared-medium.ts +126 -0
  618. package/src/stories/story-discovery.ts +176 -0
  619. package/src/stories/story-document-openers.ts +36 -0
  620. package/src/stories/story-dom-runtime.ts +78 -0
  621. package/src/stories/story-grouping.ts +111 -0
  622. package/src/stories/story-lane.ts +88 -0
  623. package/src/stories/story-mount-turn.ts +27 -0
  624. package/src/stories/story-opener.ts +260 -0
  625. package/src/stories/story-pixi-preview.ts +408 -0
  626. package/src/stories/story-presentation.ts +215 -0
  627. package/src/stories/story-registry.ts +517 -0
  628. package/src/stories/story-three-preview.ts +807 -0
  629. package/src/stories/three-story-model.ts +96 -0
  630. package/src/story-three-preview-runtime.ts +56 -0
  631. package/src/surface-keyboard.ts +112 -0
  632. package/src/surface-state.ts +124 -0
  633. package/src/tab-bootstrap.js +628 -0
  634. package/src/tab-census.ts +198 -0
  635. package/src/tab-lifecycle-client.ts +196 -0
  636. package/src/theme-library.ts +844 -0
  637. package/src/theme-preference.ts +378 -0
  638. package/src/theme.css +3437 -0
  639. package/src/three-viewport/bone-selection-highlight.ts +119 -0
  640. package/src/three-viewport/camera-fit.ts +41 -0
  641. package/src/three-viewport/interactive-renderer.ts +132 -0
  642. package/src/three-viewport/selection-brackets.ts +310 -0
  643. package/src/three-viewport/selection-outline.ts +183 -0
  644. package/src/three-viewport/skeleton-helper.ts +61 -0
  645. package/src/three-viewport/source-color.ts +167 -0
  646. package/src/three-viewport/studio-environment.ts +96 -0
  647. package/src/three-viewport-presentation.ts +23 -0
  648. package/src/tool-contribution-play.ts +74 -0
  649. package/src/tool-loader.ts +1798 -0
  650. package/src/transform-mode-request.ts +57 -0
  651. package/src/transient-hint.ts +78 -0
  652. package/src/trigger-volume-helper.ts +116 -0
  653. package/src/ui-source/adapter-region-includes.ts +241 -0
  654. package/src/ui-source/ensure-import.ts +132 -0
  655. package/src/ui-source/file-region-resolver.ts +302 -0
  656. package/src/ui-source/inspect.ts +775 -0
  657. package/src/ui-source/oid-transform.ts +1845 -0
  658. package/src/ui-source/plan-csf-story.ts +196 -0
  659. package/src/ui-source/plan-extract-component.ts +421 -0
  660. package/src/ui-source/plan-fork-component.ts +621 -0
  661. package/src/ui-source/plan-named-style.ts +95 -0
  662. package/src/ui-source/plan-source-edit.ts +353 -0
  663. package/src/ui-source/r3f-contract-resolver.ts +327 -0
  664. package/src/ui-source/r3f-diagnostic-index.ts +165 -0
  665. package/src/ui-source/r3f-environment-binding.ts +133 -0
  666. package/src/ui-source/r3f-joint-binding.ts +251 -0
  667. package/src/ui-source/r3f-lod-binding.ts +91 -0
  668. package/src/ui-source/r3f-particle-binding.ts +235 -0
  669. package/src/ui-source/r3f-physics-binding.ts +483 -0
  670. package/src/ui-source/r3f-project-contracts.ts +444 -0
  671. package/src/ui-source/relative-import-specifier.ts +34 -0
  672. package/src/ui-source/reparent-guard.ts +544 -0
  673. package/src/ui-source/source-edit-request.ts +97 -0
  674. package/src/ui-source/source-write-backend.ts +618 -0
  675. package/src/ui-source/syntactic-prop-specs.ts +296 -0
  676. package/src/ui-source/tier-source-write-backend.ts +279 -0
  677. package/src/ui-source/ts-ast.ts +171 -0
  678. package/src/ui-source/utility-class-support.ts +169 -0
  679. package/src/ui-source/write-component-default.ts +133 -0
  680. package/src/ui-source/writer.ts +1989 -0
  681. package/src/viewport-activation-timings.ts +840 -0
  682. package/src/viewport-authoring-policy.ts +186 -0
  683. package/src/viewport-controls-hint.ts +57 -0
  684. package/src/viewport-door.ts +237 -0
  685. package/src/viewport-shading-boundary.ts +12 -0
  686. package/src/vite-error-surface.ts +141 -0
  687. package/src/wait-until.ts +37 -0
  688. package/src/workspace-areas.ts +156 -0
  689. package/src/workspace-aux-commands.ts +11 -0
  690. package/src/workspace-available-documents.ts +127 -0
  691. package/src/workspace-core-utilities.ts +31 -0
  692. package/src/workspace-document-ids.ts +59 -0
  693. package/src/workspace-document-registry.ts +612 -0
  694. package/src/workspace-document-restore.ts +145 -0
  695. package/src/workspace-host-commands.ts +141 -0
  696. package/src/workspace-persistence-gate.ts +40 -0
  697. package/src/workspace-play-utilities.ts +44 -0
  698. package/src/workspace-presets.ts +425 -0
  699. package/src/workspace-regions.ts +294 -0
  700. package/src/workspace-state-persistence.ts +536 -0
  701. package/src/workspace-static-panels.ts +73 -0
  702. package/src/workspace-status-registry.ts +121 -0
  703. package/src/workspace-style.ts +239 -0
  704. package/src/workspace-utility-commands.ts +74 -0
  705. package/src/workspace-utility-registry.ts +263 -0
  706. package/src/workspace-viewport-rect.ts +97 -0
  707. package/src/world-adoption.ts +115 -0
  708. package/src/world-document-routing.ts +104 -0
  709. package/vite-plugin-creation-site-write.ts +184 -0
  710. package/vite-plugin-creation-site.ts +80 -0
  711. package/vite-plugin-game-static.ts +303 -0
  712. package/vite-plugin-module-doorways.ts +336 -0
  713. package/vite-plugin-product-contributions.ts +197 -0
  714. package/vite-plugin-project-game-static.ts +125 -0
  715. package/vite-plugin-project-jsx-js.ts +65 -0
  716. package/vite-plugin-project-root-absolute-assets.ts +359 -0
  717. package/vite-plugin-shared-react.ts +420 -0
  718. package/vite-plugin-shared-three.ts +203 -0
  719. package/vite-plugin-ui-oid.ts +2106 -0
@@ -0,0 +1,934 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import type {
5
+ CollaborationAuditEvent,
6
+ CollaborationAuthorProjection,
7
+ CollaborationCapability,
8
+ CollaborationEvent,
9
+ CollaborationEventChannel,
10
+ CollaborationParticipant,
11
+ CollaborationParticipantLocation,
12
+ CollaborationPresence,
13
+ CollaborationResume,
14
+ CollaborationRole,
15
+ CollaborationSnapshot,
16
+ ParticipantKind,
17
+ ParticipantStatus,
18
+ SourceRevision,
19
+ TeamMessage,
20
+ TeamMessageReference,
21
+ TeamPlaytest,
22
+ } from '@volter/editor-sdk/session/collaboration-types';
23
+ import {
24
+ collaborationParticipantColor,
25
+ isCollaborationAgentMention,
26
+ TEAM_MESSAGE_LIMITS,
27
+ } from '@volter/editor-sdk/session/collaboration-types';
28
+ import { canonicalProjectRoot } from './canonical-path';
29
+
30
+ export type {
31
+ CollaborationAuditEvent,
32
+ CollaborationCapability,
33
+ CollaborationParticipant,
34
+ CollaborationPresence,
35
+ CollaborationRole,
36
+ CollaborationSnapshot,
37
+ ParticipantKind,
38
+ SourceRevision,
39
+ TeamMessage,
40
+ TeamMessageReference,
41
+ TeamPlaytest,
42
+ } from '@volter/editor-sdk/session/collaboration-types';
43
+
44
+ const MAX_MESSAGES = 500;
45
+ const MAX_REVISIONS = 1_000;
46
+ const MAX_AUDIT = 1_000;
47
+ const MAX_DURABLE_EVENTS = 1_000;
48
+ const DURABLE_HISTORY_MAX_AGE_MS = 90 * 24 * 60 * 60 * 1_000;
49
+
50
+ function retainedByAge<T extends { createdAt: string }>(
51
+ values: readonly T[],
52
+ now = Date.now(),
53
+ ): T[] {
54
+ const cutoff = now - DURABLE_HISTORY_MAX_AGE_MS;
55
+ return values.filter((value) => {
56
+ const created = Date.parse(value.createdAt);
57
+ return Number.isFinite(created) && created >= cutoff;
58
+ });
59
+ }
60
+
61
+ const ROLE_CAPABILITIES: Record<CollaborationRole, readonly CollaborationCapability[]> = {
62
+ viewer: ['view'],
63
+ commenter: ['view', 'comment'],
64
+ tester: ['view', 'comment', 'test'],
65
+ editor: ['view', 'comment', 'test', 'edit'],
66
+ terminal: ['view', 'comment', 'test', 'edit', 'terminal'],
67
+ maintainer: ['view', 'comment', 'test', 'edit', 'terminal', 'maintain'],
68
+ };
69
+
70
+ export function parseTeamMessageReferences(
71
+ value: unknown,
72
+ currentRevision: number,
73
+ ): TeamMessageReference[] {
74
+ if (!Array.isArray(value)) throw new Error('Team message references must be an array.');
75
+ const revision = (candidate: unknown, field: string) => {
76
+ if (
77
+ !Number.isInteger(candidate) ||
78
+ (candidate as number) < 0 ||
79
+ (candidate as number) > currentRevision
80
+ ) {
81
+ throw new Error(`${field} must name an existing source revision.`);
82
+ }
83
+ return candidate as number;
84
+ };
85
+ const text = (candidate: unknown, field: string) => {
86
+ if (typeof candidate !== 'string' || !candidate || candidate.length > 1_000) {
87
+ throw new Error(`${field} must be a non-empty string.`);
88
+ }
89
+ return candidate;
90
+ };
91
+ return value.map((candidate) => {
92
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
93
+ throw new Error('Invalid team message reference.');
94
+ }
95
+ const reference = candidate as Record<string, unknown>;
96
+ switch (reference['type']) {
97
+ case 'worktree':
98
+ return { type: 'worktree', worktreeId: text(reference['worktreeId'], 'worktreeId') };
99
+ case 'revision':
100
+ return { type: 'revision', revision: revision(reference['revision'], 'revision') };
101
+ case 'file':
102
+ case 'asset':
103
+ return {
104
+ type: reference['type'],
105
+ path: text(reference['path'], 'path'),
106
+ revision: revision(reference['revision'], 'revision'),
107
+ };
108
+ case 'oid':
109
+ return {
110
+ type: 'oid',
111
+ oid: text(reference['oid'], 'oid'),
112
+ revision: revision(reference['revision'], 'revision'),
113
+ };
114
+ case 'screenshot':
115
+ case 'playtest':
116
+ return {
117
+ type: reference['type'],
118
+ id: text(reference['id'], 'id'),
119
+ revision: revision(reference['revision'], 'revision'),
120
+ };
121
+ case 'diff': {
122
+ const fromRevision = revision(reference['fromRevision'], 'fromRevision');
123
+ const toRevision = revision(reference['toRevision'], 'toRevision');
124
+ if (fromRevision > toRevision) throw new Error('A diff reference must move forward.');
125
+ return { type: 'diff', fromRevision, toRevision };
126
+ }
127
+ default:
128
+ throw new Error('Unknown team message reference type.');
129
+ }
130
+ });
131
+ }
132
+
133
+ const EMPTY_PRESENCE: CollaborationPresence = {
134
+ document: null,
135
+ file: null,
136
+ oid: null,
137
+ selection: [],
138
+ camera: null,
139
+ gesture: null,
140
+ };
141
+
142
+ export class CollaborationConflictError extends Error {
143
+ constructor(
144
+ readonly expectedRevision: number,
145
+ readonly currentRevision: number,
146
+ readonly resources: readonly string[],
147
+ ) {
148
+ super(
149
+ `Source changed after revision ${expectedRevision}; current revision is ${currentRevision}: ${resources.join(', ')}`,
150
+ );
151
+ }
152
+ }
153
+
154
+ export class CollaborationSession {
155
+ private revision = 0;
156
+ private readonly participants = new Map<string, CollaborationParticipant>();
157
+ private readonly assignedRoles = new Map<string, CollaborationRole>();
158
+ private readonly roleCeilings = new Map<string, CollaborationRole>();
159
+ private readonly messages: TeamMessage[] = [];
160
+ private readonly revisions: SourceRevision[] = [];
161
+ private readonly audit: CollaborationAuditEvent[] = [];
162
+ private readonly events: CollaborationEvent[] = [];
163
+ private readonly resourceRevisions = new Map<string, { revision: number; sha: string | null }>();
164
+ private readonly listeners = new Set<(snapshot: CollaborationSnapshot) => void>();
165
+ private readonly eventListeners = new Set<(event: CollaborationEvent) => void>();
166
+ private activePlaytest: TeamPlaytest | null = null;
167
+ private sequence = 0;
168
+ private persistTimer: ReturnType<typeof setTimeout> | null = null;
169
+
170
+ constructor(private readonly persistencePath?: string) {
171
+ if (!persistencePath) return;
172
+ try {
173
+ const value = JSON.parse(readFileSync(persistencePath, 'utf8')) as {
174
+ version?: unknown;
175
+ revision?: unknown;
176
+ assignedRoles?: unknown;
177
+ messages?: unknown;
178
+ revisions?: unknown;
179
+ audit?: unknown;
180
+ resourceRevisions?: unknown;
181
+ sequence?: unknown;
182
+ events?: unknown;
183
+ activePlaytest?: unknown;
184
+ };
185
+ if (value.version !== 1 && value.version !== 2) {
186
+ throw new Error(`Unsupported collaboration state version: ${String(value.version)}.`);
187
+ }
188
+ if (!Number.isInteger(value.revision))
189
+ throw new Error('Invalid collaboration state revision.');
190
+ this.revision = value.revision as number;
191
+ this.sequence =
192
+ value.version === 2 && Number.isInteger(value.sequence) ? (value.sequence as number) : 0;
193
+ if (Array.isArray(value.assignedRoles)) {
194
+ for (const entry of value.assignedRoles) {
195
+ if (
196
+ Array.isArray(entry) &&
197
+ typeof entry[0] === 'string' &&
198
+ typeof entry[1] === 'string' &&
199
+ Object.hasOwn(ROLE_CAPABILITIES, entry[1])
200
+ ) {
201
+ this.assignedRoles.set(entry[0], entry[1] as CollaborationRole);
202
+ }
203
+ }
204
+ }
205
+ if (Array.isArray(value.messages)) {
206
+ this.messages.push(
207
+ ...retainedByAge(value.messages as TeamMessage[])
208
+ .slice(-MAX_MESSAGES)
209
+ .map((message) => ({
210
+ ...message,
211
+ author: message.author ?? this.authorProjection(message.authorId),
212
+ threadId: message.threadId ?? null,
213
+ editedAt: message.editedAt ?? null,
214
+ deletedAt: message.deletedAt ?? null,
215
+ reactions: message.reactions ?? {},
216
+ })),
217
+ );
218
+ }
219
+ if (Array.isArray(value.revisions)) {
220
+ this.revisions.push(
221
+ ...retainedByAge(value.revisions as SourceRevision[])
222
+ .slice(-MAX_REVISIONS)
223
+ .map((revision) => ({
224
+ ...revision,
225
+ author: revision.author ?? this.authorProjection(revision.authorId),
226
+ })),
227
+ );
228
+ }
229
+ if (Array.isArray(value.audit)) {
230
+ this.audit.push(
231
+ ...retainedByAge(value.audit as CollaborationAuditEvent[]).slice(-MAX_AUDIT),
232
+ );
233
+ }
234
+ if (Array.isArray(value.resourceRevisions)) {
235
+ for (const entry of value.resourceRevisions) {
236
+ if (
237
+ Array.isArray(entry) &&
238
+ typeof entry[0] === 'string' &&
239
+ entry[1] &&
240
+ typeof entry[1] === 'object'
241
+ ) {
242
+ const state = entry[1] as { revision?: unknown; sha?: unknown };
243
+ if (
244
+ Number.isInteger(state.revision) &&
245
+ (typeof state.sha === 'string' || state.sha === null)
246
+ ) {
247
+ this.resourceRevisions.set(entry[0], {
248
+ revision: state.revision as number,
249
+ sha: state.sha,
250
+ });
251
+ }
252
+ }
253
+ }
254
+ }
255
+ if (value.version === 2 && Array.isArray(value.events)) {
256
+ this.events.push(
257
+ ...retainedByAge(value.events as CollaborationEvent[]).slice(-MAX_DURABLE_EVENTS),
258
+ );
259
+ }
260
+ if (value.version === 2 && value.activePlaytest && typeof value.activePlaytest === 'object') {
261
+ this.activePlaytest = { ...(value.activePlaytest as TeamPlaytest) };
262
+ }
263
+ } catch (error) {
264
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
265
+ // ANY malformed state, not only unparseable bytes. Structurally-invalid
266
+ // JSON (`"messages": [null]`, a non-array `resourceRevisions`) throws out
267
+ // of the mapping above, and a throw from this constructor takes
268
+ // `collaborationSession()` — and with it the editor's whole project open
269
+ // — down with it. Quarantine and start empty instead; the partially
270
+ // applied load is discarded too, because half a history read out of a
271
+ // file we have just declared untrustworthy is not a history.
272
+ const quarantined = `${persistencePath}.corrupt-${Date.now()}`;
273
+ renameSync(persistencePath, quarantined);
274
+ this.revision = 0;
275
+ this.sequence = 0;
276
+ this.activePlaytest = null;
277
+ this.assignedRoles.clear();
278
+ this.resourceRevisions.clear();
279
+ this.messages.length = 0;
280
+ this.revisions.length = 0;
281
+ this.audit.length = 0;
282
+ this.events.length = 0;
283
+ console.warn(`[vgai-editor] Quarantined corrupt collaboration state at ${quarantined}.`);
284
+ }
285
+ }
286
+
287
+ subscribe(listener: (snapshot: CollaborationSnapshot) => void): () => void {
288
+ this.listeners.add(listener);
289
+ return () => this.listeners.delete(listener);
290
+ }
291
+
292
+ subscribeEvents(listener: (event: CollaborationEvent) => void): () => void {
293
+ this.eventListeners.add(listener);
294
+ return () => this.eventListeners.delete(listener);
295
+ }
296
+
297
+ resume(cursor: number): CollaborationResume {
298
+ const first = this.events[0]?.sequence ?? this.sequence + 1;
299
+ if (!Number.isSafeInteger(cursor) || cursor < first - 1 || cursor > this.sequence) {
300
+ return { cursor: this.sequence, reset: true, events: [], snapshot: this.snapshot() };
301
+ }
302
+ return {
303
+ cursor: this.sequence,
304
+ reset: false,
305
+ events: this.events.filter((event) => event.sequence > cursor).map((event) => ({ ...event })),
306
+ };
307
+ }
308
+
309
+ clearHistory(): void {
310
+ this.messages.length = 0;
311
+ this.revisions.length = 0;
312
+ this.audit.length = 0;
313
+ this.events.length = 0;
314
+ this.persist();
315
+ }
316
+
317
+ flush(): void {
318
+ if (this.persistTimer) clearTimeout(this.persistTimer);
319
+ this.persistTimer = null;
320
+ this.persistNow();
321
+ }
322
+
323
+ snapshot(): CollaborationSnapshot {
324
+ return {
325
+ sequence: this.sequence,
326
+ revision: this.revision,
327
+ activePlaytest: this.activePlaytest ? { ...this.activePlaytest } : null,
328
+ participants: [...this.participants.values()].map((participant) => ({
329
+ ...participant,
330
+ location: participant.location ? { ...participant.location } : null,
331
+ agent: participant.agent ? { ...participant.agent } : null,
332
+ presence: { ...participant.presence, selection: [...participant.presence.selection] },
333
+ })),
334
+ messages: this.messages.map((message) => ({
335
+ ...message,
336
+ mentions: [...message.mentions],
337
+ references: [...message.references],
338
+ reactions: Object.fromEntries(
339
+ Object.entries(message.reactions).map(([reaction, participants]) => [
340
+ reaction,
341
+ [...participants],
342
+ ]),
343
+ ),
344
+ })),
345
+ revisions: this.revisions.map((revision) => ({
346
+ ...revision,
347
+ resources: revision.resources.map((resource) => ({ ...resource })),
348
+ })),
349
+ audit: this.audit.map((event) => ({ ...event, detail: { ...event.detail } })),
350
+ };
351
+ }
352
+
353
+ join(input: {
354
+ participantId: string;
355
+ account?: { id: string; email: string; name?: string } | null;
356
+ displayName: string;
357
+ kind: ParticipantKind;
358
+ location?: CollaborationParticipantLocation | undefined;
359
+ status?: ParticipantStatus;
360
+ agent?: { harness: string; conversationId: string } | null;
361
+ role: CollaborationRole;
362
+ /** Share-gateway roles are a connection-scoped ceiling, not a durable
363
+ * assignment that can be recovered from this browser's older identity. */
364
+ ephemeralRole?: boolean;
365
+ }): CollaborationParticipant {
366
+ const now = new Date().toISOString();
367
+ const previous = this.participants.get(input.participantId);
368
+ // `setRole`'s last-maintainer guard only covers DEMOTION, so the sole
369
+ // maintainer closing their tab leaves a room nobody can ever manage again:
370
+ // handing 'maintain' back needs 'maintain'. A durable join that ASKS for
371
+ // maintainer while the room has none is therefore the one case where the
372
+ // recalled assignment does not win. It only ever raises, so restoring a
373
+ // stored role from ignored session state is untouched, and a share
374
+ // participant never reaches here — its role is the ceiling below.
375
+ const recoversMaintainer =
376
+ !input.ephemeralRole && input.role === 'maintainer' && !this.hasMaintainer();
377
+ const role =
378
+ input.ephemeralRole || recoversMaintainer
379
+ ? input.role
380
+ : (previous?.role ?? this.assignedRoles.get(input.participantId) ?? input.role);
381
+ if (input.ephemeralRole) this.roleCeilings.set(input.participantId, input.role);
382
+ else {
383
+ this.roleCeilings.delete(input.participantId);
384
+ this.assignedRoles.set(input.participantId, role);
385
+ }
386
+ const participant: CollaborationParticipant = {
387
+ participantId: input.participantId,
388
+ account: input.account ?? previous?.account ?? null,
389
+ displayName: input.displayName,
390
+ kind: input.kind,
391
+ color:
392
+ previous?.color ?? collaborationParticipantColor(input.account?.id ?? input.participantId),
393
+ status: input.status ?? previous?.status ?? 'active',
394
+ location: input.location ? { ...input.location } : (previous?.location ?? null),
395
+ agent: input.agent ? { ...input.agent } : null,
396
+ role,
397
+ capabilities: [...ROLE_CAPABILITIES[role]],
398
+ connectedAt: previous?.connectedAt ?? now,
399
+ lastSeenAt: now,
400
+ presence: previous?.presence ?? EMPTY_PRESENCE,
401
+ };
402
+ this.participants.set(input.participantId, participant);
403
+ this.recordAudit('join', input.participantId, {
404
+ role,
405
+ kind: input.kind,
406
+ ...(participant.account ? { accountId: participant.account.id } : {}),
407
+ });
408
+ this.emit('participants', participant);
409
+ return participant;
410
+ }
411
+
412
+ updateParticipant(
413
+ participantId: string,
414
+ patch: {
415
+ displayName?: string;
416
+ status?: ParticipantStatus;
417
+ location?: CollaborationParticipantLocation | undefined;
418
+ agent?: { harness: string; conversationId: string } | null;
419
+ },
420
+ ): void {
421
+ const participant = this.require(participantId, 'view');
422
+ this.participants.set(participantId, {
423
+ ...participant,
424
+ ...(patch.displayName ? { displayName: patch.displayName } : {}),
425
+ ...(patch.status ? { status: patch.status } : {}),
426
+ ...(patch.location ? { location: { ...patch.location } } : {}),
427
+ ...(patch.agent !== undefined ? { agent: patch.agent ? { ...patch.agent } : null } : {}),
428
+ lastSeenAt: new Date().toISOString(),
429
+ });
430
+ this.emit('participants', this.participants.get(participantId), false);
431
+ }
432
+
433
+ leave(participantId: string): void {
434
+ if (!this.participants.delete(participantId)) return;
435
+ this.roleCeilings.delete(participantId);
436
+ this.recordAudit('leave', participantId, {});
437
+ this.emit('participants', { participantId, left: true });
438
+ }
439
+
440
+ setRole(actorId: string, participantId: string, role: CollaborationRole): void {
441
+ this.require(actorId, 'maintain');
442
+ const participant = this.participants.get(participantId);
443
+ if (!participant) throw new Error(`Unknown participant: ${participantId}`);
444
+ const ceiling = this.roleCeilings.get(participantId);
445
+ if (
446
+ ceiling &&
447
+ ROLE_CAPABILITIES[role].some((capability) => !ROLE_CAPABILITIES[ceiling].includes(capability))
448
+ ) {
449
+ throw new Error(`The ${ceiling} share invitation is this participant's role ceiling.`);
450
+ }
451
+ if (
452
+ participant.role === 'maintainer' &&
453
+ role !== 'maintainer' &&
454
+ [...this.participants.values()].filter((candidate) => candidate.role === 'maintainer')
455
+ .length === 1
456
+ ) {
457
+ throw new Error('A collaboration session must keep at least one maintainer.');
458
+ }
459
+ this.participants.set(participantId, {
460
+ ...participant,
461
+ role,
462
+ capabilities: [...ROLE_CAPABILITIES[role]],
463
+ lastSeenAt: new Date().toISOString(),
464
+ });
465
+ if (!ceiling) this.assignedRoles.set(participantId, role);
466
+ this.recordAudit('role', actorId, { participantId, role });
467
+ this.emit('participants', this.participants.get(participantId));
468
+ }
469
+
470
+ updatePresence(participantId: string, presence: CollaborationPresence): void {
471
+ const participant = this.require(participantId, 'view');
472
+ this.participants.set(participantId, {
473
+ ...participant,
474
+ lastSeenAt: new Date().toISOString(),
475
+ presence: { ...presence, selection: [...presence.selection] },
476
+ });
477
+ this.emit('presence', { participantId, presence }, false);
478
+ }
479
+
480
+ assertSourceMutation(
481
+ participantId: string,
482
+ expectedRevision: number,
483
+ paths: readonly string[],
484
+ ): void {
485
+ this.require(participantId, 'edit');
486
+ if (
487
+ !Number.isInteger(expectedRevision) ||
488
+ expectedRevision < 0 ||
489
+ expectedRevision > this.revision
490
+ ) {
491
+ throw new Error(`Invalid expected revision: ${expectedRevision}.`);
492
+ }
493
+ const changed = paths.filter(
494
+ (path) => (this.resourceRevisions.get(path)?.revision ?? 0) > expectedRevision,
495
+ );
496
+ if (changed.length > 0) {
497
+ throw new CollaborationConflictError(expectedRevision, this.revision, changed);
498
+ }
499
+ }
500
+
501
+ resourceStates(paths: readonly string[]): readonly {
502
+ path: string;
503
+ revision: number;
504
+ sha: string | null;
505
+ }[] {
506
+ return paths.map((path) => {
507
+ const state = this.resourceRevisions.get(path);
508
+ return { path, revision: state?.revision ?? 0, sha: state?.sha ?? null };
509
+ });
510
+ }
511
+
512
+ recordSourceMutation(input: {
513
+ authorId: string;
514
+ source: 'editor' | 'filesystem';
515
+ resources: readonly { path: string; sha: string | null }[];
516
+ }): SourceRevision | null {
517
+ const changed = input.resources.filter(
518
+ (resource) => this.resourceRevisions.get(resource.path)?.sha !== resource.sha,
519
+ );
520
+ if (changed.length === 0) return null;
521
+ const invalidatedPlaytest = this.activePlaytest;
522
+ this.activePlaytest = null;
523
+ this.revision += 1;
524
+ const revision: SourceRevision = {
525
+ revision: this.revision,
526
+ authorId: input.authorId,
527
+ author: this.authorProjection(input.authorId),
528
+ source: input.source,
529
+ resources: changed.map((resource) => ({ ...resource })),
530
+ createdAt: new Date().toISOString(),
531
+ };
532
+ this.revisions.push(revision);
533
+ if (this.revisions.length > MAX_REVISIONS) {
534
+ this.revisions.splice(0, this.revisions.length - MAX_REVISIONS);
535
+ }
536
+ for (const resource of changed) {
537
+ this.resourceRevisions.set(resource.path, { revision: this.revision, sha: resource.sha });
538
+ }
539
+ this.recordAudit('revision', input.authorId, {
540
+ revision: this.revision,
541
+ source: input.source,
542
+ resources: changed.map((resource) => resource.path),
543
+ });
544
+ if (invalidatedPlaytest) {
545
+ this.recordAudit('playtest', input.authorId, {
546
+ id: invalidatedPlaytest.id,
547
+ action: 'invalidated',
548
+ revision: this.revision,
549
+ });
550
+ }
551
+ this.emit('revisions', {
552
+ revision,
553
+ activePlaytest: this.activePlaytest,
554
+ });
555
+ return revision;
556
+ }
557
+
558
+ /** Establish the current checkout baseline, or fold changes made while the
559
+ * editor was stopped into one new revision. The caller supplies the complete
560
+ * project-owned file set, so previously-known missing paths are deletions. */
561
+ synchronizeSourceSnapshot(
562
+ authorId: string,
563
+ resources: readonly { path: string; sha: string }[],
564
+ ): SourceRevision | null {
565
+ const current = new Map(resources.map((resource) => [resource.path, resource.sha]));
566
+ const changed: Array<{ path: string; sha: string | null }> = [];
567
+ for (const [path, state] of this.resourceRevisions) {
568
+ const sha = current.get(path) ?? null;
569
+ if (state.sha !== sha) changed.push({ path, sha });
570
+ }
571
+ for (const resource of resources) {
572
+ if (!this.resourceRevisions.has(resource.path)) {
573
+ this.resourceRevisions.set(resource.path, { revision: this.revision, sha: resource.sha });
574
+ }
575
+ }
576
+ if (changed.length > 0) {
577
+ return this.recordSourceMutation({ authorId, source: 'filesystem', resources: changed });
578
+ }
579
+ this.persist();
580
+ return null;
581
+ }
582
+
583
+ postMessage(input: {
584
+ authorId: string;
585
+ text: string;
586
+ mentions?: readonly string[];
587
+ references?: readonly TeamMessageReference[];
588
+ threadId?: string | null;
589
+ }): TeamMessage {
590
+ this.require(input.authorId, 'comment');
591
+ if (input.mentions?.some(isCollaborationAgentMention)) {
592
+ this.require(input.authorId, 'terminal');
593
+ }
594
+ const text = input.text.trim();
595
+ if (!text) throw new Error('A team message cannot be empty.');
596
+ if (text.length > TEAM_MESSAGE_LIMITS.text) {
597
+ throw new Error(`A team message cannot exceed ${TEAM_MESSAGE_LIMITS.text} characters.`);
598
+ }
599
+ if ((input.mentions?.length ?? 0) > TEAM_MESSAGE_LIMITS.mentions)
600
+ throw new Error('A team message has too many mentions.');
601
+ if (
602
+ input.mentions?.some(
603
+ (mention) => !mention || mention.length > TEAM_MESSAGE_LIMITS.mentionLength,
604
+ )
605
+ ) {
606
+ throw new Error(
607
+ `A team message mention must be 1 through ${TEAM_MESSAGE_LIMITS.mentionLength} characters.`,
608
+ );
609
+ }
610
+ if ((input.references?.length ?? 0) > TEAM_MESSAGE_LIMITS.references) {
611
+ throw new Error('A team message has too many references.');
612
+ }
613
+ if (input.threadId && !this.messages.some((message) => message.id === input.threadId)) {
614
+ throw new Error('The team-message thread no longer exists.');
615
+ }
616
+ const message: TeamMessage = {
617
+ id: randomUUID(),
618
+ authorId: input.authorId,
619
+ author: this.authorProjection(input.authorId),
620
+ text,
621
+ mentions: [...(input.mentions ?? [])],
622
+ references: parseTeamMessageReferences(input.references ?? [], this.revision),
623
+ threadId: input.threadId ?? null,
624
+ editedAt: null,
625
+ deletedAt: null,
626
+ reactions: {},
627
+ revision: this.revision,
628
+ createdAt: new Date().toISOString(),
629
+ };
630
+ this.messages.push(message);
631
+ if (this.messages.length > MAX_MESSAGES) {
632
+ this.messages.splice(0, this.messages.length - MAX_MESSAGES);
633
+ }
634
+ this.recordAudit('message', input.authorId, { messageId: message.id });
635
+ this.emit('messages', message);
636
+ return message;
637
+ }
638
+
639
+ editMessage(actorId: string, messageId: string, text: string): TeamMessage {
640
+ const actor = this.require(actorId, 'comment');
641
+ const index = this.messages.findIndex((message) => message.id === messageId);
642
+ if (index < 0) throw new Error('That team message no longer exists.');
643
+ const previous = this.messages[index]!;
644
+ // A deleted message is a tombstone, not a draft: editing one would put text
645
+ // back under a deletion the room has already seen.
646
+ if (previous.deletedAt) throw new Error('That team message no longer exists.');
647
+ if (previous.authorId !== actorId && !actor.capabilities.includes('maintain')) {
648
+ throw new Error('Only the author or a maintainer can edit that message.');
649
+ }
650
+ const nextText = text.trim();
651
+ if (!nextText || nextText.length > TEAM_MESSAGE_LIMITS.text) {
652
+ throw new Error(`A team message must be 1 through ${TEAM_MESSAGE_LIMITS.text} characters.`);
653
+ }
654
+ const next = { ...previous, text: nextText, editedAt: new Date().toISOString() };
655
+ this.messages[index] = next;
656
+ this.recordAudit('message', actorId, { messageId, action: 'edit' });
657
+ this.emit('messages', next);
658
+ return next;
659
+ }
660
+
661
+ deleteMessage(actorId: string, messageId: string): TeamMessage {
662
+ const actor = this.require(actorId, 'comment');
663
+ const index = this.messages.findIndex((message) => message.id === messageId);
664
+ if (index < 0) throw new Error('That team message no longer exists.');
665
+ const previous = this.messages[index]!;
666
+ if (previous.authorId !== actorId && !actor.capabilities.includes('maintain')) {
667
+ throw new Error('Only the author or a maintainer can delete that message.');
668
+ }
669
+ const next: TeamMessage = {
670
+ ...previous,
671
+ text: '',
672
+ mentions: [],
673
+ references: [],
674
+ deletedAt: new Date().toISOString(),
675
+ };
676
+ this.messages[index] = next;
677
+ this.recordAudit('message', actorId, { messageId, action: 'delete' });
678
+ this.emit('messages', next);
679
+ return next;
680
+ }
681
+
682
+ toggleMessageReaction(actorId: string, messageId: string, reaction: string): TeamMessage {
683
+ this.require(actorId, 'comment');
684
+ if (!reaction || reaction.length > 16) throw new Error('Invalid message reaction.');
685
+ const index = this.messages.findIndex((message) => message.id === messageId);
686
+ if (index < 0) throw new Error('That team message no longer exists.');
687
+ const previous = this.messages[index]!;
688
+ if (previous.deletedAt) throw new Error('That team message no longer exists.');
689
+ const reactions = Object.fromEntries(
690
+ Object.entries(previous.reactions).map(([key, participants]) => [key, [...participants]]),
691
+ );
692
+ const participants = new Set(reactions[reaction] ?? []);
693
+ if (participants.has(actorId)) participants.delete(actorId);
694
+ else participants.add(actorId);
695
+ if (participants.size > 0) reactions[reaction] = [...participants];
696
+ else delete reactions[reaction];
697
+ const next = { ...previous, reactions };
698
+ this.messages[index] = next;
699
+ // Reactions are the one message mutation that left no audit trail, so a
700
+ // room's own log could not answer who reacted to what.
701
+ this.recordAudit('message', actorId, { messageId, action: 'reaction', reaction });
702
+ this.emit('messages', next);
703
+ return next;
704
+ }
705
+
706
+ recordAgentInvocation(participantId: string, detail: Readonly<Record<string, unknown>>): void {
707
+ this.require(participantId, 'terminal');
708
+ this.recordAudit('agent', participantId, detail);
709
+ this.emit('audit', this.audit.at(-1));
710
+ }
711
+
712
+ recordPlaytest(participantId: string, detail: Readonly<Record<string, unknown>>): void {
713
+ this.require(participantId, 'test');
714
+ this.recordAudit('playtest', participantId, detail);
715
+ this.emit('audit', this.audit.at(-1));
716
+ }
717
+
718
+ recordGitAction(participantId: string, detail: Readonly<Record<string, unknown>>): void {
719
+ this.require(participantId, 'maintain');
720
+ this.recordAudit('git', participantId, detail);
721
+ this.emit('audit', this.audit.at(-1));
722
+ }
723
+
724
+ startPlaytest(participantId: string, playtest: TeamPlaytest): void {
725
+ this.require(participantId, 'test');
726
+ if (this.activePlaytest) {
727
+ throw new Error(`Team Test ${this.activePlaytest.id} is already active.`);
728
+ }
729
+ this.activePlaytest = { ...playtest };
730
+ this.recordAudit('playtest', participantId, { ...playtest, action: 'start' });
731
+ this.emit('team-test', this.activePlaytest);
732
+ }
733
+
734
+ stopPlaytest(participantId: string, playtestId: string): boolean {
735
+ this.require(participantId, 'test');
736
+ if (this.activePlaytest?.id !== playtestId) return false;
737
+ this.activePlaytest = null;
738
+ this.recordAudit('playtest', participantId, { id: playtestId, action: 'stop' });
739
+ this.emit('team-test', null);
740
+ return true;
741
+ }
742
+
743
+ roleFor(participantId: string): CollaborationRole | null {
744
+ return (
745
+ this.participants.get(participantId)?.role ?? this.assignedRoles.get(participantId) ?? null
746
+ );
747
+ }
748
+
749
+ /** Whether a CURRENT participant holds this capability. Callers that must
750
+ * check authority before a side effect outside this session use it so they
751
+ * do not have to copy a whole snapshot to read one flag. Every role includes
752
+ * `view`, so asking for `view` asks "is this a current participant". */
753
+ allows(participantId: string, capability: CollaborationCapability): boolean {
754
+ return this.participants.get(participantId)?.capabilities.includes(capability) ?? false;
755
+ }
756
+
757
+ /** The verified account bound to a participant, or null when the participant
758
+ * is unknown or local. Only the share paths bind an account, so this is how
759
+ * a loopback caller's claimed identity is told apart from a remote one. */
760
+ accountIdFor(participantId: string): string | null {
761
+ return this.participants.get(participantId)?.account?.id ?? null;
762
+ }
763
+
764
+ /** Whether anyone currently in the room can manage roles at all. */
765
+ hasMaintainer(): boolean {
766
+ for (const participant of this.participants.values()) {
767
+ if (participant.role === 'maintainer') return true;
768
+ }
769
+ return false;
770
+ }
771
+
772
+ private require(
773
+ participantId: string,
774
+ capability: CollaborationCapability,
775
+ ): CollaborationParticipant {
776
+ const participant = this.participants.get(participantId);
777
+ if (!participant) throw new Error(`Unknown participant: ${participantId}`);
778
+ if (!participant.capabilities.includes(capability)) {
779
+ throw new Error(`${participant.displayName} does not have '${capability}' permission.`);
780
+ }
781
+ return participant;
782
+ }
783
+
784
+ private authorProjection(participantId: string): CollaborationAuthorProjection {
785
+ const participant = this.participants.get(participantId);
786
+ return participant
787
+ ? {
788
+ participantId,
789
+ account: participant.account ? { ...participant.account } : null,
790
+ displayName: participant.displayName,
791
+ kind: participant.kind,
792
+ color: participant.color,
793
+ location: participant.location ? { ...participant.location } : null,
794
+ }
795
+ : {
796
+ participantId,
797
+ account: null,
798
+ displayName: participantId === 'host' ? 'Host filesystem' : participantId,
799
+ kind: participantId.startsWith('agent:') ? 'agent' : 'human',
800
+ color: collaborationParticipantColor(participantId),
801
+ location: null,
802
+ };
803
+ }
804
+
805
+ private recordAudit(
806
+ type: CollaborationAuditEvent['type'],
807
+ participantId: string,
808
+ detail: Readonly<Record<string, unknown>>,
809
+ ): void {
810
+ this.audit.push({
811
+ id: randomUUID(),
812
+ type,
813
+ participantId,
814
+ createdAt: new Date().toISOString(),
815
+ detail,
816
+ });
817
+ if (this.audit.length > MAX_AUDIT) this.audit.splice(0, this.audit.length - MAX_AUDIT);
818
+ }
819
+
820
+ /** Local ignored collaboration history is bounded private plaintext. It is
821
+ * deliberately not an account-token store or encrypted chat service. */
822
+ private pruneAgedHistory(): void {
823
+ const cutoff = Date.now() - DURABLE_HISTORY_MAX_AGE_MS;
824
+ const prune = <T extends { createdAt: string }>(values: T[]) => {
825
+ let remove = 0;
826
+ while (remove < values.length && Date.parse(values[remove]!.createdAt) < cutoff) remove += 1;
827
+ if (remove > 0) values.splice(0, remove);
828
+ };
829
+ prune(this.messages);
830
+ prune(this.revisions);
831
+ prune(this.audit);
832
+ prune(this.events);
833
+ }
834
+
835
+ private persist(): void {
836
+ if (!this.persistencePath || this.persistTimer) return;
837
+ this.persistTimer = setTimeout(() => {
838
+ this.persistTimer = null;
839
+ try {
840
+ this.persistNow();
841
+ } catch (error) {
842
+ // biome-ignore lint/suspicious/noConsole: the dev server's terminal is its own report channel; a failed history write must not take the session down.
843
+ console.error(
844
+ `[vgai-editor] Collaboration history write failed: ${error instanceof Error ? error.message : String(error)}`,
845
+ );
846
+ }
847
+ }, 25);
848
+ this.persistTimer.unref?.();
849
+ }
850
+
851
+ private persistNow(): void {
852
+ if (!this.persistencePath) return;
853
+ mkdirSync(dirname(this.persistencePath), { recursive: true, mode: 0o700 });
854
+ const temp = `${this.persistencePath}.${process.pid}.${randomUUID()}.tmp`;
855
+ // Single line, not pretty-printed: this is private machine state rewritten
856
+ // on a 25ms debounce for up to 1,000 messages plus 1,000 events, and the
857
+ // indent was buying nothing but bytes to write.
858
+ writeFileSync(
859
+ temp,
860
+ `${JSON.stringify({
861
+ version: 2,
862
+ sequence: this.sequence,
863
+ revision: this.revision,
864
+ assignedRoles: [...this.assignedRoles],
865
+ messages: this.messages,
866
+ revisions: this.revisions,
867
+ audit: this.audit,
868
+ resourceRevisions: [...this.resourceRevisions],
869
+ events: this.events,
870
+ activePlaytest: this.activePlaytest,
871
+ })}\n`,
872
+ { mode: 0o600 },
873
+ );
874
+ renameSync(temp, this.persistencePath);
875
+ try {
876
+ chmodSync(this.persistencePath, 0o600);
877
+ } catch {
878
+ /* a filesystem without modes */
879
+ }
880
+ }
881
+
882
+ private emit(channel: CollaborationEventChannel, payload: unknown, persist = true): void {
883
+ this.sequence += 1;
884
+ const event: CollaborationEvent = {
885
+ sequence: this.sequence,
886
+ channel,
887
+ createdAt: new Date().toISOString(),
888
+ payload,
889
+ };
890
+ if (persist) {
891
+ this.events.push(event);
892
+ this.pruneAgedHistory();
893
+ if (this.events.length > MAX_DURABLE_EVENTS) {
894
+ this.events.splice(0, this.events.length - MAX_DURABLE_EVENTS);
895
+ }
896
+ this.persist();
897
+ }
898
+ for (const listener of this.eventListeners) listener(event);
899
+ const snapshot = this.snapshot();
900
+ for (const listener of this.listeners) listener(snapshot);
901
+ }
902
+ }
903
+
904
+ /**
905
+ * One room registry for the whole editor process, including Vite's bundled
906
+ * config module graph.
907
+ *
908
+ * In dev, `editor-server.ts` is loaded by the host while the root Vite config
909
+ * is bundled and loaded through Vite's config runner. Both import this file,
910
+ * but Node can therefore evaluate two module instances. A module-local map
911
+ * made `/__editor/collaboration` join one room while `/__ui-source/apply`
912
+ * authorized against another empty room, rejecting every ordinary inspector
913
+ * write as an unknown participant. The room is process state, so its registry
914
+ * must be process-owned too; canonical project roots remain the per-room key.
915
+ */
916
+ const collaborationSessionScope = globalThis as typeof globalThis & {
917
+ __VGAI_COLLABORATION_SESSIONS__?: Map<string, CollaborationSession>;
918
+ };
919
+ // `const`, not `let`: a `let` typed `Map | undefined` cannot carry its
920
+ // narrowing into `collaborationSession` below (a closure may observe a later
921
+ // assignment), so every use inside the function was a type error.
922
+ const sessions: Map<string, CollaborationSession> =
923
+ collaborationSessionScope.__VGAI_COLLABORATION_SESSIONS__ ?? new Map();
924
+ collaborationSessionScope.__VGAI_COLLABORATION_SESSIONS__ = sessions;
925
+
926
+ export function collaborationSession(projectRoot: string): CollaborationSession {
927
+ const root = canonicalProjectRoot(projectRoot);
928
+ let session = sessions.get(root);
929
+ if (!session) {
930
+ session = new CollaborationSession(join(root, '.vgai', 'collaboration.json'));
931
+ sessions.set(root, session);
932
+ }
933
+ return session;
934
+ }