@volter/sdk 0.0.0-stage → 0.5.203

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 +1166 -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 +317 -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 +403 -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
@@ -0,0 +1,1166 @@
1
+ /**
2
+ * The product door onto EDITOR CHROME — read and drive what a person sees in
3
+ * the editor's named surfaces, through the session, without play mode.
4
+ *
5
+ * ## Why this exists (WO: "No product door drives or reads EDITOR CHROME")
6
+ *
7
+ * the editor's `eval` command's `page()` step is play-mode-gated and rooted at the GAME
8
+ * container, so an editor surface that is not a running game — a capability's
9
+ * workspace document, the Data sheet, the Project Tools catalog — could be
10
+ * neither driven nor read through the product at all. The Sheets build could
11
+ * not live-verify a TSV paste or a sheet-tab click: it had a screenshot
12
+ * (`editor.captureActiveDocument`) and nothing else. That gap is the defect the
13
+ * doctrine names — when a live surface can only be diagnosed out of band, the
14
+ * product's own doors have failed.
15
+ *
16
+ * ## The API decision, and why it is this small
17
+ *
18
+ * ONE wire command (`document-probe`) carrying a step union, exposed on the
19
+ * live side as `editor.document.{query,click,drag,key,type,paste,select}`.
20
+ * Each verb is a thing an agent cannot otherwise do and cannot fake:
21
+ *
22
+ * - `query` — READ. What is rendered, as data: tag, role, text, attributes,
23
+ * value/checked/disabled, rect. The one door that answers "what does the
24
+ * human see" for a surface that is not a canvas.
25
+ * - `click` — a REAL user gesture (pointerdown/mousedown/mouseup/click plus
26
+ * focus), not `element.click()`, because a component listening on
27
+ * `pointerdown` (react-data-grid's cell selection does) is invisible to the
28
+ * synthetic shortcut. `clicks: 2` is a real DOUBLE click (numbered
29
+ * `detail` plus the `dblclick` React's `onDoubleClick` listens for) — the
30
+ * Outliner's own rename gesture, and an option on the same verb rather
31
+ * than a second one because it is the same gesture on the same target.
32
+ * - `type` — text into a focused field, character by character, committed
33
+ * with Enter. `paste` cannot stand in: an untrusted `ClipboardEvent`
34
+ * performs no default action, so a plain `<input>` keeps its old value.
35
+ * - `key` — keydown/keyup on the explicit target, else on whatever inside the
36
+ * scope has focus. Arrow keys ARE the spreadsheet's cursor. (No `keypress`:
37
+ * it is deprecated and no shipped surface listens for it.)
38
+ * - `paste` — a real `ClipboardEvent` with a populated `DataTransfer`, which
39
+ * is the only way to exercise a paste handler; nothing else in the product
40
+ * can produce one. Verified live against the Data sheet's own `onPaste`,
41
+ * which read the TSV and wrote the file.
42
+ * - `select` — choose a value on a `<select>`. `click` genuinely cannot do
43
+ * this: the option list of a native dropdown is drawn by the OS, so there
44
+ * is nothing inside the document's container for a pointer gesture to
45
+ * resolve against. And a plain `element.value = x` is a no-op against
46
+ * React, which caches the last value it wrote on the node and puts it back
47
+ * on the next render. So this verb goes through the PROTOTYPE's own value
48
+ * setter (which bypasses that cache) and then dispatches `input`/`change` —
49
+ * the established spelling from this repo's own probes, and the one React's
50
+ * synthetic-event layer honours. Without it the animation transport's clip
51
+ * and speed — two `<select>`s — were undrivable through the product.
52
+ *
53
+ * There is deliberately NO `screenshot` verb: `editor.captureActiveDocument()`
54
+ * already captures the document's content (`editor-view-presentation.ts`'s
55
+ * `activeDocumentContent`); this door's scope is the document's whole box —
56
+ * that content plus its header strip and shelf rail — and a synonym would be
57
+ * a second name for one behavior.
58
+ *
59
+ * ## Scope is a VOCABULARY, and the contract is that it is closed
60
+ *
61
+ * This is NOT general page automation, and the refusal is how that stays true.
62
+ * Every step resolves against ONE NAMED SURFACE — `document` (the default),
63
+ * `header`, `shelf`, `rail`, `outliner`, `content` ({@link DocumentProbeScope}) — and a
64
+ * selector that matches nothing inside it, or a resolved element that is not a
65
+ * descendant of it, is REFUSED with a message naming the scope rather than
66
+ * silently reaching into the rest of the page.
67
+ *
68
+ * ## How a scope finds its element: the owner's own stamp, never a class guess
69
+ *
70
+ * The Properties rail and the Outliner are VS CODE VIEWS (`volter.properties`,
71
+ * `volter.outliner`) holding React portals of ours, so their roots are not
72
+ * anywhere near the document's box and cannot be reached by walking down from
73
+ * it. They are found by the id THE WORKBENCH REGISTERED: the contribution
74
+ * hands each view's body element over as a named part, and `offerVolterPart`
75
+ * (`frame/bridge.tsx`) stamps it `data-volter-part="<part>"` at the moment of
76
+ * the handover. A pane that is disposed takes its stamped element with it, so
77
+ * a closed view answers "not open" instead of matching a stale node. The same
78
+ * shape as `activeDocumentContainer`'s `data-workspace-document-id` and as the
79
+ * portal pair below: the surface publishes its own identity, and this module
80
+ * never guesses from a class name.
81
+ *
82
+ * `rail` and `outliner` are EDITOR CHROME rather than the active document, so
83
+ * (unlike `document`/`header`/`shelf`) they are reachable while the Game
84
+ * document is active and while no document is open at all. The `editor.
85
+ * hierarchy()` / `editor.inspect()` doors still report what those panels
86
+ * RESOLVED; this door reports what they DREW and drives their controls —
87
+ * WALK 5's beats 7 and 12 are the gap it closes (`editor.setField('name')`
88
+ * writes the name, but it is not the Outliner's rename, and nothing at all
89
+ * could click a Properties tab).
90
+ *
91
+ * Two deliberate consequences of the containment rule:
92
+ *
93
+ * - A PORTALED overlay THIS DOCUMENT OPENED is in scope; anyone else's is
94
+ * not. Amended 2026-09-21 (B6), and what changed is the DOM rather than the
95
+ * rule: this note used to say a popover portaled to the theme root recorded
96
+ * no ownership, so any heuristic wide enough to reach it would also reach
97
+ * the editor's chrome menus. `ThemeRootPortal` records it now — an anchor
98
+ * span where the portal was declared and a matching stamp on what landed —
99
+ * so {@link scopeRoots} walks that pair from inside the document's box and
100
+ * reaches exactly the overlays the document opened. A chrome menu's anchor
101
+ * is in the chrome, so it is still refused by the same containment check.
102
+ * Without this a document's own header MENU could be opened by its trigger
103
+ * and never chosen from, which is "a control is accepted through its own
104
+ * click" failing one step short (measured on the Model document's Add
105
+ * menu). Widening further — every `.volter-menu` on the root — is still the
106
+ * "general automation framework" this module exists to not become.
107
+ * - The GAME document is refused outright by the three scopes that ARE the
108
+ * active document (see `resolveDocumentScope`): a game is driven through
109
+ * its own registered commands and the play-gated `page` door, never
110
+ * through synthetic DOM gestures. `rail` and `outliner` are the editor's
111
+ * panels around it and stay open.
112
+ */
113
+
114
+ import type {
115
+ DocumentKeyStep,
116
+ DocumentProbeResult,
117
+ DocumentProbeScope,
118
+ DocumentProbeStep,
119
+ ProbedElement,
120
+ } from '@volter/sdk/document-probe';
121
+ import { MAX_DOCUMENT_KEY_HOLD_MS } from '@volter/sdk/document-probe';
122
+ import { surfaceHoldsKeyboard } from '@volter/sdk/kit/surface-keyboard';
123
+ import { GAME_DOCUMENT_ID } from '@volter/sdk/kit/workspace-document-ids';
124
+ import { framedCapture } from '@volter/sdk/kit/framed-document-capture';
125
+ import {
126
+ activeWorkspaceDocument,
127
+ activeWorkspaceDocumentId,
128
+ activeWorkspaceDocumentViewId,
129
+ openWorkspaceDocuments,
130
+ } from '@volter/sdk/kit/workspace-document-registry';
131
+
132
+ /** The active document's mounted content element — what the `document`,
133
+ * `header` and `shelf` scopes resolve from. Shared with
134
+ * `editor-view-presentation.ts`'s capture so a probe and a screenshot can
135
+ * never disagree about what "the active document" is. */
136
+ export function activeDocumentContainer(documentId: string): HTMLElement | null {
137
+ return (
138
+ Array.from(document.querySelectorAll<HTMLElement>('[data-workspace-document-id]')).find(
139
+ (element) =>
140
+ element.dataset['workspaceDocumentId'] === documentId &&
141
+ (activeWorkspaceDocumentViewId() === null ||
142
+ element.dataset['workspaceViewId'] === activeWorkspaceDocumentViewId()),
143
+ ) ?? null
144
+ );
145
+ }
146
+
147
+ interface Scope {
148
+ readonly container: HTMLElement;
149
+ /** Roots the scope also covers beside its container (the inspector's card, for 'rail'). */
150
+ readonly extraRoots?: readonly HTMLElement[];
151
+ readonly name: DocumentProbeScope;
152
+ readonly id: string;
153
+ readonly title: string;
154
+ }
155
+
156
+ /** THE VOCABULARY, and the one place each word is spelled. Every name resolves
157
+ * through its surface's OWN stamp — see this module's header. */
158
+ const SCOPE_NAMES: readonly DocumentProbeScope[] = [
159
+ 'document',
160
+ 'header',
161
+ 'shelf',
162
+ 'rail',
163
+ 'outliner',
164
+ 'content',
165
+ 'utility',
166
+ 'menubar',
167
+ 'area',
168
+ ];
169
+
170
+ /** The two scopes that are VS Code views: the part id the contribution hands
171
+ * over (`frame/bridge.tsx`'s `offerVolterPart` stamps it), and the view id the
172
+ * workbench registered it under (`volter.contribution.ts`). */
173
+ const VIEW_SCOPES = {
174
+ rail: { part: 'properties', view: 'volter.properties', title: 'Properties' },
175
+ outliner: { part: 'outliner', view: 'volter.outliner', title: 'Outliner' },
176
+ content: { part: 'content', view: 'volter.content', title: 'Content' },
177
+ } as const satisfies Record<string, { part: string; view: string; title: string }>;
178
+
179
+ /** The two scopes that are strips of the ACTIVE DOCUMENT's own box, by the
180
+ * `data-testid` their components write (`DocumentHeaderStrip`,
181
+ * `DocumentShelfRail`). */
182
+ const DOCUMENT_STRIPS = {
183
+ header: { prefix: 'document-header', title: 'header strip' },
184
+ shelf: { prefix: 'document-shelf', title: 'tool shelf' },
185
+ } as const satisfies Record<string, { prefix: string; title: string }>;
186
+
187
+ /**
188
+ * EVERY ROOT THIS SCOPE COVERS — the document's own box, and whatever it
189
+ * PORTALED out of itself.
190
+ *
191
+ * `ThemeRootPortal` writes an anchor span where the portal was declared and
192
+ * stamps both ends with one generated id (`data-volter-portal` in place,
193
+ * `data-volter-portal-content` on what landed at the theme root). So a menu the
194
+ * document's own header opened is found by walking that pair from INSIDE the
195
+ * box — not by a heuristic over every `.volter-menu` on the root, which would
196
+ * also reach the editor's chrome menus and is exactly the widening this
197
+ * module's header rules out. A portal the chrome opened has its anchor in the
198
+ * chrome, so it is not a root here and stays refused.
199
+ *
200
+ * Re-read per step rather than cached with the scope: a menu opens and closes
201
+ * between steps, which is the whole point of driving one.
202
+ */
203
+ function scopeRoots(scope: Scope): HTMLElement[] {
204
+ const roots: HTMLElement[] = [scope.container, ...(scope.extraRoots ?? [])];
205
+ // TRANSITIVE, because a portal opens a portal: a menu's PANEL is portaled,
206
+ // and a submenu opened from one of its rows is portaled from inside THAT
207
+ // panel — so the second anchor is not in the document's box at all. One
208
+ // level deep found the Add menu's rows and none of Mesh's (measured
209
+ // 2026-09-21 on the Model document). The walk terminates because each id is
210
+ // taken once.
211
+ for (let index = 0; index < roots.length; index++) {
212
+ for (const anchor of roots[index]!.querySelectorAll<HTMLElement>('[data-volter-portal]')) {
213
+ const id = anchor.dataset['volterPortal'];
214
+ if (id === undefined) continue;
215
+ const content = document.querySelector<HTMLElement>(
216
+ `[data-volter-portal-content="${CSS.escape(id)}"]`,
217
+ );
218
+ if (content && !roots.includes(content)) roots.push(content);
219
+ }
220
+ }
221
+ // A document whose subject runs in a same-origin frame (the Build Player)
222
+ // hands over that frame's own subject, as it does to the capture door.
223
+ const framed = framedCapture(scope.container)?.container;
224
+ if (framed && !roots.includes(framed)) roots.push(framed);
225
+ return roots;
226
+ }
227
+
228
+ /** A registered VIEW's handed-over body, by the part id the workbench stamped
229
+ * on it. `null` when the view is closed — its pane took the element with it. */
230
+ function viewPart(part: string): HTMLElement | null {
231
+ return document.querySelector<HTMLElement>(`[data-volter-part="${part}"]`);
232
+ }
233
+
234
+ /** The utility view showing in the panel: its body is the one of the stamped
235
+ * bodies (`frame/bridge.tsx`'s `setUtilityBody`) that has a box on screen. */
236
+ function resolveUtilityScope(): Scope {
237
+ const showing = [...document.querySelectorAll<HTMLElement>('[data-volter-utility]')].find((body) => {
238
+ const box = body.getBoundingClientRect();
239
+ return box.width > 0 && box.height > 0;
240
+ });
241
+ if (!showing) {
242
+ throw new Error(
243
+ "No utility view is showing, so scope 'utility' has nothing to reach. Show one in the " +
244
+ 'panel (View: Open View…) and retry.',
245
+ );
246
+ }
247
+ const id = showing.dataset['volterUtility'] ?? '';
248
+ return { container: showing, name: 'utility', id, title: id };
249
+ }
250
+
251
+ /** THE WORKSPACE'S AREAS: every open document a workspace placed in an area (`descriptor.area`),
252
+ * as it is on screen. The Model workspace's bottom area is one (the Timeline and its Action and
253
+ * NLA editors, or the Game panel); a person clicks there every session, and no other scope
254
+ * reaches it, because an area is neither the active centre document nor a VS Code view. */
255
+ function resolveAreaScope(): Scope {
256
+ const shown = openWorkspaceDocuments()
257
+ .filter((open) => open.descriptor.area)
258
+ .flatMap((open) =>
259
+ Array.from(document.querySelectorAll<HTMLElement>('[data-workspace-document-id]')).filter(
260
+ (element) => element.dataset['workspaceDocumentId'] === open.descriptor.id,
261
+ ),
262
+ )
263
+ .filter((element) => {
264
+ const box = element.getBoundingClientRect();
265
+ return box.width > 0 && box.height > 0;
266
+ });
267
+ const [first, ...rest] = shown;
268
+ if (!first) {
269
+ throw new Error("No workspace area is showing, so scope 'area' has nothing to reach.");
270
+ }
271
+ const id = first.dataset['workspaceDocumentId'] ?? '';
272
+ return { container: first, extraRoots: rest, name: 'area', id, title: 'workspace areas' };
273
+ }
274
+
275
+ /** The application menu bar, by the stamp `ApplicationMenus` writes on its own root. The menus
276
+ * its triggers open are portaled, and {@link scopeRoots} reaches them through their anchors. */
277
+ function resolveMenubarScope(): Scope {
278
+ const bar = document.querySelector<HTMLElement>('[data-testid="app-menubar"]');
279
+ if (!bar) {
280
+ throw new Error("The application menus are not on screen, so scope 'menubar' has nothing to reach.");
281
+ }
282
+ return { container: bar, name: 'menubar', id: 'app-menubar', title: 'application menus' };
283
+ }
284
+
285
+ function resolveScope(name: DocumentProbeScope): Scope {
286
+ if (name === 'utility') return resolveUtilityScope();
287
+ if (name === 'menubar') return resolveMenubarScope();
288
+ if (name === 'area') return resolveAreaScope();
289
+ const view =
290
+ name === 'rail' || name === 'outliner' || name === 'content' ? VIEW_SCOPES[name] : null;
291
+ if (view) {
292
+ const container = viewPart(view.part);
293
+ // THE INSPECTOR AS A CARD. The rail's subject is the inspector, which a person may show as a
294
+ // card over the viewport instead of the Properties column (`inspector-presentation.ts`); the
295
+ // workspace's layout host mounts that card outside every document's box, so scope 'rail'
296
+ // covers it too — the same inspector in its other projection, whether or not the Properties
297
+ // view is open beside it.
298
+ // The visible card: every view of the active document mounts one, and a parked view's is
299
+ // in the page with no box.
300
+ const card =
301
+ name === 'rail'
302
+ ? ([
303
+ ...document.querySelectorAll<HTMLElement>('[data-testid="inspector-panel"][data-volter-inspector-presentation="card"]'),
304
+ ].find((candidate) => {
305
+ const box = candidate.getBoundingClientRect();
306
+ return box.width > 0 && box.height > 0;
307
+ }) ?? null)
308
+ : null;
309
+ if (card && !container) return { container: card, name, id: view.view, title: 'Inspector card' };
310
+ if (card && container) return { container, extraRoots: [card], name, id: view.view, title: `${view.title} and the inspector card` };
311
+ if (!container) {
312
+ throw new Error(
313
+ `The ${view.title} view (${view.view}) is not open, so scope '${name}' has nothing to ` +
314
+ 'reach. Open it from the workbench (View: Open View…) and retry — this door reads ' +
315
+ 'and drives what is on screen, and reports its absence rather than an empty match list.',
316
+ );
317
+ }
318
+ return { container, name, id: view.view, title: view.title };
319
+ }
320
+ return resolveDocumentScope(name);
321
+ }
322
+
323
+ /** `document`, `header` and `shelf` — the ACTIVE CENTRE DOCUMENT's box and the
324
+ * two strips inside it. */
325
+ function resolveDocumentScope(name: DocumentProbeScope): Scope {
326
+ const active = activeWorkspaceDocument();
327
+ if (!active)
328
+ throw new Error(
329
+ `No active editor document, so scope '${name}' has nothing to reach. The editor's own ` +
330
+ "views are still readable: scope 'outliner', scope 'rail', and scope 'content'.",
331
+ );
332
+ const id = activeWorkspaceDocumentId() ?? active.descriptor.id;
333
+ // The Game document is the one center document with its OWN doors — and the
334
+ // one where a synthetic DOM gesture is the exact instrument the doctrine
335
+ // bans ("never pilot a game with synthetic key events"). Its input is
336
+ // play-gated (`gated-globals.ts`), so a probe
337
+ // here would either silently do nothing (not playing) or bypass the play
338
+ // path's contract (playing). Refuse toward the honest doors instead of
339
+ // becoming an ungated second one. Its HEADER STRIP is the exception: the
340
+ // editor's own chrome (the Play bar, the resolution, the audio control), not
341
+ // the game's DOM, and otherwise no door reached a control a person clicks
342
+ // there every session.
343
+ if (id === GAME_DOCUMENT_ID && name !== 'header') {
344
+ throw new Error(
345
+ "The Game document is out of this door's scope: drive and read a game through its own " +
346
+ 'doors — `game.commands()`/`game.command(n)`/`game.state(n)`, or `page(step)` during ' +
347
+ "play — never through synthetic DOM gestures. Its header strip (the editor's own Play " +
348
+ "bar and controls) is scope 'header'; the editor's views around it stay readable: scope " +
349
+ "'outliner' and scope 'rail'.",
350
+ );
351
+ }
352
+ const container = activeDocumentContainer(id);
353
+ if (!container) {
354
+ throw new Error(
355
+ `The active document's surface is not mounted: ${id} (${active.title}). ` +
356
+ 'Present it first (`editor.present`) and retry.',
357
+ );
358
+ }
359
+ // The scope is the document's WHOLE box — its header strip and its shelf
360
+ // rail as well as its content (`WorkspaceDocumentSurface`: the strip is a
361
+ // sibling of the content element the id is written on). Three builders
362
+ // measured the same wall: a document's own header controls and menus were
363
+ // unreachable and unreadable through the product, so a header could not be
364
+ // driven by its own click. The Game document's refusal above still holds.
365
+ const box = container.closest<HTMLElement>('.volter-dock-document') ?? container;
366
+ if (name === 'document') return { container: box, name, id, title: active.title };
367
+ // `header` / `shelf` — MEASURED to be inside the box already (the strips are
368
+ // this element's own children), so these names buy AIM rather than reach: a
369
+ // selector that also matches in the content can be pointed at one strip
370
+ // without counting indices. A document that draws no strip (`runtime:
371
+ // false` and no toolbar, or a workspace whose skin hides headers) refuses by
372
+ // name rather than answering zero matches.
373
+ const strip = DOCUMENT_STRIPS[name as keyof typeof DOCUMENT_STRIPS];
374
+ const element = box.querySelector<HTMLElement>(`[data-testid="${strip.prefix}:${id}"]`);
375
+ if (!element) {
376
+ throw new Error(
377
+ `The active document "${active.title}" (${id}) draws no ${strip.title}, so scope ` +
378
+ `'${name}' has nothing to reach. Its whole box is scope 'document'.`,
379
+ );
380
+ }
381
+ return { container: element, name, id, title: `${active.title} — ${strip.title}` };
382
+ }
383
+
384
+ /** The refusal that keeps this door scoped — it names the scope every time,
385
+ * and the vocabulary, because "wrong scope" is the likeliest cause. */
386
+ function outOfScope(scope: Scope, detail: string): Error {
387
+ return new Error(
388
+ `Out of scope: ${detail}. This step ran in scope '${scope.name}' — ` +
389
+ `"${scope.title}" (${scope.id}) — and reaches ONLY inside it. The other surfaces are ` +
390
+ `named, not walked to: ${SCOPE_NAMES.filter((name) => name !== scope.name)
391
+ .map((name) => `'${name}'`)
392
+ .join(', ')} (pass \`{ scope }\`). The rest of the page is deliberately unreachable.`,
393
+ );
394
+ }
395
+
396
+ const MAX_TEXT = 400;
397
+
398
+ /**
399
+ * The sentinel a custom property is resolved against.
400
+ *
401
+ * `getComputedStyle(el).getPropertyValue('--x')` answers with the DECLARED
402
+ * text, so a theme token reads back as its own `color-mix(…)` algebra rather
403
+ * than the colour it paints. To get the paint, the value has to go through a
404
+ * property the browser actually resolves. A throwaway span inside the element
405
+ * inherits the same custom-property cascade, so `color: var(--x)` on it
406
+ * computes to `rgb(…)`.
407
+ *
408
+ * The sentinel is how a NON-colour token stays honest: `color` silently keeps
409
+ * its previous value when the new one does not parse, so a token holding a
410
+ * length would otherwise be reported as whatever colour happened to be there.
411
+ * Seed the sentinel, apply the var, and an unchanged reading means "this did
412
+ * not resolve as a colour" — report the declared text instead of a lie.
413
+ */
414
+ const STYLE_PROBE_SENTINEL = 'rgb(1, 2, 3)';
415
+
416
+ /**
417
+ * `background-color`'s INITIAL value, which is what the probe below computes
418
+ * to whenever the substituted token is not a colour — CSS's own answer for a
419
+ * declaration that is invalid at computed-value time on a NON-INHERITED
420
+ * property.
421
+ */
422
+ const STYLE_PROBE_INITIAL = 'rgba(0, 0, 0, 0)';
423
+
424
+ /**
425
+ * What a custom property PAINTS, its declared text when it is not a colour,
426
+ * or the empty string when THE PROPERTY IS NOT DECLARED AT ALL.
427
+ *
428
+ * That last case is the one this door got wrong first and a builder caught: an
429
+ * undeclared `var(--x)` makes the whole declaration invalid, so the probe fell
430
+ * back and the reading came back as the surrounding text colour — a confident
431
+ * wrong answer, identical for a real token, a misspelled one and a group the
432
+ * palette does not declare. (Measured under Classic:
433
+ * `--volter-viewport-background` and a deliberately nonexistent name both
434
+ * answered `rgb(197, 200, 206)`, and a reader nearly concluded Classic
435
+ * declares a viewport group.) A custom property's computed value is the empty
436
+ * string exactly when it is undeclared, so that is the answer.
437
+ *
438
+ * THE PAINT PROPERTY IS `background-color`, AND THAT IS THE WHOLE TRICK. It
439
+ * used to be `color`, with a sentinel seeded first and the reasoning "an
440
+ * unchanged reading means this did not resolve as a colour". That reasoning is
441
+ * wrong, because `color` is INHERITED: a declaration invalid at computed-value
442
+ * time does not keep its previous value, it takes the INHERITED one — so a
443
+ * token holding a length read back as whatever colour the surrounding text
444
+ * happened to be, the same confident lie one paragraph up. Measured 2026-09-21
445
+ * on the Outliner: `--volter-tree-row-height`, which holds `20px`, answered
446
+ * `rgb(195, 195, 195)` while a reader was measuring row heights with it.
447
+ * `background-color` is NOT inherited, so the same invalid declaration lands
448
+ * on its initial value — `rgba(0, 0, 0, 0)`, a constant this module knows —
449
+ * and "did this resolve as a colour" becomes a fact instead of a guess. The
450
+ * sentinel is still seeded, so a browser that somehow leaves the declaration
451
+ * untouched is also caught. A token whose value IS `transparent` reports its
452
+ * declared text, which is the more useful of the two true answers.
453
+ */
454
+ function resolveCustomProperty(element: HTMLElement, name: string): string {
455
+ const declared = getComputedStyle(element).getPropertyValue(name).trim();
456
+ if (declared === '') return '';
457
+ const owner = element.ownerDocument;
458
+ const probe = owner.createElement('span');
459
+ // Out of flow and invisible: this must not reflow the surface being measured.
460
+ probe.style.position = 'absolute';
461
+ probe.style.pointerEvents = 'none';
462
+ probe.style.visibility = 'hidden';
463
+ probe.style.backgroundColor = STYLE_PROBE_SENTINEL;
464
+ element.appendChild(probe);
465
+ try {
466
+ probe.style.backgroundColor = `var(${name})`;
467
+ const painted = getComputedStyle(probe).backgroundColor.trim();
468
+ return painted === STYLE_PROBE_SENTINEL || painted === STYLE_PROBE_INITIAL ? declared : painted;
469
+ } finally {
470
+ probe.remove();
471
+ }
472
+ }
473
+
474
+ function resolveStyles(element: HTMLElement, names: readonly string[]): Record<string, string> {
475
+ const computed = getComputedStyle(element);
476
+ const styles: Record<string, string> = {};
477
+ for (const name of names) {
478
+ styles[name] = name.startsWith('--')
479
+ ? resolveCustomProperty(element, name)
480
+ : // A standard property is already RESOLVED by the browser here, which is
481
+ // the whole reason to ask for it: the token's expression has become the
482
+ // rgb the person sees.
483
+ computed.getPropertyValue(cssPropertyName(name)).trim();
484
+ }
485
+ return styles;
486
+ }
487
+
488
+ /** `backgroundColor` → `background-color`; a name already dashed passes through. */
489
+ function cssPropertyName(name: string): string {
490
+ return name.includes('-') ? name : name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
491
+ }
492
+
493
+ function describeElement(
494
+ element: Element,
495
+ index: number,
496
+ styles?: readonly string[],
497
+ ): ProbedElement {
498
+ const html = element as HTMLElement;
499
+ const attributes: Record<string, string> = {};
500
+ for (const attribute of Array.from(element.attributes)) {
501
+ attributes[attribute.name] = attribute.value;
502
+ }
503
+ const rect = html.getBoundingClientRect();
504
+ const value = (element as HTMLInputElement).value;
505
+ const checked = (element as HTMLInputElement).checked;
506
+ // `||`, not `??`: in a BACKGROUNDED tab — this door's primary caller —
507
+ // Chrome answers `innerText` with the empty string for everything, so the
508
+ // nullish fallback never fired and every probe read a blank document
509
+ // (measured live, 2026-08-14). `textContent` is the honest answer there.
510
+ const text = (html.innerText || element.textContent || '').trim();
511
+ return {
512
+ index,
513
+ tag: element.tagName.toLowerCase(),
514
+ text: text.length > MAX_TEXT ? `${text.slice(0, MAX_TEXT)}…` : text,
515
+ attributes,
516
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
517
+ ...(typeof value === 'string' ? { value } : {}),
518
+ ...(typeof checked === 'boolean' ? { checked } : {}),
519
+ ...(html.hasAttribute('disabled') ? { disabled: true } : {}),
520
+ ...(styles && styles.length > 0 ? { styles: resolveStyles(html, styles) } : {}),
521
+ };
522
+ }
523
+
524
+ /** Resolve one target inside the scope, refusing loudly rather than reaching out. */
525
+ function resolveTarget(scope: Scope, selector: string, index: number): HTMLElement {
526
+ const matches = matchesIn(scope, selector);
527
+ const element = matches[index];
528
+ if (!element) {
529
+ throw outOfScope(
530
+ scope,
531
+ `${JSON.stringify(selector)} matched ${matches.length} element(s), so index ${index} does not exist`,
532
+ );
533
+ }
534
+ // querySelectorAll on an element is already descendants-only; this asserts
535
+ // the invariant rather than trusting it. A PORTALED overlay the document
536
+ // opened is one of the roots ({@link scopeRoots}), so it passes here by
537
+ // being inside the root that owns it and not by any relaxation.
538
+ if (!scopeRoots(scope).some((root) => root.contains(element))) {
539
+ throw outOfScope(
540
+ scope,
541
+ `${JSON.stringify(selector)} resolved outside the document's container`,
542
+ );
543
+ }
544
+ return element;
545
+ }
546
+
547
+ function pointerInit(element: HTMLElement, at: readonly [number, number] = [0.5, 0.5]): MouseEventInit {
548
+ const rect = element.getBoundingClientRect();
549
+ return {
550
+ bubbles: true,
551
+ cancelable: true,
552
+ composed: true,
553
+ view: element.ownerDocument.defaultView ?? window,
554
+ clientX: rect.x + rect.width * at[0],
555
+ clientY: rect.y + rect.height * at[1],
556
+ button: 0,
557
+ buttons: 1,
558
+ };
559
+ }
560
+
561
+ /**
562
+ * A REAL click: the same event sequence a mouse produces. `element.click()`
563
+ * dispatches only `click`, so a component that selects on `pointerdown` (the
564
+ * sheet grid's cell selection) never sees the gesture at all.
565
+ *
566
+ * `clicks` > 1 is a REAL double click and not two calls to this function: the
567
+ * browser numbers consecutive presses in `detail`, and a `dblclick` follows
568
+ * the second `click`. React's `onDoubleClick` listens for that `dblclick` and
569
+ * nothing else, so a rename driven by two separate single clicks never starts
570
+ * (measured against the Outliner's own `onDoubleClick` → `onStartEditing`).
571
+ */
572
+ /**
573
+ * What a real press's default action does with focus, which an untrusted `mousedown` never does:
574
+ * focus the nearest element that can hold it, the pressed one or an ancestor (a `tabindex`, a
575
+ * natively focusable control, an editable region). A bare `element.focus()` on a plain `<div>` does
576
+ * nothing, and focus stays wherever a pointer handler along the way put it.
577
+ */
578
+ function focusAsPressed(element: HTMLElement): void {
579
+ let focusable: HTMLElement | null = element;
580
+ while (focusable && !(focusable.hasAttribute('tabindex') || focusable.tabIndex >= 0 || focusable.isContentEditable)) {
581
+ focusable = focusable.parentElement;
582
+ }
583
+ (focusable ?? element).focus({ preventScroll: true });
584
+ }
585
+
586
+ /** A point given as fractions of `element`'s box, in its own document's client pixels. */
587
+ function pointAt(element: HTMLElement, at: readonly [number, number] = [0.5, 0.5]): { clientX: number; clientY: number } {
588
+ const rect = element.getBoundingClientRect();
589
+ return { clientX: rect.x + rect.width * at[0], clientY: rect.y + rect.height * at[1] };
590
+ }
591
+
592
+ /** What a framed page hit-tests at that point of `element`: the element a mouse press there reaches. */
593
+ function pressedInFrame(element: HTMLElement, at?: readonly [number, number]): HTMLElement {
594
+ const { clientX, clientY } = pointAt(element, at);
595
+ const hit = element.ownerDocument.elementFromPoint(clientX, clientY);
596
+ return (hit as HTMLElement | null) ?? element;
597
+ }
598
+
599
+ async function dispatchClick(
600
+ element: HTMLElement,
601
+ clicks = 1,
602
+ at?: readonly [number, number],
603
+ modifiers: { altKey?: boolean; ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean } = {},
604
+ point?: { clientX: number; clientY: number },
605
+ ): Promise<void> {
606
+ const init = { ...pointerInit(element, at), ...point, ...modifiers };
607
+ const total = Math.max(1, Math.round(clicks));
608
+ const doc = element.ownerDocument;
609
+ for (let n = 1; n <= total; n++) {
610
+ const down = { ...init, detail: n };
611
+ const up = { ...init, buttons: 0, detail: n };
612
+ withoutPointerCapture(element, () => {
613
+ element.dispatchEvent(new PointerEvent('pointerdown', { ...down, pointerType: 'mouse' }));
614
+ element.dispatchEvent(new MouseEvent('mousedown', down));
615
+ focusAsPressed(element);
616
+ });
617
+ // A MOUSE'S PRESS AND RELEASE ARE TWO TASKS, and the page renders between them. Dispatched
618
+ // in one, a press that unmounts its own target (a menu closing on an outside `pointerdown`)
619
+ // still delivered its click, so this door passed Edit › Undo while a person's click did
620
+ // nothing (measured 2026-09-28 with a real click on the browser-substrate page). The release
621
+ // goes where a mouse's would: the pressed element if it is still there, else what is under
622
+ // the point, and a vanished element gets no click.
623
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
624
+ const released = element.isConnected
625
+ ? element
626
+ : ((doc.elementFromPoint(init.clientX ?? 0, init.clientY ?? 0) as HTMLElement | null) ?? doc.body);
627
+ withoutPointerCapture(released, () => {
628
+ released.dispatchEvent(new PointerEvent('pointerup', { ...up, pointerType: 'mouse' }));
629
+ released.dispatchEvent(new MouseEvent('mouseup', up));
630
+ if (released === element) element.dispatchEvent(new MouseEvent('click', up));
631
+ });
632
+ if (released !== element) return;
633
+ }
634
+ if (total >= 2) {
635
+ withoutPointerCapture(element, () =>
636
+ element.dispatchEvent(new MouseEvent('dblclick', { ...init, buttons: 0, detail: total })),
637
+ );
638
+ }
639
+ }
640
+
641
+ /**
642
+ * TYPE `text` into a field the way a person does — one character at a time,
643
+ * through the PROTOTYPE's value setter, between real `keydown`/`keyup`.
644
+ *
645
+ * Three traps this exists for, each measured on a shipped surface:
646
+ * - `paste` cannot stand in. An untrusted `ClipboardEvent` performs NO
647
+ * default action, so a plain `<input>` with no paste handler keeps its old
648
+ * value and the probe reports a click it never made.
649
+ * - `element.value = x` is invisible to React (the value tracker it installs
650
+ * on the node compares the new value against itself and re-renders the old
651
+ * one) — the same trap {@link setSelectValue} documents, and the same cure.
652
+ * - a field may commit on a KEY rather than on `input`. The Outliner's rename
653
+ * reads `event.currentTarget.value` inside `onKeyDown` for Enter; a numeric
654
+ * field in the Properties rail blurs on Enter. So the keys are real and the
655
+ * Enter is the caller's choice, not an implicit one.
656
+ */
657
+ function typeInto(element: HTMLElement, text: string, replace: boolean, enter: boolean): void {
658
+ const editable =
659
+ element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement ? element : null;
660
+ if (!editable) {
661
+ throw new Error(
662
+ `Cannot type into a <${element.tagName.toLowerCase()}>: this verb drives a text field. ` +
663
+ "Click the control that opens one first (the Outliner's rename is a double click — " +
664
+ '`click(selector, { clicks: 2 })`), then `type` into the field it puts up.',
665
+ );
666
+ }
667
+ const proto =
668
+ element instanceof HTMLTextAreaElement
669
+ ? HTMLTextAreaElement.prototype
670
+ : HTMLInputElement.prototype;
671
+ const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
672
+ const write = (next: string) => {
673
+ if (setter) setter.call(editable, next);
674
+ else editable.value = next;
675
+ };
676
+ editable.focus();
677
+ let held = editable.value;
678
+ if (replace && held !== '') {
679
+ // What select-all-and-delete produces, in one edit: a person's first
680
+ // keystroke over a selected field is a replacement, not N backspaces.
681
+ editable.select?.();
682
+ held = '';
683
+ write(held);
684
+ editable.dispatchEvent(
685
+ new InputEvent('input', {
686
+ bubbles: true,
687
+ composed: true,
688
+ inputType: 'deleteContentBackward',
689
+ }),
690
+ );
691
+ }
692
+ for (const character of [...text]) {
693
+ const init: KeyboardEventInit = {
694
+ key: character,
695
+ bubbles: true,
696
+ cancelable: true,
697
+ composed: true,
698
+ };
699
+ editable.dispatchEvent(new KeyboardEvent('keydown', init));
700
+ held += character;
701
+ write(held);
702
+ editable.dispatchEvent(
703
+ new InputEvent('input', {
704
+ bubbles: true,
705
+ composed: true,
706
+ inputType: 'insertText',
707
+ data: character,
708
+ }),
709
+ );
710
+ editable.dispatchEvent(new KeyboardEvent('keyup', init));
711
+ }
712
+ if (enter) {
713
+ const init: KeyboardEventInit = {
714
+ key: 'Enter',
715
+ code: 'Enter',
716
+ bubbles: true,
717
+ cancelable: true,
718
+ composed: true,
719
+ };
720
+ editable.dispatchEvent(new KeyboardEvent('keydown', init));
721
+ editable.dispatchEvent(new KeyboardEvent('keyup', init));
722
+ }
723
+ }
724
+
725
+ /**
726
+ * A synthetic pointer has no id the browser knows, so a listener that calls
727
+ * `setPointerCapture(event.pointerId)` — three's OrbitControls and
728
+ * TransformControls do, on every press — throws `NotFoundError` INTO THE
729
+ * CONSOLE whenever a document yields the press to the viewport (measured
730
+ * 2026-09-02: every missed click on an Asset Lab canvas). Listeners run
731
+ * synchronously inside `dispatchEvent`, so the capture calls are made
732
+ * harmless for exactly the dispatch and restored after; a real pointer is
733
+ * never affected.
734
+ */
735
+ function withoutPointerCapture(element: HTMLElement, dispatch: () => void): void {
736
+ // On the PROTOTYPE, not the target: the viewport's controls listen on the
737
+ // canvas's container and capture THERE, so the event's whole bubble path
738
+ // has to be covered. Synchronous, restored in `finally`, and a real pointer
739
+ // (which never dispatches through here) is untouched. The element's own
740
+ // realm's prototype: a framed page (the Build Player) has its own.
741
+ const realm = element.ownerDocument.defaultView as (Window & typeof globalThis) | null;
742
+ const proto = (realm ?? window).Element.prototype;
743
+ const set = proto.setPointerCapture;
744
+ const release = proto.releasePointerCapture;
745
+ proto.setPointerCapture = () => {};
746
+ proto.releasePointerCapture = () => {};
747
+ try {
748
+ dispatch();
749
+ } finally {
750
+ proto.setPointerCapture = set;
751
+ proto.releasePointerCapture = release;
752
+ }
753
+ }
754
+
755
+ /** A REAL drag: press at `from`, move through `steps` points, release at `to`,
756
+ * all in the element's own box fractions — `dispatchClick`'s sequence with
757
+ * the moves a mouse makes between press and release. Moves carry
758
+ * `buttons: 1` (the primary button is held), the release `buttons: 0`. */
759
+ /** THE DRAG A `hold` LEFT PRESSED, until a `release` step lets go of it where it was left. */
760
+ let heldDrag: { element: HTMLElement; end: MouseEventInit; button: 0 | 1 | 2 } | null = null;
761
+
762
+ function releaseDrag(): HTMLElement {
763
+ const held = heldDrag;
764
+ if (held === null)
765
+ throw new Error('Nothing is held: release lets go of a drag that `hold: true` left pressed, and none is.');
766
+ heldDrag = null;
767
+ withoutPointerCapture(held.element, () => {
768
+ held.element.dispatchEvent(new PointerEvent('pointerup', { ...held.end, pointerType: 'mouse' }));
769
+ held.element.dispatchEvent(new MouseEvent('mouseup', held.end));
770
+ if (held.button === 2) held.element.dispatchEvent(new MouseEvent('contextmenu', held.end));
771
+ });
772
+ return held.element;
773
+ }
774
+
775
+ function dispatchDrag(
776
+ element: HTMLElement,
777
+ from: readonly [number, number],
778
+ to: readonly [number, number],
779
+ steps: number,
780
+ modifiers: { altKey?: boolean; ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean } = {},
781
+ via: readonly (readonly [number, number])[] = [],
782
+ button: 0 | 1 | 2 = 0,
783
+ hold = false,
784
+ ): void {
785
+ const rect = element.getBoundingClientRect();
786
+ // `buttons` is a bitmask in a different order from `button`: primary 1,
787
+ // secondary 2, middle 4.
788
+ const held = button === 0 ? 1 : button === 2 ? 2 : 4;
789
+ const at = (fx: number, fy: number): MouseEventInit => ({
790
+ bubbles: true,
791
+ cancelable: true,
792
+ composed: true,
793
+ view: window,
794
+ clientX: rect.x + rect.width * fx,
795
+ clientY: rect.y + rect.height * fy,
796
+ button,
797
+ buttons: held,
798
+ altKey: modifiers.altKey ?? false,
799
+ ctrlKey: modifiers.ctrlKey ?? false,
800
+ metaKey: modifiers.metaKey ?? false,
801
+ shiftKey: modifiers.shiftKey ?? false,
802
+ });
803
+ const start = at(from[0], from[1]);
804
+ withoutPointerCapture(element, () => {
805
+ // A REAL POINTER HOVERS BEFORE IT PRESSES, and some targets latch on the
806
+ // hover rather than on the press. three's `TransformControls` is the
807
+ // worked case: `pointerHover` is what sets `this.axis` from the picker
808
+ // under the cursor, and `pointerDown` does nothing at all while `axis` is
809
+ // null — so a synthetic drag that began with `pointerdown` handed the
810
+ // press to the VIEWPORT instead, which read it as a click on empty space
811
+ // and cleared the selection. Measured 2026-09-21 on a Blender Model
812
+ // document: dragging the X arrow of a freshly duplicated cube moved
813
+ // nothing and deselected it.
814
+ //
815
+ // Same family as the `button: -1` note below, and the same rule: mirror
816
+ // what the platform does. `buttons: 0` because nothing is held yet.
817
+ element.dispatchEvent(
818
+ new PointerEvent('pointermove', {
819
+ ...start,
820
+ button: -1,
821
+ buttons: 0,
822
+ pointerType: 'mouse',
823
+ }),
824
+ );
825
+ element.dispatchEvent(new MouseEvent('mousemove', { ...start, buttons: 0 }));
826
+ element.dispatchEvent(new PointerEvent('pointerdown', { ...start, pointerType: 'mouse' }));
827
+ element.dispatchEvent(new MouseEvent('mousedown', start));
828
+ focusAsPressed(element);
829
+ const count = Math.max(1, Math.round(steps));
830
+ const path: (readonly [number, number])[] = [from, ...via, to];
831
+ for (let leg = 0; leg + 1 < path.length; leg++) {
832
+ const a = path[leg] as readonly [number, number];
833
+ const b = path[leg + 1] as readonly [number, number];
834
+ for (let i = 1; i <= count; i++) {
835
+ const t = i / count;
836
+ const move = at(a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t);
837
+ // `button: -1` ON A `pointermove`, because that is what a real mouse
838
+ // reports: `PointerEvent.button` names the button whose STATE CHANGED,
839
+ // and on a move none did. Held-ness lives in `buttons: 1` (kept by
840
+ // `at`). This is not a detail — three.js guards on it exactly:
841
+ // `TransformControls.pointerMove` returns at
842
+ // `pointer.button !== -1` (r180, :472), so a drag of a gizmo handle
843
+ // latched the axis, drew the drag helper line, and then moved NOTHING,
844
+ // forever (measured 2026-09-21 on a Blender Model document, where it
845
+ // read as "the transform provider is not writing"). `MouseEvent`'s
846
+ // legacy `mousemove` keeps `button: 0` — a real browser reports that
847
+ // one as 0, and mirroring the platform is the whole job here.
848
+ element.dispatchEvent(
849
+ new PointerEvent('pointermove', { ...move, button: -1, pointerType: 'mouse' }),
850
+ );
851
+ element.dispatchEvent(new MouseEvent('mousemove', move));
852
+ }
853
+ }
854
+ const end = { ...at(to[0], to[1]), buttons: 0 };
855
+ if (hold) {
856
+ heldDrag = { element, end, button };
857
+ return;
858
+ }
859
+ element.dispatchEvent(new PointerEvent('pointerup', { ...end, pointerType: 'mouse' }));
860
+ element.dispatchEvent(new MouseEvent('mouseup', end));
861
+ // A released primary button clicks; a released secondary one asks for the
862
+ // context menu, as the platform does.
863
+ if (button === 2) element.dispatchEvent(new MouseEvent('contextmenu', end));
864
+ else if (button === 0 && from[0] === to[0] && from[1] === to[1]) {
865
+ element.dispatchEvent(new MouseEvent('click', end));
866
+ }
867
+ });
868
+ }
869
+
870
+ /** The element a key goes to: the explicit target, else whatever inside the
871
+ * scope currently has focus, else the container itself. */
872
+ function keyTarget(scope: Scope, explicit: HTMLElement | null): HTMLElement {
873
+ if (explicit) return explicit;
874
+ const focused = document.activeElement;
875
+ if (focused instanceof HTMLElement && scopeRoots(scope).some((root) => root.contains(focused)))
876
+ return focused;
877
+ return scope.container;
878
+ }
879
+
880
+ /** Run a selector inside the scope, turning a malformed one into an honest error. */
881
+ function matchesIn(scope: Scope, selector: string): HTMLElement[] {
882
+ try {
883
+ return scopeRoots(scope).flatMap((root) =>
884
+ Array.from(root.querySelectorAll<HTMLElement>(selector)),
885
+ );
886
+ } catch (error) {
887
+ throw new Error(`Invalid selector ${JSON.stringify(selector)}: ${String(error)}`);
888
+ }
889
+ }
890
+
891
+ /** An optional `selector` resolves to a target; its absence means "the focused
892
+ * element inside the scope" — the shared shape of the `key` and `paste` steps. */
893
+ function gestureTarget(scope: Scope, step: { selector?: string; index?: number }): HTMLElement {
894
+ const explicit =
895
+ step.selector === undefined ? null : resolveTarget(scope, step.selector, step.index ?? 0);
896
+ return keyTarget(scope, explicit);
897
+ }
898
+
899
+ /** The physical key a person presses for `key`, as `KeyboardEvent.code` names it on a US
900
+ * layout: a keystroke that says `code: 'w'` is one no keybinding resolver or game input map
901
+ * recognises. */
902
+ const PUNCTUATION_CODES: Readonly<Record<string, string>> = {
903
+ ' ': 'Space', '`': 'Backquote', '-': 'Minus', '=': 'Equal', '[': 'BracketLeft', ']': 'BracketRight',
904
+ '\\': 'Backslash', ';': 'Semicolon', "'": 'Quote', ',': 'Comma', '.': 'Period', '/': 'Slash',
905
+ };
906
+ function codeOf(key: string): string {
907
+ if (/^[a-z]$/i.test(key)) return `Key${key.toUpperCase()}`;
908
+ if (/^[0-9]$/.test(key)) return `Digit${key}`;
909
+ return PUNCTUATION_CODES[key] ?? key;
910
+ }
911
+
912
+ /** The legacy `keyCode` browsers still give every keystroke and VS Code's keybinding
913
+ * resolution reads; a synthesized event carries 0 unless it is set. */
914
+ const NAMED_KEY_CODES: Readonly<Record<string, number>> = {
915
+ Backspace: 8, Tab: 9, Enter: 13, Escape: 27, ' ': 32, PageUp: 33, PageDown: 34, End: 35, Home: 36,
916
+ ArrowLeft: 37, ArrowUp: 38, ArrowRight: 39, ArrowDown: 40, Delete: 46,
917
+ };
918
+ function keyCodeOf(key: string): number {
919
+ if (/^[a-z0-9]$/i.test(key)) return key.toUpperCase().charCodeAt(0);
920
+ const fn = /^F([1-9]|1[0-2])$/.exec(key);
921
+ if (fn) return 111 + Number(fn[1]);
922
+ return NAMED_KEY_CODES[key] ?? 0;
923
+ }
924
+
925
+ /** A numpad key's legacy code follows its PHYSICAL key: Numpad0..9 are 96..105 and the
926
+ * decimal 110, where the digit they type would say 48..57. */
927
+ function numpadKeyCode(code: string | undefined): number | null {
928
+ const digit = code === undefined ? null : /^Numpad([0-9])$/.exec(code);
929
+ if (digit) return 96 + Number(digit[1]);
930
+ return code === 'NumpadDecimal' ? 110 : null;
931
+ }
932
+
933
+ function keyEvent(type: 'keydown' | 'keyup', step: DocumentKeyStep): KeyboardEvent {
934
+ const event = new KeyboardEvent(type, keyInit(step));
935
+ const keyCode = numpadKeyCode(step.code) ?? keyCodeOf(step.key);
936
+ Object.defineProperty(event, 'keyCode', { get: () => keyCode });
937
+ Object.defineProperty(event, 'which', { get: () => keyCode });
938
+ return event;
939
+ }
940
+
941
+ function keyInit(step: DocumentKeyStep): KeyboardEventInit {
942
+ return {
943
+ key: step.key,
944
+ code: step.code ?? codeOf(step.key),
945
+ bubbles: true,
946
+ cancelable: true,
947
+ composed: true,
948
+ ...(step.ctrlKey ? { ctrlKey: true } : {}),
949
+ ...(step.metaKey ? { metaKey: true } : {}),
950
+ ...(step.shiftKey ? { shiftKey: true } : {}),
951
+ ...(step.altKey ? { altKey: true } : {}),
952
+ };
953
+ }
954
+
955
+ /**
956
+ * Set a `<select>`'s value the way React can see it.
957
+ *
958
+ * `HTMLSelectElement.prototype`'s own `value` setter is called explicitly
959
+ * because React installs a value tracker on the node: assigning through the
960
+ * instance updates that tracker too, so React compares the new value against
961
+ * itself, concludes nothing changed, and re-renders the OLD value. Going
962
+ * through the prototype descriptor leaves the tracker stale, which is exactly
963
+ * what makes the following `change` read as a real user edit.
964
+ */
965
+ function setSelectValue(element: HTMLSelectElement, value: string): void {
966
+ const options = [...element.options].map((option) => option.value);
967
+ if (!options.includes(value)) {
968
+ throw new Error(
969
+ `No option with value ${JSON.stringify(value)} on this <select> — it offers ` +
970
+ `${options.map((option) => JSON.stringify(option)).join(', ') || '(no options)'}. ` +
971
+ "The value is the option's `value`, not its label; `query` reports both.",
972
+ );
973
+ }
974
+ const setter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set;
975
+ if (setter) setter.call(element, value);
976
+ else element.value = value;
977
+ element.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
978
+ element.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
979
+ }
980
+
981
+ const PROBE_ACTIONS = ['query', 'click', 'drag', 'release', 'key', 'paste', 'select', 'type'] as const;
982
+
983
+ /**
984
+ * THE WIRE'S TWO FREE-FORM FIELDS, both refused by name.
985
+ *
986
+ * The wire hands the step in untyped (`cmd['step']`), so an unknown verb or an
987
+ * unknown scope must refuse HERE — an exhaustive switch alone would fall
988
+ * through and answer `ok` with nothing, and an unrecognized scope would
989
+ * quietly fall back to the document and answer about a surface the caller did
990
+ * not ask for. Both are the silent pass-through the strict-input rule bans.
991
+ */
992
+ function acceptStep(step: DocumentProbeStep): Scope {
993
+ const action = (step as { action?: unknown } | null | undefined)?.action;
994
+ if (!PROBE_ACTIONS.includes(action as (typeof PROBE_ACTIONS)[number])) {
995
+ throw new Error(
996
+ `Unknown document-probe action ${JSON.stringify(action)}. This door has exactly ` +
997
+ `${PROBE_ACTIONS.length} verbs: ${PROBE_ACTIONS.join(', ')} (\`editor.document.*\`).`,
998
+ );
999
+ }
1000
+ const requested = (step as { scope?: unknown }).scope ?? 'document';
1001
+ if (!SCOPE_NAMES.includes(requested as DocumentProbeScope)) {
1002
+ throw new Error(
1003
+ `Unknown scope ${JSON.stringify(requested)}. This door addresses exactly these ` +
1004
+ `surfaces: ${SCOPE_NAMES.map((name) => `'${name}'`).join(', ')}.`,
1005
+ );
1006
+ }
1007
+ return resolveScope(requested as DocumentProbeScope);
1008
+ }
1009
+
1010
+ export async function runDocumentProbe(step: DocumentProbeStep): Promise<DocumentProbeResult> {
1011
+ const scope = acceptStep(step);
1012
+ // WHILE THE GAME HEARS THE KEYBOARD, THIS DOOR READS AND CLICKS ONLY. Every event it dispatches
1013
+ // bubbles to \`window\`, where the game's own listeners pass the same gate this reads
1014
+ // (\`surfaceHoldsKeyboard\`, \`gated-globals.ts\`): while the Game document is active and no view
1015
+ // outside the editor area holds focus, a key, a typed string, a paste or a drag in any scope
1016
+ // would drive the game with synthetic input, which is what refusing the Game document protects.
1017
+ // With focus in a view (a field of the Network inspector, the rail), the game hears none of it.
1018
+ if (
1019
+ activeWorkspaceDocumentId() === GAME_DOCUMENT_ID &&
1020
+ surfaceHoldsKeyboard() &&
1021
+ step.action !== 'query' &&
1022
+ step.action !== 'click' &&
1023
+ step.action !== 'select'
1024
+ ) {
1025
+ throw new Error(
1026
+ `'${step.action}' is refused while the Game document holds the keyboard: its events would ` +
1027
+ "reach the game. Query, click and select still work; click into a view's field first, or " +
1028
+ "drive the game through the game's own doors.",
1029
+ );
1030
+ }
1031
+ const where = { name: scope.name, id: scope.id, title: scope.title };
1032
+ /** Every gesture answers with the element it drove, so a transcript proves
1033
+ * WHAT was driven and not merely that something was. */
1034
+ const drove = (element: HTMLElement): DocumentProbeResult => ({
1035
+ scope: where,
1036
+ matched: 1,
1037
+ elements: [describeElement(element, 0)],
1038
+ });
1039
+ switch (step.action) {
1040
+ case 'query': {
1041
+ const matches = matchesIn(scope, step.selector);
1042
+ return {
1043
+ scope: where,
1044
+ matched: matches.length,
1045
+ elements: matches
1046
+ .slice(0, step.limit ?? 25)
1047
+ .map((element, index) => describeElement(element, index, step.styles)),
1048
+ };
1049
+ }
1050
+ case 'click': {
1051
+ const element = resolveTarget(scope, step.selector, step.index ?? 0);
1052
+ const at = step.at;
1053
+ if (at !== undefined && !(Array.isArray(at) && at.length === 2 && at.every((v) => typeof v === 'number' && Number.isFinite(v)))) {
1054
+ throw new Error(`click's \`at\` is [x, y], fractions of the element's box; got ${JSON.stringify(at)}.`);
1055
+ }
1056
+ // Inside a framed page the press lands where a mouse would: on whatever the page
1057
+ // hit-tests at that point (its stacking and `pointer-events` decide), which the answer names.
1058
+ const pressed = element.ownerDocument === document ? element : pressedInFrame(element, at);
1059
+ await dispatchClick(pressed, step.clicks ?? 1, at && pressed === element ? at : undefined, {
1060
+ ...(step.altKey ? { altKey: true } : {}),
1061
+ ...(step.ctrlKey ? { ctrlKey: true } : {}),
1062
+ ...(step.metaKey ? { metaKey: true } : {}),
1063
+ ...(step.shiftKey ? { shiftKey: true } : {}),
1064
+ }, pressed === element ? undefined : pointAt(element, at));
1065
+ return drove(pressed);
1066
+ }
1067
+ case 'type': {
1068
+ const target = gestureTarget(scope, step);
1069
+ typeInto(target, step.text, step.replace ?? true, step.enter ?? true);
1070
+ return drove(target);
1071
+ }
1072
+ case 'drag': {
1073
+ if (step.button !== undefined && ![0, 1, 2].includes(step.button))
1074
+ throw new Error(`Unknown button ${JSON.stringify(step.button)}: 0 primary, 1 middle, 2 secondary.`);
1075
+ const element = resolveTarget(scope, step.selector, step.index ?? 0);
1076
+ // Points are FRACTIONS of the element's box. A pixel value lands far outside it and the
1077
+ // gesture silently goes somewhere else, so a point outside 0..1 is refused by name.
1078
+ const box = element.getBoundingClientRect();
1079
+ const points: [string, readonly [number, number]][] = [
1080
+ ['from', step.from],
1081
+ ...(step.via ?? []).map((point, index): [string, readonly [number, number]] => [`via[${index}]`, point]),
1082
+ ['to', step.to],
1083
+ ];
1084
+ for (const [name, point] of points) {
1085
+ const inside = point.every((value) => Number.isFinite(value) && value >= 0 && value <= 1);
1086
+ if (inside) continue;
1087
+ if (step.leave && name !== 'from' && point.every((value) => Number.isFinite(value))) continue;
1088
+ throw new Error(
1089
+ `drag ${name} ${JSON.stringify(point)} is not a fraction of the element's box: points are ` +
1090
+ `[x, y] from 0 to 1 from its top-left ([0.5, 0.5] is its centre). This element is ` +
1091
+ `${Math.round(box.width)} x ${Math.round(box.height)} px at (${Math.round(box.x)}, ${Math.round(box.y)}); ` +
1092
+ `a page pixel px becomes (px - ${Math.round(box.x)}) / ${Math.round(box.width)}.`,
1093
+ );
1094
+ }
1095
+ dispatchDrag(
1096
+ element,
1097
+ step.from,
1098
+ step.to,
1099
+ step.steps ?? 8,
1100
+ {
1101
+ ...(step.altKey === undefined ? {} : { altKey: step.altKey }),
1102
+ ...(step.ctrlKey === undefined ? {} : { ctrlKey: step.ctrlKey }),
1103
+ ...(step.metaKey === undefined ? {} : { metaKey: step.metaKey }),
1104
+ ...(step.shiftKey === undefined ? {} : { shiftKey: step.shiftKey }),
1105
+ },
1106
+ step.via ?? [],
1107
+ step.button ?? 0,
1108
+ step.hold === true,
1109
+ );
1110
+ return drove(element);
1111
+ }
1112
+ case 'release':
1113
+ return drove(releaseDrag());
1114
+ case 'key': {
1115
+ if (step.holdMs !== undefined && (!Number.isFinite(step.holdMs) ||
1116
+ step.holdMs < 0 || step.holdMs > MAX_DOCUMENT_KEY_HOLD_MS)) {
1117
+ throw new Error(`key holdMs must be finite, nonnegative and at most ${MAX_DOCUMENT_KEY_HOLD_MS}.`);
1118
+ }
1119
+ const target = gestureTarget(scope, step);
1120
+ // A person's keystroke lands where their click put focus; what the workbench decides a
1121
+ // chord means follows that focus (its `volter.stage.focused` context), so the target takes
1122
+ // focus first unless focus is already inside it.
1123
+ if (!(document.activeElement instanceof Node && target.contains(document.activeElement))) {
1124
+ // As a click does: the nearest element that can hold focus, the target or an ancestor.
1125
+ focusAsPressed(target);
1126
+ }
1127
+ // And the keystroke itself goes to the element holding focus, as a person's does: a
1128
+ // target that contains the focused element (the document itself, with no selector) is where
1129
+ // it bubbles through, not where it starts.
1130
+ const active = document.activeElement;
1131
+ const receiver = active instanceof HTMLElement && target.contains(active) ? active : target;
1132
+ receiver.dispatchEvent(keyEvent('keydown', step));
1133
+ try {
1134
+ if (step.holdMs) await new Promise((settle) => setTimeout(settle, step.holdMs));
1135
+ } finally {
1136
+ receiver.dispatchEvent(keyEvent('keyup', step));
1137
+ }
1138
+ return drove(target);
1139
+ }
1140
+ case 'select': {
1141
+ const element = resolveTarget(scope, step.selector, step.index ?? 0);
1142
+ if (!(element instanceof HTMLSelectElement)) {
1143
+ throw new Error(
1144
+ `${JSON.stringify(step.selector)} resolved a <${element.tagName.toLowerCase()}>, not a ` +
1145
+ "<select>. This verb sets a dropdown's value; a button or a checkbox takes `click`.",
1146
+ );
1147
+ }
1148
+ setSelectValue(element, step.value);
1149
+ return drove(element);
1150
+ }
1151
+ case 'paste': {
1152
+ const target = gestureTarget(scope, step);
1153
+ const data = new DataTransfer();
1154
+ data.setData('text/plain', step.text);
1155
+ target.dispatchEvent(
1156
+ new ClipboardEvent('paste', {
1157
+ bubbles: true,
1158
+ cancelable: true,
1159
+ composed: true,
1160
+ clipboardData: data,
1161
+ }),
1162
+ );
1163
+ return drove(target);
1164
+ }
1165
+ }
1166
+ }