@volter/sdk 0.0.0-stage → 0.5.204

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 (523) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +20 -0
  3. package/README.md +38 -3
  4. package/package.json +510 -4
  5. package/src/account.ts +210 -0
  6. package/src/chrome.ts +88 -0
  7. package/src/client.ts +1646 -0
  8. package/src/commands.ts +66 -0
  9. package/src/contributions.ts +619 -0
  10. package/src/css-numeric-style.ts +97 -0
  11. package/src/document-probe.ts +282 -0
  12. package/src/editor-view.ts +225 -0
  13. package/src/extension.ts +40 -0
  14. package/src/generations.ts +178 -0
  15. package/src/host.ts +1157 -0
  16. package/src/http-transport.browser.ts +14 -0
  17. package/src/http-transport.node.ts +19 -0
  18. package/src/index.ts +131 -0
  19. package/src/kit/CapabilityCoverageSection.tsx +185 -0
  20. package/src/kit/account-client.ts +333 -0
  21. package/src/kit/action-registry.ts +317 -0
  22. package/src/kit/active-product.ts +76 -0
  23. package/src/kit/active-project.ts +155 -0
  24. package/src/kit/adapter-editor-config.ts +25 -0
  25. package/src/kit/adapter-module.ts +7 -0
  26. package/src/kit/adapter-observation.ts +49 -0
  27. package/src/kit/animation/animation-clock.ts +479 -0
  28. package/src/kit/animation/stage-transport.ts +385 -0
  29. package/src/kit/api/assets.ts +365 -0
  30. package/src/kit/api/project-open.ts +355 -0
  31. package/src/kit/api/project-source.ts +180 -0
  32. package/src/kit/api/project-state.ts +110 -0
  33. package/src/kit/api/relay.ts +270 -0
  34. package/src/kit/api/themes.ts +45 -0
  35. package/src/kit/api-asset-library-wire.ts +45 -0
  36. package/src/kit/api-base.ts +10 -0
  37. package/src/kit/api-build.ts +99 -0
  38. package/src/kit/api-git-wire.ts +56 -0
  39. package/src/kit/api-logs.ts +92 -0
  40. package/src/kit/api-project-identity.ts +74 -0
  41. package/src/kit/api-settings.ts +36 -0
  42. package/src/kit/api-worktrees.ts +205 -0
  43. package/src/kit/asset-capabilities.ts +344 -0
  44. package/src/kit/asset-compare-core.ts +171 -0
  45. package/src/kit/asset-editor-context.tsx +101 -0
  46. package/src/kit/asset-events.ts +96 -0
  47. package/src/kit/asset-inspector-actions.ts +87 -0
  48. package/src/kit/asset-selection-viewer-registry.ts +113 -0
  49. package/src/kit/asset-selection.ts +146 -0
  50. package/src/kit/asset-thumbnails.ts +25 -0
  51. package/src/kit/asset-viewers.ts +115 -0
  52. package/src/kit/asset-workflow/asset-import-jobs.ts +106 -0
  53. package/src/kit/asset-workflow/asset-ledger-backend.ts +126 -0
  54. package/src/kit/asset-workflow/asset-ledger.ts +156 -0
  55. package/src/kit/asset-workflow/asset-materialization-report.ts +140 -0
  56. package/src/kit/asset-workflow/asset-pack-manifest.ts +320 -0
  57. package/src/kit/asset-workflow/asset-types.ts +142 -0
  58. package/src/kit/asset-workflow/audio-preview-player.ts +193 -0
  59. package/src/kit/asset-workflow/audio-waveform.ts +22 -0
  60. package/src/kit/asset-workflow/cloud-asset-client.ts +263 -0
  61. package/src/kit/asset-workflow/hosted-asset-materialization.ts +236 -0
  62. package/src/kit/asset-workflow/image-view-scale.ts +31 -0
  63. package/src/kit/asset-workflow/import-contract.ts +124 -0
  64. package/src/kit/asset-workflow/ledger-write-lock.ts +244 -0
  65. package/src/kit/asset-workflow/pixi-spritesheet.ts +197 -0
  66. package/src/kit/asset-workflow/preview-resource-lifetime.ts +44 -0
  67. package/src/kit/asset-workflow/project-asset-commands.ts +20 -0
  68. package/src/kit/asset-workflow/project-content.ts +288 -0
  69. package/src/kit/asset-workflow/project-source-index.ts +545 -0
  70. package/src/kit/asset-workflow/thumbnail-system.ts +256 -0
  71. package/src/kit/authoring/active-adapter.ts +200 -0
  72. package/src/kit/authoring/active-systems.ts +422 -0
  73. package/src/kit/authoring/adapter-key.ts +18 -0
  74. package/src/kit/authoring/authoring-asset-url.ts +27 -0
  75. package/src/kit/authoring/bootstrap-state.ts +49 -0
  76. package/src/kit/authoring/boundary-authoring-adapter.ts +189 -0
  77. package/src/kit/authoring/canvas-scene-guides.ts +84 -0
  78. package/src/kit/authoring/composite-authoring-adapter.ts +2109 -0
  79. package/src/kit/authoring/consumer-actions.ts +531 -0
  80. package/src/kit/authoring/design-time-layers.ts +852 -0
  81. package/src/kit/authoring/design-time-mount-registry.ts +244 -0
  82. package/src/kit/authoring/edit-mode-authoring.ts +637 -0
  83. package/src/kit/authoring/empty-project-authoring.ts +22 -0
  84. package/src/kit/authoring/instance-source-menu.ts +135 -0
  85. package/src/kit/authoring/layered-pick.ts +185 -0
  86. package/src/kit/authoring/mounted-root-subjects.ts +146 -0
  87. package/src/kit/authoring/no-authoring-adapter.ts +59 -0
  88. package/src/kit/authoring/object3d-document-persistence.ts +122 -0
  89. package/src/kit/authoring/panel-authoring.ts +121 -0
  90. package/src/kit/authoring/project-authoring-session.ts +105 -0
  91. package/src/kit/authoring/provenance.ts +99 -0
  92. package/src/kit/authoring/react-canvas-navigation.ts +259 -0
  93. package/src/kit/authoring/react-design-canvas-style.ts +20 -0
  94. package/src/kit/authoring/react-story-board.ts +917 -0
  95. package/src/kit/authoring/selection-scope.ts +195 -0
  96. package/src/kit/authoring/shell-document-ops.ts +169 -0
  97. package/src/kit/authoring/story-board-chrome-fit.ts +107 -0
  98. package/src/kit/authoring/story-board-presentation.ts +111 -0
  99. package/src/kit/authoring/three-root.ts +67 -0
  100. package/src/kit/authoring/viewport-tool-context.ts +73 -0
  101. package/src/kit/authoring/viewport-tool-owner.ts +38 -0
  102. package/src/kit/authoring/world-session-state.ts +101 -0
  103. package/src/kit/authoring-seam-evidence.ts +300 -0
  104. package/src/kit/availability-tick.ts +66 -0
  105. package/src/kit/bitmap-label.ts +120 -0
  106. package/src/kit/boot-routing.ts +392 -0
  107. package/src/kit/breakpoint-state.ts +43 -0
  108. package/src/kit/build-identity.ts +16 -0
  109. package/src/kit/bytes-codec.ts +62 -0
  110. package/src/kit/cancellation-reason.ts +58 -0
  111. package/src/kit/canvas-frames.ts +88 -0
  112. package/src/kit/capture-camera-pose.ts +77 -0
  113. package/src/kit/capture-size.ts +88 -0
  114. package/src/kit/chrome-registry.ts +159 -0
  115. package/src/kit/chrome-slot-registry.ts +91 -0
  116. package/src/kit/collaboration-client.ts +264 -0
  117. package/src/kit/collaboration-presence.ts +41 -0
  118. package/src/kit/command-dispatch.ts +19 -0
  119. package/src/kit/command-listener.ts +2182 -0
  120. package/src/kit/command-registry.ts +71 -0
  121. package/src/kit/component-board-registry.ts +205 -0
  122. package/src/kit/component-states-registry.ts +199 -0
  123. package/src/kit/components/AlignToolbar.tsx +204 -0
  124. package/src/kit/components/ApplicationMenus.tsx +372 -0
  125. package/src/kit/components/AssetEditorShell.tsx +216 -0
  126. package/src/kit/components/AssetInspectorToolSection.tsx +124 -0
  127. package/src/kit/components/BoardRulers.tsx +354 -0
  128. package/src/kit/components/CanvasAddNodeDialogs.tsx +529 -0
  129. package/src/kit/components/CanvasSceneViewport.tsx +1195 -0
  130. package/src/kit/components/ChromeSlot.tsx +20 -0
  131. package/src/kit/components/CodeView.tsx +470 -0
  132. package/src/kit/components/CompactInspectorShell.tsx +39 -0
  133. package/src/kit/components/ConsolePanel.tsx +273 -0
  134. package/src/kit/components/GameplaySessionTimeline.tsx +295 -0
  135. package/src/kit/components/InspectionProjection.tsx +932 -0
  136. package/src/kit/components/Inspector.tsx +270 -0
  137. package/src/kit/components/InspectorCanvasPreview.tsx +35 -0
  138. package/src/kit/components/InspectorFieldsSection.tsx +290 -0
  139. package/src/kit/components/InspectorStoriesSection.tsx +92 -0
  140. package/src/kit/components/InspectorToolSection.tsx +96 -0
  141. package/src/kit/components/InspectorTransformSection.tsx +245 -0
  142. package/src/kit/components/LightExplorerPanel.tsx +433 -0
  143. package/src/kit/components/MediaProperties.tsx +145 -0
  144. package/src/kit/components/ProjectHeader.tsx +328 -0
  145. package/src/kit/components/ReactCanvasControls.tsx +358 -0
  146. package/src/kit/components/RootSelectionOverlay.tsx +3688 -0
  147. package/src/kit/components/RootTextEditor.tsx +79 -0
  148. package/src/kit/components/SaveStatus.tsx +70 -0
  149. package/src/kit/components/SurfaceCrashBoundary.tsx +105 -0
  150. package/src/kit/components/SurfaceStateOverlay.tsx +24 -0
  151. package/src/kit/components/ToolContributionSurfaces.tsx +49 -0
  152. package/src/kit/components/ToolHost.tsx +380 -0
  153. package/src/kit/components/Toolbar.tsx +811 -0
  154. package/src/kit/components/TransientHint.tsx +44 -0
  155. package/src/kit/components/VersionControlSection.tsx +470 -0
  156. package/src/kit/components/ViewportOverlaysMenu.tsx +177 -0
  157. package/src/kit/components/VolterLogo.tsx +18 -0
  158. package/src/kit/components/WorktreeSwitcher.tsx +712 -0
  159. package/src/kit/components/account-documents.tsx +1162 -0
  160. package/src/kit/components/asset-documents.tsx +794 -0
  161. package/src/kit/components/asset-editor-persistence.ts +216 -0
  162. package/src/kit/components/asset-selection-section.tsx +545 -0
  163. package/src/kit/components/asset-thumbnails.tsx +307 -0
  164. package/src/kit/components/asset-viewers/AudioViewer.tsx +201 -0
  165. package/src/kit/components/asset-viewers/GenericJsonViewer.tsx +102 -0
  166. package/src/kit/components/asset-viewers/ImageViewer.tsx +300 -0
  167. package/src/kit/components/asset-viewers/JsonAssetDocument.tsx +98 -0
  168. package/src/kit/components/asset-viewers/OnlineAssetDetail.tsx +426 -0
  169. package/src/kit/components/asset-viewers/SourceAssetViewer.tsx +356 -0
  170. package/src/kit/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
  171. package/src/kit/components/asset-viewers/VideoViewer.tsx +101 -0
  172. package/src/kit/components/asset-viewers/shader-source.ts +144 -0
  173. package/src/kit/components/board-guides.ts +150 -0
  174. package/src/kit/components/canvas-scene-hotkeys.ts +37 -0
  175. package/src/kit/components/canvas-temporary-pivot.ts +34 -0
  176. package/src/kit/components/core-utilities.tsx +94 -0
  177. package/src/kit/components/inspector-preview-section.tsx +223 -0
  178. package/src/kit/components/inspector-revert-label.ts +20 -0
  179. package/src/kit/components/inspector-selection.ts +42 -0
  180. package/src/kit/components/inspector-stories-gating.ts +171 -0
  181. package/src/kit/components/inspector-transform-subject.ts +11 -0
  182. package/src/kit/components/inspector-transform.ts +88 -0
  183. package/src/kit/components/kind-documents.tsx +544 -0
  184. package/src/kit/components/primitives/DraftColorInput.tsx +74 -0
  185. package/src/kit/components/project-tool-documents.tsx +402 -0
  186. package/src/kit/components/scene-documents.tsx +221 -0
  187. package/src/kit/components/status-contributions.tsx +407 -0
  188. package/src/kit/components/tool-documents.tsx +302 -0
  189. package/src/kit/components/tool-schema-form.tsx +262 -0
  190. package/src/kit/components/use-after-paint.ts +41 -0
  191. package/src/kit/components/use-project-image-assets.ts +86 -0
  192. package/src/kit/components/workspace-history.ts +32 -0
  193. package/src/kit/components/world-documents.tsx +570 -0
  194. package/src/kit/components/world-overlay-gestures.ts +1939 -0
  195. package/src/kit/composite-screenshot.ts +2238 -0
  196. package/src/kit/content-entry-source-registry.ts +184 -0
  197. package/src/kit/contribution-surfaces.ts +48 -0
  198. package/src/kit/coverage/canvas-reveal.ts +192 -0
  199. package/src/kit/coverage/design-time-surfaces.ts +101 -0
  200. package/src/kit/coverage/ontology-invariants.ts +466 -0
  201. package/src/kit/coverage/session-vitals.ts +503 -0
  202. package/src/kit/crash-null-boundary.ts +36 -0
  203. package/src/kit/creation-site-edit.ts +1491 -0
  204. package/src/kit/creation-site-registry.ts +160 -0
  205. package/src/kit/delegate-harness-registry.ts +134 -0
  206. package/src/kit/document-areas.ts +70 -0
  207. package/src/kit/document-context-registry.ts +193 -0
  208. package/src/kit/document-open-registry.ts +200 -0
  209. package/src/kit/document-play-extension.ts +221 -0
  210. package/src/kit/document-preview-source.ts +20 -0
  211. package/src/kit/document-renderer-session.ts +138 -0
  212. package/src/kit/document-stage-sessions.ts +26 -0
  213. package/src/kit/document-viewports.ts +120 -0
  214. package/src/kit/editor-api.ts +46 -0
  215. package/src/kit/editor-chrome-capture.ts +136 -0
  216. package/src/kit/editor-commands.ts +176 -0
  217. package/src/kit/editor-console.ts +580 -0
  218. package/src/kit/editor-current-view.ts +56 -0
  219. package/src/kit/editor-document-probe.ts +1168 -0
  220. package/src/kit/editor-git-client.ts +115 -0
  221. package/src/kit/editor-hotkeys.ts +728 -0
  222. package/src/kit/editor-lease-view.ts +39 -0
  223. package/src/kit/editor-lease.ts +415 -0
  224. package/src/kit/editor-mode.ts +19 -0
  225. package/src/kit/editor-notifications.ts +140 -0
  226. package/src/kit/editor-presence.ts +563 -0
  227. package/src/kit/editor-presentation-activity.ts +58 -0
  228. package/src/kit/editor-presentation-notice.ts +42 -0
  229. package/src/kit/editor-runtime.tsx +147 -0
  230. package/src/kit/editor-server-response.ts +86 -0
  231. package/src/kit/editor-session-attribution.ts +85 -0
  232. package/src/kit/editor-session-mode.ts +54 -0
  233. package/src/kit/editor-state-facets.ts +74 -0
  234. package/src/kit/editor-view-presentation.ts +777 -0
  235. package/src/kit/environment-images.ts +58 -0
  236. package/src/kit/eyedropper-session.ts +60 -0
  237. package/src/kit/files/file-provider.ts +62 -0
  238. package/src/kit/files/project-files.ts +270 -0
  239. package/src/kit/finders/index.ts +137 -0
  240. package/src/kit/finders/scenes-from-entrypoint-selection.ts +387 -0
  241. package/src/kit/frame/frame-parts.ts +30 -0
  242. package/src/kit/framed-document-capture.ts +34 -0
  243. package/src/kit/game-globals-prelude.ts +143 -0
  244. package/src/kit/game-surface-defaults.ts +33 -0
  245. package/src/kit/gameplay-dom-recording.ts +318 -0
  246. package/src/kit/gameplay-export-state.ts +14 -0
  247. package/src/kit/gameplay-replay.ts +417 -0
  248. package/src/kit/gameplay-session-time.ts +9 -0
  249. package/src/kit/gameplay-sessions.ts +204 -0
  250. package/src/kit/hierarchy-component-marks.ts +298 -0
  251. package/src/kit/hierarchy-internals.ts +197 -0
  252. package/src/kit/hierarchy-kind-icon.ts +217 -0
  253. package/src/kit/hierarchy-menu-registry.ts +67 -0
  254. package/src/kit/hierarchy-node-rows.ts +307 -0
  255. package/src/kit/hierarchy-panel-view.ts +280 -0
  256. package/src/kit/hierarchy-projection.ts +76 -0
  257. package/src/kit/hierarchy-row-media.ts +45 -0
  258. package/src/kit/hierarchy-row-model.ts +308 -0
  259. package/src/kit/hierarchy-rows.ts +11 -0
  260. package/src/kit/hierarchy-walk.ts +86 -0
  261. package/src/kit/history/editor-session.ts +25 -0
  262. package/src/kit/history/history-commands.ts +147 -0
  263. package/src/kit/history/history-delegate.ts +187 -0
  264. package/src/kit/history/history-limit-notices.ts +43 -0
  265. package/src/kit/history/history-service.ts +1189 -0
  266. package/src/kit/history/persistence-coordinator.ts +35 -0
  267. package/src/kit/history/project-file-history.ts +386 -0
  268. package/src/kit/history/project-root-history-backends.ts +139 -0
  269. package/src/kit/history/resource-registry.ts +209 -0
  270. package/src/kit/history/snapshot-store.ts +103 -0
  271. package/src/kit/history/source-history-backend.ts +546 -0
  272. package/src/kit/history-types.ts +124 -0
  273. package/src/kit/hmr-registration-group.ts +67 -0
  274. package/src/kit/hmr-stable-react-context.ts +23 -0
  275. package/src/kit/hotkeys.ts +190 -0
  276. package/src/kit/inference-diagnostics.ts +69 -0
  277. package/src/kit/initial-project.ts +80 -0
  278. package/src/kit/inspection/active-subject.ts +571 -0
  279. package/src/kit/inspection/active-surface.ts +142 -0
  280. package/src/kit/inspection/compose-subject.ts +1055 -0
  281. package/src/kit/inspection/compose.ts +7 -0
  282. package/src/kit/inspection/display.ts +171 -0
  283. package/src/kit/inspection/document-subject.ts +109 -0
  284. package/src/kit/inspection/game-subject.ts +85 -0
  285. package/src/kit/inspection/null-subject.ts +119 -0
  286. package/src/kit/inspection/serialize.ts +357 -0
  287. package/src/kit/inspection/use-active-inspection.ts +180 -0
  288. package/src/kit/inspection-model.ts +542 -0
  289. package/src/kit/inspection-node-media.ts +58 -0
  290. package/src/kit/inspector-presentation.ts +203 -0
  291. package/src/kit/inspector-property-grouping.ts +64 -0
  292. package/src/kit/inspector-section-registry.ts +221 -0
  293. package/src/kit/instance-source-actions.ts +163 -0
  294. package/src/kit/js-heap.ts +71 -0
  295. package/src/kit/key-actions.ts +91 -0
  296. package/src/kit/keymap-presets.ts +428 -0
  297. package/src/kit/layout-policy.ts +31 -0
  298. package/src/kit/light-explorer-model.ts +134 -0
  299. package/src/kit/live-canvas-frame.ts +55 -0
  300. package/src/kit/live-document.ts +296 -0
  301. package/src/kit/live-gesture-lock.ts +50 -0
  302. package/src/kit/live-seam-evidence.ts +11 -0
  303. package/src/kit/live-session-registry.ts +220 -0
  304. package/src/kit/live-transition.ts +391 -0
  305. package/src/kit/manifest-project.ts +107 -0
  306. package/src/kit/module-fetch-diagnosis.ts +192 -0
  307. package/src/kit/mount-failure-report.ts +154 -0
  308. package/src/kit/native-selection-style.ts +497 -0
  309. package/src/kit/object3d-document-write-policy.ts +137 -0
  310. package/src/kit/packaged-runtime.ts +108 -0
  311. package/src/kit/palettes/maya.palette.json +57 -0
  312. package/src/kit/palettes/substance.palette.json +57 -0
  313. package/src/kit/performance-profiler.ts +367 -0
  314. package/src/kit/performance-sources.ts +69 -0
  315. package/src/kit/photograph-notice.ts +141 -0
  316. package/src/kit/play-boot-phase.ts +166 -0
  317. package/src/kit/play-camera-flight.ts +35 -0
  318. package/src/kit/png-encode.worker.ts +26 -0
  319. package/src/kit/presentation-surface.ts +248 -0
  320. package/src/kit/product-command.ts +90 -0
  321. package/src/kit/project-adapter.ts +1140 -0
  322. package/src/kit/project-asset-refresh.ts +23 -0
  323. package/src/kit/project-asset-roots.ts +68 -0
  324. package/src/kit/project-local-state.ts +151 -0
  325. package/src/kit/project-manager.ts +243 -0
  326. package/src/kit/project-module-changes.ts +201 -0
  327. package/src/kit/project-module-split.ts +270 -0
  328. package/src/kit/project-play-layers.ts +25 -0
  329. package/src/kit/project-provenance.ts +115 -0
  330. package/src/kit/project-ready.ts +42 -0
  331. package/src/kit/project-shape.ts +68 -0
  332. package/src/kit/project-tools.ts +107 -0
  333. package/src/kit/projection-types.ts +44 -0
  334. package/src/kit/readiness.ts +113 -0
  335. package/src/kit/renderer-resource-counts.ts +27 -0
  336. package/src/kit/reported-play-state.ts +90 -0
  337. package/src/kit/resolve-contributed-command.ts +14 -0
  338. package/src/kit/resolve-relative-specifier.ts +33 -0
  339. package/src/kit/retained-document-states.ts +91 -0
  340. package/src/kit/scene-document-plan.ts +320 -0
  341. package/src/kit/scene-live-open.ts +210 -0
  342. package/src/kit/scoped-game-css.ts +152 -0
  343. package/src/kit/served-url.ts +5 -0
  344. package/src/kit/session-close.ts +17 -0
  345. package/src/kit/session-tombstone.ts +127 -0
  346. package/src/kit/settings/settings-provider.ts +82 -0
  347. package/src/kit/settings-store.ts +348 -0
  348. package/src/kit/shell-document-state.ts +27 -0
  349. package/src/kit/shell-store-door.ts +45 -0
  350. package/src/kit/shell-store.ts +722 -0
  351. package/src/kit/source-conflict.ts +122 -0
  352. package/src/kit/stage-context.ts +377 -0
  353. package/src/kit/stage-invalidation.ts +25 -0
  354. package/src/kit/stage-store-registry.ts +69 -0
  355. package/src/kit/startup-failure.ts +80 -0
  356. package/src/kit/state-report-deferral.ts +73 -0
  357. package/src/kit/storage/host-files-storage.ts +97 -0
  358. package/src/kit/storage/http-storage.ts +174 -0
  359. package/src/kit/storage/index.ts +75 -0
  360. package/src/kit/storage/mem-storage.ts +158 -0
  361. package/src/kit/storage/path-lock.ts +44 -0
  362. package/src/kit/storage/paths.ts +26 -0
  363. package/src/kit/storage-types.ts +127 -0
  364. package/src/kit/stories/StoryPreviewMount.tsx +306 -0
  365. package/src/kit/stories/compose-project-stories.ts +255 -0
  366. package/src/kit/stories/prefabs-finder.ts +54 -0
  367. package/src/kit/stories/prefabs-from-stories.ts +182 -0
  368. package/src/kit/stories/project-story-regions.ts +24 -0
  369. package/src/kit/stories/story-capture.ts +579 -0
  370. package/src/kit/stories/story-declared-medium.ts +126 -0
  371. package/src/kit/stories/story-discovery.ts +176 -0
  372. package/src/kit/stories/story-dom-runtime.ts +78 -0
  373. package/src/kit/stories/story-grouping.ts +111 -0
  374. package/src/kit/stories/story-mount-turn.ts +27 -0
  375. package/src/kit/stories/story-presentation.ts +215 -0
  376. package/src/kit/stories/story-preview-component.ts +7 -0
  377. package/src/kit/stories/story-registry.ts +530 -0
  378. package/src/kit/stories-scope.ts +35 -0
  379. package/src/kit/story-document-openers.ts +36 -0
  380. package/src/kit/story-thumbnails.ts +47 -0
  381. package/src/kit/surface-keyboard.ts +101 -0
  382. package/src/kit/surface-state.ts +135 -0
  383. package/src/kit/system-seam-evidence.ts +72 -0
  384. package/src/kit/tab-census.ts +202 -0
  385. package/src/kit/tab-lifecycle-client.ts +227 -0
  386. package/src/kit/theme-library.ts +897 -0
  387. package/src/kit/theme-preference.ts +429 -0
  388. package/src/kit/three-viewport-presentation.ts +23 -0
  389. package/src/kit/tool-contribution-play.ts +74 -0
  390. package/src/kit/tool-loader.ts +1918 -0
  391. package/src/kit/transform-mode-request.ts +66 -0
  392. package/src/kit/transient-hint.ts +78 -0
  393. package/src/kit/transport-strip.tsx +174 -0
  394. package/src/kit/ui-source/adapter-region-includes.ts +238 -0
  395. package/src/kit/ui-source/file-region-resolver.ts +302 -0
  396. package/src/kit/ui-source/inspect.ts +775 -0
  397. package/src/kit/ui-source/source-write-backend.ts +605 -0
  398. package/src/kit/ui-source/tier-source-write-backend.ts +279 -0
  399. package/src/kit/user-local-state.ts +105 -0
  400. package/src/kit/viewport-activation-timings.ts +840 -0
  401. package/src/kit/viewport-editor-controls.ts +22 -0
  402. package/src/kit/viewport-presentation.ts +668 -0
  403. package/src/kit/viewport-surface-status.tsx +55 -0
  404. package/src/kit/wait-until.ts +37 -0
  405. package/src/kit/worker-call-metrics.ts +166 -0
  406. package/src/kit/workspace-areas.ts +191 -0
  407. package/src/kit/workspace-aux-commands.ts +11 -0
  408. package/src/kit/workspace-available-documents.ts +142 -0
  409. package/src/kit/workspace-core-utilities.ts +31 -0
  410. package/src/kit/workspace-document-ids.ts +59 -0
  411. package/src/kit/workspace-document-registry.ts +624 -0
  412. package/src/kit/workspace-document-restore.ts +146 -0
  413. package/src/kit/workspace-host-commands.ts +141 -0
  414. package/src/kit/workspace-persistence-gate.ts +40 -0
  415. package/src/kit/workspace-play-utilities.ts +44 -0
  416. package/src/kit/workspace-presets.ts +446 -0
  417. package/src/kit/workspace-regions.ts +276 -0
  418. package/src/kit/workspace-static-panels.ts +73 -0
  419. package/src/kit/workspace-status-registry.ts +121 -0
  420. package/src/kit/workspace-storage.ts +35 -0
  421. package/src/kit/workspace-style.ts +226 -0
  422. package/src/kit/workspace-utility-commands.ts +74 -0
  423. package/src/kit/workspace-utility-registry.ts +263 -0
  424. package/src/kit/world-adoption-event.ts +23 -0
  425. package/src/kit/world-adoption.ts +115 -0
  426. package/src/kit/world-canvas-viewport-state.ts +35 -0
  427. package/src/kit/world-document-routing.ts +104 -0
  428. package/src/kit/world-pan-state.ts +198 -0
  429. package/src/kit/write-pipe.ts +173 -0
  430. package/src/layout-arrangements.ts +5 -0
  431. package/src/layouts.tsx +108 -0
  432. package/src/looks.ts +16 -0
  433. package/src/project/output-roots.ts +73 -0
  434. package/src/project/tab-census.ts +155 -0
  435. package/src/project-tool-catalog.ts +104 -0
  436. package/src/selection.tsx +107 -0
  437. package/src/services.ts +18 -0
  438. package/src/session/build-report.ts +22 -0
  439. package/src/session/collaboration-types.ts +262 -0
  440. package/src/session/command-table.ts +327 -0
  441. package/src/session/discovery.ts +100 -0
  442. package/src/session/editor-brand.ts +48 -0
  443. package/src/session/editor-compatibility.ts +329 -0
  444. package/src/session/editor-control-lifecycle.ts +68 -0
  445. package/src/session/editor-control-protocol.ts +5 -0
  446. package/src/session/entrypoint-selection-readers.ts +66 -0
  447. package/src/session/entrypoint-selection-source.ts +120 -0
  448. package/src/session/game-css-scope.ts +30 -0
  449. package/src/session/hosted-attachment.ts +225 -0
  450. package/src/session/limited-view.ts +82 -0
  451. package/src/session/product-create.ts +24 -0
  452. package/src/session/product-locator.ts +478 -0
  453. package/src/session/project-module-url.ts +242 -0
  454. package/src/session/project-serving.ts +164 -0
  455. package/src/session/project-upgrade.ts +669 -0
  456. package/src/session/registry-format.ts +210 -0
  457. package/src/session/relative-path-guard.ts +56 -0
  458. package/src/session/scoped-game-css.ts +461 -0
  459. package/src/session/source-glob.ts +15 -0
  460. package/src/session/tool-contribution-convention.ts +123 -0
  461. package/src/session/workbench-locator.ts +712 -0
  462. package/src/session.ts +41 -0
  463. package/src/share.ts +160 -0
  464. package/src/source-analysis.ts +28 -0
  465. package/src/source-authoring.ts +439 -0
  466. package/src/tools/errors.ts +91 -0
  467. package/src/tools/provider-execution.ts +70 -0
  468. package/src/tools/registry.ts +341 -0
  469. package/src/tools/types.ts +159 -0
  470. package/src/transport.ts +100 -0
  471. package/src/types.ts +1693 -0
  472. package/src/views.ts +164 -0
  473. package/src/widgets/design-system.ts +93 -0
  474. package/src/widgets/editor-appearance.ts +151 -0
  475. package/src/widgets/editor-material.ts +83 -0
  476. package/src/widgets/icon-set-registry.ts +105 -0
  477. package/src/widgets/index.ts +71 -0
  478. package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
  479. package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
  480. package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
  481. package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
  482. package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
  483. package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
  484. package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
  485. package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
  486. package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
  487. package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
  488. package/src/widgets/inspector-widgets/color-utils.ts +201 -0
  489. package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
  490. package/src/widgets/inspector-widgets/index.ts +25 -0
  491. package/src/widgets/inspector-widgets/shared.tsx +140 -0
  492. package/src/widgets/interactive-edit-scope.ts +33 -0
  493. package/src/widgets/patterns/Dialog.tsx +140 -0
  494. package/src/widgets/patterns/Fields.tsx +44 -0
  495. package/src/widgets/patterns/List.tsx +25 -0
  496. package/src/widgets/patterns/StateSurface.tsx +40 -0
  497. package/src/widgets/patterns/Surfaces.tsx +122 -0
  498. package/src/widgets/patterns/Tabs.tsx +80 -0
  499. package/src/widgets/patterns/Toolbar.tsx +72 -0
  500. package/src/widgets/patterns/Tree.tsx +72 -0
  501. package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
  502. package/src/widgets/primitives/Button.tsx +62 -0
  503. package/src/widgets/primitives/ColorInput.tsx +78 -0
  504. package/src/widgets/primitives/DraftTextInput.tsx +63 -0
  505. package/src/widgets/primitives/EditorIcon.tsx +157 -0
  506. package/src/widgets/primitives/FormControls.tsx +88 -0
  507. package/src/widgets/primitives/HoverPreview.tsx +96 -0
  508. package/src/widgets/primitives/JsonInput.tsx +113 -0
  509. package/src/widgets/primitives/Layout.tsx +100 -0
  510. package/src/widgets/primitives/Menu.tsx +161 -0
  511. package/src/widgets/primitives/NumberInput.tsx +169 -0
  512. package/src/widgets/primitives/Panel.tsx +80 -0
  513. package/src/widgets/primitives/SectionHeader.tsx +77 -0
  514. package/src/widgets/primitives/Text.tsx +54 -0
  515. package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
  516. package/src/widgets/primitives/Tooltip.tsx +204 -0
  517. package/src/widgets/primitives/Vec3Input.tsx +70 -0
  518. package/src/widgets/primitives/banner-tones.ts +32 -0
  519. package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
  520. package/src/widgets/primitives/editor-icons.ts +254 -0
  521. package/src/widgets/primitives/panel-header-styles.ts +42 -0
  522. package/src/widgets/theme.ts +2841 -0
  523. package/src/widgets/z-index.ts +25 -0
