@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
@@ -6,7 +6,17 @@
6
6
  * This module translates them into store method calls.
7
7
  */
8
8
 
9
+ import { waitForHoldBudget } from '@engine/runtime/debug-bridge';
9
10
  import { parseSceneFile, SceneParseError } from '@engine/scene/parse';
11
+ import {
12
+ type AssetPreviewBackground,
13
+ captureModelAssetPreview,
14
+ captureObjectAssetPreview,
15
+ } from './asset-preview';
16
+ import { getActiveSystems } from './authoring/active-systems';
17
+ import { isAssetDocumentId, openAssetDocument } from './components/asset-documents';
18
+ import { openBuildProfilesDocument } from './components/build-documents';
19
+ import { capturePlayComposite } from './composite-screenshot';
10
20
  import { connectEvents, reportCommandResult, reportEditorState } from './editor-api';
11
21
  import { editorConsole } from './editor-console';
12
22
  import { getEditorMode } from './editor-mode';
@@ -14,10 +24,21 @@ import type { AssetKind, EditorStore, HelperVisibility } from './editor-store';
14
24
  import {
15
25
  enterPlayMode,
16
26
  exitPlayMode,
27
+ getGameContainer,
28
+ getPlayCanvas,
29
+ getPlayRuntimeAccess,
30
+ getPlayStartedAt,
31
+ isPlayModeActive,
17
32
  pausePlayMode,
18
33
  resumePlayMode,
19
34
  stepPlayMode,
20
35
  } from './play-mode';
36
+ import { createPageShim, PageShimUnsupportedError } from './playwright-shim';
37
+ import {
38
+ activateWorkspaceDocument,
39
+ activeWorkspaceDocumentId,
40
+ closeWorkspaceDocument,
41
+ } from './workspace-document-registry';
21
42
 
22
43
  export interface EditorCommand {
23
44
  type: string;
@@ -25,10 +46,14 @@ export interface EditorCommand {
25
46
  [key: string]: unknown;
26
47
  }
27
48
 
49
+ /** Keep in sync with `../server/server-utils.ts`'s `CommandResult` — this is
50
+ * the browser half of the same wire shape. `data` is the payload leg for
51
+ * commands that answer with data (`capture-viewport`'s base64 PNG, the
52
+ * Wave 5 debug-seam reads); on failures it carries structured markers
53
+ * (`{ code: ... }`) the SDK matches on, never message prose. */
28
54
  export interface CommandResult {
29
55
  ok: boolean;
30
56
  error?: string;
31
- /** Optional payload for commands that answer with data (e.g. `capture-viewport`'s base64 PNG). */
32
57
  data?: Record<string, unknown>;
33
58
  }
34
59
 
@@ -50,13 +75,56 @@ function collectCamera(store: EditorStore):
50
75
  };
51
76
  }
52
77
 
