@volter/editor-game 0.5.67 → 0.5.69

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 (173) hide show
  1. package/NOTICE +7 -7
  2. package/contributions/autoplay.service.ts +1 -1
  3. package/contributions/build-player.document.tsx +95 -0
  4. package/contributions/build-profiles.document.tsx +1 -1
  5. package/contributions/build-progress.status.tsx +2 -2
  6. package/contributions/build.header.tsx +1 -1
  7. package/contributions/connection.status.tsx +1 -1
  8. package/contributions/coverage.service.ts +2 -2
  9. package/contributions/game-eval.command.ts +6 -6
  10. package/contributions/gameplay.command.ts +2 -2
  11. package/contributions/generations.status.tsx +1 -1
  12. package/contributions/ingest.service.ts +2 -2
  13. package/contributions/play.command.ts +6 -6
  14. package/contributions/react/react-inspector.service.ts +1 -1
  15. package/package.json +9 -8
  16. package/serving/index.ts +1 -1
  17. package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
  18. package/src/asset-budget/asset-budget-model.ts +14 -14
  19. package/src/asset-budget/optimize-apply.ts +8 -8
  20. package/src/bridge/call.ts +3 -3
  21. package/src/bridge/dispatch.ts +12 -12
  22. package/src/bridge/screenshot.ts +6 -6
  23. package/src/build/BuildProfilesPanel.tsx +19 -8
  24. package/src/build/build-session.ts +35 -0
  25. package/src/canvas/canvas-board/CanvasBoardDocument.tsx +78 -5
  26. package/src/canvas/canvas-board/canvas-component-board.ts +1 -1
  27. package/src/canvas/canvas-design-mount.ts +1 -1
  28. package/src/coverage/live-project-verbs.ts +3 -3
  29. package/src/coverage/native-system-coverage.ts +3 -3
  30. package/src/coverage/root-coverage.ts +2 -2
  31. package/src/coverage/session-coverage.ts +2 -2
  32. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +2 -2
  33. package/src/game-document/DevicePresetPicker.tsx +6 -6
  34. package/src/game-document/GameCaptureFrameButton.tsx +1 -1
  35. package/src/game-document/GameDocument.tsx +2 -2
  36. package/src/game-document/GamePanel.tsx +9 -9
  37. package/src/game-document/InstanceInspectorPicker.tsx +1 -1
  38. package/src/game-document/device-preview.ts +4 -4
  39. package/src/game-document/physics-debug.ts +2 -2
  40. package/src/generation/GenerationActivity.tsx +22 -22
  41. package/src/generation/GenerationGallery.css +79 -79
  42. package/src/generation/generation-documents.tsx +25 -25
  43. package/src/generation/generation-presentation.ts +1 -1
  44. package/src/host/adapter-reach.ts +1 -1
  45. package/src/host/adapter-runtime-bindings.ts +5 -5
  46. package/src/host/after-paint.ts +3 -3
  47. package/src/host/api/configurations.ts +1 -1
  48. package/src/host/authoring/canvas-runtime-recognition.ts +2 -2
  49. package/src/host/authoring/contract-hierarchy-authoring.ts +6 -6
  50. package/src/host/authoring/contract-scenes-stories.ts +5 -5
  51. package/src/host/authoring/ingest-data-writer.ts +1 -1
  52. package/src/host/authoring/ingest-source-persistence.ts +2 -2
  53. package/src/host/authoring/mount-isolated-pixi-screen.ts +1 -1
  54. package/src/host/authoring/pixi-authoring-adapter.ts +206 -28
  55. package/src/host/authoring/pixi-creatable-kinds.ts +80 -0
  56. package/src/host/authoring/pixi-live-write-target.ts +6 -5
  57. package/src/host/authoring/pixi-source-write-target.ts +118 -20
  58. package/src/host/authoring/pixi-structure-history.ts +3 -3
  59. package/src/host/binding-resolver.ts +6 -6
  60. package/src/host/browser-transpile.ts +10 -10
  61. package/src/host/canvas-preview-frames.ts +1 -1
  62. package/src/host/components/CameraAuthoringOverlay.tsx +14 -14
  63. package/src/host/components/HeaderTelemetry.tsx +11 -11
  64. package/src/host/components/PixiIsolationSceneContent.tsx +3 -3
  65. package/src/host/components/ResolutionPicker.tsx +4 -4
  66. package/src/host/components/scene-document.tsx +3 -3
  67. package/src/host/components/world-root-stage-binding.tsx +1 -1
  68. package/src/host/components/world-root-stage.ts +1 -1
  69. package/src/host/coverage/authoring-read-probe.ts +1 -1
  70. package/src/host/coverage/capability-coverage.ts +22 -22
  71. package/src/host/coverage/game-contract-seam-evidence.ts +8 -8
  72. package/src/host/coverage/project-verb-coverage.ts +1 -1
  73. package/src/host/coverage/system-adapter-coverage.ts +19 -6
  74. package/src/host/game-location-guard.ts +3 -3
  75. package/src/host/game-module-access.ts +6 -6
  76. package/src/host/game-realm-page.ts +3 -3
  77. package/src/host/game-realm-reclaim.ts +1 -1
  78. package/src/host/gameplay-recording.ts +5 -5
  79. package/src/host/gated-globals.ts +13 -13
  80. package/src/host/ingest/registry.ts +14 -14
  81. package/src/host/play-control-hook.ts +1 -1
  82. package/src/host/playwright-shim.ts +2 -2
  83. package/src/host/projection/pixi.ts +2 -2
  84. package/src/host/react-mount-runtime.ts +1 -1
  85. package/src/host/roots/canvas-root.tsx +13 -1
  86. package/src/host/roots/r3f-root.tsx +1 -1
  87. package/src/host/scene-view-drawability.ts +1 -1
  88. package/src/host/sdk/tools.ts +1 -1
  89. package/src/host/served-bundle-runtime-modules.ts +2 -0
  90. package/src/host/staged-projects.ts +1 -1
  91. package/src/host/stories/story-pixi-preview.ts +1 -1
  92. package/src/host/stories/story-three-preview.ts +1 -1
  93. package/src/host/three-ingest-runtime.ts +1 -1
  94. package/src/ingest/active-ingest.ts +1 -1
  95. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
  96. package/src/ingest/authoring/ingest-root-adapter.ts +11 -11
  97. package/src/ingest/capture-wait-report.ts +4 -4
  98. package/src/ingest/discovery-public-ingest.ts +4 -4
  99. package/src/ingest/dom-stub-mark.ts +1 -1
  100. package/src/ingest/entry-load.ts +1 -1
  101. package/src/ingest/game-contract-realm.ts +3 -3
  102. package/src/ingest/game-pointer-lock.ts +1 -1
  103. package/src/ingest/ingest-canvas-scene-document.tsx +3 -3
  104. package/src/ingest/ingest-evidence-hook.ts +6 -6
  105. package/src/ingest/ingest-frame-snapshot.ts +2 -2
  106. package/src/ingest/ingest-play-commands.ts +1 -1
  107. package/src/ingest/ingest-play-control.ts +11 -11
  108. package/src/ingest/ingest-render-debug.ts +5 -5
  109. package/src/ingest/ingest-siblings.ts +3 -3
  110. package/src/ingest/module-mode.ts +2 -2
  111. package/src/ingest/mount-canvas-ingest-root.ts +10 -10
  112. package/src/ingest/mount-coverage.ts +3 -3
  113. package/src/ingest/mount-dom-ingest-root.ts +7 -7
  114. package/src/ingest/mount-ingest-root.ts +10 -10
  115. package/src/ingest/mount-three-ingest-root.ts +5 -5
  116. package/src/ingest/resolve-three.ts +2 -2
  117. package/src/ingest/served-bundle.ts +6 -6
  118. package/src/ingest/served-html-boot.ts +8 -8
  119. package/src/ingest/surface-dom.ts +9 -9
  120. package/src/ingest/surface-three.ts +1 -1
  121. package/src/ingest/types.ts +1 -1
  122. package/src/ingest/unmount-ingest-root.ts +3 -3
  123. package/src/network/NetworkInspectorPanel.tsx +521 -54
  124. package/src/network/network-inspector-model.ts +6 -0
  125. package/src/play/play-boot-stall.ts +1 -1
  126. package/src/play/play-mode.ts +31 -30
  127. package/src/play/play-recording.ts +12 -12
  128. package/src/play-bar/PlayBar.tsx +8 -8
  129. package/src/play-bar/PlayerCountPicker.tsx +3 -3
  130. package/src/profiler/FrameDebuggerPanel.tsx +2 -2
  131. package/src/profiler/PerformancePanel.tsx +4 -4
  132. package/src/react/design-time-react-mount.ts +5 -5
  133. package/src/react/dom-authoring-adapter.ts +3 -3
  134. package/src/react/react-inspector-section.tsx +2 -2
  135. package/src/react/react-world-authoring-adapter.ts +20 -20
  136. package/src/react/story-documents/story-documents.tsx +4 -4
  137. package/src/react/ui-component-board.ts +1 -1
  138. package/src/runtime/adapter/audio-meter.ts +1 -1
  139. package/src/runtime/adapter/ingest/contract-debug-adapter.ts +4 -4
  140. package/src/runtime/adapter/ingest/contract-system-adapters.ts +9 -9
  141. package/src/runtime/adapter/ingest/merge-debug-adapters.ts +2 -2
  142. package/src/runtime/adapter/ingest/observation-debug-adapter.ts +1 -1
  143. package/src/runtime/adapter/native-debug-module.ts +5 -5
  144. package/src/runtime/audio/pose-guard.ts +1 -1
  145. package/src/runtime/core/game-scoped-slot.ts +1 -1
  146. package/src/runtime/create-runtime.ts +15 -15
  147. package/src/runtime/debug-bridge.ts +25 -25
  148. package/src/runtime/debug-registry.ts +17 -17
  149. package/src/runtime/dev/instruments.ts +1 -1
  150. package/src/runtime/dev/performance-profiler.ts +2 -2
  151. package/src/runtime/dev/register-render-vitals.ts +2 -2
  152. package/src/runtime/dev/static-batch-advisor.ts +3 -3
  153. package/src/runtime/dev-build.ts +3 -3
  154. package/src/runtime/game.ts +5 -5
  155. package/src/runtime/input-router.ts +24 -11
  156. package/src/runtime/mount-manifest.ts +9 -9
  157. package/src/runtime/pixi/authoring.ts +1 -1
  158. package/src/runtime/pixi/ingest.ts +1 -1
  159. package/src/runtime/presentation.ts +1 -1
  160. package/src/runtime/render-control.ts +21 -21
  161. package/src/runtime/render-seed.ts +3 -3
  162. package/src/runtime/run-ticks-settled.ts +1 -1
  163. package/src/services/game-audio.ts +3 -3
  164. package/src/services/game-network.ts +125 -15
  165. package/src/three/authoring/model-asset-inspector-section.css +15 -15
  166. package/src/three/authoring/model-asset-inspector-section.tsx +7 -7
  167. package/src/three/authoring/r3f-design-session.ts +10 -10
  168. package/src/three/authoring/r3f-source-authoring-adapter.ts +5 -5
  169. package/src/three/authoring/three-authoring-adapter.ts +5 -4
  170. package/src/three/authoring/typed-three-inspector.ts +1 -1
  171. package/src/three/story-documents/three-story-documents.tsx +3 -3
  172. package/src/three/three-board/board-scene.ts +9 -9
  173. package/src/three/three-board/three-component-board.ts +1 -1