package/src/client.ts ADDED
@@ -0,0 +1,1646 @@
1
+ import type { GenerationJobsDocument } from '@volter/sdk/generations';
2
+ import type { Dispatcher } from 'undici';
3
+ import { createDispatcher, dispatchFetch } from '#http-transport';
4
+ import type { DocumentProbeResult, DocumentProbeStep } from './document-probe.js';
5
+ import type {
6
+ ActiveDocumentCapture,
7
+ AssetCompareCapture,
8
+ AssetCompareOptions,
9
+ AssetKind,
10
+ AssetPreviewCapture,
11
+ AssetPreviewOptions,
12
+ AssetPreviewShotSetDefinition,
13
+ AssetPreviewSource,
14
+ CaptureDimensions,
15
+ DocumentCameraPose,
16
+ DocumentLookOutcome,
17
+ StageFrameCostReading,
18
+ DocumentTableProjection,
19
+ EditorChromeCapture,
20
+ EditorChromeCaptureOptions,
21
+ EditorState,
22
+ EditorView,
23
+ EditorWorkspaceName,
24
+ GameCapture,
25
+ GameplayRecordingCapture,
26
+ GameplayRecordingOptions,
27
+ GameplayRecordingStarted,
28
+ GameplayRecordingTimeline,
29
+ GameplayReplayCapture,
30
+ HelperVisibility,
31
+ InspectedFieldWrite,
32
+ InspectedHierarchy,
33
+ InspectedInspection,
34
+ LabeledShotSetCapture,
35
+ ModelPlayLogReading,
36
+ PlayStarted,
37
+ PresentedEditorView,
38
+ ProjectInfo,
39
+ ProjectTemplate,
40
+ ProjectToolCatalog,
41
+ ProjectToolOutcome,
42
+ RecentProject,
43
+ ShadingMode,
44
+ StoryCaptureOptions,
45
+ StoryVariantCapture,
46
+ StructureOp,
47
+ StructureOpOptions,
48
+ StructureOpResult,
49
+ TransformMode,
50
+ TransformSpace,
51
+ Vec3Value,
52
+ ViewPreset,
53
+ ViewportCapture,
54
+ ViewportTab,
55
+ } from './types.js';
56
+
57
+ // The editor's default origin, spelled out because this package deliberately
58
+ // does not depend on `@volter/project`. `DEFAULT_EDITOR_PORT` in
59
+ // `packages/project/src/manifest/editor-port.ts` is the owner of the number and
60
+ // of the never-`localhost` rule; keep this in step with it.
61
+ const DEFAULT_URL = 'http://127.0.0.1:20173';
62
+
63
+ /**
64
+ * A refused editor command, carrying the relay's own STRUCTURED failure code
65
+ * alongside the prose.
66
+ *
67
+ * `/__editor/command` has always answered `{ ok: false, error, code }` and
68
+ * this client has always dropped the `code` on the floor, so every caller that
69
+ * wanted to react to a specific refusal had to substring-match an English
70
+ * sentence. the editor's `screenshot` command's loop-recovery fallback is the first caller that
71
+ * genuinely must branch (`BRIDGE_SCREENSHOT_STALE` has a working recovery;
72
+ * "not in play mode" does not), and a fallback keyed on prose would fire on
73
+ * the wrong failure the first time someone rewords the message.
74
+ */
75
+ export class EditorCommandError extends Error {
76
+ readonly code: string | undefined;
77
+ /**
78
+ * True when the RELAY ended the command itself rather than the editor
79
+ * answering it — the HTTP 504 that `server/server-utils.ts`'s
80
+ * `commandResponseFor` gives any `timedOut` result, or this client's own
81
+ * deadline below.
82
+ *
83
+ * Read it as "no answer", not as "the budget expired". `editor-server.ts`
84
+ * raises `timedOut` for five conditions and only one of them takes the full
85
+ * budget: the command's timer expiring, the controlling tab's socket dying,
86
+ * the receipt window closing unanswered, a beating-but-dead tab, and no tab
87
+ * present at all. The last four can fail in milliseconds.
88
+ *
89
+ * A caller that converges by retrying (the editor's `restart` command) needs the distinction
90
+ * because a refusal the editor ANSWERED may go differently next time, while
91
+ * a command the relay abandoned tells you nothing new on a second identical
92
+ * attempt — and when the abandonment was a 120s budget, re-running it three
93
+ * times is `restart-readiness.ts`'s 361-seconds-of-silence defect.
94
+ */
95
+ readonly timedOut: boolean;
96
+ constructor(message: string, code?: string | undefined, timedOut = false) {
97
+ super(message);
98
+ this.name = 'EditorCommandError';
99
+ this.code = code;
100
+ this.timedOut = timedOut;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * The client's own ceiling on ONE relayed command.
106
+ *
107
+ * A backstop for a LOST server, not a per-command budget: the server already
108
+ * owns per-type budgets (`server/server-utils.ts`'s `relayCommandTimeoutMs`)
109
+ * and its timer must always be the one that fires, because its message names
110
+ * the tab and the remedy while this one can only say "no answer". So this is
111
+ * deliberately ONE number, comfortably above the longest server budget
112
+ * (120s, `play`/`capture-story-variants`) rather than a mirror of that table —
113
+ * a second copy of it would drift silently, and the drift would show up as
114
+ * this timer winning a race it must always lose.
115
+ *
116
+ * Without it a `fetch` with no `AbortSignal` waits on the OS: a dev server
117
+ * that stops answering mid-command holds the CLI open indefinitely, with no
118
+ * output and nothing to read.
119
+ */
120
+ const COMMAND_DEADLINE_MS = 150_000;
121
+ /** The Blender lane's own ceiling: a chunk is a whole modeling step, not a tick. */
122
+ const BLENDER_DEADLINE_MS = 30 * 60_000;
123
+ /** undici's default `headersTimeout`; a command deadline beyond it needs its own dispatcher (see `command`). */
124
+ const UNDICI_DEFAULT_HEADERS_TIMEOUT_MS = 300_000;
125
+
126
+ /**
127
+ * Ceiling on {@link EditorClient.getUnresolvedConsole}. The CLI drains this
128
+ * on every verb, including ones that never wait for a command envelope, so
129
+ * a silent hang here would become a silent hang on the editor's `sessions` command. The
130
+ * server route is a plain in-process GET; 1.5s is already longer than it
131
+ * should ever take.
132
+ */
133
+ const CONSOLE_DRAIN_TIMEOUT_MS = 1_500;
134
+
135
+ /**
136
+ * Node's `fetch` collapses EVERY network-layer failure into one two-word
137
+ * `TypeError: fetch failed`. The real reason — `ECONNREFUSED`, `ECONNRESET`,
138
+ * `EPIPE`, a DNS miss — lives only on `error.cause` (sometimes two links down,
139
+ * or inside an `AggregateError`), and nothing prints it unless something walks
140
+ * the chain. That is the whole reason `project.bake.preview` was observed
141
+ * failing with a bare "fetch failed" and no way to tell a dead editor from a
142
+ * momentary one (WORK.md, cold barrel 2026-08-29): the code below used to
143
+ * rethrow the `TypeError` untouched, on the belief — stated in a comment right
144
+ * where it happened — that "a connection refused / DNS failure still surfaces
145
+ * as itself". It does not. This walks the chain so the message can say which.
146
+ */
147
+ function describeFetchFailure(error: unknown): { code: string; detail: string } {
148
+ const messages: string[] = [];
149
+ let current: unknown = error;
150
+ for (let depth = 0; depth < 8; depth++) {
151
+ if (!(current instanceof Error)) break;
152
+ if (current.message) messages.push(current.message);
153
+ const code = (current as { code?: unknown }).code;
154
+ if (typeof code === 'string' && code !== '') {
155
+ return { code, detail: messages.join(' <- ') };
156
+ }
157
+ const aggregate = (current as { errors?: unknown }).errors;
158
+ if (Array.isArray(aggregate) && aggregate.length > 0) {
159
+ const inner = describeFetchFailure(aggregate[0]);
160
+ return { code: inner.code, detail: [...messages, inner.detail].join(' <- ') };
161
+ }
162
+ current = (current as { cause?: unknown }).cause;
163
+ }
164
+ return { code: 'UNKNOWN', detail: messages.join(' <- ') || String(error) };
165
+ }
166
+
167
+ /**
168
+ * Transport failures where a second attempt is worth making: the connection
169
+ * itself failed or died, rather than the editor answering something unwelcome.
170
+ * A dev server that is restarting (any watched source edit restarts it) is
171
+ * unreachable for a fraction of a second and reachable again after — which is
172
+ * exactly the "fails, then succeeds unchanged" shape that was reported.
173
+ */
174
+ const RETRYABLE_TRANSPORT_CODES = new Set([
175
+ 'ECONNREFUSED',
176
+ 'ECONNRESET',
177
+ 'EPIPE',
178
+ 'ETIMEDOUT',
179
+ 'EHOSTUNREACH',
180
+ 'UND_ERR_SOCKET',
181
+ 'UND_ERR_CONNECT_TIMEOUT',
182
+ ]);
183
+
184
+ /** Gap before the one automatic retry — long enough for a dev-server restart's
185
+ * listen socket to come back, short enough to stay invisible. */
186
+ const TRANSPORT_RETRY_DELAY_MS = 400;
187
+
188
+ /** Context accompanying one {@link EditorClient} response observation. */
189
+ export interface EditorEnvelopeObservation {
190
+ /** True only when this body came from the console-ledger endpoint and its
191
+ * `entries` field is therefore the complete named console set. Command
192
+ * payloads may also own an unrelated `entries` field. */
193
+ readonly unresolvedConsoleComplete: boolean;
194
+ }
195
+
196
+ /**
197
+ * The GAME DEBUG PLANE, as a contribution's client sees it.
198
+ *
199
+ * Deliberately the same two words `@volter/live`'s session binding uses
200
+ * (`game.state(name)` / `game.command(name, ...args)`), because it is the same
201
+ * plane: whatever the running game registered through `ctx.debug` — a provider
202
+ * read by name, a command invoked by name. A tool contribution that wants the
203
+ * game's own vitals in its panel has this door and no other; there is
204
+ * deliberately no per-capability method, because a game names its own
205
+ * providers and commands.
206
+ *
207
+ * Both legs reject LOUDLY (`EditorCommandError`) rather than answer with a
208
+ * placeholder: play not running is `'not in play mode — start play before
209
+ * using the debug seam'`, and an unregistered command carries
210
+ * `code: 'DEBUG_COMMAND_NOT_REGISTERED'`. A panel decides what to show for
211
+ * those; the client never invents one.
212
+ */
213
+ /** What one undo/redo step reports back — `moved` is false when there was
214
+ * nothing left in that direction, which is an answer, not an error. */
215
+ export interface HistoryStep {
216
+ readonly moved: boolean;
217
+ readonly canUndo: boolean;
218
+ readonly canRedo: boolean;
219
+ readonly undoLabel: string | null;
220
+ readonly redoLabel: string | null;
221
+ }
222
+
223
+ /** What `open()` acknowledges: the workspace document the scene-table entry
224
+ * resolved to, and the title its tab now carries — the game's own word for
225
+ * that composition, not a filename. */
226
+ export interface OpenedDocument {
227
+ readonly documentId: string;
228
+ readonly title: string;
229
+ /**
230
+ * The GAME's own answer, present only when opening navigated a running game
231
+ * (a scene the adapter declares reachable through the game's scenes
232
+ * contract, or a native swap-slot remount): what was asked for, and which
233
+ * scene the game reports it is in once its own navigation settled. `current`
234
+ * can differ from `requested` — that is the game's reading, not a host claim.
235
+ */
236
+ readonly scene?: { readonly requested: string; readonly current: string | null };
237
+ /**
238
+ * Present when opening restarted play at a native swap-slot key rather than
239
+ * navigating a live contract — the slot is a module-level const.
240
+ */
241
+ readonly restart?: true;
242
+ }
243
+
244
+ export interface GameDebugDoor {
245
+ /**
246
+ * Read ONE registered state provider by name (`'bot.tester'`). `undefined`
247
+ * when the running game registered no such provider — or no debug adapter at
248
+ * all, which is an honest answer rather than a refusal.
249
+ */
250
+ state(name: string): Promise<unknown>;
251
+ /** Invoke ONE registered debug command by name, with its own arguments. */
252
+ command(name: string, ...args: unknown[]): Promise<unknown>;
253
+ }
254
+
255
+ export class EditorClient {
256
+ private readonly baseUrl: string;
257
+ private readonly fetchOverride: typeof fetch | undefined;
258
+
259
+ /**
260
+ * The running game's debug plane — see {@link GameDebugDoor}. It rides the
261
+ * SAME `/__editor/command` relay every other method here uses (relay cases
262
+ * `inspect-gameplay-state` / `invoke-debug-command`, `command-listener.ts`),
263
+ * so a tool contribution reaches the game through the client it already has
264
+ * rather than a second channel of its own.
265
+ */
266
+ readonly game: GameDebugDoor;
267
+
268
+ /**
269
+ * Called with the raw body of EVERY response this client receives — command
270
+ * envelopes and `/__editor/state` alike, on success AND on refusal.
271
+ *
272
+ * It exists for exactly one contract: the server stamps `unresolvedConsole`
273
+ * onto every envelope (`server-utils.ts`'s `commandResponseFor`), and the CLI
274
+ * has to see those counts to be loud about them. Routing that through a
275
+ * single observer here — rather than teaching each of the CLI's output sites
276
+ * to unpack a response — is what keeps the loudness contract ONE mechanism.
277
+ * The observer must not throw; anything it raises is swallowed, because a
278
+ * reporting hook may never break the command it is reporting on.
279
+ */
280
+ private readonly transport:
281
+ | ((body: Record<string, unknown>) => Promise<Record<string, unknown>>)
282
+ | null;
283
+ private readonly onEnvelope:
284
+ | ((body: unknown, observation: EditorEnvelopeObservation) => void)
285
+ | null;
286
+
287
+ constructor(opts?: {
288
+ url?: string;
289
+ /** An authenticated transport for the same HTTP routes (e.g. a hosted tab). */
290
+ fetch?: typeof fetch;
291
+ onEnvelope?: (body: unknown, observation: EditorEnvelopeObservation) => void;
292
+ /**
293
+ * An IN-PAGE command channel, for a client that lives inside the editor
294
+ * page itself and has no editor server to reach. Given, every command goes through it instead of
295
+ * `POST /__editor/command`, and answers in the route's own body shape
296
+ * (`{ ok: true, ...data }` / `{ ok: false, error, code? }`).
297
+ */
298
+ transport?: (body: Record<string, unknown>) => Promise<Record<string, unknown>>;
299
+ }) {
300
+ if (opts !== undefined && (typeof opts !== 'object' || opts === null || Array.isArray(opts))) {
301
+ throw new TypeError(
302
+ 'EditorClient options must be an object. Use new EditorClient({ url: "http://127.0.0.1:20173" }), not new EditorClient("...").',
303
+ );
304
+ }
305
+ const unknownOptions = Object.keys(opts ?? {}).filter(
306
+ (key) => key !== 'url' && key !== 'onEnvelope' && key !== 'transport' && key !== 'fetch',
307
+ );
308
+ if (unknownOptions.length > 0) {
309
+ throw new Error(
310
+ `EditorClient: unknown option${unknownOptions.length === 1 ? '' : 's'} ${unknownOptions.map((key) => `"${key}"`).join(', ')}. Use { url: "http://127.0.0.1:<port>" } to target an editor.`,
311
+ );
312
+ }
313
+ this.baseUrl = (opts?.url ?? DEFAULT_URL).replace(/\/$/, '');
314
+ this.fetchOverride = opts?.fetch;
315
+ this.onEnvelope = opts?.onEnvelope ?? null;
316
+ this.transport = opts?.transport ?? null;
317
+ this.game = {
318
+ state: async (name: string): Promise<unknown> => {
319
+ // `keys` narrows the relay to the one provider asked for, so a panel
320
+ // polling one vital never drags the whole plane's `stateAll()` across
321
+ // the wire. A game with no debug adapter answers `{ state: null }`.
322
+ const data = await this.command<{ state: Record<string, unknown> | null }>({
323
+ type: 'inspect-gameplay-state',
324
+ keys: [name],
325
+ });
326
+ return data.state?.[name];
327
+ },
328
+ command: async (name: string, ...args: unknown[]): Promise<unknown> =>
329
+ (await this.command<{ result: unknown }>({ type: 'invoke-debug-command', name, args }))
330
+ .result,
331
+ };
332
+ }
333
+
334
+ /**
335
+ * `retryTransport` opts a command into ONE automatic retry after a transport
336
+ * failure (see {@link RETRYABLE_TRANSPORT_CODES}). It is deliberately
337
+ * OPT-IN and off by default: a socket that died after the request was written
338
+ * cannot prove the editor did not already run the command, so a blanket retry
339
+ * would risk playing/stopping/writing twice. Read-only relays — the captures —
340
+ * have no such hazard and turn it on.
341
+ */
342
+ private async command<T extends object = Record<string, never>>(
343
+ body: Record<string, unknown>,
344
+ options?: { retryTransport?: boolean; deadlineMs?: number },
345
+ ): Promise<T> {
346
+ const type = String(body['type'] ?? 'command');
347
+ const deadlineMs = options?.deadlineMs ?? COMMAND_DEADLINE_MS;
348
+ if (this.transport) {
349
+ const answered = (await this.transport(body)) as {
350
+ ok: boolean;
351
+ error?: string;
352
+ code?: string;
353
+ } & T;
354
+ if (!answered.ok) {
355
+ throw new EditorCommandError(
356
+ answered.error ?? `Editor command "${type}" failed`,
357
+ answered.code,
358
+ );
359
+ }
360
+ return answered;
361
+ }
362
+ const url = `${this.baseUrl}/__editor/command`;
363
+ const request = JSON.stringify(body);
364
+ let res: Response | undefined;
365
+ let retried = false;
366
+ let commandDispatcher: Dispatcher | undefined;
367
+ for (;;) {
368
+ try {
369
+ const init: RequestInit = {
370
+ method: 'POST',
371
+ headers: { 'Content-Type': 'application/json' },
372
+ body: request,
373
+ signal: AbortSignal.timeout(deadlineMs),
374
+ };
375
+ // The command route answers only when the command completes, and
376
+ // Node's fetch (undici) gives a server 300 s to send response HEADERS
377
+ // regardless of the abort signal: a modeling step measured at eleven
378
+ // minutes in the tab died as UND_ERR_HEADERS_TIMEOUT well inside its
379
+ // half-hour budget. A deadline past that default carries its own
380
+ // dispatcher, through undici's own fetch so the two agree.
381
+ commandDispatcher = deadlineMs > UNDICI_DEFAULT_HEADERS_TIMEOUT_MS
382
+ ? createDispatcher(deadlineMs + 30_000)
383
+ : undefined;
384
+ res = this.fetchOverride ? await this.fetchOverride(url, init) : await dispatchFetch(url, init, commandDispatcher);
385
+ break;
386
+ } catch (error) {
387
+ await commandDispatcher?.destroy?.();
388
+ commandDispatcher = undefined;
389
+ // Only the deadline is reshaped into a "no answer" verdict; everything
390
+ // else is a TRANSPORT failure, and Node hides its reason behind a bare
391
+ // `fetch failed` (see `describeFetchFailure`).
392
+ if ((error as { name?: string } | null)?.name === 'TimeoutError') {
393
+ throw new EditorCommandError(
394
+ `The editor at ${this.baseUrl} never answered "${type}" ` +
395
+ `within ${Math.round(deadlineMs / 1000)}s — past every server-side budget, so ` +
396
+ 'the server itself is not answering. Check the terminal that started this editor session.',
397
+ undefined,
398
+ true,
399
+ );
400
+ }
401
+ const { code, detail } = describeFetchFailure(error);
402
+ if (options?.retryTransport === true && !retried && RETRYABLE_TRANSPORT_CODES.has(code)) {
403
+ retried = true;
404
+ await new Promise((resolve) => setTimeout(resolve, TRANSPORT_RETRY_DELAY_MS));
405
+ continue;
406
+ }
407
+ throw new EditorCommandError(
408
+ `POST ${url} ("${type}") never reached the editor: ${code}${detail ? ` (${detail})` : ''}.` +
409
+ (retried
410
+ ? ` Retried once after ${TRANSPORT_RETRY_DELAY_MS}ms; it failed the same way.`
411
+ : '') +
412
+ (options?.retryTransport === true
413
+ ? ''
414
+ : ' Not retried automatically: this command can change editor state, and a socket that' +
415
+ ' died after the request was written cannot prove the editor did not already run it.') +
416
+ ' A transport failure means the port stopped answering, not that the editor refused —' +
417
+ ' the dev server restarts on any watched source edit, and it shuts itself down after an' +
418
+ ' idle window. Check the terminal that started this editor session and confirm the port' +
419
+ ' this client resolved.',
420
+ code,
421
+ false,
422
+ );
423
+ }
424
+ }
425
+ let data: { ok: boolean; error?: string; code?: string } & T;
426
+ try {
427
+ data = (await this.readJson(res)) as typeof data;
428
+ } finally {
429
+ await commandDispatcher?.destroy?.();
430
+ }
431
+ if (!data.ok) {
432
+ throw new EditorCommandError(
433
+ data.error ?? `Editor command failed: ${res.status}`,
434
+ data.code,
435
+ // 504 is every `timedOut` result (`commandResponseFor`) — the relay
436
+ // gave up, on any of its five grounds. A 200 body with `ok: false` is
437
+ // an ANSWER from the editor, however unwelcome. See `timedOut` above.
438
+ res.status === 504,
439
+ );
440
+ }
441
+ return data;
442
+ }
443
+
444
+ // --- Play control ---
445
+
446
+ /** `opts.seed` (D15/T-D15.6, objection-4 fix) — the editor's `play --seed <n>` command's
447
+ * explicit config leg, relayed as `cmd['seed']`; `handleCommand`'s
448
+ * `'play'` case threads it into `enterPlayMode`'s highest-precedence seed
449
+ * argument (beats manifest.determinism.defaultSeed/?volter-seed=). Omitted,
450
+ * boot seeding falls back to that precedence unchanged.
451
+ *
452
+ * `opts.name` (the editor's `play --name <text>` command) — an OPTIONAL label for this run,
453
+ * relayed as `cmd['name']` and slugified server-side into the run's
454
+ * `logs/play-*.jsonl` filename and its session-journal line. Findability
455
+ * only: no registry, no uniqueness, no lookup verb — grep and `ls` are the
456
+ * query engine. Omitted, the filename keeps its exact unnamed shape. */
457
+ /* `opts.record` (the editor's `play --record <name>` command) — NAMES this run's recording
458
+ * file. It does not ENABLE recording: every relayed play records, with no
459
+ * flag (see `@volter/editor-game`'s `src/play/play-recording.ts`). Omitted, the clip is named for
460
+ * the durable Gameplay Session; named, it becomes an explicit keepsake in
461
+ * `.volter/recordings/<name>.webm`. */
462
+ async play(opts?: {
463
+ seed?: number;
464
+ name?: string | null;
465
+ record?: string | null;
466
+ }): Promise<PlayStarted> {
467
+ return this.command<PlayStarted>({
468
+ type: 'play',
469
+ ...(opts?.seed !== undefined ? { seed: opts.seed } : {}),
470
+ ...(opts?.name ? { name: opts.name } : {}),
471
+ ...(opts?.record ? { record: opts.record } : {}),
472
+ });
473
+ }
474
+
475
+ /** Dispose the current play session and mount it again from fresh project entry source. */
476
+ async restart(): Promise<void> {
477
+ await this.command({ type: 'play' });
478
+ }
479
+
480
+ /** Refuse shutdown if a document cannot be saved. Does not stop the server. */
481
+ async prepareClose(): Promise<void> {
482
+ await this.command({ type: 'session-prepare-close' });
483
+ }
484
+
485
+ /** Stops play, and finalizes this run's recording before the surface it was
486
+ * photographing is torn down. The capture is absent when nothing recorded. */
487
+ async stop(): Promise<{ recording?: GameplayRecordingCapture }> {
488
+ return this.command<{ recording?: GameplayRecordingCapture }>({ type: 'stop' });
489
+ }
490
+
491
+ async pause(): Promise<void> {
492
+ await this.command({ type: 'pause' });
493
+ }
494
+
495
+ async resume(): Promise<void> {
496
+ await this.command({ type: 'resume' });
497
+ }
498
+
499
+ async step(): Promise<void> {
500
+ await this.command({ type: 'step' });
501
+ }
502
+
503
+ // --- Selection ---
504
+
505
+ async select(id: string | null): Promise<void> {
506
+ await this.command({ type: 'select', id });
507
+ }
508
+
509
+ async selectMultiple(ids: string[]): Promise<void> {
510
+ await this.command({ type: 'select-multiple', ids });
511
+ }
512
+
513
+ async selectAll(): Promise<void> {
514
+ await this.command({ type: 'select-all' });
515
+ }
516
+
517
+ // --- Viewport ---
518
+
519
+ async focusEntity(id: string): Promise<void> {
520
+ await this.command({ type: 'focus-entity', id });
521
+ }
522
+
523
+ async focusSelection(): Promise<void> {
524
+ await this.command({ type: 'focus-selection' });
525
+ }
526
+
527
+ /**
528
+ * Frame the EDIT viewport camera on one entity — the strict sibling of
529
+ * {@link focusEntity}. Same framing; an id the scene does not know is a
530
+ * refusal naming the id (`EditorCommandError`, code `ENTITY_NOT_FOUND`)
531
+ * rather than `focusEntity`'s silent no-op, so a caller that frames an
532
+ * entity before capturing it cannot photograph the wrong thing.
533
+ */
534
+ async frameEntity(id: string): Promise<void> {
535
+ await this.command({ type: 'frame-entity', id });
536
+ }
537
+
538
+ async viewPreset(preset: ViewPreset): Promise<void> {
539
+ await this.command({ type: 'view-preset', preset });
540
+ }
541
+
542
+ /**
543
+ * LOOK AROUND THE OPEN MODEL, visibly. Swings the active Object3D
544
+ * document's camera — the one on the human's screen — by `azimuth`/
545
+ * `elevation` radians, animated over `duration` seconds, and resolves when
546
+ * the move ends. A human drag during the move cancels it where it stands
547
+ * (`cancelledBy: 'human'`); the promise still resolves.
548
+ */
549
+ async orbitDocument(options: {
550
+ azimuth?: number;
551
+ elevation?: number;
552
+ duration?: number;
553
+ }): Promise<DocumentLookOutcome> {
554
+ return this.command<DocumentLookOutcome>({ type: 'document-orbit', ...options });
555
+ }
556
+
557
+ /** What a frame of a 3D document's stage costs, uncapped (`document-frame-cost`). */
558
+ async frameCostDocument(options?: { readonly frames?: number; readonly stage?: string; readonly quality?: 'full' | 'navigation' }): Promise<StageFrameCostReading> {
559
+ return this.command<StageFrameCostReading>({ type: 'document-frame-cost', ...options });
560
+ }
561
+
562
+ /** A slow full revolution around the open document's subject, at a constant rate. */
563
+ async turntableDocument(options?: {
564
+ seconds?: number;
565
+ revolutions?: number;
566
+ }): Promise<DocumentLookOutcome> {
567
+ return this.command<DocumentLookOutcome>({ type: 'document-turntable', ...options });
568
+ }
569
+
570
+ /**
571
+ * Frame the open document's subject (its selection if it has one). `fit`
572
+ * scales the fitted distance: 1 is the toolbar Frame button's tight fit.
573
+ */
574
+ async frameDocument(fit?: number): Promise<DocumentCameraPose> {
575
+ return this.command<DocumentCameraPose>({
576
+ type: 'document-frame',
577
+ ...(fit === undefined ? {} : { fit }),
578
+ });
579
+ }
580
+
581
+ async setCamera(position: Vec3Value, target: Vec3Value, fov?: number): Promise<void> {
582
+ await this.command({
583
+ type: 'set-camera',
584
+ position,
585
+ target,
586
+ ...(fov === undefined ? {} : { fov }),
587
+ });
588
+ }
589
+
590
+ async captureViewport(size?: number): Promise<ViewportCapture> {
591
+ const data = await this.command<ViewportCapture>({
592
+ type: 'capture-viewport',
593
+ ...(size === undefined ? {} : { size }),
594
+ });
595
+ return { base64: data.base64, mimeType: data.mimeType };
596
+ }
597
+
598
+ /**
599
+ * Unit 4 (live-front-door wave) — capture the RUNNING GAME (the editor's
600
+ * `screenshot`'s wire leg). Sends the SAME `bridge-screenshot` relay op
601
+ * `@volter/game-live`'s `RelayTransport.screenshot` (and therefore
602
+ * `game.screenshot()` on the relay path) already sends, so all three
603
+ * surfaces composite the identical full game stack — canvas(es) plus the
604
+ * HUD/react DOM layers — rather than any of them inventing a second,
605
+ * subtly-different capture path. Contrast {@link captureViewport}, which
606
+ * captures the EDITOR viewport's canvas and would silently hand back an
607
+ * editor-only (HUD-less, possibly not-even-playing) image.
608
+ *
609
+ * Rejects — loudly, via `command`'s own `{ok:false}` unwrap — when play
610
+ * mode isn't running ("not in play mode — start play before using the
611
+ * debug seam") or no game canvas is mounted yet. Never returns a blank or
612
+ * editor-only frame as a stand-in.
613
+ *
614
+ * `opts.refreshStarvedFrame` is the loop-starvation leg: without recent rAF
615
+ * progress the canvas holds a provably stale frame and the relay
616
+ * refuses it with `BRIDGE_SCREENSHOT_STALE` rather than pass it off as
617
+ * current. Setting this asks the relay to render exactly ONE deterministic
618
+ * tick (`runTicks(1, {render:'last'})`) first — the same escape
619
+ * `@volter/game-live`'s `RelayTransport.screenshot` has always used, which is why
620
+ * the editor's `eval` command could recover these frames while the editor's `screenshot` command could not.
621
+ * Off by default: a caller who does not ask must never be handed a frame
622
+ * that only exists because the capture drove the game.
623
+ */
624
+ async captureGame(opts?: { refreshStarvedFrame?: boolean }): Promise<GameCapture> {
625
+ const data = await this.command<GameCapture>({
626
+ type: 'bridge-screenshot',
627
+ ...(opts?.refreshStarvedFrame === true ? { refreshStarvedFrame: true } : {}),
628
+ });
629
+ const layers = data.layers;
630
+ const flatness = data.flatness;
631
+ return {
632
+ base64: data.base64,
633
+ mimeType: data.mimeType,
634
+ composite: data.composite === true,
635
+ ...(layers && Number.isInteger(layers.canvases) && Number.isInteger(layers.domOverlays)
636
+ ? { layers }
637
+ : {}),
638
+ // Pass the pixel-honesty fields through as the page reported them: the
639
+ // warning sentence is written where the pixels are, so nothing here
640
+ // re-derives (or softens) it.
641
+ ...(flatness && typeof flatness.dominantFraction === 'number' ? { flatness } : {}),
642
+ ...(data.loopRecoveryFrame === true ? { loopRecoveryFrame: true } : {}),
643
+ // Same pass-through rule: the recorded-run notice is written where the
644
+ // pixels are, so nothing here re-derives or softens it.
645
+ ...(data.recording && typeof data.recording.notice === 'string'
646
+ ? { recording: data.recording }
647
+ : {}),
648
+ };
649
+ }
650
+
651
+ /** Start recording the same clean running-game composite `captureGame`
652
+ * photographs. Recording state lives in the editor page, so another process
653
+ * may stop it later through the same project session. */
654
+ async startGameplayRecording(
655
+ options: GameplayRecordingOptions = {},
656
+ ): Promise<GameplayRecordingStarted> {
657
+ return this.command<GameplayRecordingStarted>({
658
+ type: 'bridge-recording-start',
659
+ ...(options.fps !== undefined ? { fps: options.fps } : {}),
660
+ ...(options.name !== undefined ? { name: options.name } : {}),
661
+ ...(options.format !== undefined ? { format: options.format } : {}),
662
+ });
663
+ }
664
+
665
+ /** Export a paused run as fixed-step video. Advances game state; maximum
666
+ * five minutes. `audio` describes the muxed track, or is `false` when the
667
+ * world implements no `AudioAdapter.renderOffline` and the file is
668
+ * genuinely silent — read it, never assume either. */
669
+ async exportGameplayVideo(options: { frames: number; fps?: number; name?: string }): Promise<{
670
+ path: string;
671
+ frames: number;
672
+ fps: number;
673
+ durationMs: number;
674
+ wallMs: number;
675
+ width: number;
676
+ height: number;
677
+ audio:
678
+ | false
679
+ | {
680
+ codec: 'opus';
681
+ sampleRate: number;
682
+ channels: number;
683
+ durationSeconds: number;
684
+ rms: number;
685
+ peak: number;
686
+ };
687
+ }> {
688
+ return this.command(
689
+ { type: 'bridge-recording-export', ...options },
690
+ { deadlineMs: 610_000, retryTransport: false },
691
+ );
692
+ }
693
+
694
+ /** Stop the page-owned recorder and return its WebM path and metadata. */
695
+ async stopGameplayRecording(): Promise<GameplayRecordingCapture> {
696
+ return this.command<GameplayRecordingCapture>({ type: 'bridge-recording-stop' });
697
+ }
698
+
699
+ /** Read the active capture's monotonic media position. This is the only clock
700
+ * suitable for selecting intervals inside the finalized recording. */
701
+ async getGameplayRecordingTimeline(): Promise<GameplayRecordingTimeline> {
702
+ const timeline = await this.command<GameplayRecordingTimeline>({
703
+ type: 'bridge-recording-timeline',
704
+ });
705
+ return { startedAt: timeline.startedAt, elapsedMs: timeline.elapsedMs };
706
+ }
707
+
708
+ /** Encode a recorded canvas/DOM interval into a normal composite WebM. */
709
+ async exportGameplayReplay(options: {
710
+ replayPath: string;
711
+ fps?: number;
712
+ startMs?: number;
713
+ endMs?: number;
714
+ name?: string;
715
+ }): Promise<{
716
+ path: string;
717
+ frames: number;
718
+ fps: number;
719
+ durationMs: number;
720
+ width: number;
721
+ height: number;
722
+ audio: boolean;
723
+ }> {
724
+ return this.command(
725
+ { type: 'bridge-recording-replay-export', ...options },
726
+ { deadlineMs: 610_000, retryTransport: false },
727
+ );
728
+ }
729
+
730
+ async captureGameplayReplay(
731
+ replayPath: string,
732
+ positionMs: number,
733
+ ): Promise<GameplayReplayCapture> {
734
+ return this.command<GameplayReplayCapture>({
735
+ type: 'bridge-recording-replay-capture',
736
+ replayPath,
737
+ positionMs,
738
+ });
739
+ }
740
+
741
+ /**
742
+ * Capture an isolated, deterministic four-view preview through the editor's
743
+ * native Asset Lab. The SDK delegates rendering to the editor; it never
744
+ * loads, clones, or interprets Three.js assets itself.
745
+ */
746
+ async captureAssetPreview(
747
+ source: AssetPreviewSource,
748
+ options: AssetPreviewOptions = {},
749
+ ): Promise<AssetPreviewCapture> {
750
+ const data = await this.command<AssetPreviewCapture>(
751
+ {
752
+ type: 'capture-asset-preview',
753
+ ...source,
754
+ ...options,
755
+ },
756
+ // Photographing changes nothing, and this is the relay the editor's `screenshot
757
+ // <module>` command and `project.bake.preview` ride — the lane where a momentary
758
+ // transport failure cost a cold agent three probe modules.
759
+ { retryTransport: true },
760
+ );
761
+ return {
762
+ width: data.width,
763
+ height: data.height,
764
+ // Absent from editors that predate orientation reporting.
765
+ ...(data.orientation ? { orientation: data.orientation } : {}),
766
+ views: data.views,
767
+ contactSheet: data.contactSheet,
768
+ };
769
+ }
770
+
771
+ /**
772
+ * A project-defined labeled shot set (the editor's `screenshot <target> --shots <set>` command):
773
+ * the DEFINITION travels with the command (project data — see
774
+ * `AssetPreviewShotSetDefinition`; the CLI resolves it from the registered
775
+ * `project.<set>.previewShots` tool), and the editor's generic
776
+ * capture engine renders it — see `packages/editor-threejs/src/kit/asset-preview.ts`'s
777
+ * `captureShotSetAssetPreview`. Throws (via `command`'s `{ok:false}`
778
+ * unwrap) with a clear message naming the missing joint(s) when the asset
779
+ * lacks a bone the definition requires.
780
+ */
781
+ async captureShotSetPreview(
782
+ source: AssetPreviewSource,
783
+ definition: AssetPreviewShotSetDefinition,
784
+ options: AssetPreviewOptions = {},
785
+ ): Promise<LabeledShotSetCapture> {
786
+ const data = await this.command<LabeledShotSetCapture>(
787
+ {
788
+ type: 'capture-asset-preview',
789
+ ...source,
790
+ ...options,
791
+ shotSet: definition,
792
+ },
793
+ { retryTransport: true },
794
+ );
795
+ return {
796
+ width: data.width,
797
+ height: data.height,
798
+ shots: data.shots,
799
+ // An older editor predates the empty-frame guard and sends none.
800
+ warnings: data.warnings ?? [],
801
+ contactSheet: data.contactSheet,
802
+ };
803
+ }
804
+
805
+ /**
806
+ * B8.4 — score the asset against a reference GLB (the editor's `screenshot
807
+ * <model.glb> --compare <ref.glb>` command): matched orthographic front + side silhouettes
808
+ * (equal-height bounding-box framing, both yaw-normalized to face the
809
+ * camera), per-view IoU numbers, and overlay evidence images. The
810
+ * reference GLB's raw bytes travel base64 in the command; the editor
811
+ * renders and scores — the SDK never interprets Three.js assets itself.
812
+ */
813
+ async captureAssetComparePreview(
814
+ source: AssetPreviewSource,
815
+ refGlbBase64: string,
816
+ options: AssetCompareOptions = {},
817
+ ): Promise<AssetCompareCapture> {
818
+ const { refForward, ...dimensions } = options;
819
+ const data = await this.command<AssetCompareCapture>({
820
+ type: 'capture-asset-preview',
821
+ ...source,
822
+ ...dimensions,
823
+ compare: { glbBase64: refGlbBase64, ...(refForward ? { forward: refForward } : {}) },
824
+ });
825
+ return { width: data.width, height: data.height, views: data.views };
826
+ }
827
+
828
+ /**
829
+ * The STORY lane (the editor's `screenshot <file>.stories.tsx` command): every CSF export of
830
+ * one project story file rendered in the live session's DOM and captured
831
+ * through the same composite leg {@link captureGame} uses, returned as
832
+ * per-export images plus one variant sheet. `options.story` narrows to a
833
+ * single export.
834
+ *
835
+ * Rendering happens in the EDITOR — the SDK never imports, composes or
836
+ * mounts a CSF module itself; the session already owns that machinery for
837
+ * its Stories panel and this drives it.
838
+ */
839
+ async captureStoryVariants(
840
+ modulePath: string,
841
+ options: StoryCaptureOptions = {},
842
+ ): Promise<StoryVariantCapture> {
843
+ const data = await this.command<StoryVariantCapture>({
844
+ type: 'capture-story-variants',
845
+ modulePath,
846
+ ...options,
847
+ });
848
+ return {
849
+ modulePath: data.modulePath,
850
+ width: data.width,
851
+ height: data.height,
852
+ variants: data.variants,
853
+ contactSheet: data.contactSheet,
854
+ };
855
+ }
856
+
857
+ // --- Panels ---
858
+
859
+ async showViewport(tab: ViewportTab): Promise<void> {
860
+ await this.command({ type: 'viewport-tab', tab });
861
+ }
862
+
863
+ /** Focus a static workspace panel by the key the editor's panel registry
864
+ * holds; an unknown key refuses naming the keys it does hold. */
865
+ async showPanel(panel: string): Promise<void> {
866
+ await this.command({ type: 'show-panel', panel });
867
+ }
868
+
869
+ /** Show several instances of the running game split-screen — multiplayer
870
+ * authoring. Pass a total `count` (default "Player N" labels) or an array of
871
+ * `names` (its length is the count; index 0 is the primary). Requires a live
872
+ * play session. */
873
+ async setInstanceCount(countOrNames: number | string[]): Promise<void> {
874
+ await this.command(
875
+ Array.isArray(countOrNames)
876
+ ? { type: 'set-instance-count', names: countOrNames }
877
+ : { type: 'set-instance-count', count: countOrNames },
878
+ );
879
+ }
880
+
881
+ async openAsset(path: string, kind: AssetKind): Promise<void> {
882
+ await this.command({ type: 'open-asset-tab', path, kind });
883
+ }
884
+
885
+ /** SELECT a project asset — the other half of the browser's
886
+ * selection-vs-open contract (single click selects and fills the
887
+ * Inspector; double click opens a document). */
888
+ async selectAsset(path: string): Promise<void> {
889
+ await this.command({ type: 'select-asset', path });
890
+ }
891
+
892
+ async closeAsset(key: string): Promise<void> {
893
+ await this.command({ type: 'close-asset-tab', key });
894
+ }
895
+
896
+ async toggleCommandPalette(): Promise<void> {
897
+ await this.command({ type: 'toggle-command-palette' });
898
+ }
899
+
900
+ async toggleConsole(): Promise<void> {
901
+ await this.command({ type: 'toggle-console' });
902
+ }
903
+
904
+ /** Switch the editor's NAMED WORKSPACE — the task-named layout memory
905
+ * (`game`/`model`/`sculpt`/`texture`/`animate`/`look`). Resolves once the
906
+ * dock has finished rebuilding, so a following capture photographs the
907
+ * arrangement that was asked for. */
908
+ async setWorkspace(workspace: EditorWorkspaceName): Promise<void> {
909
+ await this.command({ type: 'set-workspace', workspace });
910
+ }
911
+
912
+ /** Apply a STYLE BUNDLE — palette, material, icon set and region defaults
913
+ * in one gesture (`classic`/`glass`/`maya`/`substance`, or one a package
914
+ * the project or product composes carries, such as `blender`). */
915
+ async setStyle(style: string): Promise<void> {
916
+ await this.command({ type: 'set-style', style });
917
+ }
918
+
919
+ /** A document stage's viewport PRESENTATION (`kit/viewport-presentation`), resolved; with a
920
+ * `layer`, that choice is recorded for the view first, as the toolbar records it. */
921
+ /** A view's presentation, answered resolved. With a LAYER the person's choice is recorded
922
+ * first; with a STRING, the named view (`*.view.ts`) of that id is put on the view whole. */
923
+ async viewportPresentation(
924
+ documentId: string,
925
+ layer?: import('./kit/viewport-presentation').PresentationLayer | string,
926
+ ): Promise<{
927
+ presentation: import('./kit/viewport-presentation').ViewportPresentation;
928
+ binding: { stageKind: string; documentLayer: import('./kit/viewport-presentation').PresentationLayer | null } | null;
929
+ bound: { viewId: string; stageKind: string }[];
930
+ lastDraw: import('./kit/viewport-presentation').ViewDrawReport | null;
931
+ presets: string[];
932
+ viewPresets: string[];
933
+ /** The environment images registered (`kit/environment-images`), for `lighting.preview.environment.image`. */
934
+ environmentImages: string[];
935
+ }> {
936
+ return this.command({
937
+ type: 'viewport-presentation',
938
+ documentId,
939
+ ...(typeof layer === 'string' ? { preset: layer } : layer ? { layer } : {}),
940
+ });
941
+ }
942
+
943
+ /** Set the MATERIAL apart from the bundle that usually carries it.
944
+ * Answers with what the chrome wears afterwards. */
945
+ async setAppearance(appearance: {
946
+ readonly material?: string;
947
+ }): Promise<{ material: string; style: string | null }> {
948
+ return this.command<{ material: string; style: string | null }>({
949
+ type: 'set-appearance',
950
+ ...appearance,
951
+ });
952
+ }
953
+
954
+ async showBuild(): Promise<void> {
955
+ await this.command({ type: 'show-build' });
956
+ }
957
+
958
+ /** Atomically present a durable editor view and return its shareable URL. */
959
+ async present(view: EditorView): Promise<PresentedEditorView> {
960
+ const presented = await this.command<PresentedEditorView>({ type: 'present-view', view });
961
+ return { view: presented.view, url: presented.url, warnings: presented.warnings };
962
+ }
963
+
964
+ /**
965
+ * The INSPECTION SUBJECT the editor is showing right now, as data — the
966
+ * serialized projection of the inspection model (design:
967
+ * `docs/ARCHITECTURE-CORE.md` §Editor chrome, "The Inspection Model").
968
+ *
969
+ * The same subject a human reads in the inspector: identity, presentation,
970
+ * verbs, and the identified sections in display order — with a `fields`
971
+ * section's CURRENT VALUES read through the same io the field rows edit
972
+ * through. With nothing selected it answers the active surface's own
973
+ * no-selection subject when it has one, exactly as the panel does; it never
974
+ * reports another surface's, and when the panel itself is unmounted it
975
+ * answers `{none: true}` rather than a subject nobody is looking at. A
976
+ * `custom` section body is a named opaque (`{kind, id, title}`) — the editor
977
+ * renders those with React — plus its displayed values under `data` when it
978
+ * has any (the Transform section's position/rotation/scale).
979
+ */
980
+ async inspect(): Promise<InspectedInspection> {
981
+ const data = await this.command<{ subject: InspectedInspection }>({ type: 'inspect' });
982
+ return data.subject;
983
+ }
984
+
985
+ /** Run one verb exposed by the active Inspector subject, by its id. */
986
+ async runInspectionAction(actionId: string): Promise<InspectedInspection> {
987
+ const data = await this.command<{ subject: InspectedInspection }>({
988
+ type: 'run-inspection-action',
989
+ actionId,
990
+ });
991
+ return data.subject;
992
+ }
993
+
994
+ /**
995
+ * Run ONE command by id — the door to everything the command palette lists.
996
+ *
997
+ * Under the Code-OSS frame this is the workbench's own `ICommandService`, so
998
+ * any command id works: a view's `volter.<view>.<verb>`, an editor action's
999
+ * `volter.action.<id>`, or one of VS Code's own. Standalone the editor's `edit` command has no
1000
+ * command service and answers the `volter.<view>.<verb>` shape directly off
1001
+ * the views registry, refusing anything else BY NAME.
1002
+ *
1003
+ * The result is whatever the command answered — a view verb's state, or
1004
+ * `null` for a command that returns nothing.
1005
+ */
1006
+ async runCommand(commandId: string, args?: unknown): Promise<unknown> {
1007
+ const data = await this.command<{ result: unknown }>({
1008
+ type: 'run-command',
1009
+ commandId,
1010
+ ...(args === undefined ? {} : { args }),
1011
+ });
1012
+ return data.result;
1013
+ }
1014
+
1015
+ /**
1016
+ * One STRUCTURE op on the authored tree — the hierarchy context menu's own
1017
+ * verbs, on the same helpers, for a caller with no pointer to right-click
1018
+ * with. `id`/`ids` default to the current selection.
1019
+ */
1020
+ async structureOp(op: StructureOp, options: StructureOpOptions = {}): Promise<StructureOpResult> {
1021
+ return this.command<StructureOpResult>({ type: 'structure-op', op, ...options });
1022
+ }
1023
+
1024
+ /** "Extract Component…" — the hierarchy row's action, as a command. Answers
1025
+ * the action's own sentence, which NAMES the files it created. */
1026
+ async extractComponent(options: { id?: string; name?: string } = {}): Promise<{ hint: string }> {
1027
+ return this.command<{ hint: string }>({ type: 'extract-component', ...options });
1028
+ }
1029
+
1030
+ /** "Fork Component…" — extract's twin: one new file, one callsite retargeted. */
1031
+ async forkComponent(options: { id?: string } = {}): Promise<{ hint: string }> {
1032
+ return this.command<{ hint: string }>({ type: 'fork-component', ...options });
1033
+ }
1034
+
1035
+ /**
1036
+ * The HIERARCHY PANEL's actual rendered row tree, as data.
1037
+ *
1038
+ * The same rows a human is looking at: the adapter's tree after the component
1039
+ * marks fold implementation subtrees, after the internals reveal, after the
1040
+ * document promotion, the child cap, the collapse state, the search filter
1041
+ * and the selection scope. Works in play mode and edit mode alike — the
1042
+ * answer reports which (`playState`, `activeViewportTab`), because a
1043
+ * play-mode tree and an edit-mode tree come from different adapters.
1044
+ *
1045
+ * Deliberately NOT `status().entities`, which walks the raw adapter tree and
1046
+ * therefore answers a different question: a panel defect is invisible in it.
1047
+ *
1048
+ * Each row carries `childCount` (what its caret opens), `internalChildCount`
1049
+ * (what is folded behind "Reveal Internals") and `expandable` (whether the
1050
+ * panel draws a caret at all) — so "this subtree exists but the UI offers no
1051
+ * way to open it" is a readable fact rather than something only a human
1052
+ * squinting at the panel can notice.
1053
+ *
1054
+ * Rejects, naming the panel, when no hierarchy panel is mounted: an empty
1055
+ * tree would be a fabricated answer about a surface nobody is being shown.
1056
+ */
1057
+ async hierarchy(): Promise<InspectedHierarchy> {
1058
+ const data = await this.command<{ hierarchy: InspectedHierarchy }>({ type: 'hierarchy' });
1059
+ return data.hierarchy;
1060
+ }
1061
+
1062
+ /** Run the Hierarchy panel's own Expand All action. */
1063
+ async expandHierarchyAll(): Promise<void> {
1064
+ await this.command<Record<string, never>>({ type: 'expand-hierarchy-all' });
1065
+ }
1066
+
1067
+ /** Run the Hierarchy panel's own Collapse All action — Expand All's other
1068
+ * half, and the only way back to the tree's rest state through the product
1069
+ * (see `HierarchyPanelSnapshot.collapseAll`). */
1070
+ async collapseHierarchyAll(): Promise<void> {
1071
+ await this.command<Record<string, never>>({ type: 'collapse-hierarchy-all' });
1072
+ }
1073
+
1074
+ /**
1075
+ * Write one editable path through the active Inspector's own IO.
1076
+ *
1077
+ * The answer carries `write` as well as the subject, because an ack alone
1078
+ * cannot be believed: a write with no persistence route open succeeds and
1079
+ * changes no byte, and `write.persisted` is how the caller tells the two
1080
+ * apart without diffing the tree (`InspectedWriteDestination` in `types.ts`).
1081
+ */
1082
+ async setInspectionField(path: string, value: unknown): Promise<InspectedFieldWrite> {
1083
+ const data = await this.command<InspectedFieldWrite>({
1084
+ type: 'set-inspection-field',
1085
+ path,
1086
+ value,
1087
+ });
1088
+ return { subject: data.subject, write: data.write };
1089
+ }
1090
+
1091
+ /**
1092
+ * REMOVE one editable path's authored override — the other half of the write
1093
+ * door, and the only one that can express byte-ABSENCE.
1094
+ *
1095
+ * {@link setInspectionField} writes a VALUE, so reverting a property an
1096
+ * authoring gesture ADDED puts the default back EXPLICITLY and leaves the
1097
+ * source one attribute heavier than it started. This drops the property, so
1098
+ * whatever governs it in its absence takes over — the same `io.remove` the
1099
+ * Inspector's revert arrow calls, the same persistence pipe, the same awaited
1100
+ * `{ destination, persisted }` ack.
1101
+ *
1102
+ * Rejects with `code: 'REMOVAL_UNAVAILABLE'` when the field does not declare
1103
+ * itself removable or the lane implements no removal door. That refusal is a
1104
+ * MISSING SEAM, not a failed removal, and it is coded rather than phrased
1105
+ * precisely so a caller can grade the two differently.
1106
+ */
1107
+ async removeInspectionField(path: string): Promise<InspectedFieldWrite> {
1108
+ const data = await this.command<InspectedFieldWrite>({
1109
+ type: 'remove-inspection-field',
1110
+ path,
1111
+ });
1112
+ return { subject: data.subject, write: data.write };
1113
+ }
1114
+
1115
+ /**
1116
+ * OPEN one piece of the adapter's SCENE TABLE by id — a scene, a prefab, or
1117
+ * a story state, because the table makes them siblings (they differ only in
1118
+ * instance site). The ids are exactly what `getState().adapter.scenes.entries`
1119
+ * reports, so the table is both the menu and the address space.
1120
+ *
1121
+ * With a game LIVE in the session, opening a scene the adapter declares
1122
+ * reachable through that game's own scenes contract NAVIGATES it — the same
1123
+ * switch the editor's own scene picker makes — and the answer carries the
1124
+ * game's own reading (`scene`).
1125
+ *
1126
+ * Rejects with a coded reason rather than prose: `SCENE_NOT_FOUND` (and it
1127
+ * names the ids that DO exist), `SCENE_NOT_OPENABLE` carrying the adapter's
1128
+ * own declared reason for a scene it says nothing can reach,
1129
+ * `SCENE_NAVIGATION_NOT_RUNNING` for a live-only scene with no game running,
1130
+ * `SCENE_CONTRACT_UNAVAILABLE` / `SCENE_NOT_IN_CONTRACT` (naming the ids the
1131
+ * game itself publishes) / `SCENE_SWITCH_FAILED` when the running game's own
1132
+ * navigation cannot take it, `SCENE_NOT_OPENABLE_LIVE` when this session has
1133
+ * no remount for a native swap-slot scene,
1134
+ * `SCENE_TABLE_UNAVAILABLE` before the adapter has loaded, and
1135
+ * `SCENE_DOCUMENT_NOT_MOUNTED` when the host has no document for a piece the
1136
+ * table says is openable — a host gap, not a table statement.
1137
+ */
1138
+ async open(id: string): Promise<OpenedDocument> {
1139
+ return this.command<OpenedDocument>({ type: 'open', id });
1140
+ }
1141
+
1142
+ /**
1143
+ * Undo / redo one project transaction — the same queue the keyboard shortcut
1144
+ * drives. `moved` is false when there was nothing left in that direction.
1145
+ */
1146
+ async undo(): Promise<HistoryStep> {
1147
+ return this.command<HistoryStep>({ type: 'undo' });
1148
+ }
1149
+
1150
+ async redo(): Promise<HistoryStep> {
1151
+ return this.command<HistoryStep>({ type: 'redo' });
1152
+ }
1153
+
1154
+ /** Read the editor's actual current durable projection. */
1155
+ async currentView(): Promise<EditorView> {
1156
+ const data = await this.command<{ view: EditorView }>({ type: 'current-view' });
1157
+ return data.view;
1158
+ }
1159
+
1160
+ /**
1161
+ * Capture the active center document exactly as presented to the user.
1162
+ *
1163
+ * A number is a SQUARE of that size (the default shape); `{width, height}`
1164
+ * asks for a shaped frame — a video-aspect look that needs no crop. Both are
1165
+ * bounded by the relay budget; see {@link CaptureDimensions}.
1166
+ */
1167
+ /** Photograph the editor PAGE itself — every panel as the person sees it, at
1168
+ * `scale` output pixels per CSS pixel (default `devicePixelRatio`), which is
1169
+ * what a 1 px border or a glyph edge is judged through. */
1170
+ async captureEditorChrome(options?: EditorChromeCaptureOptions): Promise<EditorChromeCapture> {
1171
+ return this.command<EditorChromeCapture>({
1172
+ type: 'capture-editor-chrome',
1173
+ ...(options?.scale === undefined ? {} : { scale: options.scale }),
1174
+ ...(options?.region === undefined ? {} : { region: options.region }),
1175
+ ...(options?.name === undefined ? {} : { name: options.name }),
1176
+ });
1177
+ }
1178
+
1179
+ /** With a view, present and capture it in one request so document discovery
1180
+ * cannot retarget the capture between two client calls. */
1181
+ async captureActiveDocument(
1182
+ size?: CaptureDimensions,
1183
+ view?: EditorView,
1184
+ ): Promise<ActiveDocumentCapture> {
1185
+ return this.command<ActiveDocumentCapture>({
1186
+ type: 'capture-active-document',
1187
+ ...(view ? { view } : {}),
1188
+ ...(typeof size === 'number' ? { size } : {}),
1189
+ ...(typeof size === 'object' && size !== null
1190
+ ? { width: size.width, height: size.height }
1191
+ : {}),
1192
+ });
1193
+ }
1194
+
1195
+ /**
1196
+ * Read or drive the ACTIVE center document's own DOM — the scoped
1197
+ * editor-chrome door, and the read/gesture half of the same subject
1198
+ * {@link captureActiveDocument} photographs. NOT play-mode gated, and NOT
1199
+ * page automation: a target outside the active document's container is
1200
+ * refused by name. Design and scope contract:
1201
+ * `packages/sdk/src/kit/editor-document-probe.ts`.
1202
+ */
1203
+ async documentProbe(step: DocumentProbeStep): Promise<DocumentProbeResult> {
1204
+ return this.command<DocumentProbeResult>({ type: 'document-probe', step });
1205
+ }
1206
+
1207
+ /**
1208
+ * Run a wire-carried step against the ACTIVE document's published context
1209
+ * (`packages/sdk/src/kit/document-context-registry.ts`) — the REPL door over
1210
+ * an open document, in Edit mode. `src` is the step's own `toString()`;
1211
+ * same serialization contract as `page-script` (no closures survive).
1212
+ */
1213
+ /**
1214
+ * The Blender lane's doors (`blender-execute`, `blender-scene-info`,
1215
+ * `blender-object-info`, `blender-screenshot-view`, `blender-read-file`,
1216
+ * `blender-write-file`, `blender-list-files`, `blender-start`,
1217
+ * `blender-status`): Blender runs in the editor tab's worker, and
1218
+ * `volter blender-mcp` is transport onto these. `blender-status` is the only
1219
+ * one that creates nothing — it answers whether this tab already has a
1220
+ * session, which is how a caller survives an editor restart.
1221
+ */
1222
+ async blender<T extends object = Record<string, unknown>>(
1223
+ type: `blender-${string}`,
1224
+ fields: Record<string, unknown> = {},
1225
+ ): Promise<T> {
1226
+ // One modeling chunk can run for minutes in the tab (an exact boolean
1227
+ // over a dense mesh measured 80-90 s under Wasm); the relay's server-side
1228
+ // budget for blender-execute is the same half hour.
1229
+ return this.command<T>({ type, ...fields }, { deadlineMs: BLENDER_DEADLINE_MS });
1230
+ }
1231
+
1232
+ /**
1233
+ * Model Play's log (`model-play-log`, contributed by `@volter/play`): what the
1234
+ * running play script logged with `play.log`, and the runner's lifecycle entries, stamped
1235
+ * with simulation time and frame. `documentId` names the model document (the active Play's
1236
+ * when omitted); `since` keeps entries at or after that many simulation seconds; `kind`
1237
+ * keeps one kind.
1238
+ */
1239
+ async modelPlayLog(query: { readonly documentId?: string; readonly since?: number; readonly kind?: string } = {}): Promise<ModelPlayLogReading> {
1240
+ const { ok: _ok, ...reading } = await this.command<ModelPlayLogReading & { ok: boolean }>({ type: 'model-play-log', ...query });
1241
+ return reading;
1242
+ }
1243
+
1244
+ async documentScript<T = unknown>(src: string): Promise<T> {
1245
+ const outcome = await this.command<{ result: T }>({ type: 'document-script', src });
1246
+ return outcome.result;
1247
+ }
1248
+
1249
+ // --- Display (set semantics) ---
1250
+
1251
+ async setGrid(enabled: boolean): Promise<void> {
1252
+ await this.command({ type: 'set-grid', enabled });
1253
+ }
1254
+
1255
+ async setHelpers(enabled: boolean): Promise<void> {
1256
+ await this.command({ type: 'set-helpers', enabled });
1257
+ }
1258
+
1259
+ async setStats(enabled: boolean): Promise<void> {
1260
+ await this.command({ type: 'set-stats', enabled });
1261
+ }
1262
+
1263
+ async setShadingMode(mode: ShadingMode): Promise<void> {
1264
+ await this.command({ type: 'set-shading-mode', mode });
1265
+ }
1266
+
1267
+ async setHelperType(helperType: keyof HelperVisibility, enabled: boolean): Promise<void> {
1268
+ await this.command({ type: 'set-helper-type', helperType, enabled });
1269
+ }
1270
+
1271
+ // --- Transform tools (set semantics) ---
1272
+
1273
+ async setTransformMode(mode: TransformMode): Promise<void> {
1274
+ await this.command({ type: 'set-transform-mode', mode });
1275
+ }
1276
+
1277
+ async setTransformSpace(space: TransformSpace): Promise<void> {
1278
+ await this.command({ type: 'set-transform-space', space });
1279
+ }
1280
+
1281
+ async setSnap(enabled: boolean): Promise<void> {
1282
+ await this.command({ type: 'set-snap', enabled });
1283
+ }
1284
+
1285
+ // --- Project management ---
1286
+
1287
+ async createProject(
1288
+ name: string,
1289
+ location: string,
1290
+ template: ProjectTemplate = 'default',
1291
+ exampleId?: string,
1292
+ ): Promise<ProjectInfo> {
1293
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/create-project`, {
1294
+ method: 'POST',
1295
+ headers: { 'Content-Type': 'application/json' },
1296
+ body: JSON.stringify({ name, location, template, ...(exampleId ? { exampleId } : {}) }),
1297
+ });
1298
+ const data = (await this.readJson(res)) as {
1299
+ ok?: boolean;
1300
+ error?: string;
1301
+ path?: string;
1302
+ config?: ProjectInfo['config'];
1303
+ };
1304
+ if (!res.ok) {
1305
+ throw new Error(data.error ?? `Create project failed: ${res.status}`);
1306
+ }
1307
+ return { path: data.path as string, config: data.config as ProjectInfo['config'] };
1308
+ }
1309
+
1310
+ async openProject(path: string): Promise<void> {
1311
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/open-project`, {
1312
+ method: 'POST',
1313
+ headers: { 'Content-Type': 'application/json' },
1314
+ body: JSON.stringify({ path }),
1315
+ });
1316
+ const body = (await this.readJson(res)) as { error?: string };
1317
+ if (!res.ok) {
1318
+ throw new Error(body.error ?? `Open project failed: ${res.status}`);
1319
+ }
1320
+ }
1321
+
1322
+ async getProject(): Promise<ProjectInfo | null> {
1323
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/project`);
1324
+ const data = (await this.readJson(res)) as { project: ProjectInfo | null; error?: string };
1325
+ if (!res.ok) throw new Error(data.error ?? `Failed to get project: ${res.status}`);
1326
+ return data.project;
1327
+ }
1328
+
1329
+ async listRecentProjects(): Promise<RecentProject[]> {
1330
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/recent-projects`);
1331
+ const data = (await this.readJson(res)) as { projects: RecentProject[]; error?: string };
1332
+ if (!res.ok) throw new Error(data.error ?? `Failed to list projects: ${res.status}`);
1333
+ return data.projects;
1334
+ }
1335
+
1336
+ // --- Registered project tools ---
1337
+
1338
+ /** List tools explicitly registered in `package.json#volter.tools`.
1339
+ * The editor server loads callable metadata in Node; modules never enter the
1340
+ * editor browser merely because they were listed. */
1341
+ async listProjectTools(): Promise<ProjectToolCatalog> {
1342
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/project-tools`);
1343
+ const body = await this.readJson(res);
1344
+ if (!res.ok) throw new Error(`Failed to list project tools: ${res.status}`);
1345
+ return body as ProjectToolCatalog;
1346
+ }
1347
+
1348
+ /** Execute one Node-hosted project tool through the shared validated
1349
+ * dispatcher. Write/destructive tools require `confirm:true`. */
1350
+ async runProjectTool(
1351
+ name: string,
1352
+ input: unknown = {},
1353
+ options: { confirm?: boolean; instance?: string } = {},
1354
+ ): Promise<ProjectToolOutcome> {
1355
+ // Provider subscriptions can outlive Node fetch's five-minute header limit.
1356
+ // Match browser fetch for this operation; dispose its sockets after reading the result.
1357
+ const dispatcher = createDispatcher(0);
1358
+ try {
1359
+ const res = await this.httpFetch(
1360
+ `${this.baseUrl}/__editor/project-tools/run`,
1361
+ {
1362
+ method: 'POST',
1363
+ headers: { 'Content-Type': 'application/json' },
1364
+ body: JSON.stringify({
1365
+ name,
1366
+ input,
1367
+ confirm: options.confirm === true,
1368
+ // Omitted (not null) when unset — the wire body is JSON and the tool
1369
+ // host reads absence as "the sole instance", same convention as the
1370
+ // relay's `instance`.
1371
+ ...(options.instance !== undefined ? { instance: options.instance } : {}),
1372
+ }),
1373
+ },
1374
+ dispatcher,
1375
+ );
1376
+ const body = (await this.readJson(res)) as ProjectToolOutcome;
1377
+ if (!body || typeof body !== 'object' || typeof body.ok !== 'boolean') {
1378
+ throw new Error(`Project tool returned an invalid response (${res.status}).`);
1379
+ }
1380
+ return body;
1381
+ } finally {
1382
+ await dispatcher?.destroy?.();
1383
+ }
1384
+ }
1385
+
1386
+ // --- First-party generation job activity ---
1387
+
1388
+ /** Read the one project-local generation job ledger. Provider-native
1389
+ * request/result shapes remain on their registered operations. */
1390
+ async listGenerationJobs(): Promise<GenerationJobsDocument> {
1391
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/generations`);
1392
+ const body = await this.readJson(res);
1393
+ if (!res.ok) throw new Error(`Failed to list generation jobs: ${res.status}`);
1394
+ return body as GenerationJobsDocument;
1395
+ }
1396
+
1397
+ /** Forget operational job state. Accepted provenance and project assets
1398
+ * are deliberately unaffected. */
1399
+ async forgetGenerationJob(id: string): Promise<boolean> {
1400
+ const res = await this.httpFetch(
1401
+ `${this.baseUrl}/__editor/generations/${encodeURIComponent(id)}`,
1402
+ {
1403
+ method: 'DELETE',
1404
+ },
1405
+ );
1406
+ const body = (await this.readJson(res)) as { removed?: boolean; error?: string };
1407
+ if (!res.ok) throw new Error(body.error ?? `Failed to forget generation job: ${res.status}`);
1408
+ return body.removed === true;
1409
+ }
1410
+
1411
+ // --- Logs ---
1412
+
1413
+ async getLogEntries(): Promise<
1414
+ Array<{ t: number; level: string; msg: string; source?: string }>
1415
+ > {
1416
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/log-entries`);
1417
+ const data = (await this.readJson(res)) as {
1418
+ entries: Array<{ t: number; level: string; msg: string; source?: string }>;
1419
+ };
1420
+ if (!res.ok) return [];
1421
+ return data.entries;
1422
+ }
1423
+
1424
+ // --- State ---
1425
+
1426
+ /**
1427
+ * The document table the host resolved — every scene, prefab, page, model,
1428
+ * shot, take … the project's finders produced (`getState().adapter.scenes`
1429
+ * is the same projection). A command, so it answers wherever the control
1430
+ * channel reaches, not only where `/__editor/state` is served.
1431
+ */
1432
+ async documentTable(): Promise<DocumentTableProjection> {
1433
+ return this.command<DocumentTableProjection>({ type: 'document-table' });
1434
+ }
1435
+
1436
+ /**
1437
+ * Reload the editor's page and wait until the new document answers — the
1438
+ * host's `page-reload`, for a product with no game client (`@volter/game-live`'s
1439
+ * `page.reload()` is the same order with the same two witnesses). A tab's
1440
+ * epoch count rising proves a new document loaded; a host command answering
1441
+ * proves it can be driven. It is sent only once that tab reports its command
1442
+ * listener ready, because a command sent before is filed as a stall.
1443
+ */
1444
+ async reloadPage(timeoutMs = 60_000): Promise<void> {
1445
+ const epochs = async () => {
1446
+ const tabs = (await this.getState().catch(() => ({ tabs: [] }) as Partial<EditorState>)).tabs ?? [];
1447
+ const byTab = tabs as ReadonlyArray<{ tabId8: string; epochCount: number; commandListener?: unknown }>;
1448
+ return new Map(byTab.map((tab) => [tab.tabId8, tab] as const));
1449
+ };
1450
+ const pause = () => new Promise((settle) => setTimeout(settle, 250));
1451
+ const before = await epochs();
1452
+ const deadline = Date.now() + timeoutMs;
1453
+ await this.command({ type: 'page-reload' });
1454
+ let reloaded: string[] = [];
1455
+ while (reloaded.length === 0) {
1456
+ if (Date.now() > deadline) throw new Error('The reload was ordered and no tab of this session reported a new page load.');
1457
+ await pause();
1458
+ const now = await epochs();
1459
+ reloaded = [...now].filter(([tabId, tab]) => tab.epochCount > (before.get(tabId)?.epochCount ?? 0)).map(([tabId]) => tabId);
1460
+ }
1461
+ for (;;) {
1462
+ if (Date.now() > deadline) throw new Error('A new page loaded, but its command listener did not become ready.');
1463
+ const now = await epochs();
1464
+ if (reloaded.some((tabId) => {
1465
+ const listener = now.get(tabId)?.commandListener;
1466
+ return typeof listener !== 'string' || listener === 'ready';
1467
+ })) break;
1468
+ await pause();
1469
+ }
1470
+ for (;;) {
1471
+ try {
1472
+ await this.command({ type: 'current-view' });
1473
+ return;
1474
+ } catch (error) {
1475
+ if (Date.now() > deadline) throw error;
1476
+ await pause();
1477
+ }
1478
+ }
1479
+ }
1480
+
1481
+ async getState(): Promise<EditorState> {
1482
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/state`);
1483
+ const state = (await this.readJson(res)) as EditorState;
1484
+ if (!res.ok) throw new Error(`Failed to get editor state: ${res.status} ${res.statusText}`);
1485
+ return state;
1486
+ }
1487
+
1488
+ /**
1489
+ * The complete unresolved console set the session is holding right now.
1490
+ *
1491
+ * Command envelopes only carry COUNTS (`unresolvedConsole` on
1492
+ * `commandResponseFor`). The named conditions live on GET `/__editor/console`.
1493
+ * This is the method that turns "a command that exits before an envelope
1494
+ * arrives" into a real reading: the CLI calls it at start and at exit
1495
+ * through the same {@link onEnvelope} observer every other response uses.
1496
+ * A session that does not answer within {@link CONSOLE_DRAIN_TIMEOUT_MS} is
1497
+ * a thrown error the caller treats as "nothing learned", never a hang.
1498
+ */
1499
+ async getUnresolvedConsole(opts?: { all?: boolean }): Promise<unknown> {
1500
+ const res = await this.httpFetch(
1501
+ `${this.baseUrl}/__editor/console${opts?.all === true ? '?all=1' : ''}`,
1502
+ { signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS) },
1503
+ );
1504
+ // Status first: a 404's empty body would otherwise die inside readJson
1505
+ // with a parse error that hides the one fact the caller classifies on
1506
+ // (does this server SERVE the console route at all?).
1507
+ if (!res.ok) {
1508
+ throw new Error(`Failed to read unresolved console: ${res.status}`);
1509
+ }
1510
+ return await this.readJson(res, true);
1511
+ }
1512
+
1513
+ /** Acknowledge one named console condition. The response is observed and
1514
+ * hydrated through the same path as every other client response. */
1515
+ async acknowledgeConsole(input: {
1516
+ readonly id: string;
1517
+ readonly reason: string;
1518
+ readonly by: string;
1519
+ }): Promise<unknown> {
1520
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/console/ack`, {
1521
+ method: 'POST',
1522
+ headers: { 'Content-Type': 'application/json' },
1523
+ body: JSON.stringify(input),
1524
+ signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS * 4),
1525
+ });
1526
+ const body = await this.readJson(res);
1527
+ return body;
1528
+ }
1529
+
1530
+ /**
1531
+ * `fetch` for this client's plain routes, with the ONE thing Node's `fetch`
1532
+ * will not do: name why it failed.
1533
+ *
1534
+ * `readJson` below already owns "the server answered the wrong thing"; this
1535
+ * owns "nothing answered at all", which used to reach the caller as the bare
1536
+ * `TypeError: fetch failed` with the real code buried on `.cause`. No retry
1537
+ * here — these routes create projects, run tools and acknowledge console
1538
+ * conditions, so repeating one is the caller's decision. The relayed
1539
+ * `command` path above has its own opt-in retry for the read-only captures.
1540
+ */
1541
+ private async httpFetch(
1542
+ url: string,
1543
+ init?: RequestInit,
1544
+ dispatcher?: Dispatcher,
1545
+ ): Promise<Response> {
1546
+ try {
1547
+ return this.fetchOverride ? await this.fetchOverride(url, init) : await dispatchFetch(url, init, dispatcher);
1548
+ } catch (error) {
1549
+ if ((error as { name?: string } | null)?.name === 'TimeoutError') throw error;
1550
+ const { code, detail } = describeFetchFailure(error);
1551
+ throw new EditorCommandError(
1552
+ `${init?.method ?? 'GET'} ${url} never reached the editor: ${code}` +
1553
+ `${detail ? ` (${detail})` : ''}. Nothing answered on that port — check the terminal ` +
1554
+ 'that started this editor session and confirm the port this client resolved.',
1555
+ code,
1556
+ false,
1557
+ );
1558
+ }
1559
+ }
1560
+
1561
+ /** Parse one JSON body and hand it to {@link onEnvelope}.
1562
+ *
1563
+ * A command/state envelope carries current counts but not the named set. If
1564
+ * an observer is installed, do the bounded console GET before resolving the
1565
+ * original request. That makes a subsequent `process.exit()` safe: the
1566
+ * observer has already received every condition and occurrence count. */
1567
+ private async readJson(res: Response, consoleComplete = false): Promise<unknown> {
1568
+ // Every one of this client's twelve routes funnels through here, so this is
1569
+ // where "did the editor server answer?" is asked — the same question, and
1570
+ // the same JSON-content-type rule, that
1571
+ // `packages/sdk/src/kit/editor-server-response.ts` owns on the browser side.
1572
+ // It is asked again rather than imported because THIS package is published
1573
+ // and depends on neither `@volter/project` nor the editor bundle (see
1574
+ // `DEFAULT_URL` above for that policy).
1575
+ //
1576
+ // Not theoretical here: `baseUrl` is whatever `--url`/`VOLTER_EDITOR_URL`
1577
+ // says, so the CLI is routinely pointed at a SHARE TUNNEL or a static host
1578
+ // — both of which answer `200 text/html` for a route nothing serves, and
1579
+ // `res.json()` then died as `Unexpected token '<'`, naming neither the URL
1580
+ // nor the cause.
1581
+ const contentType = res.headers.get('content-type') ?? '';
1582
+ if (!contentType.includes('application/json')) {
1583
+ throw new Error(
1584
+ `The editor at ${this.baseUrl} answered with its page fallback ` +
1585
+ `(${contentType || 'no content-type'}) rather than JSON, so no editor server handled ` +
1586
+ 'the request. Check that this URL is a running editor session.',
1587
+ );
1588
+ }
1589
+ const body: unknown = await res.json();
1590
+ this.observe(body, { unresolvedConsoleComplete: consoleComplete });
1591
+ if (
1592
+ this.onEnvelope !== null &&
1593
+ !consoleComplete &&
1594
+ body !== null &&
1595
+ typeof body === 'object' &&
1596
+ 'unresolvedConsole' in body
1597
+ ) {
1598
+ try {
1599
+ const consoleRes = await this.httpFetch(`${this.baseUrl}/__editor/console`, {
1600
+ signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS),
1601
+ });
1602
+ if (consoleRes.ok) {
1603
+ const consoleBody: unknown = await consoleRes.json();
1604
+ this.observe(consoleBody, { unresolvedConsoleComplete: true });
1605
+ }
1606
+ } catch {
1607
+ // The original response remains authoritative. A reporting follow-up
1608
+ // may degrade to its count-only envelope, never break the command.
1609
+ }
1610
+ }
1611
+ return body;
1612
+ }
1613
+
1614
+ /** Hand one response body to {@link onEnvelope}, never letting it throw. */
1615
+ private observe(body: unknown, observation: EditorEnvelopeObservation): void {
1616
+ if (this.onEnvelope === null) return;
1617
+ try {
1618
+ this.onEnvelope(body, observation);
1619
+ } catch {
1620
+ // A reporting hook may never break the command it is reporting on.
1621
+ }
1622
+ }
1623
+
1624
+ /**
1625
+ * Whether an editor browser tab is connected to the server *right now*.
1626
+ * Unlike {@link getState}, this reflects live SSE connections, not cached
1627
+ * state — use it to check whether commands will actually reach an editor.
1628
+ */
1629
+ async isConnected(): Promise<boolean> {
1630
+ const state = await this.getState();
1631
+ return state.connected === true;
1632
+ }
1633
+
1634
+ async waitForState(
1635
+ predicate: (s: EditorState) => boolean,
1636
+ timeoutMs = 10_000,
1637
+ ): Promise<EditorState> {
1638
+ const start = Date.now();
1639
+ while (Date.now() - start < timeoutMs) {
1640
+ const state = await this.getState();
1641
+ if (predicate(state)) return state;
1642
+ await new Promise((r) => setTimeout(r, 100));
1643
+ }
1644
+ throw new Error(`waitForState timed out after ${timeoutMs}ms`);
1645
+ }
1646
+ }