78
+ /** #145 — what the human's tab is actually showing right now: page
79
+ * visibility + window focus. `null` outside a real browser (the relay's
80
+ * node-side tests). The agent reads this to know whether the user is
81
+ * LOOKING at the shared session before deciding how to narrate/verify;
82
+ * `connectCommandListener` re-reports state on visibilitychange/focus/blur
83
+ * so the server's `/__editor/state` snapshot stays current between
84
+ * commands. */
85
+ function collectPresence(): { visibility: DocumentVisibilityState; focused: boolean } | null {
86
+ if (typeof document === 'undefined') return null;
87
+ return { visibility: document.visibilityState, focused: document.hasFocus() };
88
+ }
89
+
53
90
  export function collectState(store: EditorStore): Record<string, unknown> {
54
91
  return {
55
92
  playState: store.playState,
93
+ presence: collectPresence(),
94
+ // Target-blaster friction #3 (#146 ledger): the play-verify loop's error
95
+ // channel — the same current-run-fenced uncaught-error list the relay
96
+ // snapshot carries (see collectPageErrors), so `vgai status` answers
97
+ // "did anything go wrong since play started" without a second command.
98
+ // [] while stopped or when nothing threw.
99
+ pageErrors: collectPageErrors(),
100
+ // Wave 5: the live loop's time-scale, so `play.status` reports the real
101
+ // applied value after a `set-time-scale` instead of an honest-gap null.
102
+ timeScale: getPlayRuntimeAccess()?.loop.timeScale ?? null,
103
+ // Issue #175 — the REAL loop liveness (`GameLoop.liveness`), NOT `store.
104
+ // playState` above: `playState` is editor UI state that stays 'playing'
105
+ // even while the T2.1 idle throttle has hidden-paused the loop (a hidden
106
+ // tab stops ticking outright, deliberately — see game-loop.ts). Reading
107
+ // only `playState` is exactly the gap that let a frozen game report as
108
+ // healthy (measured: playState "playing", sim speed 0.00x over 32.9s
109
+ // wall). `null` outside play mode — there is no loop to report on.
110
+ loopLiveness: getPlayRuntimeAccess()?.loop.liveness ?? null,
111
+ // D15/T-D15.6: the live session's ctx.random root seed (real — reflects
112
+ // the boot seed or the last successful play.seed.set), and whether the
113
+ // running project declares determinism.seededRandom at all. Both null/
114
+ // false when no first-party Game is running.
115
+ seed: getPlayRuntimeAccess()?.random?.seed ?? null,
116
+ deterministic: getPlayRuntimeAccess()?.determinismDeclared ?? false,
56
117
  selectedEntityId: store.selectedEntityId,
57
118
  selectedEntityIds: [...store.selectedEntityIds],
58
119
  activeViewportTab: store.activeViewportTab,
59
- activeTabKey: store.activeTabKey,
120
+ // W2: asset viewers are center workspace documents now; this facet keeps
121
+ // its legacy meaning for control-API consumers — the active ASSET
122
+ // document's key, or '__inspector__' (the right rail always shows the
123
+ // Inspector since W2).
124
+ activeTabKey: (() => {
125
+ const activeId = activeWorkspaceDocumentId();
126
+ return activeId && isAssetDocumentId(activeId) ? activeId : '__inspector__';
127
+ })(),
60
128
  showGrid: store.showGrid,
61
129
  showHelpers: store.showHelpers,
62
130
  showStats: store.showStats,
@@ -67,6 +135,8 @@ export function collectState(store: EditorStore): Record<string, unknown> {
67
135
  snapEnabled: store.snapEnabled,
68
136
  entityCount: store.allEntities().length,
69
137
  savePath: store.savePath,
138
+ // SDK clients need the same persistence truth the editor UI exposes.
139
+ saveState: store.saveState,
70
140
  // Camera facet (B3-followup): real pose when a viewport is bound, absent
71
141
  // otherwise — never a fabricated value (see EditorStore.cameraPose).
72
142
  camera: collectCamera(store),
@@ -79,6 +149,475 @@ export function collectState(store: EditorStore): Record<string, unknown> {
79
149
  };
80
150
  }
81
151
 
152
+ /** Surface a thrown engine error carrying a machine-readable `code`
153
+ * (`DebugError`, `InputActionError` — duck-typed, so no engine-module
154
+ * identity coupling) as a structured relay failure: `data.code` plus the
155
+ * error's own data (registered-name lists, zod issues). The SDK maps these
156
+ * to operation error codes — never by parsing message prose. */
157
+ function structuredErrorResult(err: unknown): CommandResult {
158
+ const message = err instanceof Error ? err.message : String(err);
159
+ const e = err as { code?: unknown; data?: unknown };
160
+ if (typeof e?.code === 'string') {
161
+ const extra = typeof e.data === 'object' && e.data !== null ? e.data : {};
162
+ return { ok: false, error: message, data: { ...extra, code: e.code } };
163
+ }
164
+ return { ok: false, error: message };
165
+ }
166
+
167
+ const notPlayingResult = (): CommandResult => ({
168
+ ok: false,
169
+ error: 'not in play mode — start play before using the debug seam',
170
+ });
171
+
172
+ /** Duck-typed structured error (see `structuredErrorResult` above) — a plain
173
+ * `Error` carrying a machine-readable `code` (and optional `data`), the same
174
+ * shape `DebugError`/`InputActionError` already throw. Used by
175
+ * `dispatchBridgeMethod` below for its OWN precondition failures (no debug
176
+ * adapter installed, no InputManager wired) so they survive
177
+ * `structuredErrorResult` exactly like an engine-thrown error would. */
178
+ function bridgeError(code: string, message: string, data?: Record<string, unknown>): Error {
179
+ const err = new Error(message) as Error & { code: string; data?: unknown };
180
+ err.code = code;
181
+ if (data) err.data = data;
182
+ return err;
183
+ }
184
+
185
+ /**
186
+ * #140 — the generic session-wire dispatch table: the SAME method-name
187
+ * surface `window.__vgai` exposes (`@engine/runtime/debug-bridge`'s
188
+ * `VgaiDebugHandle` — `state`/`stateAll`/`providers`/`commands`/`events`/
189
+ * `snapshot`/`invoke`/`runTicks`/`input.*`), built from the SAME
190
+ * registry-backed accessors this file's own `list-gameplay-state`/
191
+ * `inject-input`/`run-ticks` cases already use. This is what makes the
192
+ * `bridge-call` relay op (below) a session-generic primitive rather than a
193
+ * probe-specific one — any client of the live session (the `@vgai/probe`
194
+ * `RelayTransport`, a future one-shot `vgai game state/call/hold` REPL) can
195
+ * issue `{method, callArgs}` against it with no dependency on a probe run's
196
+ * own lifecycle/env, and get byte-identical dispatch to the page-transport's
197
+ * `page.evaluate(bridgeCallInPage)` (D17) — without requiring the editor tab
198
+ * to be loaded with `?vgai-debug=1` (that gate exists for the bridge's own
199
+ * production exposure; this relay is already local-loopback-only — see
200
+ * `editor-server.ts`'s CSRF/origin guard).
201
+ */
202
+ /** #146 — uncaught page errors from the CURRENT play run, for the relay
203
+ * `snapshot`. Reads the editor console's 'runtime'-source error entries
204
+ * (fed by ViewportPanel's window error/unhandledrejection capture — the
205
+ * same events, same message format as `window.__vgai`'s own pageErrors
206
+ * ring buffer), fenced to entries logged at/after play start. Same 100-cap
207
+ * as the bridge (`PAGE_ERROR_CAP`), keeping the most recent. */
208
+ function collectPageErrors(): string[] {
209
+ const startedAt = getPlayStartedAt();
210
+ if (startedAt === null) return [];
211
+ return editorConsole
212
+ .getEntries()
213
+ .filter((e) => e.level === 'error' && e.source === 'runtime' && e.timestamp >= startedAt)
214
+ .slice(-100)
215
+ .map((e) => (e.count > 1 ? `${e.message} (×${e.count})` : e.message));
216
+ }
217
+
218
+ async function dispatchBridgeMethod(method: string, callArgs: unknown[]): Promise<unknown> {
219
+ const debug = getActiveSystems().debug;
220
+ const requireDebug = () => {
221
+ if (!debug) {
222
+ throw bridgeError(
223
+ 'DEBUG_ADAPTER_UNAVAILABLE',
224
+ `session wire: "${method}" — this game exposes no debug adapter`,
225
+ );
226
+ }
227
+ return debug;
228
+ };
229
+ switch (method) {
230
+ case 'state':
231
+ return requireDebug().state(callArgs[0] as string);
232
+ case 'stateAll':
233
+ return requireDebug().stateAll();
234
+ case 'providers':
235
+ return requireDebug().providers();
236
+ case 'commands':
237
+ return requireDebug().commands();
238
+ case 'events':
239
+ return requireDebug().events(
240
+ callArgs[0] as number | undefined,
241
+ callArgs[1] as number | undefined,
242
+ );
243
+ case 'snapshot': {
244
+ const adapter = requireDebug();
245
+ return {
246
+ time: adapter.state('time') as { simSeconds: number; tick: number },
247
+ state: adapter.stateAll(),
248
+ // Run-4 friction #5 — same `sinceTick`/`sinceSeq` contract as the
249
+ // in-page bridge's `snapshot()` (`runtime/debug-bridge.ts`): both
250
+ // doors must fence identically, or a probe running `--in-editor`
251
+ // (this door) vs. standalone (that door) could disagree about which
252
+ // events are "new".
253
+ events: adapter.events(
254
+ callArgs[0] as number | undefined,
255
+ callArgs[1] as number | undefined,
256
+ ),
257
+ // #146 (closes the #140 honest-empty gap): the editor page already
258
+ // funnels window error/unhandledrejection into the editor console as
259
+ // level 'error' / source 'runtime' (ViewportPanel's error-capture
260
+ // block) — the SAME events and message format `window.__vgai`'s own
261
+ // handle buffers. Fenced to the current play run via
262
+ // getPlayStartedAt() so a probe never reads a previous run's stale
263
+ // failures, capped at 100 for parity with the bridge's ring buffer.
264
+ pageErrors: collectPageErrors(),
265
+ };
266
+ }
267
+ case 'invoke': {
268
+ const name = callArgs[0] as string;
269
+ const invokeArgs = (callArgs[1] as unknown[]) ?? [];
270
+ return requireDebug().invoke(name, invokeArgs);
271
+ }
272
+ case 'runTicks': {
273
+ const runtime = getPlayRuntimeAccess();
274
+ if (!runtime?.runTicks) {
275
+ throw bridgeError(
276
+ 'RUN_TICKS_UNAVAILABLE',
277
+ 'this play session has no first-party Game to run ticks on',
278
+ );
279
+ }
280
+ const n = callArgs[0] as number;
281
+ const opts = callArgs[1] as { render?: 'last' | 'all' | 'none' } | undefined;
282
+ runtime.runTicks(n, opts);
283
+ return undefined;
284
+ }
285
+ case 'input.setVirtualAction':
286
+ case 'input.tapVirtualAction':
287
+ case 'input.clearVirtualActions':
288
+ // Wave-2 bridge↔wire coverage-parity gate (`bridge-wire-parity.test.ts`)
289
+ // closed this trio's gap: `startRecording`/`stopRecording`/`isRecording`
290
+ // exist on the page bridge's `VgaiDebugInputHandle` (`runtime/
291
+ // debug-bridge.ts`) but had no relay case at all — an agent driving the
292
+ // input-trace recorder over the session wire (RelayTransport) instead of
293
+ // `?vgai-debug=1` simply had no door to it. Same single optional
294
+ // trailing `worldId` shape as `clearVirtualActions`.
295
+ case 'input.startRecording':
296
+ case 'input.stopRecording':
297
+ case 'input.isRecording': {
298
+ const runtime = getPlayRuntimeAccess();
299
+ // D15/T-D15.5 (rebase composition with #140): this wire is a THIRD
300
+ // door to the virtual-input target — it must resolve through the SAME
301
+ // `DebugRegistry.getVirtualInputTarget(worldId?)` the page bridge
302
+ // (`window.__vgai.input.*`) and the `inject-input` relay case use, so
303
+ // no two doors can ever disagree about which world an unqualified
304
+ // actuation targets. Each method takes the page bridge's own optional
305
+ // trailing `worldId` (byte-parity with `VgaiDebugInputHandle`); an
306
+ // explicit, unregistered id throws the registry's structured
307
+ // `DEBUG_INPUT_WORLD_NOT_FOUND`, shipped back over the wire as-is.
308
+ const worldId = (
309
+ method === 'input.setVirtualAction'
310
+ ? callArgs[2]
311
+ : method === 'input.tapVirtualAction'
312
+ ? callArgs[1]
313
+ : callArgs[0]
314
+ ) as string | undefined;
315
+ const inputTarget = runtime ? runtime.getInputTarget(worldId) : null;
316
+ if (!inputTarget) {
317
+ throw bridgeError(
318
+ 'DEBUG_INPUT_UNAVAILABLE',
319
+ `session wire: ${method}() has no virtual-input target wired — no default threejs ` +
320
+ "world has mounted yet, or this project's mount path never wired one",
321
+ );
322
+ }
323
+ if (method === 'input.setVirtualAction') {
324
+ return inputTarget.setVirtualAction(
325
+ callArgs[0] as string,
326
+ callArgs[1] as boolean | number | { x: number; y: number },
327
+ );
328
+ }
329
+ if (method === 'input.tapVirtualAction') {
330
+ return inputTarget.tapVirtualAction(callArgs[0] as string);
331
+ }
332
+ if (method === 'input.clearVirtualActions') {
333
+ inputTarget.clearVirtualActions();
334
+ return undefined;
335
+ }
336
+ if (method === 'input.startRecording') {
337
+ inputTarget.startInputRecording();
338
+ return undefined;
339
+ }
340
+ if (method === 'input.stopRecording') {
341
+ inputTarget.stopInputRecording();
342
+ return undefined;
343
+ }
344
+ return inputTarget.isInputRecording();
345
+ }
346
+ case 'input.scheduleActionAtTick': {
347
+ // Wave-2 bridge↔wire coverage-parity gate: `window.__vgai.input.
348
+ // scheduleActionAtTick` (`runtime/debug-bridge.ts`, D15/T-D15.5) had NO
349
+ // bridge-call case — it was reachable over the relay ONLY via the
350
+ // separate `inject-input` command's `atTick` field (see that case,
351
+ // above), never by this method name. Same `getInputTarget(worldId?)`
352
+ // resolution as the other `input.*` cases; forwards `tick`/`action`/
353
+ // `value` straight through and returns whatever the target returns
354
+ // (`void`, same as the page bridge's own `scheduleActionAtTick`).
355
+ const runtime = getPlayRuntimeAccess();
356
+ const tick = callArgs[0] as number;
357
+ const action = callArgs[1] as string;
358
+ const value = callArgs[2] as boolean | number | { x: number; y: number };
359
+ const worldId = callArgs[3] as string | undefined;
360
+ const inputTarget = runtime ? runtime.getInputTarget(worldId) : null;
361
+ if (!inputTarget) {
362
+ throw bridgeError(
363
+ 'DEBUG_INPUT_UNAVAILABLE',
364
+ 'session wire: input.scheduleActionAtTick() has no virtual-input target wired — no ' +
365
+ "default threejs world has mounted yet, or this project's mount path never wired one",
366
+ );
367
+ }
368
+ return inputTarget.scheduleActionAtTick(tick, action, value);
369
+ }
370
+ case 'input.injectPointerDelta':
371
+ case 'input.injectPointerPosition': {
372
+ // Wave-2 pointer-dispatch op: `window.__vgai.input.injectPointerDelta`/
373
+ // `injectPointerPosition` (`runtime/debug-bridge.ts`) had no relay
374
+ // case — this is the HONEST gameplay-seam reading of "pointer
375
+ // dispatch" (docs/SHARED-SESSION-SPEC.md): the engine's existing
376
+ // virtual pointer-injection, never a synthesized DOM mouse event (that
377
+ // boundary belongs to the future playwright shim). Same
378
+ // `getInputTarget(worldId?)` resolution as every other `input.*` case.
379
+ const runtime = getPlayRuntimeAccess();
380
+ const sourceId = callArgs[0] as string;
381
+ const value = callArgs[1] as { x: number; y: number };
382
+ const worldId = callArgs[2] as string | undefined;
383
+ const inputTarget = runtime ? runtime.getInputTarget(worldId) : null;
384
+ if (!inputTarget) {
385
+ throw bridgeError(
386
+ 'DEBUG_INPUT_UNAVAILABLE',
387
+ `session wire: ${method}() has no virtual-input target wired — no default threejs ` +
388
+ "world has mounted yet, or this project's mount path never wired one",
389
+ );
390
+ }
391
+ if (method === 'input.injectPointerDelta') {
392
+ inputTarget.injectPointerDelta(sourceId, value);
393
+ } else {
394
+ inputTarget.injectPointerPosition(sourceId, value);
395
+ }
396
+ return undefined;
397
+ }
398
+ case 'holdFor': {
399
+ // #140/D17: byte-identical semantics to `window.__vgai.holdFor` (door
400
+ // a, `runtime/debug-bridge.ts`) — same `getInputTarget(worldId?)`
401
+ // resolution as the `input.*` cases above, same `waitForHoldBudget`
402
+ // poll/stall loop (imported, not hand-copied, so the two doors can
403
+ // never silently drift apart), reached through `requireDebug().state
404
+ // ('time')` instead of a `DebugRegistry` (this relay only has
405
+ // `getActiveSystems().debug`, not the registry itself).
406
+ const runtime = getPlayRuntimeAccess();
407
+ const worldId = callArgs[2] as string | undefined;
408
+ const inputTarget = runtime ? runtime.getInputTarget(worldId) : null;
409
+ if (!inputTarget) {
410
+ throw bridgeError(
411
+ 'DEBUG_INPUT_UNAVAILABLE',
412
+ 'session wire: holdFor() has no virtual-input target wired — no default threejs ' +
413
+ "world has mounted yet, or this project's mount path never wired one",
414
+ );
415
+ }
416
+ const action = callArgs[0] as string;
417
+ const simSeconds = callArgs[1] as number;
418
+ const setResult = inputTarget.setVirtualAction(action, true);
419
+ if (!setResult.delivered) {
420
+ inputTarget.clearVirtualActions();
421
+ return { delivered: false, reason: setResult.reason };
422
+ }
423
+ const { stalled } = await waitForHoldBudget(
424
+ requireDebug(),
425
+ simSeconds,
426
+ runtime?.runTicks ? (n) => runtime.runTicks?.(n, { render: 'last' }) : undefined,
427
+ );
428
+ inputTarget.clearVirtualActions();
429
+ return stalled
430
+ ? { delivered: false, reason: 'play stopped during hold' }
431
+ : { delivered: true };
432
+ }
433
+ default:
434
+ throw bridgeError('UNKNOWN_BRIDGE_METHOD', `session wire: unknown bridge method "${method}"`);
435
+ }
436
+ }
437
+
438
+ /** `bridge-call` relay op handler — session not playing is the one
439
+ * precondition checked up front (mirrors every other debug-seam case in
440
+ * this file); everything else is `dispatchBridgeMethod`'s own structured
441
+ * failure, caught here and shipped back via `structuredErrorResult` so
442
+ * `code`/`data` survive the relay round trip byte-equivalent to the
443
+ * page-transport's `unwrap()` path (`packages/vgai-probe/src/client.ts`). */
444
+ async function handleBridgeCall(cmd: EditorCommand): Promise<CommandResult> {
445
+ if (!isPlayModeActive()) return notPlayingResult();
446
+ const method = cmd['method'] as string;
447
+ const callArgs = (cmd['callArgs'] as unknown[] | undefined) ?? [];
448
+ try {
449
+ const result = await dispatchBridgeMethod(method, callArgs);
450
+ return { ok: true, data: { result } };
451
+ } catch (err) {
452
+ return structuredErrorResult(err);
453
+ }
454
+ }
455
+
456
+ /** `bridge-screenshot` relay op handler — captures the live play-mode game
457
+ * view and returns it in the same `{base64, mimeType}` shape
458
+ * `capture-viewport` already uses. #146: the capture is now the FULL game
459
+ * stack — canvas(es) plus the DOM UI layers (React adapter roots,
460
+ * react-world layers) composited via `capturePlayComposite` — because a
461
+ * canvas-only frame of a game whose score/health lives in the HUD is not
462
+ * "what the game looks like". When the composite leg can't run (no
463
+ * container, no DOM Image/XMLSerializer — e.g. the node-side relay tests —
464
+ * or a rasterization failure), it degrades to the original canvas-only
465
+ * `toDataURL` capture (readable because every runtime-mounted canvas sets
466
+ * `preserveDrawingBuffer: true` — `create-runtime.ts`, D5 §1/§4), marked
467
+ * honestly with `composite: false`. A caller (`RelayTransport.screenshot`
468
+ * in `@vgai/probe`) writes the PNG to disk — this relay op stays a pure
469
+ * "hand back the pixels" primitive, matching `bridge-call`'s own
470
+ * session-generic, probe-lifecycle-free shape. */
471
+ async function handleBridgeScreenshot(cmd: EditorCommand): Promise<CommandResult> {
472
+ if (!isPlayModeActive()) return notPlayingResult();
473
+ const canvas = getPlayCanvas();
474
+ // A paused canvas is provably stale, but a React-only root has no canvas:
475
+ // its DOM is self-driven and `capturePlayComposite` has its own bounded
476
+ // hidden-tab paint fallback. Do not apply a canvas freshness rule to the
477
+ // DOM-only adapter surface.
478
+ const runtime = getPlayRuntimeAccess();
479
+ if (canvas && runtime?.loop.liveness === 'hidden-paused') {
480
+ if (cmd['refreshHiddenFrame'] === true && runtime.runTicks) {
481
+ try {
482
+ runtime.runTicks(1, { render: 'last' });
483
+ } catch (err) {
484
+ return structuredErrorResult(err);
485
+ }
486
+ } else {
487
+ return {
488
+ ok: false,
489
+ error:
490
+ 'cannot capture a fresh game frame while the editor tab is hidden — focus/open the editor tab and retry, or request a deterministic hidden-frame refresh',
491
+ data: { code: 'BRIDGE_SCREENSHOT_STALE' },
492
+ };
493
+ }
494
+ }
495
+ const container = getGameContainer();
496
+ if (
497
+ container &&
498
+ typeof Image !== 'undefined' &&
499
+ typeof XMLSerializer !== 'undefined' &&
500
+ typeof container.getBoundingClientRect === 'function'
501
+ ) {
502
+ try {
503
+ const composite = await capturePlayComposite(container);
504
+ if (composite.layers.canvases === 0 && composite.layers.domOverlays === 0) {
505
+ throw new Error('composite screenshot: no game layers');
506
+ }
507
+ return {
508
+ ok: true,
509
+ data: {
510
+ base64: composite.base64,
511
+ mimeType: composite.mimeType,
512
+ composite: true,
513
+ layers: composite.layers,
514
+ },
515
+ };
516
+ } catch {
517
+ // Degrade to the canvas-only leg below — a HUD-less capture beats no
518
+ // capture, and `composite: false` keeps the degradation visible.
519
+ }
520
+ }
521
+ // A native React game has no canvas by design. The composite leg above is
522
+ // therefore the PRIMARY capture path for a React-only root, not an
523
+ // enhancement after a canvas precondition. Only fail once both the full
524
+ // game-stack capture and the canvas fallback are unavailable.
525
+ if (!canvas) {
526
+ return {
527
+ ok: false,
528
+ error: 'no play-mode game surface is mounted yet',
529
+ data: { code: 'BRIDGE_SCREENSHOT_UNAVAILABLE' },
530
+ };
531
+ }
532
+ try {
533
+ const dataUrl = canvas.toDataURL('image/png');
534
+ const comma = dataUrl.indexOf(',');
535
+ const base64 = comma >= 0 ? dataUrl.slice(comma + 1) : dataUrl;
536
+ return { ok: true, data: { base64, mimeType: 'image/png', composite: false } };
537
+ } catch (err) {
538
+ // e.g. a tainted canvas (a cross-origin texture loaded with no CORS
539
+ // headers, which WebGL refuses to read back even with
540
+ // preserveDrawingBuffer:true) — a distinct, rarer failure from "no
541
+ // canvas at all" above, surfaced honestly rather than swallowed.
542
+ return {
543
+ ok: false,
544
+ error: `bridge-screenshot: canvas capture failed — ${err instanceof Error ? err.message : String(err)}`,
545
+ data: { code: 'BRIDGE_SCREENSHOT_UNAVAILABLE' },
546
+ };
547
+ }
548
+ }
549
+
550
+ /**
551
+ * `page-script` relay op handler — Wave-2 "the playwright shim in the editor
552
+ * page + script-mode wire op" (`docs/SHARED-SESSION-SPEC.md` §7). Standalone
553
+ * command (like `bridge-screenshot`), NOT a `bridge-call` method: this is
554
+ * editor-page UI automation, not the game-debug seam, so it deliberately
555
+ * stays outside `dispatchBridgeMethod`'s switch — the bridge↔wire parity gate
556
+ * (`bridge-wire-parity.test.ts`) only walks THAT switch, and must never need
557
+ * to know about this op.
558
+ *
559
+ * `cmd['src']` is a UI-automation step's `Function.prototype.toString()`
560
+ * source (`@vgai/probe`'s `RelayTransport.runPageScript` — see that file's
561
+ * doc comment for the client-facing half of the wire contract). Reconstructed
562
+ * here with `new Function` and run against a fresh `createPageShim` rooted at
563
+ * the SAME game container `bridge-screenshot`'s composite capture uses
564
+ * (`getGameContainer` — never editor chrome). `PageShimUnsupportedError`
565
+ * (thrown the instant the step touches an unimplemented `Page`/`Locator`
566
+ * member) is distinguished from any other step failure so a caller can tell
567
+ * "this shim doesn't support that yet" apart from "the step's own assertion
568
+ * failed" — both are structured failures, never a hang or a silent
569
+ * mis-report.
570
+ */
571
+ async function handlePageScript(cmd: EditorCommand): Promise<CommandResult> {
572
+ if (!isPlayModeActive()) return notPlayingResult();
573
+ const src = cmd['src'];
574
+ if (typeof src !== 'string') {
575
+ return { ok: false, error: 'page-script requires a string "src" (the step\'s toString())' };
576
+ }
577
+ const container = getGameContainer();
578
+ if (!container) {
579
+ return {
580
+ ok: false,
581
+ error: 'page-script: no play-mode game container is mounted yet',
582
+ data: { code: 'PAGE_SCRIPT_ERROR' },
583
+ };
584
+ }
585
+ let step: (page: unknown) => unknown;
586
+ try {
587
+ // Reconstructing a wire-carried step's source is the documented wire
588
+ // contract (Wave-2 decision 2) — the same limitation class as
589
+ // Playwright's own evaluate serialization: closures over anything
590
+ // outside the step's own body do NOT survive the trip.
591
+ step = new Function('page', `return (${src})(page)`) as (page: unknown) => unknown;
592
+ } catch (err) {
593
+ return {
594
+ ok: false,
595
+ error: `page-script: failed to reconstruct the step function from source — ${
596
+ err instanceof Error ? err.message : String(err)
597
+ }`,
598
+ data: { code: 'PAGE_SCRIPT_ERROR' },
599
+ };
600
+ }
601
+ const page = createPageShim(container);
602
+ try {
603
+ const result = await step(page);
604
+ return { ok: true, data: { result: result === undefined ? null : result } };
605
+ } catch (err) {
606
+ if (err instanceof PageShimUnsupportedError) {
607
+ return {
608
+ ok: false,
609
+ error: err.message,
610
+ data: { code: 'PAGE_SHIM_UNSUPPORTED', member: err.member, supported: [...err.supported] },
611
+ };
612
+ }
613
+ return {
614
+ ok: false,
615
+ error: `page-script: step threw — ${err instanceof Error ? err.message : String(err)}`,
616
+ data: { code: 'PAGE_SCRIPT_ERROR' },
617
+ };
618
+ }
619
+ }
620
+
82
621
  /** Validate the current scene document against the Zod schema. */
83
622
  function validateCurrentScene(store: EditorStore): CommandResult {
84
623
  const doc = store.getDocument();
@@ -93,6 +632,47 @@ function validateCurrentScene(store: EditorStore): CommandResult {
93
632
  }
94
633
  }
95
634
 
635
+ async function handleAssetPreviewCommand(
636
+ store: EditorStore,
637
+ cmd: EditorCommand,
638
+ ): Promise<CommandResult> {
639
+ const assetPath = cmd['assetPath'];
640
+ const entityId = cmd['entityId'];
641
+ if ((typeof assetPath === 'string') === (typeof entityId === 'string')) {
642
+ return {
643
+ ok: false,
644
+ error: 'capture-asset-preview requires exactly one of assetPath or entityId.',
645
+ };
646
+ }
647
+ const width = cmd['width'];
648
+ const height = cmd['height'];
649
+ const background = cmd['background'];
650
+ if (
651
+ (width !== undefined && typeof width !== 'number') ||
652
+ (height !== undefined && typeof height !== 'number') ||
653
+ (background !== undefined && background !== 'neutral' && background !== 'transparent')
654
+ ) {
655
+ return { ok: false, error: 'Invalid asset preview dimensions or background.' };
656
+ }
657
+ if (typeof entityId === 'string' && !store.objectMap.has(entityId)) {
658
+ return { ok: false, error: `Entity not found: ${entityId}` };
659
+ }
660
+ try {
661
+ const options = {
662
+ ...(typeof width === 'number' ? { width } : {}),
663
+ ...(typeof height === 'number' ? { height } : {}),
664
+ ...(background ? { background: background as AssetPreviewBackground } : {}),
665
+ };
666
+ const capture =
667
+ typeof assetPath === 'string'
668
+ ? await captureModelAssetPreview(assetPath, options)
669
+ : captureObjectAssetPreview(store.objectMap.get(entityId as string)!, options);
670
+ return { ok: true, data: { ...capture } };
671
+ } catch (err) {
672
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
673
+ }
674
+ }
675
+
96
676
  /**
97
677
  * Exported (alongside `collectState` above) so unit tests can dispatch
98
678
  * commands directly against a headless `EditorStore`, with no SSE/server
@@ -107,6 +687,21 @@ export async function handleCommand(
107
687
  switch (cmd['type']) {
108
688
  // Play control
109
689
  case 'play': {
690
+ // Fix for the ghost-runtime bug — deliberately no `isPlayModeActive()`
691
+ // guard here: `play` on an already-playing (or still-booting) session
692
+ // is a RESTART, not a no-op, matching the SDK's own documented contract
693
+ // (`play.start`'s summary: "Start (or restart) play mode" —
694
+ // `packages/vgai-sdk/src/play/lifecycle-operations.ts`). What used to
695
+ // make a restart unsafe was `enterPlayMode()` having no reentrancy
696
+ // guard for the async-boot window before it assigns `_session` — two
697
+ // overlapping calls ran their entire prologues concurrently and
698
+ // corrupted shared module state (proven by a real console-patching
699
+ // stack overflow — see `play-mode-reentry-ghost.test.ts`). That is
700
+ // fixed at the source in `enterPlayMode()` itself (a FIFO queue
701
+ // serializes every invocation), so every caller of THIS case — CLI,
702
+ // SDK, a UI Play-button click racing a relayed command — gets a safe,
703
+ // clean restart for free without needing a guard here too.
704
+ //
110
705
  // Validate before play — enterPlayMode() also validates, but we need
111
706
  // the error message to flow back to the SDK/CLI.
112
707
  const validation = validateCurrentScene(store);
@@ -114,7 +709,11 @@ export async function handleCommand(
114
709
  return validation;
115
710
  }
116
711
  try {
117
- await enterPlayMode();
712
+ // D15/T-D15.6 (objection-4 fix) — `vgai play --seed <n>` relays as
713
+ // `cmd['seed']`; `enterPlayMode`'s explicit-config seed leg (beats
714
+ // manifest.determinism.defaultSeed/?vgai-seed=).
715
+ const seed = cmd['seed'];
716
+ await enterPlayMode(typeof seed === 'number' ? seed : undefined);
118
717
  } catch (err) {
119
718
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
120
719
  }
@@ -178,15 +777,35 @@ export async function handleCommand(
178
777
  case 'viewport-tab':
179
778
  store.setActiveViewportTab(cmd['tab'] as 'scene' | 'game');
180
779
  break;
181
- case 'active-tab':
182
- store.setActiveTab(cmd['key'] as string);
780
+ // W2: the legacy right-rail tab commands keep their key vocabulary but
781
+ // now drive the CENTER workspace documents (`asset-documents.tsx`).
782
+ // '__inspector__' ("show the Inspector") is a no-op now: the right rail
783
+ // ALWAYS shows the Inspector — nothing displaces it anymore (§10.3).
784
+ case 'active-tab': {
785
+ const key = cmd['key'] as string;
786
+ if (key !== '__inspector__') activateWorkspaceDocument(key);
183
787
  break;
788
+ }
184
789
  case 'open-asset-tab':
185
- store.openAssetTab(cmd['path'] as string, cmd['kind'] as AssetKind);
790
+ openAssetDocument(store, cmd['path'] as string, cmd['kind'] as AssetKind);
186
791
  break;
187
- case 'close-asset-tab':
188
- store.closeAssetTab(cmd['key'] as string);
792
+ case 'close-asset-tab': {
793
+ // Legacy key vocabulary: the deleted store-era `closeAssetTab` only
794
+ // ever acted on asset tabs and no-op'd for anything else (scene/game/
795
+ // story keys included). `closeWorkspaceDocument` itself has no such
796
+ // guard — it closes ANY registered id, pinned or not (`closeable:
797
+ // false` is only a UI-affordance rule, not a registry invariant; see
798
+ // workspace-document-registry.ts) — so this command must keep the
799
+ // no-op itself. `isAssetDocumentId` is exactly the legacy asset key
800
+ // vocabulary check (project asset path / `asset-lab:entity:<id>` /
801
+ // `online:<source>:<id>` — see asset-documents.tsx), so gating on it
802
+ // blocks the pinned `workspace:scene`/`workspace:game` ids (and any
803
+ // story `story:<path>#<name>` id) while preserving the real behavior
804
+ // for actual asset tabs.
805
+ const key = cmd['key'] as string;
806
+ if (isAssetDocumentId(key)) closeWorkspaceDocument(key);
189
807
  break;
808
+ }
190
809
  case 'toggle-command-palette':
191
810
  store.toggleCommandPalette();
192
811
  break;
@@ -194,7 +813,10 @@ export async function handleCommand(
194
813
  window.dispatchEvent(new CustomEvent('editor:toggle-console'));
195
814
  break;
196
815
  case 'show-build':
197
- window.dispatchEvent(new CustomEvent('editor:show-build-tab'));
816
+ // W3 build split: the config surface is the Build Profiles CENTER
817
+ // document (build-documents.tsx); the log streams into the Build
818
+ // Output utility. Same command vocabulary, new (semantic) home.
819
+ openBuildProfilesDocument(store);
198
820
  break;
199
821
  case 'capture-viewport': {
200
822
  // Fresh, unthrottled on-demand capture (see EditorStore.captureViewportImage's
@@ -213,6 +835,8 @@ export async function handleCommand(
213
835
  const base64 = comma >= 0 ? dataUrl.slice(comma + 1) : dataUrl;
214
836
  return { ok: true, data: { base64, mimeType: 'image/png' } };
215
837
  }
838
+ case 'capture-asset-preview':
839
+ return handleAssetPreviewCommand(store, cmd);
216
840
 
217
841
  // Scene loading
218
842
  case 'open-scene': {
@@ -266,16 +890,242 @@ export async function handleCommand(
266
890
  if (store.snapEnabled !== (cmd['enabled'] as boolean)) store.toggleSnap();
267
891
  break;
268
892
 
893
+ // Debug seam relay (Wave 5, docs/SYNTHETIC-PLAYER-SPEC.md §3.5): all six
894
+ // cases read/actuate the LIVE play session's game-scoped debug registry
895
+ // (`SystemAdapters.debug`, installed by `setActiveSystems` on play enter)
896
+ // or its InputManager/GameLoop (via `getPlayRuntimeAccess`). A stopped
897
+ // session answers with a structured failure, never a fabricated success;
898
+ // a running game that exposes NO debug adapter (a non-first-party mount)
899
+ // degrades to the honest empty/null shape.
900
+ case 'list-gameplay-state': {
901
+ const debug = getActiveSystems().debug;
902
+ if (!debug) {
903
+ return isPlayModeActive() ? { ok: true, data: { providers: [] } } : notPlayingResult();
904
+ }
905
+ return { ok: true, data: { providers: debug.providers() } };
906
+ }
907
+ case 'inspect-gameplay-state': {
908
+ const debug = getActiveSystems().debug;
909
+ if (!debug) {
910
+ return isPlayModeActive() ? { ok: true, data: { state: null } } : notPlayingResult();
911
+ }
912
+ const keys = cmd['keys'] as string[] | undefined;
913
+ try {
914
+ const state: Record<string, unknown> = {};
915
+ if (keys && keys.length > 0) {
916
+ for (const key of keys) state[key] = debug.state(key);
917
+ } else {
918
+ Object.assign(state, debug.stateAll());
919
+ }
920
+ return { ok: true, data: { state } };
921
+ } catch (err) {
922
+ return structuredErrorResult(err);
923
+ }
924
+ }
925
+ case 'list-debug-commands': {
926
+ const debug = getActiveSystems().debug;
927
+ if (!debug) {
928
+ return isPlayModeActive() ? { ok: true, data: { commands: [] } } : notPlayingResult();
929
+ }
930
+ return { ok: true, data: { commands: debug.commands() } };
931
+ }
932
+ case 'invoke-debug-command': {
933
+ const debug = getActiveSystems().debug;
934
+ if (!debug) {
935
+ if (!isPlayModeActive()) return notPlayingResult();
936
+ // Honest not-registered shape: no adapter means NOTHING is registered.
937
+ return {
938
+ ok: false,
939
+ error: `debug: no command registered under "${cmd['name']}" — this game exposes no debug adapter`,
940
+ data: { code: 'DEBUG_COMMAND_NOT_REGISTERED', registered: [] },
941
+ };
942
+ }
943
+ try {
944
+ const result = await debug.invoke(cmd['name'] as string, (cmd['args'] as unknown[]) ?? []);
945
+ return { ok: true, data: { result } };
946
+ } catch (err) {
947
+ return structuredErrorResult(err);
948
+ }
949
+ }
950
+ case 'inject-input': {
951
+ const runtime = getPlayRuntimeAccess();
952
+ if (!runtime) {
953
+ return { ok: false, error: 'not in play mode — start play before injecting input' };
954
+ }
955
+ // D15/T-D15.5 (review objection 2's fix) — resolved through the SAME
956
+ // per-world `DebugRegistry.getVirtualInputTarget(worldId?)` the debug
957
+ // bridge's `window.__vgai.input.*` calls, instead of the old
958
+ // `Game.input` (always the DEFAULT world only) — an explicit
959
+ // `cmd['worldId']` reaches that world specifically; omitted resolves
960
+ // to the same default both doors now share. Throws
961
+ // `DEBUG_INPUT_WORLD_NOT_FOUND` (caught below) for an explicit,
962
+ // unregistered world id.
963
+ let input: ReturnType<typeof runtime.getInputTarget>;
964
+ try {
965
+ input = runtime.getInputTarget(cmd['worldId'] as string | undefined);
966
+ } catch (err) {
967
+ return structuredErrorResult(err);
968
+ }
969
+ if (!input) {
970
+ return {
971
+ ok: false,
972
+ error: 'this play session has no first-party InputManager to inject into',
973
+ };
974
+ }
975
+ try {
976
+ // Action-level is primary (frozen decision, spec §3.2): the honest
977
+ // gated path — a gated actuation surfaces {delivered:false, reason},
978
+ // never a false ok. The four legacy kinds map to the persistent
979
+ // test-source injectors (read back only through declared test_*
980
+ // bindings, so `delivered` here means "the source was written").
981
+ if (cmd['kind'] === 'action') {
982
+ const atTick = cmd['atTick'];
983
+ // D15/T-D15.5 — `atTick` defers to InputManager.scheduleActionAtTick
984
+ // instead of an immediate setVirtualAction. scheduleActionAtTick
985
+ // throws InputActionError/a valueType mismatch (same as
986
+ // setVirtualAction) or InputTickError ('TICK_ALREADY_PASSED',
987
+ // data.currentTick) — all caught by this case's outer try/catch
988
+ // below via structuredErrorResult.
989
+ if (typeof atTick === 'number') {
990
+ input.scheduleActionAtTick(
991
+ atTick,
992
+ cmd['action'] as string,
993
+ cmd['value'] as boolean | number | { x: number; y: number },
994
+ );
995
+ return { ok: true, data: { scheduled: true, tick: atTick } };
996
+ }
997
+ const outcome = input.setVirtualAction(
998
+ cmd['action'] as string,
999
+ cmd['value'] as boolean | number | { x: number; y: number },
1000
+ );
1001
+ return { ok: true, data: outcome };
1002
+ }
1003
+ const sourceId = cmd['sourceId'] as string;
1004
+ switch (cmd['kind']) {
1005
+ case 'axis':
1006
+ input.injectAxis(sourceId, cmd['value'] as number);
1007
+ break;
1008
+ case 'vector2':
1009
+ input.injectVector2(sourceId, cmd['value'] as { x: number; y: number });
1010
+ break;
1011
+ case 'pointerDelta':
1012
+ input.injectPointerDelta(sourceId, cmd['value'] as { x: number; y: number });
1013
+ break;
1014
+ case 'pointerPosition':
1015
+ input.injectPointerPosition(sourceId, cmd['value'] as { x: number; y: number });
1016
+ break;
1017
+ default:
1018
+ return { ok: false, error: `unknown injection kind "${cmd['kind']}"` };
1019
+ }
1020
+ return { ok: true, data: { delivered: true } };
1021
+ } catch (err) {
1022
+ return structuredErrorResult(err);
1023
+ }
1024
+ }
1025
+ case 'set-time-scale': {
1026
+ const runtime = getPlayRuntimeAccess();
1027
+ if (!runtime) {
1028
+ return { ok: false, error: 'not in play mode — start play before setting time-scale' };
1029
+ }
1030
+ runtime.loop.timeScale = cmd['timeScale'] as number;
1031
+ // Read back after the loop's own [0,8] clamp so the caller sees the
1032
+ // value actually applied.
1033
+ return { ok: true, data: { timeScale: runtime.loop.timeScale } };
1034
+ }
1035
+ // D15/T-D15.6 — the `play.seed.set` relay case (`docs/D15-DETERMINISM-
1036
+ // DESIGN.md` §2.d): reaches the live session's `ctx.random.reseed(seed)`
1037
+ // through `getPlayRuntimeAccess()`'s `random` accessor. Gated on
1038
+ // `determinismDeclared` (the running project's manifest
1039
+ // `determinism.seededRandom`, per T4.1: reseeding a project with no
1040
+ // documented determinism contract would silently imply one exists) —
1041
+ // the structured `DETERMINISM_NOT_DECLARED` marker the SDK maps to its
1042
+ // own declared error code.
1043
+ case 'set-seed': {
1044
+ const runtime = getPlayRuntimeAccess();
1045
+ if (!runtime) {
1046
+ return { ok: false, error: 'not in play mode — start play before setting the seed' };
1047
+ }
1048
+ if (!runtime.determinismDeclared) {
1049
+ return {
1050
+ ok: false,
1051
+ error:
1052
+ "this project's manifest does not declare determinism.seededRandom — " +
1053
+ 'play.seed.set has no documented contract to reseed against (D15)',
1054
+ data: { code: 'DETERMINISM_NOT_DECLARED' },
1055
+ };
1056
+ }
1057
+ if (!runtime.random) {
1058
+ return {
1059
+ ok: false,
1060
+ error: 'this play session has no first-party ctx.random to reseed',
1061
+ };
1062
+ }
1063
+ runtime.random.reseed(cmd['seed'] as number);
1064
+ // Read back the live, post-reseed value (mirrors set-time-scale's own
1065
+ // read-after-apply convention immediately above).
1066
+ return { ok: true, data: { seed: runtime.random.seed } };
1067
+ }
1068
+ // D15/T-D15.4 — the `play.runTicks` relay case (`docs/D15-DETERMINISM-
1069
+ // DESIGN.md` §2.b): reaches the live session's `GameInternal.runTicks`
1070
+ // through `getPlayRuntimeAccess()`'s `runTicks` accessor (itself backed
1071
+ // by the SAME `DebugRegistry.getRunTicksTarget()` the bridge's
1072
+ // `window.__vgai.runTicks` calls — one implementation, byte-identical
1073
+ // behavior across doors, D17). `runTicks` itself throws a structured
1074
+ // `DebugError` (`RUN_TICKS_PAUSED`) while paused — never a silent no-op —
1075
+ // which `structuredErrorResult` below carries through as `data.code`.
1076
+ case 'run-ticks': {
1077
+ const runtime = getPlayRuntimeAccess();
1078
+ if (!runtime) {
1079
+ return { ok: false, error: 'not in play mode — start play before running ticks' };
1080
+ }
1081
+ if (!runtime.runTicks) {
1082
+ return {
1083
+ ok: false,
1084
+ error: 'this play session has no first-party Game to run ticks on',
1085
+ data: { code: 'RUN_TICKS_UNAVAILABLE' },
1086
+ };
1087
+ }
1088
+ try {
1089
+ const n = cmd['n'] as number;
1090
+ const render = cmd['render'] as 'last' | 'all' | 'none' | undefined;
1091
+ runtime.runTicks(n, render ? { render } : undefined);
1092
+ return { ok: true };
1093
+ } catch (err) {
1094
+ return structuredErrorResult(err);
1095
+ }
1096
+ }
1097
+
1098
+ // #140 — the session-generic debug-seam relay: see `dispatchBridgeMethod`'s
1099
+ // doc comment above for why this is a distinct primitive from the six
1100
+ // named debug-seam cases above it (list-gameplay-state etc.) rather than
1101
+ // a duplicate of them.
1102
+ case 'bridge-call':
1103
+ return handleBridgeCall(cmd);
1104
+ case 'bridge-screenshot':
1105
+ return handleBridgeScreenshot(cmd);
1106
+
1107
+ // Wave-2 "the playwright shim in the editor page + script-mode wire op"
1108
+ // (`docs/SHARED-SESSION-SPEC.md` §7) — a STANDALONE relay command (like
1109
+ // `bridge-screenshot` above), deliberately NOT a `bridge-call` method: it
1110
+ // is editor-page UI automation (the in-page Playwright shim), not the
1111
+ // game-debug seam the bridge↔wire parity gate governs. See
1112
+ // `handlePageScript`'s own doc comment.
1113
+ case 'page-script':
1114
+ return handlePageScript(cmd);
1115
+
1116
+ // Version-skew honesty (B3-followup false-ack fix + Wave 5's AC-A0.3):
1117
+ // every unrecognized command type used to fall through to `return {ok:
1118
+ // true}` below — a silent lie: the browser did NOTHING, but the SDK/CLI
1119
+ // caller was told it succeeded. Reporting a structured failure must hold
1120
+ // for ANY unrecognized `type`, not just the ones known about today. The
1121
+ // `data.code` marker is what the SDK maps to its `*_UNSUPPORTED` codes;
1122
+ // the prose is for humans only.
269
1123
  default:
270
- // Every unrecognized command type used to fall through to `return { ok:
271
- // true }` below — a silent, false ack: the browser did NOTHING, but the
272
- // SDK/CLI caller was told it succeeded. Every "honest gap" op in
273
- // packages/vgai-sdk/src/editor/*-operations.ts (camera.get,
274
- // hierarchy.inspect pre-wiring, story.open) exists specifically to avoid
275
- // ever sending a command that would land here. Reporting a structured
276
- // failure — never a fabricated success — is the fix, and it must hold for
277
- // ANY unrecognized `type`, not just the ones known about today.
278
- return { ok: false, error: `Unknown editor command: ${String(cmd['type'])}` };
1124
+ return {
1125
+ ok: false,
1126
+ error: `unknown command type "${cmd['type']}" — editor page predates this CLI`,
1127
+ data: { code: 'UNKNOWN_COMMAND_TYPE' },
1128
+ };
279
1129
  }
280
1130
 
281
1131
  return { ok: true };
@@ -322,5 +1172,29 @@ export function connectCommandListener(store: EditorStore): () => void {
322
1172
  window.location.reload();
323
1173
  });
324
1174
 
325
- return () => source.close();
1175
+ // #145 — presence freshness: the state snapshot the server holds is only
1176
+ // re-POSTed after commands, so visibility/focus changes between commands
1177
+ // would go stale. Report on the three events that change presence.
1178
+ const reportPresence = () => reportEditorState(collectState(store));
1179
+ document.addEventListener('visibilitychange', reportPresence);
1180
+ window.addEventListener('focus', reportPresence);
1181
+ window.addEventListener('blur', reportPresence);
1182
+
1183
+ // pageErrors freshness (target-blaster friction #3): a runtime error
1184
+ // between commands must reach the server snapshot too, or `vgai status`
1185
+ // reads stale-clean. Deferred a tick so ViewportPanel's own
1186
+ // error-capture listener (which feeds editorConsole — the list
1187
+ // collectPageErrors reads) runs FIRST regardless of registration order.
1188
+ const reportAfterError = () => setTimeout(reportPresence, 0);
1189
+ window.addEventListener('error', reportAfterError);
1190
+ window.addEventListener('unhandledrejection', reportAfterError);
1191
+
1192
+ return () => {
1193
+ document.removeEventListener('visibilitychange', reportPresence);
1194
+ window.removeEventListener('focus', reportPresence);
1195
+ window.removeEventListener('blur', reportPresence);
1196
+ window.removeEventListener('error', reportAfterError);
1197
+ window.removeEventListener('unhandledrejection', reportAfterError);
1198
+ source.close();
1199
+ };
326
1200
  }