@vgai/editor 0.2.0 → 0.4.0-canary.20260715.0

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 (430) hide show
  1. package/README.md +24 -0
  2. package/dist/assets/axe-C7YBtOuM.js +30 -0
  3. package/dist/assets/browserAll-DDtGGE48.js +1 -0
  4. package/dist/assets/create-runtime-DbTlBlKG.js +1 -0
  5. package/dist/assets/{game-BI111o5f.js → game-BE0r7DMI.js} +3 -3
  6. package/dist/assets/game-BdTHR2_X.js +1 -0
  7. package/dist/assets/{game-pt_1lQLv.js → game-C3iQGSqo.js} +1 -1
  8. package/dist/assets/game-CcHfYxjP.js +1 -0
  9. package/dist/assets/game-Cqzj_lpD.js +1 -0
  10. package/dist/assets/game-DSZdeCHu.js +1 -0
  11. package/dist/assets/game-Dch9sAPc.js +1 -0
  12. package/dist/assets/{game-xsOho_wg.js → game-SAFn90pA.js} +3 -3
  13. package/dist/assets/game-loop-DMIgSMV-.js +1 -0
  14. package/dist/assets/{game-source-CvidN7AB.js → game-source-Dk83w26t.js} +1 -1
  15. package/dist/assets/{index-Bp5cecD6.js → index-4idIsHHJ.js} +2 -2
  16. package/dist/assets/{index-DL7RcQ0n.js → index-C-LhOTBN.js} +1 -1
  17. package/dist/assets/{index-Cs8xwv3e.js → index-CDkxceqE.js} +1 -1
  18. package/dist/assets/index-CQEfi217.js +15412 -0
  19. package/dist/assets/index-Dvi-V0NE.css +1 -0
  20. package/dist/assets/ingest-siblings-s0P5I1Ub.js +1 -0
  21. package/dist/assets/mount-manifest-DXMOlF79.js +1 -0
  22. package/dist/assets/react-18-C9eHn8x1.js +1 -0
  23. package/dist/assets/webworkerAll-Fn0e-ouF.js +1 -0
  24. package/dist/assets/world-documents-GM0a9Kv7.js +1 -0
  25. package/dist/index.html +15 -13
  26. package/dist/ingest/racing-game/vgai.game.json +5 -4
  27. package/dist/ingest/simcity/vgai.game.json +5 -4
  28. package/dist/ingest/tanks/vgai.game.json +5 -4
  29. package/dist/project-scripts.bundle.js +3917 -0
  30. package/dist/scenes/untitled.vscn.json +2 -0
  31. package/dist-server/packaged.mjs +7697 -3435
  32. package/package.json +31 -4
  33. package/scripts/probe-reporter.ts +349 -0
  34. package/scripts/run-probe.ts +2260 -0
  35. package/server/asset-catalog-audit.ts +268 -0
  36. package/server/asset-catalog-v2.ts +146 -0
  37. package/server/asset-library-routes.ts +276 -58
  38. package/server/canonical-path.ts +1 -1
  39. package/server/dev.ts +148 -44
  40. package/server/editor-server.ts +888 -61
  41. package/server/editor-sse.ts +115 -4
  42. package/server/harness-chat-service.ts +796 -0
  43. package/server/local-asset-catalog.ts +157 -22
  44. package/server/packaged.ts +51 -21
  45. package/server/process-shutdown.ts +136 -0
  46. package/server/prod.ts +9 -6
  47. package/server/project-hmr-files.ts +41 -0
  48. package/server/project-operations.ts +343 -0
  49. package/server/project-optimize-deps-entries.ts +4 -4
  50. package/server/project-output-writer.ts +156 -0
  51. package/server/project-validation.ts +140 -0
  52. package/server/project-view.ts +8 -50
  53. package/server/server-utils.ts +75 -10
  54. package/server/session-registry.ts +12 -0
  55. package/src/EditorContext.tsx +107 -3
  56. package/src/action-registry.ts +115 -17
  57. package/src/adapter-resolver.ts +213 -478
  58. package/src/asset-events.ts +64 -1
  59. package/src/asset-preview.ts +906 -0
  60. package/src/asset-selection.ts +121 -0
  61. package/src/asset-workflow/asset-capabilities.ts +259 -0
  62. package/src/asset-workflow/asset-editor-errors.ts +46 -0
  63. package/src/asset-workflow/asset-import-pipeline.ts +176 -0
  64. package/src/asset-workflow/asset-library-query.ts +107 -0
  65. package/src/asset-workflow/asset-types.ts +192 -0
  66. package/src/asset-workflow/asset-workflow-quality.ts +65 -0
  67. package/src/asset-workflow/import-contract.ts +113 -0
  68. package/src/asset-workflow/model-conversion.ts +126 -0
  69. package/src/asset-workflow/model-inspection.ts +185 -0
  70. package/src/asset-workflow/project-asset-commands.ts +23 -0
  71. package/src/asset-workflow/project-asset-health.ts +153 -0
  72. package/src/asset-workflow/project-asset-operations.ts +119 -0
  73. package/src/asset-workflow/thumbnail-system.ts +172 -0
  74. package/src/authoring/active-systems.ts +15 -0
  75. package/src/authoring/boundary-authoring-adapter.ts +16 -5
  76. package/src/authoring/composite-authoring-adapter.ts +305 -124
  77. package/src/authoring/design-time-layers.ts +300 -117
  78. package/src/authoring/edit-mode-authoring.ts +278 -293
  79. package/src/authoring/embed-only-adapter.ts +4 -2
  80. package/src/authoring/first-party-hierarchy-menu.ts +7 -2
  81. package/src/authoring/first-party-inspector/AddSectionMenu.tsx +5 -12
  82. package/src/authoring/first-party-inspector/EnvironmentSection.tsx +53 -126
  83. package/src/authoring/first-party-inspector/HeaderSection.tsx +54 -65
  84. package/src/authoring/first-party-inspector/is-first-party-selection.ts +30 -11
  85. package/src/authoring/first-party-inspector/register.tsx +13 -2
  86. package/src/authoring/ingest-authoring-adapter.ts +142 -45
  87. package/src/authoring/mount-failure-report.ts +5 -5
  88. package/src/authoring/provenance.ts +41 -0
  89. package/src/authoring/react-dom-authoring-adapter.ts +92 -92
  90. package/src/authoring/react-inspector-section.tsx +489 -51
  91. package/src/authoring/react-world-authoring-adapter.ts +541 -306
  92. package/src/authoring/story-overlay.ts +1 -1
  93. package/src/authoring/story-types.ts +11 -0
  94. package/src/authoring/vgai-scene-authoring-adapter.ts +54 -25
  95. package/src/authoring/viewport-tool-context.ts +78 -0
  96. package/src/authoring/world-canvas-viewport-state.ts +35 -0
  97. package/src/authoring/world-hidden-viewport.ts +186 -0
  98. package/src/authoring/world-pan-state.ts +33 -17
  99. package/src/authoring/world-session-state.ts +1 -1
  100. package/src/authoring/world2d-authoring-adapter.ts +109 -52
  101. package/src/availability-tick.ts +56 -0
  102. package/src/browser-play-entry.ts +5 -5
  103. package/src/browser-project-scripts.ts +1 -0
  104. package/src/build-identity.ts +15 -0
  105. package/src/command-listener.ts +893 -19
  106. package/src/components/AlignToolbar.tsx +74 -63
  107. package/src/components/AppRoot.tsx +193 -26
  108. package/src/components/ApplicationMenus.tsx +272 -0
  109. package/src/components/AssetBrowser.tsx +1450 -293
  110. package/src/components/AssetEditorShell.tsx +141 -0
  111. package/src/components/AssetImportDetails.tsx +218 -0
  112. package/src/components/AssetLibraryPanel.tsx +14 -0
  113. package/src/components/CameraInfo.tsx +11 -20
  114. package/src/components/CenterDocuments.tsx +585 -0
  115. package/src/components/CommandPalette.tsx +75 -30
  116. package/src/components/ConsolePanel.css +55 -0
  117. package/src/components/ConsolePanel.tsx +57 -112
  118. package/src/components/DebugConsolePanel.tsx +461 -0
  119. package/src/components/DebuggerPanel.tsx +165 -0
  120. package/src/components/DefaultEditorLayout.tsx +167 -231
  121. package/src/components/GameHierarchy.tsx +873 -290
  122. package/src/components/GamePerformanceOverlay.tsx +46 -0
  123. package/src/components/HarnessChatPanel.tsx +2352 -0
  124. package/src/components/HistoryPanel.tsx +226 -0
  125. package/src/components/Inspector.tsx +153 -99
  126. package/src/components/InspectorToolSection.tsx +6 -4
  127. package/src/components/MountFailureBanner.tsx +31 -48
  128. package/src/components/NetworkSection.tsx +25 -14
  129. package/src/components/OnlineAssetBrowser.tsx +794 -343
  130. package/src/components/OverlayReportBanner.tsx +38 -64
  131. package/src/components/PerformancePanel.tsx +206 -0
  132. package/src/components/PlayBar.tsx +93 -263
  133. package/src/components/PrefabSaveDialog.tsx +55 -127
  134. package/src/components/ProjectHeader.css +60 -0
  135. package/src/components/ProjectHeader.tsx +170 -105
  136. package/src/components/ProjectScreen.tsx +246 -184
  137. package/src/components/ReactCanvasControls.tsx +185 -0
  138. package/src/components/ResolutionPicker.tsx +85 -0
  139. package/src/components/RootActivateDialog.tsx +39 -68
  140. package/src/components/SaveDialog.tsx +55 -127
  141. package/src/components/SaveStatus.tsx +11 -29
  142. package/src/components/StartupLoadingScreen.tsx +96 -0
  143. package/src/components/StateWatchPanel.tsx +302 -0
  144. package/src/components/StatsOverlay.tsx +12 -5
  145. package/src/components/StatusBar.tsx +64 -0
  146. package/src/components/ThemeManagerDialog.css +89 -0
  147. package/src/components/ThemeManagerDialog.tsx +371 -0
  148. package/src/components/ToolHost.tsx +26 -15
  149. package/src/components/Toolbar.tsx +117 -214
  150. package/src/components/VgaiVoxelLogo.tsx +89 -0
  151. package/src/components/ViewportOverlay.tsx +97 -172
  152. package/src/components/ViewportPanel.tsx +130 -31
  153. package/src/components/WorkspaceDock.tsx +891 -0
  154. package/src/components/WorldSelectionOverlay.tsx +51 -63
  155. package/src/components/WorldTextEditor.tsx +9 -5
  156. package/src/components/asset-documents.tsx +390 -0
  157. package/src/components/asset-editor-persistence.ts +44 -0
  158. package/src/components/asset-selection-section.tsx +338 -0
  159. package/src/components/asset-viewers/AudioViewer.tsx +73 -21
  160. package/src/components/asset-viewers/GenericJsonViewer.tsx +26 -11
  161. package/src/components/asset-viewers/ImageViewer.tsx +43 -11
  162. package/src/components/asset-viewers/InputMapViewer.tsx +512 -134
  163. package/src/components/asset-viewers/MaterialAssetEditor.tsx +540 -0
  164. package/src/components/asset-viewers/ModelViewer.tsx +805 -121
  165. package/src/components/asset-viewers/OnlineAssetDetail.tsx +177 -60
  166. package/src/components/asset-viewers/SourceAssetViewer.tsx +121 -0
  167. package/src/components/asset-workflow.css +506 -0
  168. package/src/components/build-documents.tsx +696 -0
  169. package/src/components/core-utilities.tsx +75 -0
  170. package/src/components/data-documents.tsx +364 -0
  171. package/src/components/{DataPanel.tsx → data-editor.tsx} +138 -288
  172. package/src/components/debugger-panel-state.ts +57 -0
  173. package/src/components/inspector-widgets/AlignmentGrid.tsx +8 -14
  174. package/src/components/inspector-widgets/BorderEditor.tsx +41 -33
  175. package/src/components/inspector-widgets/ColorPicker.tsx +39 -39
  176. package/src/components/inspector-widgets/FontPicker.tsx +13 -21
  177. package/src/components/inspector-widgets/GradientEditor.tsx +11 -19
  178. package/src/components/inspector-widgets/ScrubbableInput.tsx +6 -3
  179. package/src/components/inspector-widgets/ShadowEditor.tsx +20 -33
  180. package/src/components/inspector-widgets/shared.tsx +90 -49
  181. package/src/components/inspectors/AnimationSection.tsx +39 -40
  182. package/src/components/inspectors/AudioSection.tsx +28 -39
  183. package/src/components/inspectors/BoneAttachmentSection.tsx +19 -24
  184. package/src/components/inspectors/CameraSection.tsx +14 -23
  185. package/src/components/inspectors/ComponentsSection.tsx +39 -43
  186. package/src/components/inspectors/JointsSection.tsx +34 -43
  187. package/src/components/inspectors/LightSection.tsx +90 -35
  188. package/src/components/inspectors/MaterialSection.tsx +137 -174
  189. package/src/components/inspectors/MeshSection.tsx +9 -17
  190. package/src/components/inspectors/NavigationSection.tsx +8 -15
  191. package/src/components/inspectors/ParticleSection.tsx +91 -76
  192. package/src/components/inspectors/PhysicsSection.tsx +42 -46
  193. package/src/components/inspectors/PostProcessingSection.tsx +28 -36
  194. package/src/components/inspectors/RenderingSection.tsx +12 -20
  195. package/src/components/inspectors/ShadowSection.tsx +7 -8
  196. package/src/components/inspectors/SplineSection.tsx +31 -42
  197. package/src/components/inspectors/TransformSection.tsx +14 -22
  198. package/src/components/patterns/Dialog.tsx +95 -0
  199. package/src/components/patterns/Fields.tsx +44 -0
  200. package/src/components/patterns/List.tsx +25 -0
  201. package/src/components/patterns/StateSurface.tsx +40 -0
  202. package/src/components/patterns/Surfaces.tsx +115 -0
  203. package/src/components/patterns/Tabs.tsx +80 -0
  204. package/src/components/patterns/Toolbar.tsx +51 -0
  205. package/src/components/patterns/Tree.tsx +72 -0
  206. package/src/components/primitives/Button.tsx +46 -0
  207. package/src/components/primitives/ColorInput.tsx +11 -47
  208. package/src/components/primitives/EditorIcon.tsx +99 -0
  209. package/src/components/primitives/FormControls.tsx +71 -0
  210. package/src/components/primitives/JsonInput.tsx +15 -2
  211. package/src/components/primitives/Layout.tsx +100 -0
  212. package/src/components/primitives/Menu.tsx +105 -0
  213. package/src/components/primitives/NumberInput.tsx +8 -35
  214. package/src/components/primitives/Panel.tsx +49 -44
  215. package/src/components/primitives/SectionHeader.tsx +10 -28
  216. package/src/components/primitives/Text.tsx +46 -0
  217. package/src/components/primitives/Tooltip.tsx +20 -12
  218. package/src/components/primitives/Vec3Input.tsx +3 -0
  219. package/src/components/primitives/banner-tones.ts +33 -0
  220. package/src/components/primitives/editor-icons.ts +102 -0
  221. package/src/components/primitives/panel-header-styles.ts +30 -0
  222. package/src/components/project-operation-documents.tsx +226 -0
  223. package/src/components/status-contributions.tsx +175 -0
  224. package/src/components/story-documents-register.ts +15 -0
  225. package/src/components/story-documents.tsx +1185 -0
  226. package/src/components/tool-documents.tsx +110 -0
  227. package/src/components/workspace-dock.css +293 -0
  228. package/src/components/workspace-history.ts +33 -0
  229. package/src/components/world-documents.tsx +145 -0
  230. package/src/components/xstate-documents.tsx +209 -0
  231. package/src/composite-screenshot.ts +248 -0
  232. package/src/design-system/index.ts +70 -0
  233. package/src/design-system-stories/Foundation.stories.tsx +183 -0
  234. package/src/design-system-stories/Patterns.stories.tsx +251 -0
  235. package/src/design-system-stories/Workspace.stories.tsx +538 -0
  236. package/src/doctor/report.ts +57 -13
  237. package/src/editor-api.ts +295 -31
  238. package/src/editor-compatibility.ts +200 -0
  239. package/src/editor-console.ts +35 -17
  240. package/src/editor-hotkeys.ts +85 -33
  241. package/src/editor-store.ts +538 -233
  242. package/src/editor-viewport.ts +160 -34
  243. package/src/entity-factory.ts +14 -3
  244. package/src/gizmo-registry.ts +21 -5
  245. package/src/harness-chat-editor-context.ts +37 -0
  246. package/src/harness-chat-indicator.ts +68 -0
  247. package/src/harness-chat-layout.ts +53 -0
  248. package/src/harness-chat-lifecycle.ts +170 -0
  249. package/src/harness-chat-reconnect.ts +85 -0
  250. package/src/harness-chat-types.ts +409 -0
  251. package/src/hierarchy-drop.ts +50 -0
  252. package/src/hierarchy-expansion-state.ts +100 -0
  253. package/src/hierarchy-menu-registry.ts +1 -1
  254. package/src/hierarchy-projection.ts +76 -0
  255. package/src/history/editor-session.ts +21 -0
  256. package/src/history/history-commands.ts +147 -0
  257. package/src/history/history-service.ts +904 -0
  258. package/src/history/json-history-resource.ts +165 -0
  259. package/src/history/persistence-coordinator.ts +35 -0
  260. package/src/history/project-file-history.ts +266 -0
  261. package/src/history/project-root-history-backends.ts +53 -0
  262. package/src/history/resource-registry.ts +209 -0
  263. package/src/history/snapshot-store.ts +108 -0
  264. package/src/history/source-history-backend.ts +374 -0
  265. package/src/history/types.ts +100 -0
  266. package/src/hmr-registration-group.ts +67 -0
  267. package/src/hmr-stable-react-context.ts +23 -0
  268. package/src/hosted-example.ts +12 -2
  269. package/src/hotkeys.ts +89 -18
  270. package/src/ingest/discovery-react.ts +9 -10
  271. package/src/ingest/discovery.ts +4 -4
  272. package/src/ingest/discovery2d.ts +5 -5
  273. package/src/ingest/games/bloom-composer/vgai.game.json +3 -3
  274. package/src/ingest/games/embed-bundle/vgai.game.json +3 -3
  275. package/src/ingest/games/games-fps/vgai.game.json +3 -3
  276. package/src/ingest/games/iframe-reachable/vgai.game.json +3 -3
  277. package/src/ingest/games/interactive-cubes/vgai.game.json +3 -3
  278. package/src/ingest/games/mismatched-three/vgai.game.json +3 -3
  279. package/src/ingest/games/own-three-bundled/vgai.game.json +3 -3
  280. package/src/ingest/games/pointerlock-maze/vgai.game.json +7 -4
  281. package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +3 -3
  282. package/src/ingest/games-2d/bunnymark/vgai.game.json +3 -3
  283. package/src/ingest/games-2d/flappy/vgai.game.json +3 -3
  284. package/src/ingest/games-2d/flappy-pixi/vgai.game.json +3 -3
  285. package/src/ingest/games-2d/puzzling-potions/vgai.game.json +3 -3
  286. package/src/ingest/games-2d/tilemap-camera/vgai.game.json +3 -3
  287. package/src/ingest/games-react/react-rpg/vgai.game.json +3 -3
  288. package/src/ingest/types.ts +1 -1
  289. package/src/ingest-mode.ts +47 -52
  290. package/src/ingest-siblings.ts +23 -33
  291. package/src/initial-scene.ts +1 -2
  292. package/src/main.tsx +16 -8
  293. package/src/manifest-project.ts +10 -12
  294. package/src/model-thumbnail.ts +261 -26
  295. package/src/module-mode.ts +44 -50
  296. package/src/play-log-events.ts +26 -0
  297. package/src/play-mode.ts +495 -401
  298. package/src/play-scene-adoption.ts +23 -0
  299. package/src/playground/main.ts +68 -24
  300. package/src/playwright-shim.ts +473 -0
  301. package/src/project-manager.ts +90 -10
  302. package/src/project-operations.ts +112 -0
  303. package/src/project-story-discovery.ts +25 -0
  304. package/src/project-tool-discovery.ts +43 -0
  305. package/src/scene-serializer.ts +0 -6
  306. package/src/scene-sync.ts +7 -2
  307. package/src/script-loader.ts +3 -3
  308. package/src/skeleton-helper.ts +57 -0
  309. package/src/storage/seed.ts +68 -8
  310. package/src/stories/StoryPreviewMount.tsx +265 -0
  311. package/src/stories/action-stories.fixture.tsx +44 -0
  312. package/src/stories/button-stories.fixture.tsx +50 -0
  313. package/src/stories/compose-project-stories.ts +182 -0
  314. package/src/stories/loader-stories.fixture.tsx +53 -0
  315. package/src/stories/story-discovery.ts +130 -0
  316. package/src/stories/story-registry.ts +146 -0
  317. package/src/theme-css.ts +1049 -0
  318. package/src/theme-library.ts +238 -0
  319. package/src/theme-preference.ts +130 -0
  320. package/src/theme.ts +527 -0
  321. package/src/tool-loader.ts +80 -24
  322. package/src/ui-source/inspect.ts +27 -4
  323. package/src/ui-source/source-write-backend.ts +83 -22
  324. package/src/widgets/index.ts +9 -18
  325. package/src/workspace-aux-commands.ts +11 -0
  326. package/src/workspace-dock-controller.ts +64 -0
  327. package/src/workspace-document-ids.ts +5 -0
  328. package/src/workspace-document-registry.ts +367 -0
  329. package/src/workspace-state-persistence.ts +455 -0
  330. package/src/workspace-status-registry.ts +110 -0
  331. package/src/workspace-utility-commands.ts +49 -0
  332. package/src/workspace-utility-registry.ts +161 -0
  333. package/src/world-document-routing.ts +16 -0
  334. package/src/xstate-inspect/XStateMachineInspector.tsx +544 -0
  335. package/src/xstate-inspect/character-animation-machine.fixture.ts +74 -0
  336. package/src/xstate-inspect/use-live-actor-state.ts +39 -0
  337. package/src/xstate-inspect/xstate-graph.ts +265 -0
  338. package/src/xstate-inspect/xstate-layout.ts +76 -0
  339. package/src/z-index.ts +26 -0
  340. package/template/.claude/skills/editor/SKILL.md +129 -90
  341. package/template/AGENTS.md +1110 -0
  342. package/template/CLAUDE.md +1 -512
  343. package/template/IDIOMS.md +50 -0
  344. package/template/check-idioms.ts +706 -0
  345. package/template/manifest-entry-modules-plugin.ts +53 -0
  346. package/template/package.json +13 -4
  347. package/template/public/scenes/default.scn2d.json +6 -0
  348. package/template/public/scenes/default.vscn.json +7 -1
  349. package/template/public/scenes/untitled.vscn.json +10 -0
  350. package/template/scripts/playtest.ts +40 -0
  351. package/template/server/loopback.ts +75 -0
  352. package/template/server/rooms/arena-room.ts +69 -1
  353. package/template/server/rooms/game-room.ts +68 -1
  354. package/template/src/data/README.md +9 -4
  355. package/template/src/data/data-ref.ts +35 -0
  356. package/template/src/data/tuning.schema.ts +1 -1
  357. package/template/src/main.ts +41 -62
  358. package/template/src/manifest-entry-modules.d.ts +3 -0
  359. package/template/src/operations/README.md +88 -0
  360. package/template/src/runtime/pixi-adapter.ts +24 -0
  361. package/template/src/scripts/components/data-spinner.ts +43 -2
  362. package/template/src/scripts/components/spin-lap-math.ts +27 -0
  363. package/template/src/tools/README.md +5 -3
  364. package/template/src/tools/example.tool.tsx +13 -7
  365. package/template/src/ui/Button.stories.tsx +66 -0
  366. package/template/src/ui/RebindPanel.tsx +2 -6
  367. package/template/src/ui/game.tsx +17 -0
  368. package/template/tests/acceptance/example.spec.ts +64 -0
  369. package/template/tests/logic/example.test.ts +31 -0
  370. package/template/tests/logic/room.test.ts +100 -0
  371. package/template/tests/tsconfig.json +30 -0
  372. package/template/validate-manifest.ts +5 -5
  373. package/template/validate-scenes.ts +50 -18
  374. package/template/vgai.game.json +20 -13
  375. package/template/vite.config.ts +77 -16
  376. package/vite-plugin-ui-oid.ts +261 -17
  377. package/dist/assets/CCDIKSolver-Dz-ysxZF.js +0 -1
  378. package/dist/assets/SelectionBox-BXsv_6aH.js +0 -1
  379. package/dist/assets/browser-project-scripts-DOru4L1x.js +0 -2
  380. package/dist/assets/browserAll-CEkWYlFk.js +0 -1
  381. package/dist/assets/create-runtime-DYIWr3va.js +0 -1
  382. package/dist/assets/data-asset-Bb9sIM7i.js +0 -5
  383. package/dist/assets/game-8SDZPPgH.js +0 -1
  384. package/dist/assets/game-CevorxyV.js +0 -1
  385. package/dist/assets/game-Di7XlywD.js +0 -1
  386. package/dist/assets/game-DjnCaw8R.js +0 -1
  387. package/dist/assets/game-Q9A0aXSV.js +0 -1
  388. package/dist/assets/index-CPVV6pjw.js +0 -1
  389. package/dist/assets/index-CpOnYyeD.js +0 -213
  390. package/dist/assets/index-Dr8H_6Rl.js +0 -1
  391. package/dist/assets/index-_bCaipUk.js +0 -13650
  392. package/dist/assets/ingest-siblings-Dz8eEqrV.js +0 -1
  393. package/dist/assets/mount-manifest-Bhk0ohsl.js +0 -1
  394. package/dist/assets/scene-query-BivAHhJu.js +0 -1
  395. package/dist/assets/webworkerAll-BLrH0F6F.js +0 -1
  396. package/dist/assets/xstate-animation-binding-1NGLmw6y.js +0 -1
  397. package/dist/assets/xstate.esm-BgUD33nl.js +0 -3
  398. package/src/authoring/scene-ui-layer.ts +0 -485
  399. package/src/authoring/ui-authoring-adapter.ts +0 -325
  400. package/src/authoring/ui-hierarchy-menu.ts +0 -40
  401. package/src/authoring/ui-inspector-sections.tsx +0 -156
  402. package/src/authoring/undo-timeline.ts +0 -145
  403. package/src/components/BottomPanel.tsx +0 -326
  404. package/src/components/BuildPanel.tsx +0 -455
  405. package/src/components/FolderTree.tsx +0 -179
  406. package/src/components/RightPanel.tsx +0 -166
  407. package/src/components/WorldContextMenu.tsx +0 -257
  408. package/src/components/asset-viewers/AssetViewer.tsx +0 -38
  409. package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +0 -129
  410. package/src/scene-ui-mode/active-scene-ui.ts +0 -58
  411. package/src/scene-ui-mode/scene-ui-palette-panel.tsx +0 -152
  412. package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +0 -29
  413. package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +0 -74
  414. package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +0 -49
  415. package/src/scene-ui-mode/ui-prefab-io.ts +0 -76
  416. package/src/ui-editor/inspector.tsx +0 -563
  417. package/src/ui-editor/node-ops.ts +0 -266
  418. package/src/ui-editor/overlay.tsx +0 -380
  419. package/src/ui-editor/palette.tsx +0 -114
  420. package/src/ui-editor/react-store.ts +0 -78
  421. package/src/ui-editor/timeline-panel.tsx +0 -443
  422. package/src/ui-editor/ui-edit-store.ts +0 -552
  423. package/src/ui-editor/ui-prefab.ts +0 -95
  424. package/template/project.json +0 -9
  425. package/template/src/scripts/main.ts +0 -94
  426. package/template/src/ui/DefaultHud.tsx +0 -40
  427. package/template/src/ui/game-state.tsx +0 -37
  428. package/template/src/ui/mount-default-hud.tsx +0 -19
  429. package/template/src/ui/mount-ui.ts +0 -15
  430. package/vite-plugin-react-world-provider.ts +0 -81