@@ -109,19 +109,19 @@ function recordContractSystemUse(member: string, value: unknown, name?: string):
109
109
 
110
110
  /**
111
111
  * #140 — the generic session-wire dispatch table: the SAME method-name
112
- * surface `window.__vgai` exposes (`@volter/editor-game/runtime/debug-bridge`'s
113
- * `VgaiDebugHandle` — `state`/`stateAll`/`providers`/`commands`/`events`/
112
+ * surface `window.__volter` exposes (`@volter/editor-game/runtime/debug-bridge`'s
113
+ * `VolterDebugHandle` — `state`/`stateAll`/`providers`/`commands`/`events`/
114
114
  * `snapshot`/`invoke`/`runTicks`/`input.*`), built from the SAME
115
115
  * registry-backed accessors the sibling
116
116
  * `gameplay.command.ts` verbs (`list-gameplay-state`/`inject-input`/`run-ticks`)
117
117
  * already use. This is what makes the
118
118
  * `bridge-call` relay op (below) a session-generic primitive rather than a
119
119
  * test-specific one — any client of the live session (the `@volter/editor-live`
120
- * `RelayTransport`, a future one-shot `vgai game state/call/hold` REPL) can
120
+ * `RelayTransport`, a future one-shot `volter game state/call/hold` REPL) can
121
121
  * issue `{method, callArgs}` against it with no dependency on a proof run's
122
122
  * own lifecycle/env, and get byte-identical dispatch to the page-transport's
123
123
  * `page.evaluate(bridgeCallInPage)` (D17) — without requiring the editor tab
124
- * to be loaded with `?vgai-debug=1` (that gate exists for the bridge's own
124
+ * to be loaded with `?volter-debug=1` (that gate exists for the bridge's own
125
125
  * production exposure; this relay is already local-loopback-only — see
126
126
  * `editor-server.ts`'s CSRF/origin guard).
127
127
  */
@@ -228,7 +228,7 @@ export async function dispatchBridgeMethod(
228
228
  // funnels window error/unhandledrejection into the editor console as
229
229
  // level 'error' / source 'runtime' (`installEditorConsoleCapture`'s
230
230
  // boot-time listeners) — the SAME events and message format
231
- // `window.__vgai`'s own handle buffers. Fenced to the current play run
231
+ // `window.__volter`'s own handle buffers. Fenced to the current play run
232
232
  // via `inPlayRun` so a probe never reads a previous run's stale
233
233
  // failures, capped at 100 for parity with the bridge's ring buffer.
234
234
  pageErrors: collectPlayRunPageErrors(),
@@ -308,10 +308,10 @@ export async function dispatchBridgeMethod(
308
308
  case 'input.clearVirtualActions':
309
309
  // bridge↔wire coverage-parity gate (`bridge-wire-parity.test.ts`)
310
310
  // closed this trio's gap: `startRecording`/`stopRecording`/`isRecording`
311
- // exist on the page bridge's `VgaiDebugInputHandle` (`runtime/
311
+ // exist on the page bridge's `VolterDebugInputHandle` (`runtime/
312
312
  // debug-bridge.ts`) but had no relay case at all — an agent driving the
313
313
  // input-trace recorder over the session wire (RelayTransport) instead of
314
- // `?vgai-debug=1` simply had no door to it. Same single optional
314
+ // `?volter-debug=1` simply had no door to it. Same single optional
315
315
  // trailing `worldId` shape as `clearVirtualActions`.
316
316
  case 'input.startRecording':
317
317
  case 'input.stopRecording':
@@ -320,10 +320,10 @@ export async function dispatchBridgeMethod(
320
320
  // D15/T-D15.5 (rebase composition with #140): this wire is a THIRD
321
321
  // door to the virtual-input target — it must resolve through the SAME
322
322
  // `DebugRegistry.getVirtualInputTarget(worldId?)` the page bridge
323
- // (`window.__vgai.input.*`) and the `inject-input` relay case use, so
323
+ // (`window.__volter.input.*`) and the `inject-input` relay case use, so
324
324
  // no two doors can ever disagree about which world an unqualified
325
325
  // actuation targets. Each method takes the page bridge's own optional
326
- // trailing `worldId` (byte-parity with `VgaiDebugInputHandle`); an
326
+ // trailing `worldId` (byte-parity with `VolterDebugInputHandle`); an
327
327
  // explicit, unregistered id throws the registry's structured
328
328
  // `DEBUG_INPUT_WORLD_NOT_FOUND`, shipped back over the wire as-is.
329
329
  const worldId = (
@@ -365,7 +365,7 @@ export async function dispatchBridgeMethod(
365
365
  return inputTarget.isInputRecording();
366
366
  }
367
367
  case 'input.scheduleActionAtTick': {
368
- // bridge↔wire coverage-parity gate: `window.__vgai.input.
368
+ // bridge↔wire coverage-parity gate: `window.__volter.input.
369
369
  // scheduleActionAtTick` (`runtime/debug-bridge.ts`, D15/T-D15.5) had NO
370
370
  // bridge-call case — it was reachable over the relay ONLY via the
371
371
  // separate `inject-input` command's `atTick` field (see that case,
@@ -390,7 +390,7 @@ export async function dispatchBridgeMethod(
390
390
  }
391
391
  case 'input.injectPointerDelta':
392
392
  case 'input.injectPointerPosition': {
393
- // pointer-dispatch op: `window.__vgai.input.injectPointerDelta`/
393
+ // pointer-dispatch op: `window.__volter.input.injectPointerDelta`/
394
394
  // `injectPointerPosition` (`runtime/debug-bridge.ts`) had no relay case
395
395
  // — this is the HONEST gameplay-seam reading of "pointer dispatch": the
396
396
  // engine's existing virtual pointer-injection, never a synthesized DOM
@@ -417,7 +417,7 @@ export async function dispatchBridgeMethod(
417
417
  return undefined;
418
418
  }
419
419
  case 'holdFor': {
420
- // #140/D17: byte-identical semantics to `window.__vgai.holdFor` (door
420
+ // #140/D17: byte-identical semantics to `window.__volter.holdFor` (door
421
421
  // a, `runtime/debug-bridge.ts`) — same `getInputTarget(worldId?)`
422
422
  // resolution as the `input.*` cases above, same `waitForHoldBudget`
423
423
  // poll/stall loop (imported, not hand-copied, so the two doors can
@@ -51,7 +51,7 @@ import { captureLiveCanvasFrame } from './live-frames';
51
51
  *
52
52
  * BOTH legs read a canvas, and a canvas is only readable after its frame when
53
53
  * its WebGL context was created with `preserveDrawingBuffer: true`. Every
54
- * canvas the vgai RUNTIME mounts sets it (`@volter/editor-game/runtime/create-runtime`,
54
+ * canvas the volter RUNTIME mounts sets it (`@volter/editor-game/runtime/create-runtime`,
55
55
  * D5 §1/§4); a canvas an INGESTED game created is the game's own and usually
56
56
  * does not, which read as a silently BLACK screenshot over a perfectly
57
57
  * healthy running game. {@link liveCanvasFrame} is the answer for that
@@ -102,8 +102,8 @@ function screenshotResult(
102
102
  * and the play log whose event timestamps convert an event into an offset.
103
103
  *
104
104
  * THIS USED TO BE A REFUSAL, AND THE REFUSAL WAS THE DEFECT (2026-08-29,
105
- * cold fox #3). Because `vgai play` always records, refusing the still made
106
- * `vgai screenshot` dead for the rest of the session — and the look lane's
105
+ * cold fox #3). Because `volter-game-editor play` always records, refusing the still made
106
+ * `volter-game-editor screenshot` dead for the rest of the session — and the look lane's
107
107
  * questions ("is the ear proportion right", "is the fox sunk into the
108
108
  * ground") are exactly the ones a still DOES answer. The refusal was
109
109
  * discouraging a wrong question by making a right one impossible, and the
@@ -160,7 +160,7 @@ export async function handleBridgeScreenshot(
160
160
  // Same reachability rule `handleBridgeCall` uses, and for the same reason:
161
161
  // an ingested game's mount IS the running game, so it never creates a
162
162
  // first-party play session and `isPlayModeActive()` alone made
163
- // `vgai screenshot` permanently unusable against ingest. Loosening the gate
163
+ // `volter-game-editor screenshot` permanently unusable against ingest. Loosening the gate
164
164
  // costs nothing in honesty — the ingest mount renders into the very
165
165
  // container `getInstanceContainer()` returns (`play-mode.ts`'s
166
166
  // `getGameContainer` is shared with the ingest mount paths), and when there is
@@ -171,10 +171,10 @@ export async function handleBridgeScreenshot(
171
171
  // TRUE. `hasLiveDebugPlane()` covers an ingest that publishes a debug plane
172
172
  // projected from a declared contract — which a canvas ingest has no way to
173
173
  // do (`mount-canvas-ingest-root.ts` publishes `setActiveSystems({})`), so
174
- // `vgai screenshot` answered "not in play mode" over a mounted, running,
174
+ // `volter-game-editor screenshot` answered "not in play mode" over a mounted, running,
175
175
  // self-ticking game whose play state the mount had itself set to `playing`.
176
176
  // A lying refusal is worse than a missing feature: it sends the reader to
177
- // `vgai play`, which is not the thing that was wrong. The question this gate
177
+ // `volter-game-editor play`, which is not the thing that was wrong. The question this gate
178
178
  // asks is "is a game surface live here", and a live ingest is one.
179
179
  if (!isPlayModeActive() && !hasLiveDebugPlane() && !isIngestActive()) return notPlayingResult();
180
180
  // Capture the ADDRESSED seat, not always the primary. `game.instance(id)`
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * THE BUILD PROFILES PANEL — the configuration surface the `build-profiles`
3
3
  * document draws: the project's build-role configurations as a target list,
4
- * the name/resolution/app-id fields it writes back into `vgai.project.json`
4
+ * the name/resolution/app-id fields it writes back into `volter.project.json`
5
5
  * through the project's file history, the Build trigger, and the finished
6
6
  * build's report. The LOG is not here: it streams into the Build Output tab
7
7
  * (the native Output panel’s Build channel), which is the whole point of the split — a
@@ -32,6 +32,7 @@ import {
32
32
  import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
33
33
  import {
34
34
  type BuildTarget,
35
+ buildAndRun,
35
36
  buildSessionVersion,
36
37
  cancelBuild,
37
38
  getBuildSession,
@@ -48,7 +49,7 @@ interface ProjectConfig {
48
49
  appId: string;
49
50
  }
50
51
 
51
- const MANIFEST_PATH = 'vgai.project.json';
52
+ const MANIFEST_PATH = 'volter.project.json';
52
53
 
53
54
  const DEFAULT_CONFIG: ProjectConfig = {
54
55
  name: 'My Game',
@@ -215,7 +216,7 @@ export function BuildProfilesPanel() {
215
216
  <div style={fieldRowStyle}>
216
217
  <label style={labelStyle}>Name</label>
217
218
  <TextInput
218
- className="vgai-input"
219
+ className="volter-input"
219
220
  style={{ flex: 1 }}
220
221
  value={config.name}
221
222
  onChange={(e) => updateConfig({ name: e.target.value })}
@@ -227,7 +228,7 @@ export function BuildProfilesPanel() {
227
228
  <div style={{ display: 'flex', alignItems: 'center', gap: spaceVar[2], flex: 1 }}>
228
229
  <TextInput
229
230
  type="number"
230
- className="vgai-input"
231
+ className="volter-input"
231
232
  style={{ width: 0, flex: 1 }}
232
233
  value={config.resolution.width}
233
234
  onChange={(e) => {
@@ -238,7 +239,7 @@ export function BuildProfilesPanel() {
238
239
  <span style={{ fontSize: fontSizeVar.sm, color: text[3] }}>&times;</span>
239
240
  <TextInput
240
241
  type="number"
241
- className="vgai-input"
242
+ className="volter-input"
242
243
  style={{ width: 0, flex: 1 }}
243
244
  value={config.resolution.height}
244
245
  onChange={(e) => {
@@ -252,7 +253,7 @@ export function BuildProfilesPanel() {
252
253
  <div style={fieldRowStyle}>
253
254
  <label style={labelStyle}>App ID</label>
254
255
  <TextInput
255
- className="vgai-input"
256
+ className="volter-input"
256
257
  style={{ flex: 1 }}
257
258
  value={config.appId}
258
259
  onChange={(e) => updateConfig({ appId: e.target.value })}
@@ -308,7 +309,7 @@ export function BuildProfilesPanel() {
308
309
  <a
309
310
  href={getDownloadUrl(artifact)}
310
311
  download={artifact}
311
- className="vgai-btn"
312
+ className="volter-btn"
312
313
  data-variant="secondary"
313
314
  data-size="default"
314
315
  style={{
@@ -331,6 +332,16 @@ export function BuildProfilesPanel() {
331
332
  >
332
333
  {phase === 'done' ? 'Build Again' : 'Build'}
333
334
  </Button>
335
+ <Button
336
+ data-testid="build-profiles-build-and-run"
337
+ onClick={() => {
338
+ if (!selected) return;
339
+ if (phase === 'done') resetBuildSession();
340
+ void buildAndRun(selected);
341
+ }}
342
+ >
343
+ Build And Run
344
+ </Button>
334
345
  </>
335
346
  )}
336
347
  </div>
@@ -383,7 +394,7 @@ function BuildReportSummary({ report }: { report: BuildReport }) {
383
394
  position: 'absolute',
384
395
  inset: 0,
385
396
  width: `${Math.max(2, (file.bytes / largest) * 100)}%`,
386
- background: 'color-mix(in srgb, var(--vgai-accent) 16%, transparent)',
397
+ background: 'color-mix(in srgb, var(--volter-accent) 16%, transparent)',
387
398
  }}
388
399
  />
389
400
  <div
@@ -17,6 +17,7 @@ import {
17
17
  } from '../host/api/configurations';
18
18
  import type { EditorHostOutputDiagnostic } from '@volter/editor-sdk/host';
19
19
  import { editorHost } from '@volter/editor-sdk/host';
20
+ import { BASE } from '@volter/editor-sdk/kit/api-base';
20
21
  import type { BuildReport } from '@volter/editor-sdk/session/build-report';
21
22
  import { useEffect, useState } from 'react';
22
23
  import { formatBytes } from './format-bytes';
@@ -34,6 +35,40 @@ export function openBuildProfilesDocument(): boolean {
34
35
  return editorHost().workspace.openContributedDocument(BUILD_PROFILES_DOCUMENT_ID);
35
36
  }
36
37
 
38
+ /** The Build Player document's contribution id (`build-player.document.tsx`). */
39
+ export const BUILD_PLAYER_DOCUMENT_ID = 'build-player.document';
40
+
41
+ /** Where the editor server serves the project's built `dist/` (`routes/configurations.ts`). */
42
+ export const BUILD_PLAYER_URL = `${BASE}/build/web/index.html`;
43
+
44
+ let _runs = 0;
45
+ const _runListeners = new Set<() => void>();
46
+
47
+ /** How many times the build has been run: the player reloads on each. */
48
+ export function buildRunCount(): number {
49
+ return _runs;
50
+ }
51
+
52
+ export function subscribeBuildRuns(fn: () => void): () => void {
53
+ _runListeners.add(fn);
54
+ return () => {
55
+ _runListeners.delete(fn);
56
+ };
57
+ }
58
+
59
+ /** Run the last build in the Build Player document, fresh. */
60
+ export function runBuild(): boolean {
61
+ _runs++;
62
+ for (const fn of _runListeners) fn();
63
+ return editorHost().workspace.openContributedDocument(BUILD_PLAYER_DOCUMENT_ID);
64
+ }
65
+
66
+ /** Unity's Build And Run: build, and run the result when the build succeeds. */
67
+ export async function buildAndRun(target: BuildTarget): Promise<void> {
68
+ await startBuild(target);
69
+ if (_session.phase === 'done' && _session.result?.ok) runBuild();
70
+ }
71
+
37
72
  /** A build-role configuration's id (ARCHITECTURE-CORE §The project model). */
38
73
  export type BuildTarget = string;
39
74
  export type BuildPhase = 'idle' | 'building' | 'done';
@@ -74,7 +74,8 @@ import {
74
74
  } from '@volter/editor-sdk/kit/workspace-document-registry';
75
75
  import { storyBoardPresentation } from '@volter/editor-sdk/kit/stories/story-presentation';
76
76
  import { getProjectStoryModules, subscribeProjectStoryModules } from '@volter/editor-sdk/kit/stories/story-registry';
77
- import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
77
+ import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
78
+ import type { AuthoringAdapter, EditorNode } from '@volter/editor-project/adapter';
78
79
  import {
79
80
  buildCanvasBoard,
80
81
  type CanvasBoard,
@@ -113,6 +114,8 @@ interface CanvasBoardPresentation {
113
114
  zoomAt(clientX: number, clientY: number): boolean;
114
115
  /** The exhibit the board is currently showing as active, for the Inspector. */
115
116
  activeStoryId(): string | null;
117
+ /** Select one frame by its id, as a click on it does (the layers list's row). */
118
+ selectStory(storyId: string): boolean;
116
119
  dispose(): void;
117
120
  }
118
121
 
@@ -131,6 +134,53 @@ interface CanvasBoardPresentation {
131
134
  * An ungrouped exhibit (no district) carries no title and falls back to the
132
135
  * substrate's own module-path derivation, exactly like an untitled dom story.
133
136
  */
137
+ /**
138
+ * THE BOARD'S LAYERS — Figma's layers panel lists a page's frames; this adapter hands the
139
+ * Hierarchy the board's frames the same way, each with its district beside its name. It owns no
140
+ * transform, inspector or structure: a frame is chosen here, and choosing one is the same act as
141
+ * clicking it on the board.
142
+ */
143
+ function canvasBoardAdapter(
144
+ board: CanvasBoard,
145
+ selected: () => string | null,
146
+ select: (storyId: string) => void,
147
+ ): AuthoringAdapter & { notify(): void } {
148
+ const listeners = new Set<() => void>();
149
+ const nodes: EditorNode[] = boardRows(board).map((row) => ({
150
+ id: row.id,
151
+ label: row.label,
152
+ ...(row.title ? { secondaryLabel: row.title } : {}),
153
+ kind: 'frame',
154
+ typeLabel: 'Frame',
155
+ parentId: null,
156
+ childIds: [],
157
+ }));
158
+ return {
159
+ capabilities: { transform: false, inspectorFields: false, persist: false },
160
+ hierarchy: {
161
+ roots: () => nodes,
162
+ node: (id) => nodes.find((node) => node.id === id) ?? null,
163
+ },
164
+ selection: {
165
+ get: () => {
166
+ const id = selected();
167
+ return id ? [id] : [];
168
+ },
169
+ set: (ids) => {
170
+ const id = ids.at(-1);
171
+ if (id) select(id);
172
+ },
173
+ },
174
+ subscribe(listener) {
175
+ listeners.add(listener);
176
+ return () => listeners.delete(listener);
177
+ },
178
+ notify() {
179
+ for (const listener of listeners) listener();
180
+ },
181
+ };
182
+ }
183
+
134
184
  function boardRows(board: CanvasBoard): ReactStoryBoardStory[] {
135
185
  return [
136
186
  ...board.exhibits.map((exhibit) => ({
@@ -305,6 +355,13 @@ function presentCanvasBoard(
305
355
  activeStoryId(): string | null {
306
356
  return selectedStoryId;
307
357
  },
358
+ selectStory(storyId: string): boolean {
359
+ if (!rows.some((row) => row.id === storyId)) return false;
360
+ selectedStoryId = storyId;
361
+ storyBoard?.activate(storyId, false);
362
+ onSelectionChanged?.();
363
+ return true;
364
+ },
308
365
  zoomAt(clientX: number, clientY: number): boolean {
309
366
  return storyBoard?.zoomAtClientPoint(clientX, clientY) ?? false;
310
367
  },
@@ -446,13 +503,29 @@ function CanvasBoardContent({ active }: WorkspaceDocumentContentProps) {
446
503
  // never reach the no-selection seam at all
447
504
  // (`workspace-document-registry.ts`'s `WorkspaceDocumentSelection`, and
448
505
  // `components/AssetEditorShell.tsx`, which registers the same pair).
506
+ // The layers list: the board's frames through their own adapter. The node stays null so the
507
+ // Inspector keeps describing the chosen frame through the subject published below.
508
+ const boardAdapter = useMemo(
509
+ () =>
510
+ board
511
+ ? canvasBoardAdapter(
512
+ board,
513
+ () => presentationRef.current?.activeStoryId() ?? null,
514
+ (storyId) => presentationRef.current?.selectStory(storyId),
515
+ )
516
+ : null,
517
+ [board],
518
+ );
519
+ useEffect(() => {
520
+ boardAdapter?.notify();
521
+ }, [boardAdapter, selectedStoryId]);
449
522
  useEffect(() => {
450
523
  if (!active) return;
451
524
  return registerWorkspaceDocumentSelection(CANVAS_COMPONENTS_DOCUMENT_ID, () => ({
452
- adapter: null,
525
+ adapter: boardAdapter,
453
526
  nodeId: null,
454
527
  }));
455
- }, [active]);
528
+ }, [active, boardAdapter]);
456
529
  useEffect(() => {
457
530
  if (!active || !selectedExhibit) return;
458
531
  return publishDocumentInspectionSubject(CANVAS_COMPONENTS_DOCUMENT_ID, () => ({
@@ -479,8 +552,8 @@ function CanvasBoardContent({ active }: WorkspaceDocumentContentProps) {
479
552
  return (
480
553
  <div
481
554
  data-testid="canvas-board"
482
- data-vgai-backdrop-color={active ? REACT_DESIGN_CANVAS_COLOR : undefined}
483
- data-vgai-backdrop-policy={active ? 'dark-frost' : undefined}
555
+ data-volter-backdrop-color={active ? REACT_DESIGN_CANVAS_COLOR : undefined}
556
+ data-volter-backdrop-policy={active ? 'dark-frost' : undefined}
484
557
  style={{
485
558
  position: 'absolute',
486
559
  inset: 0,
@@ -51,6 +51,6 @@ export const canvasComponentBoard: ComponentBoard = {
51
51
  `This project has no ${CANVAS_COMPONENTS_TITLE} board (${CANVAS_COMPONENTS_DOCUMENT_ID}): ` +
52
52
  "board presence keys on STORIES, not on roots, and none of this project's stories declares " +
53
53
  'the "canvas" medium. Declare it — a manifest root entry naming the story\'s module, or a ' +
54
- '`vgai.adapter.ts` regionInclude that covers it — and the board appears. `vgai console` ' +
54
+ '`volter.adapter.ts` regionInclude that covers it — and the board appears. `volter-game-editor console` ' +
55
55
  'names every story it could not place.',
56
56
  };
@@ -120,7 +120,7 @@ function createDesignGame(): GameInternal {
120
120
  * which part of the unbounded authored plane is visible. */
121
121
  function createDesignCanvas(): HTMLCanvasElement {
122
122
  const canvas = document.createElement('canvas');
123
- canvas.dataset['vgaiCanvasSceneSurface'] = 'true';
123
+ canvas.dataset['volterCanvasSceneSurface'] = 'true';
124
124
  canvas.style.position = 'absolute';
125
125
  canvas.style.inset = '0';
126
126
  canvas.style.width = '100%';
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * It reads the session's own singletons plus two project files the browser
6
6
  * cannot read for itself, and hands the facts to the pure derivation. The
7
- * report goes out the doors coverage already uses — `vgai status`'s
7
+ * report goes out the doors coverage already uses — `volter-game-editor status`'s
8
8
  * `projectCoverage` facet, and the editor console via
9
9
  * `coverage/session-coverage.ts`, which unions this family with the other two
10
10
  * so one headline counts them all. No new transport, no new panel: the console
@@ -59,7 +59,7 @@ let _filesFor: string | null = null;
59
59
 
60
60
  /**
61
61
  * Drop the cached file facts so the next read re-asks the dev server. Called
62
- * on `vgai restart` (a restart is precisely "my project's files changed —
62
+ * on `volter-game-editor restart` (a restart is precisely "my project's files changed —
63
63
  * re-read them"; the probe-measured failure was the dependency detector
64
64
  * grading rows against a package.json edit two restarts old) and by tests.
65
65
  */
@@ -148,7 +148,7 @@ export function projectVerbFacts(): ProjectVerbFacts | null {
148
148
 
149
149
  /**
150
150
  * The `project.*` report for the open project, re-derived on every read — the
151
- * scripts a project declares change while it is open (`vgai add` writes them),
151
+ * scripts a project declares change while it is open (`volter-game-editor add` writes them),
152
152
  * so a cached answer would start lying.
153
153
  */
154
154
  export function projectVerbCoverage(): CapabilityCoverageReport | null {
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * It reads the session's own singletons and hands the facts to the pure
6
6
  * derivation. The report goes out the two doors coverage already uses —
7
- * `vgai status`'s `systemCoverage` facet, and the editor console via
7
+ * `volter-game-editor status`'s `systemCoverage` facet, and the editor console via
8
8
  * `coverage/session-coverage.ts`, which unions this family with the others so
9
9
  * one headline counts them all — so an unbound slot becomes a STANDING
10
10
  * condition rather than a one-off line. No new transport, no new panel, and no
@@ -129,8 +129,8 @@ export function nativeSystemCoverage(): CapabilityCoverageReport | null {
129
129
  // NetworkingAdapter for a single-player game, telling its author to
130
130
  // register one because "this project declares a `server` block" when it
131
131
  // declares nothing of the sort. A matchmaker is a listener; no port, no
132
- // server. (`vgai edit`'s auto-boot had the identical bug — see
133
- // `packages/vgai-cli/src/colyseus-autoboot.ts`'s `readDeclaredProcess`.)
132
+ // server. (`volter-game-editor edit`'s auto-boot had the identical bug — see
133
+ // `packages/volter-cli/src/colyseus-autoboot.ts`'s `readDeclaredProcess`.)
134
134
  declaresServer: (project.config.configurations ?? []).some(
135
135
  (c) => c.kind === 'process' && typeof (c as { port?: unknown }).port === 'number',
136
136
  ),
@@ -23,13 +23,13 @@
23
23
  * Two doors, one derivation — the same shape `ingest/mount-coverage.ts` already
24
24
  * uses:
25
25
  *
26
- * - `vgai status`, through {@link rootCoverageFacet}, so an agent reads the
26
+ * - `volter-game-editor status`, through {@link rootCoverageFacet}, so an agent reads the
27
27
  * same answer without opening the editor;
28
28
  * - the editor console, through `coverage/session-coverage.ts`, which unions
29
29
  * this family with the game- and project-scoped ones so the warning's
30
30
  * headline counts every seam rather than this family's 21. The console
31
31
  * ledger turns that into a standing unresolved condition, loud on every
32
- * `vgai` command until it is closed.
32
+ * `volter` command until it is closed.
33
33
  */
34
34
 
35
35
  import { measureAdapter } from '../host/adapter-reach';
@@ -24,7 +24,7 @@
24
24
  *
25
25
  * It owns assembly and the console door. Every family still derives itself, in
26
26
  * its own file, from its own facts; nothing here decides a verdict. The
27
- * per-family facets on `vgai status` (`command-listener.ts`) stay separate for
27
+ * per-family facets on `volter-game-editor status` (`command-listener.ts`) stay separate for
28
28
  * the same reason they always were — a machine reader wants the rows keyed by
29
29
  * subject, and it is the HUMAN-facing sentence that has to be a union.
30
30
  */
@@ -90,7 +90,7 @@ export function sessionCoverageReport(
90
90
  *
91
91
  * Keyed on the rendered TEXT rather than a mount or project token, because the
92
92
  * union's inputs come and go inside one session (a root mounts, a design
93
- * session attaches, `vgai add` lands a script) and the honest rule is "say it
93
+ * session attaches, `volter-game-editor add` lands a script) and the honest rule is "say it
94
94
  * when the answer changes". A recurrence after a real change is a new condition
95
95
  * and gets said again; the server ledger sums repeats of an unchanged one.
96
96
  */
@@ -73,10 +73,10 @@ function RailColumn({ children }: { readonly children: ReactNode }) {
73
73
  width: RAIL_WIDTH,
74
74
  height: 600,
75
75
  display: 'flex',
76
- border: '1px solid var(--vgai-border-2)',
76
+ border: '1px solid var(--volter-border-2)',
77
77
  borderRadius: 8,
78
78
  overflow: 'hidden',
79
- background: 'var(--vgai-bg-1)',
79
+ background: 'var(--volter-bg-1)',
80
80
  }}
81
81
  >
82
82
  {children}
@@ -2,7 +2,7 @@
2
2
  * W2c (F13) — Game-tab device preset dropdown. Chrome-device-mode-style:
3
3
  * picking a preset constrains the game mount to that device's CSS resolution
4
4
  * + DPR (letterboxed/scaled by `GamePanel`), phone/tablet presets add the
5
- * safe-area overlay + `--vgai-safe-area-inset-*` CSS vars and auto-enable
5
+ * safe-area overlay + `--volter-safe-area-inset-*` CSS vars and auto-enable
6
6
  * pointer-as-touch. State lives in `../device-preview.ts`; this component is
7
7
  * pure UI over it.
8
8
  */
@@ -30,18 +30,18 @@ export function DevicePresetPicker({
30
30
  const isFit = active.kind === 'fit';
31
31
 
32
32
  return (
33
- <div className="vgai-playbar-popover-anchor">
33
+ <div className="volter-playbar-popover-anchor">
34
34
  <Button
35
35
  ref={ref}
36
36
  variant="ghost"
37
37
  size="comfortable"
38
38
  aria-haspopup="menu"
39
39
  aria-expanded={open}
40
- aria-controls={open ? 'vgai-device-preset-menu' : undefined}
40
+ aria-controls={open ? 'volter-device-preset-menu' : undefined}
41
41
  data-testid="device-preset-picker"
42
42
  onClick={() => setOpen((value) => !value)}
43
43
  title={
44
- 'Device preview — constrain the game to a device resolution + DPR. Phone/tablet presets show the safe area, set --vgai-safe-area-inset-top/right/bottom/left CSS vars on the game mount, and send mouse input as touch.'
44
+ 'Device preview — constrain the game to a device resolution + DPR. Phone/tablet presets show the safe area, set --volter-safe-area-inset-top/right/bottom/left CSS vars on the game mount, and send mouse input as touch.'
45
45
  }
46
46
  >
47
47
  <EditorIcon icon={faMobileScreenButton} size="md" />
@@ -49,9 +49,9 @@ export function DevicePresetPicker({
49
49
  </Button>
50
50
  {open && (
51
51
  <AnchoredMenu
52
- id="vgai-device-preset-menu"
52
+ id="volter-device-preset-menu"
53
53
  anchorRef={ref}
54
- className="vgai-resolution-menu"
54
+ className="volter-resolution-menu"
55
55
  gap={4}
56
56
  onDismiss={() => setOpen(false)}
57
57
  >
@@ -27,7 +27,7 @@ export function GameCaptureFrameButton() {
27
27
  size="compact"
28
28
  variant="ghost"
29
29
  data-testid="game-capture-frame"
30
- className="vgai-frame-capture-game-button"
30
+ className="volter-frame-capture-game-button"
31
31
  disabled={!available}
32
32
  title={
33
33
  available
@@ -60,7 +60,7 @@ function GameDebugMenu() {
60
60
  data-testid="game-debug-menu"
61
61
  aria-haspopup="menu"
62
62
  aria-expanded={open}
63
- aria-controls={open ? 'vgai-game-debug-menu' : undefined}
63
+ aria-controls={open ? 'volter-game-debug-menu' : undefined}
64
64
  onClick={() => setOpen((value) => !value)}
65
65
  >
66
66
  <span>Debug</span>
@@ -68,7 +68,7 @@ function GameDebugMenu() {
68
68
  </Button>
69
69
  {open && (
70
70
  <AnchoredMenu
71
- id="vgai-game-debug-menu"
71
+ id="volter-game-debug-menu"
72
72
  anchorRef={ref}
73
73
  gap={3}
74
74
  onDismiss={() => setOpen(false)}
@@ -74,7 +74,7 @@ import {
74
74
  * surfaces (stacked canvases + React DOM layers). */
75
75
  export function collectGameSurfaces(container: HTMLElement): Element[] {
76
76
  return Array.from(container.children).filter(
77
- (node) => (node as HTMLElement).dataset?.['vgaiRootSurface'] === 'true',
77
+ (node) => (node as HTMLElement).dataset?.['volterRootSurface'] === 'true',
78
78
  );
79
79
  }
80
80
 
@@ -250,7 +250,7 @@ function InstanceViewport({ index }: { index: number }) {
250
250
  focused={mountedId != null && focused === mountedId}
251
251
  onFocus={mountedId ? () => setFocusedInstance(mountedId) : undefined}
252
252
  >
253
- <div ref={ref} data-vgai-instance-viewport="" style={{ position: 'absolute', inset: 0 }} />
253
+ <div ref={ref} data-volter-instance-viewport="" style={{ position: 'absolute', inset: 0 }} />
254
254
  </InstanceSlot>
255
255
  );
256
256
  }
@@ -355,7 +355,7 @@ export function GamePanel({ style }: { style?: React.CSSProperties }) {
355
355
  // the same call — and switching resolution DURING play would silently drop
356
356
  // the running game's canvases with the removed element. Only nodes the game
357
357
  // HOST mounted are
358
- // stashed (world surfaces carry `data-vgai-root-surface`): the fill/device branches are keyed below so
358
+ // stashed (world surfaces carry `data-volter-root-surface`): the fill/device branches are keyed below so
359
359
  // React really remounts, but if it ever aliases the container DOM node
360
360
  // again, a blind childNodes copy would capture React-owned children too —
361
361
  // that exact aliasing put the incoming inner div in the stash and made
@@ -412,7 +412,7 @@ export function GamePanel({ style }: { style?: React.CSSProperties }) {
412
412
  // which consults `deviceEmulatedPixelRatio()` once the session exists.)
413
413
  // What size this game is PRESENTED at. Two things can name one and the precedence lives in
414
414
  // `resolvePresentedSize`: a device preset the user picked wins, and otherwise the project's own
415
- // declared `resolution` (`vgai.project.json`) puts the game on screen at its authored logical
415
+ // declared `resolution` (`volter.project.json`) puts the game on screen at its authored logical
416
416
  // size, uniformly scaled and letterboxed by the branch below — presentation scaling, never a
417
417
  // stretch mode. A project that declares neither fills the panel exactly as before.
418
418
  const declaredResolution = project?.config.resolution;
@@ -474,16 +474,16 @@ export function GamePanel({ style }: { style?: React.CSSProperties }) {
474
474
 
475
475
  // Safe-area contract: the game MOUNT container always carries the
476
476
  // inset CSS vars (0px unless the active preset declares insets), so game
477
- // UI can style with `var(--vgai-safe-area-inset-top, 0px)` etc. Real
477
+ // UI can style with `var(--volter-safe-area-inset-top, 0px)` etc. Real
478
478
  // `env(safe-area-inset-*)` cannot be injected from a same-document host
479
479
  // (env() is UA-supplied for the top-level viewport only) — the vars + the
480
480
  // visual overlay below ARE the emulation.
481
481
  const safeArea = preset.safeArea;
482
482
  const safeAreaVars = {
483
- '--vgai-safe-area-inset-top': `${safeArea?.top ?? 0}px`,
484
- '--vgai-safe-area-inset-right': `${safeArea?.right ?? 0}px`,
485
- '--vgai-safe-area-inset-bottom': `${safeArea?.bottom ?? 0}px`,
486
- '--vgai-safe-area-inset-left': `${safeArea?.left ?? 0}px`,
483
+ '--volter-safe-area-inset-top': `${safeArea?.top ?? 0}px`,
484
+ '--volter-safe-area-inset-right': `${safeArea?.right ?? 0}px`,
485
+ '--volter-safe-area-inset-bottom': `${safeArea?.bottom ?? 0}px`,
486
+ '--volter-safe-area-inset-left': `${safeArea?.left ?? 0}px`,
487
487
  } as React.CSSProperties;
488
488
 
489
489
  return (