@@ -0,0 +1,209 @@
1
+ import type { ResourceDescriptor, ResourceKey, ResourceKind } from './types';
2
+
3
+ export type ResourceIdFactory = () => string;
4
+
5
+ function defaultIdFactory(): string {
6
+ return crypto.randomUUID();
7
+ }
8
+
9
+ function resourceKey(id: string): ResourceKey {
10
+ return `resource:${id}` as ResourceKey;
11
+ }
12
+
13
+ function basename(path: string): string {
14
+ return path.slice(path.lastIndexOf('/') + 1);
15
+ }
16
+
17
+ /** Normalize a StorageBackend path without importing Node path APIs into the browser. */
18
+ export function normalizeProjectPath(input: string): string {
19
+ if (input.includes('\0')) throw new Error('Resource path cannot contain a NUL byte.');
20
+ const slashed = input.replaceAll('\\', '/');
21
+ if (slashed.startsWith('/') || /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(slashed)) {
22
+ throw new Error(`Resource path must be project-relative: "${input}".`);
23
+ }
24
+
25
+ const output: string[] = [];
26
+ for (const segment of slashed.split('/')) {
27
+ if (segment === '' || segment === '.') continue;
28
+ if (segment === '..') {
29
+ if (output.length === 0) {
30
+ throw new Error(`Resource path escapes the project root: "${input}".`);
31
+ }
32
+ output.pop();
33
+ continue;
34
+ }
35
+ output.push(segment);
36
+ }
37
+ if (output.length === 0) throw new Error('Resource path cannot be empty.');
38
+ return output.join('/');
39
+ }
40
+
41
+ function sessionSegment(input: string, label: string): string {
42
+ const value = input.trim();
43
+ if (
44
+ !value ||
45
+ value === '.' ||
46
+ value === '..' ||
47
+ value.includes('/') ||
48
+ value.includes('\\') ||
49
+ value.includes('\0')
50
+ ) {
51
+ throw new Error(`${label} must be one non-empty path segment.`);
52
+ }
53
+ return value;
54
+ }
55
+
56
+ /** Project-session registry: stable keys are independent from mutable file locations. */
57
+ export class ResourceRegistry {
58
+ private readonly descriptors = new Map<ResourceKey, ResourceDescriptor>();
59
+ private readonly keyByLocation = new Map<string, ResourceKey>();
60
+ private readonly inactive = new Set<ResourceKey>();
61
+
62
+ constructor(private readonly makeId: ResourceIdFactory = defaultIdFactory) {}
63
+
64
+ registerProject(kind: ResourceKind, path: string, displayName?: string): ResourceDescriptor {
65
+ const normalized = normalizeProjectPath(path);
66
+ const location = `project://${normalized}`;
67
+ const existing = this.existingAt(location, kind);
68
+ if (existing) return existing;
69
+ return this.add({
70
+ key: resourceKey(this.makeId()),
71
+ kind,
72
+ scope: 'project',
73
+ displayName: displayName ?? basename(normalized),
74
+ location,
75
+ sessionId: null,
76
+ });
77
+ }
78
+
79
+ registerDraft(kind: ResourceKind, displayName: string): ResourceDescriptor {
80
+ const id = this.makeId();
81
+ return this.add({
82
+ key: resourceKey(id),
83
+ kind,
84
+ scope: 'project',
85
+ displayName,
86
+ location: `draft://${id}`,
87
+ sessionId: null,
88
+ });
89
+ }
90
+
91
+ registerSession(
92
+ kind: ResourceKind,
93
+ sessionId: string,
94
+ resourceId: string,
95
+ displayName?: string,
96
+ ): ResourceDescriptor {
97
+ const normalizedSession = sessionSegment(sessionId, 'Session id');
98
+ const normalizedResource = normalizeProjectPath(resourceId);
99
+ const location = `session://${normalizedSession}/${normalizedResource}`;
100
+ const existing = this.existingAt(location, kind);
101
+ if (existing) return existing;
102
+ return this.add({
103
+ key: resourceKey(this.makeId()),
104
+ kind,
105
+ scope: 'session',
106
+ displayName: displayName ?? basename(normalizedResource),
107
+ location,
108
+ sessionId: normalizedSession,
109
+ });
110
+ }
111
+
112
+ /** Promote a draft or move a project file without changing history identity. */
113
+ setProjectLocation(key: ResourceKey, path: string, displayName?: string): ResourceDescriptor {
114
+ const current = this.requireActive(key);
115
+ if (current.scope !== 'project')
116
+ throw new Error('A session resource cannot become a project file.');
117
+ const normalized = normalizeProjectPath(path);
118
+ const location = `project://${normalized}`;
119
+ const collision = this.keyByLocation.get(location);
120
+ if (collision && collision !== key) {
121
+ throw new Error(`Another resource is already registered at "${location}".`);
122
+ }
123
+ if (current.location) this.keyByLocation.delete(current.location);
124
+ const next: ResourceDescriptor = {
125
+ ...current,
126
+ displayName: displayName ?? basename(normalized),
127
+ location,
128
+ };
129
+ this.descriptors.set(key, next);
130
+ this.keyByLocation.set(location, key);
131
+ return next;
132
+ }
133
+
134
+ get(key: ResourceKey): ResourceDescriptor | null {
135
+ return this.descriptors.get(key) ?? null;
136
+ }
137
+
138
+ getByLocation(location: string): ResourceDescriptor | null {
139
+ const key = this.keyByLocation.get(location);
140
+ return key ? (this.descriptors.get(key) ?? null) : null;
141
+ }
142
+
143
+ isActive(key: ResourceKey): boolean {
144
+ return this.descriptors.has(key) && !this.inactive.has(key);
145
+ }
146
+
147
+ /** Keep descriptors for history display, but make their drivers unavailable. */
148
+ expireSession(sessionId: string): ResourceKey[] {
149
+ const expired: ResourceKey[] = [];
150
+ for (const descriptor of this.descriptors.values()) {
151
+ if (descriptor.sessionId !== sessionId || this.inactive.has(descriptor.key)) continue;
152
+ this.inactive.add(descriptor.key);
153
+ expired.push(descriptor.key);
154
+ }
155
+ return expired;
156
+ }
157
+
158
+ expireResources(resources: Iterable<ResourceKey>): ResourceKey[] {
159
+ const expired: ResourceKey[] = [];
160
+ for (const key of resources) {
161
+ const descriptor = this.descriptors.get(key);
162
+ if (!descriptor || this.inactive.has(key)) continue;
163
+ this.inactive.add(key);
164
+ if (descriptor.location && this.keyByLocation.get(descriptor.location) === key) {
165
+ this.keyByLocation.delete(descriptor.location);
166
+ }
167
+ expired.push(key);
168
+ }
169
+ return expired;
170
+ }
171
+
172
+ clear(): void {
173
+ this.descriptors.clear();
174
+ this.keyByLocation.clear();
175
+ this.inactive.clear();
176
+ }
177
+
178
+ private add(descriptor: ResourceDescriptor): ResourceDescriptor {
179
+ if (this.descriptors.has(descriptor.key)) {
180
+ throw new Error(`Duplicate resource key "${descriptor.key}".`);
181
+ }
182
+ if (descriptor.location && this.keyByLocation.has(descriptor.location)) {
183
+ throw new Error(`Duplicate resource location "${descriptor.location}".`);
184
+ }
185
+ this.descriptors.set(descriptor.key, descriptor);
186
+ if (descriptor.location) this.keyByLocation.set(descriptor.location, descriptor.key);
187
+ return descriptor;
188
+ }
189
+
190
+ private existingAt(location: string, kind: ResourceKind): ResourceDescriptor | null {
191
+ const key = this.keyByLocation.get(location);
192
+ if (!key) return null;
193
+ const descriptor = this.descriptors.get(key)!;
194
+ if (descriptor.kind !== kind) {
195
+ throw new Error(
196
+ `Resource "${location}" is already registered as ${descriptor.kind}, not ${kind}.`,
197
+ );
198
+ }
199
+ if (this.inactive.has(key)) throw new Error(`Resource "${location}" has expired.`);
200
+ return descriptor;
201
+ }
202
+
203
+ private requireActive(key: ResourceKey): ResourceDescriptor {
204
+ const descriptor = this.descriptors.get(key);
205
+ if (!descriptor) throw new Error(`Unknown resource key "${key}".`);
206
+ if (this.inactive.has(key)) throw new Error(`Resource "${key}" has expired.`);
207
+ return descriptor;
208
+ }
209
+ }
@@ -0,0 +1,108 @@
1
+ import type { ResourceSnapshot, SnapshotRef } from './types';
2
+
3
+ interface BlobRecord {
4
+ readonly bytes: Uint8Array;
5
+ references: number;
6
+ }
7
+
8
+ function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
9
+ if (a.byteLength !== b.byteLength) return false;
10
+ for (let i = 0; i < a.byteLength; i++) {
11
+ if (a[i] !== b[i]) return false;
12
+ }
13
+ return true;
14
+ }
15
+
16
+ export async function sha256Hex(bytes: Uint8Array): Promise<string> {
17
+ const owned = Uint8Array.from(bytes);
18
+ const digest = await crypto.subtle.digest('SHA-256', owned);
19
+ return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, '0')).join('');
20
+ }
21
+
22
+ /** In-memory, content-addressed snapshot blobs for one open project session. */
23
+ export class SnapshotStore {
24
+ private readonly blobs = new Map<string, BlobRecord>();
25
+ private retainedBytes = 0;
26
+
27
+ async put(snapshot: Omit<ResourceSnapshot, 'sha256'>): Promise<SnapshotRef> {
28
+ if (!Number.isSafeInteger(snapshot.revision) || snapshot.revision < 0) {
29
+ throw new Error('Snapshot revision must be a non-negative safe integer.');
30
+ }
31
+ const bytes = Uint8Array.from(snapshot.bytes);
32
+ const sha256 = await sha256Hex(bytes);
33
+ const existing = this.blobs.get(sha256);
34
+ if (existing) {
35
+ // A real SHA-256 collision is fantastically unlikely, but silently aliasing
36
+ // different authored bytes would make undo corrupt data.
37
+ if (!bytesEqual(existing.bytes, bytes)) throw new Error(`SHA-256 collision for ${sha256}.`);
38
+ existing.references++;
39
+ } else {
40
+ this.blobs.set(sha256, { bytes, references: 1 });
41
+ this.retainedBytes += bytes.byteLength;
42
+ }
43
+ return {
44
+ revision: snapshot.revision,
45
+ contentType: snapshot.contentType,
46
+ sha256,
47
+ blobKey: sha256,
48
+ byteLength: bytes.byteLength,
49
+ };
50
+ }
51
+
52
+ retain(ref: SnapshotRef): void {
53
+ const blob = this.requireBlob(ref);
54
+ blob.references++;
55
+ }
56
+
57
+ release(ref: SnapshotRef): void {
58
+ const blob = this.requireBlob(ref);
59
+ blob.references--;
60
+ if (blob.references === 0) {
61
+ this.blobs.delete(ref.blobKey);
62
+ this.retainedBytes -= blob.bytes.byteLength;
63
+ }
64
+ }
65
+
66
+ read(ref: SnapshotRef): ResourceSnapshot {
67
+ const blob = this.requireBlob(ref);
68
+ return {
69
+ revision: ref.revision,
70
+ contentType: ref.contentType,
71
+ bytes: Uint8Array.from(blob.bytes),
72
+ sha256: ref.sha256,
73
+ };
74
+ }
75
+
76
+ has(ref: SnapshotRef): boolean {
77
+ const blob = this.blobs.get(ref.blobKey);
78
+ return !!blob && blob.bytes.byteLength === ref.byteLength;
79
+ }
80
+
81
+ get uniqueBlobCount(): number {
82
+ return this.blobs.size;
83
+ }
84
+
85
+ get uniqueByteLength(): number {
86
+ return this.retainedBytes;
87
+ }
88
+
89
+ get referenceCount(): number {
90
+ let count = 0;
91
+ for (const blob of this.blobs.values()) count += blob.references;
92
+ return count;
93
+ }
94
+
95
+ clear(): void {
96
+ this.blobs.clear();
97
+ this.retainedBytes = 0;
98
+ }
99
+
100
+ private requireBlob(ref: SnapshotRef): BlobRecord {
101
+ const blob = this.blobs.get(ref.blobKey);
102
+ if (!blob) throw new Error(`Snapshot blob "${ref.blobKey}" is not retained.`);
103
+ if (ref.sha256 !== ref.blobKey || blob.bytes.byteLength !== ref.byteLength) {
104
+ throw new Error(`Snapshot reference "${ref.blobKey}" does not match its retained blob.`);
105
+ }
106
+ return blob;
107
+ }
108
+ }
@@ -0,0 +1,374 @@
1
+ import type {
2
+ PreparedSourceEdit,
3
+ SourceEditRequest,
4
+ SourceWriteBackend,
5
+ } from '../ui-source/source-write-backend';
6
+ import { HistoryOperationError, type HistoryService } from './history-service';
7
+ import { sha256Hex } from './snapshot-store';
8
+ import type { ResourceDescriptor, ResourceDriver, ResourceKey } from './types';
9
+
10
+ const encoder = new TextEncoder();
11
+ const decoder = new TextDecoder();
12
+
13
+ interface SourceResource {
14
+ descriptor: ResourceDescriptor;
15
+ expectedSha: string | null;
16
+ }
17
+
18
+ interface GestureFile {
19
+ resourcePath: string;
20
+ before: string;
21
+ beforeSha: string;
22
+ after: string;
23
+ afterSha: string;
24
+ /** Every source state produced or observed by this gesture. */
25
+ knownShas: Set<string>;
26
+ }
27
+
28
+ interface SourceGesture {
29
+ label: string;
30
+ files: Map<string, GestureFile>;
31
+ applyOrder: string[];
32
+ }
33
+
34
+ type WriteResponse = { changed: boolean; [key: string]: unknown };
35
+
36
+ class SourceHistoryManager {
37
+ private readonly resources = new Map<string, SourceResource>();
38
+ private tail: Promise<void> = Promise.resolve();
39
+
40
+ constructor(
41
+ private readonly history: HistoryService,
42
+ private readonly backend: SourceWriteBackend,
43
+ ) {}
44
+
45
+ commit(label: string, request: SourceEditRequest): Promise<WriteResponse> {
46
+ try {
47
+ this.history.assertCanRecordAppliedChange();
48
+ } catch (error) {
49
+ return Promise.reject(error);
50
+ }
51
+ const releaseProducer = this.history.reserveProducer();
52
+ return this.enqueue(() => {
53
+ this.history.assertCanRecordAppliedChange();
54
+ return this.prepareAndCommit(label, request);
55
+ }).finally(releaseProducer);
56
+ }
57
+
58
+ /**
59
+ * Runs the complete callback while holding the manager queue. The callback's
60
+ * scoped backend writes directly into this gesture; calls on the ordinary
61
+ * backend queue after it, so unrelated inspector edits cannot be absorbed or
62
+ * interleaved. Overlapping gestures likewise become distinct queue entries.
63
+ */
64
+ runGesture<T>(
65
+ label: string,
66
+ operation: (scopedBackend: SourceWriteBackend) => Promise<T>,
67
+ ): Promise<T> {
68
+ try {
69
+ this.history.assertCanRecordAppliedChange();
70
+ } catch (error) {
71
+ return Promise.reject(error);
72
+ }
73
+ const releaseProducer = this.history.reserveProducer();
74
+ return this.enqueue(async () => {
75
+ this.history.assertCanRecordAppliedChange();
76
+ const gesture: SourceGesture = { label, files: new Map(), applyOrder: [] };
77
+ try {
78
+ const result = await operation(this.scopedBackend(gesture));
79
+ await this.recordGesture(gesture);
80
+ return result;
81
+ } catch (error) {
82
+ try {
83
+ await this.compensateGesture(gesture);
84
+ } catch (compensationError) {
85
+ throw this.history.blockForCompensationFailure(label, this.gestureResourceKeys(gesture), {
86
+ operationError: error,
87
+ compensationError,
88
+ });
89
+ }
90
+ if (!(error instanceof HistoryOperationError)) {
91
+ this.history.reportAppliedChangeFailure(label, this.gestureResourceKeys(gesture), error);
92
+ }
93
+ throw error;
94
+ }
95
+ }).finally(releaseProducer);
96
+ }
97
+
98
+ /** Run `op` after every previously queued source operation. */
99
+ private enqueue<T>(op: () => Promise<T>): Promise<T> {
100
+ const result = this.tail.then(op, op);
101
+ this.tail = result.then(
102
+ () => undefined,
103
+ () => undefined,
104
+ );
105
+ return result;
106
+ }
107
+
108
+ private scopedBackend(gesture: SourceGesture): SourceWriteBackend {
109
+ const commit = (request: SourceEditRequest) => this.applyInGesture(gesture, request);
110
+ const { runGesture: _nestedGesture, ...backend } = this.backend;
111
+ return {
112
+ ...backend,
113
+ historyManaged: true,
114
+ writeStyle: (oid, prop, value) =>
115
+ commit({ kind: 'style', oid, prop, value }) as ReturnType<SourceWriteBackend['writeStyle']>,
116
+ removeStyle: (oid, prop) =>
117
+ commit({ kind: 'style', oid, prop, value: null }) as ReturnType<
118
+ SourceWriteBackend['removeStyle']
119
+ >,
120
+ writeCss: (file, selector, prop, value) =>
121
+ commit({ kind: 'css', file, selector, prop, value }),
122
+ writeText: (oid, text) => commit({ kind: 'text', oid, text }),
123
+ writeProp: (oid, prop, value) => commit({ kind: 'prop', oid, prop, value }),
124
+ writeStruct: (oid, op, options) =>
125
+ commit({ kind: 'struct', oid, op, ...options }) as ReturnType<
126
+ SourceWriteBackend['writeStruct']
127
+ >,
128
+ writeStructMany: (oids, op) =>
129
+ commit({ kind: 'struct-many', oids, op }) as ReturnType<
130
+ NonNullable<SourceWriteBackend['writeStructMany']>
131
+ >,
132
+ };
133
+ }
134
+
135
+ private async recordGesture(gesture: SourceGesture): Promise<void> {
136
+ const changes = [...gesture.files.entries()]
137
+ .filter(([, touched]) => touched.beforeSha !== touched.afterSha)
138
+ .map(([file, touched]) => {
139
+ const resource = this.resource(file, touched.resourcePath);
140
+ return {
141
+ resource: resource.descriptor.key,
142
+ beforeBytes: encoder.encode(touched.before),
143
+ afterBytes: encoder.encode(touched.after),
144
+ contentType: 'text/plain',
145
+ };
146
+ });
147
+ if (changes.length === 0) return;
148
+ await this.history.recordAppliedTransaction(
149
+ {
150
+ label: gesture.label,
151
+ resources: changes.map((change) => change.resource),
152
+ scope: 'project',
153
+ },
154
+ changes,
155
+ );
156
+ }
157
+
158
+ private async prepareAndCommit(
159
+ label: string,
160
+ request: SourceEditRequest,
161
+ ): Promise<WriteResponse> {
162
+ const prepared = await this.backend.prepare!(request);
163
+ const response = this.response(prepared);
164
+ if (
165
+ !prepared.changed ||
166
+ !prepared.file ||
167
+ !prepared.resourcePath ||
168
+ prepared.newSource === undefined
169
+ ) {
170
+ return response;
171
+ }
172
+ const resource = this.resource(prepared.file, prepared.resourcePath);
173
+ await this.history.transaction(
174
+ { label, resources: [resource.descriptor.key], scope: 'project' },
175
+ (tx) => tx.writeText(resource.descriptor.key, prepared.newSource!, 'text/plain'),
176
+ );
177
+ return response;
178
+ }
179
+
180
+ private async applyInGesture(
181
+ gesture: SourceGesture,
182
+ request: SourceEditRequest,
183
+ ): Promise<WriteResponse> {
184
+ const prepared = await this.backend.prepare!(request);
185
+ const response = this.response(prepared);
186
+ if (
187
+ !prepared.changed ||
188
+ !prepared.file ||
189
+ !prepared.resourcePath ||
190
+ prepared.newSource === undefined ||
191
+ prepared.prevSource === undefined ||
192
+ prepared.prevSha === undefined
193
+ ) {
194
+ return response;
195
+ }
196
+ const newSha = prepared.newSha ?? (await sha256Hex(encoder.encode(prepared.newSource)));
197
+ const existing = gesture.files.get(prepared.file);
198
+ if (existing) {
199
+ existing.knownShas.add(prepared.prevSha);
200
+ existing.knownShas.add(newSha);
201
+ } else {
202
+ gesture.files.set(prepared.file, {
203
+ resourcePath: prepared.resourcePath,
204
+ before: prepared.prevSource,
205
+ beforeSha: prepared.prevSha,
206
+ after: prepared.prevSource,
207
+ afterSha: prepared.prevSha,
208
+ knownShas: new Set([prepared.prevSha, newSha]),
209
+ });
210
+ }
211
+
212
+ // The candidate is registered before apply. If transport fails after the
213
+ // server wrote, compensation rereads and recognizes this exact hash.
214
+ const touched = gesture.files.get(prepared.file)!;
215
+ touched.after = prepared.newSource;
216
+ touched.afterSha = newSha;
217
+ gesture.applyOrder.push(prepared.file);
218
+ const applied = await this.backend.applySource!(
219
+ prepared.file,
220
+ prepared.newSource,
221
+ prepared.prevSha,
222
+ );
223
+ if (!applied.applied) {
224
+ throw new Error(applied.error ?? `Failed to apply source file "${prepared.file}".`);
225
+ }
226
+ return response;
227
+ }
228
+
229
+ /** Restore all touched files in reverse order, guarded by a fresh read/hash. */
230
+ private async compensateGesture(gesture: SourceGesture): Promise<void> {
231
+ const seen = new Set<string>();
232
+ const reverseLastApplyOrder = [...gesture.applyOrder].reverse().filter((file) => {
233
+ if (seen.has(file)) return false;
234
+ seen.add(file);
235
+ return true;
236
+ });
237
+ for (const file of reverseLastApplyOrder) {
238
+ const touched = gesture.files.get(file)!;
239
+ const current = await this.backend.readSource!(file);
240
+ const currentSha = await sha256Hex(encoder.encode(current.source));
241
+ if (current.sha !== currentSha) {
242
+ throw new Error(`Source file "${file}" returned bytes that do not match its hash.`);
243
+ }
244
+ if (currentSha === touched.beforeSha) continue;
245
+ if (!touched.knownShas.has(currentSha)) {
246
+ throw new Error(`Source file "${file}" changed outside the failed gesture.`);
247
+ }
248
+ const restored = await this.backend.applySource!(file, touched.before, currentSha);
249
+ if (!restored.applied) {
250
+ throw new Error(restored.error ?? `Failed to compensate source file "${file}".`);
251
+ }
252
+ const verified = await this.backend.readSource!(file);
253
+ const verifiedSha = await sha256Hex(encoder.encode(verified.source));
254
+ if (verified.sha !== verifiedSha || verifiedSha !== touched.beforeSha) {
255
+ throw new Error(`Source file "${file}" compensated with the wrong hash.`);
256
+ }
257
+ }
258
+ }
259
+
260
+ private gestureResourceKeys(gesture: SourceGesture): ResourceKey[] {
261
+ return [...gesture.files.entries()].map(
262
+ ([file, touched]) => this.resource(file, touched.resourcePath).descriptor.key,
263
+ );
264
+ }
265
+
266
+ private response(prepared: PreparedSourceEdit): WriteResponse {
267
+ return {
268
+ changed: prepared.changed,
269
+ ...prepared.result,
270
+ file: prepared.file,
271
+ prevSource: prepared.prevSource,
272
+ newSource: prepared.newSource,
273
+ prevSha: prepared.prevSha,
274
+ newSha: prepared.newSha,
275
+ };
276
+ }
277
+
278
+ private resource(file: string, resourcePath: string): SourceResource {
279
+ const existing = this.resources.get(file);
280
+ if (existing) return existing;
281
+ const descriptor = this.history.registry.registerProject('source', resourcePath);
282
+ const resource: SourceResource = { descriptor, expectedSha: null };
283
+ const driver: ResourceDriver = {
284
+ descriptor,
285
+ capture: async () => {
286
+ const current = await this.backend.readSource!(file);
287
+ return {
288
+ revision: 0,
289
+ contentType: 'text/plain',
290
+ bytes: encoder.encode(current.source),
291
+ sha256: current.sha,
292
+ };
293
+ },
294
+ preflight: async (expected) => {
295
+ const current = await this.backend.readSource!(file);
296
+ resource.expectedSha = current.sha;
297
+ return current.sha === expected.sha256
298
+ ? { ok: true }
299
+ : { ok: false, reason: 'content-conflict', actualSha256: current.sha };
300
+ },
301
+ restore: async (snapshot) => {
302
+ const expectedSha = resource.expectedSha;
303
+ resource.expectedSha = null;
304
+ if (!expectedSha) throw new Error(`Source resource "${file}" was not preflighted.`);
305
+ const source = decoder.decode(this.history.snapshots.read(snapshot).bytes);
306
+ const result = await this.backend.applySource!(file, source, expectedSha);
307
+ if (!result.applied)
308
+ throw new Error(result.error ?? `Failed to apply source file "${file}".`);
309
+ const actual = result.sha ?? (await sha256Hex(encoder.encode(source)));
310
+ if (actual !== snapshot.sha256)
311
+ throw new Error(`Source file "${file}" restored with the wrong hash.`);
312
+ },
313
+ estimateBytes: (snapshot) => snapshot.byteLength,
314
+ };
315
+ this.history.registerDriver(driver);
316
+ this.resources.set(file, resource);
317
+ return resource;
318
+ }
319
+ }
320
+
321
+ const managers = new WeakMap<HistoryService, WeakMap<object, SourceHistoryManager>>();
322
+
323
+ /** Routes the existing React authoring seam through prepare → HistoryService → guarded apply. */
324
+ export function withProjectSourceHistory(
325
+ backend: SourceWriteBackend | undefined,
326
+ history: HistoryService | null,
327
+ ): SourceWriteBackend | undefined {
328
+ if (!backend || !history || !backend.prepare || !backend.readSource || !backend.applySource) {
329
+ return backend;
330
+ }
331
+ let byBackend = managers.get(history);
332
+ if (!byBackend) {
333
+ byBackend = new WeakMap();
334
+ managers.set(history, byBackend);
335
+ }
336
+ const identity = backend.historyIdentity ?? backend;
337
+ let manager = byBackend.get(identity);
338
+ if (!manager) {
339
+ manager = new SourceHistoryManager(history, backend);
340
+ byBackend.set(identity, manager);
341
+ }
342
+ const commit = (label: string, request: SourceEditRequest) => manager!.commit(label, request);
343
+ const activeManager = manager;
344
+ return {
345
+ ...backend,
346
+ historyManaged: true,
347
+ runGesture: (label, operation) => activeManager.runGesture(label, operation),
348
+ writeStyle: (oid, prop, value) =>
349
+ commit(`Set ${prop}`, { kind: 'style', oid, prop, value }) as ReturnType<
350
+ SourceWriteBackend['writeStyle']
351
+ >,
352
+ removeStyle: (oid, prop) =>
353
+ commit(`Remove ${prop}`, { kind: 'style', oid, prop, value: null }) as ReturnType<
354
+ SourceWriteBackend['removeStyle']
355
+ >,
356
+ writeCss: (file, selector, prop, value) =>
357
+ commit(`Set ${prop}`, { kind: 'css', file, selector, prop, value }),
358
+ writeText: (oid, text) => commit('Edit Text', { kind: 'text', oid, text }),
359
+ writeProp: (oid, prop, value) => commit(`Set ${prop}`, { kind: 'prop', oid, prop, value }),
360
+ writeStruct: (oid, op, options) =>
361
+ commit(`${op[0]?.toUpperCase() ?? ''}${op.slice(1)}`, {
362
+ kind: 'struct',
363
+ oid,
364
+ op,
365
+ ...options,
366
+ }),
367
+ writeStructMany: (oids, op) =>
368
+ commit(`${op[0]?.toUpperCase() ?? ''}${op.slice(1)} ${oids.length} Elements`, {
369
+ kind: 'struct-many',
370
+ oids,
371
+ op,
372
+ }),
373
+ };
374
+ }