@vgai/live 0.5.2 → 0.5.4

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 (62) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/editor.d.ts +78 -55
  3. package/dist/editor.js +129 -76
  4. package/dist/game-client/bridge-heartbeat.d.ts +49 -0
  5. package/dist/game-client/bridge-heartbeat.js +46 -0
  6. package/dist/game-client/bridge-transport.d.ts +75 -0
  7. package/dist/game-client/bridge-transport.js +19 -0
  8. package/dist/game-client/client.d.ts +293 -0
  9. package/dist/game-client/client.js +706 -0
  10. package/dist/game-client/errors.d.ts +57 -0
  11. package/dist/game-client/errors.js +76 -0
  12. package/dist/game-client/events-matcher.d.ts +41 -0
  13. package/dist/game-client/events-matcher.js +68 -0
  14. package/dist/game-client/failure-block.d.ts +93 -0
  15. package/dist/game-client/failure-block.js +97 -0
  16. package/dist/game-client/fast-forward.d.ts +125 -0
  17. package/dist/game-client/fast-forward.js +122 -0
  18. package/dist/game-client/hidden-recovery.d.ts +85 -0
  19. package/dist/game-client/hidden-recovery.js +105 -0
  20. package/dist/game-client/index.d.ts +40 -0
  21. package/dist/game-client/index.js +26 -0
  22. package/dist/game-client/perf-sampling.d.ts +56 -0
  23. package/dist/game-client/perf-sampling.js +85 -0
  24. package/dist/game-client/relay-transport.d.ts +100 -0
  25. package/dist/game-client/relay-transport.js +237 -0
  26. package/dist/game-client/screenshot-target.d.ts +60 -0
  27. package/dist/game-client/screenshot-target.js +68 -0
  28. package/dist/game-client/state-cap.d.ts +7 -0
  29. package/dist/game-client/state-cap.js +21 -0
  30. package/dist/game-client/types.d.ts +128 -0
  31. package/dist/game-client/types.js +15 -0
  32. package/dist/game-client/wait-for.d.ts +155 -0
  33. package/dist/game-client/wait-for.js +229 -0
  34. package/dist/game.d.ts +47 -18
  35. package/dist/game.js +59 -16
  36. package/dist/index.d.ts +46 -21
  37. package/dist/index.js +51 -20
  38. package/dist/session.d.ts +4 -4
  39. package/dist/session.js +7 -7
  40. package/dist/tools.d.ts +12 -3
  41. package/dist/tools.js +15 -6
  42. package/package.json +10 -5
  43. package/src/editor.ts +142 -96
  44. package/src/game-client/bridge-heartbeat.ts +61 -0
  45. package/src/game-client/bridge-transport.ts +73 -0
  46. package/src/game-client/client.ts +836 -0
  47. package/src/game-client/errors.ts +96 -0
  48. package/src/game-client/events-matcher.ts +106 -0
  49. package/src/game-client/failure-block.ts +199 -0
  50. package/src/game-client/fast-forward.ts +175 -0
  51. package/src/game-client/hidden-recovery.ts +149 -0
  52. package/src/game-client/index.ts +98 -0
  53. package/src/game-client/perf-sampling.ts +94 -0
  54. package/src/game-client/relay-transport.ts +311 -0
  55. package/src/game-client/screenshot-target.ts +91 -0
  56. package/src/game-client/state-cap.ts +29 -0
  57. package/src/game-client/types.ts +137 -0
  58. package/src/game-client/wait-for.ts +327 -0
  59. package/src/game.ts +96 -16
  60. package/src/index.ts +68 -31
  61. package/src/session.ts +8 -10
  62. package/src/tools.ts +19 -6
package/dist/editor.d.ts CHANGED
@@ -1,42 +1,18 @@
1
1
  /**
2
2
  * `LiveEditor` — the editor-control half of `@vgai/live`'s `{ editor, game,
3
- * page }` (SHARED-SESSION-SPEC.md, Wave 2). Methods are named after the
3
+ * page }` (Wave 2). Methods are named after the
4
4
  * ACTION, not the CLI flag spelling (e.g. `select('all')` rather than a
5
- * separate `selectAll`, `showPanel('viewport-game')` rather than
6
- * `vgai show viewport game`'s two-token shape) — see each method's own doc
5
+ * separate `selectAll`, `showPanel('viewport-play')` rather than
6
+ * `vgai show viewport play`'s two-token shape) — see each method's own doc
7
7
  * comment for the exact CLI verb / `EditorClient` call it mirrors.
8
8
  *
9
- * Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module
10
- * never hand-rolls a `fetch` to `/__editor/command` itself. The ONE
11
- * exception is `applyDiff`, which is FILE mode (writes the scene file
12
- * directly), not a live wire command — see its own doc comment.
9
+ * Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module never
10
+ * hand-rolls a `fetch` to `/__editor/command` itself. (WO-8 removed the one
11
+ * exception, `applyDiff`, which was FILE mode rather than a live wire command.)
13
12
  */
14
- import type { ActiveDocumentCapture, AssetKind, AssetPreviewCapture, AssetPreviewOptions, EditorClient, EditorState, EditorView, PresentedEditorView, ShadingMode, ViewPreset, ViewportCapture } from '@vgai/editor-sdk';
13
+ import type { ActiveDocumentCapture, AssetKind, AssetPreviewCapture, AssetPreviewOptions, AssetPreviewSource, EditorClient, EditorState, EditorView, InspectedInspection, PresentedEditorView, ShadingMode, ViewPreset, ViewportCapture } from '@vgai/editor-sdk';
15
14
  /** `vgai show <...>`'s four sub-verbs folded into one action name — see `showPanel`. */
16
- export type PanelName = 'viewport-scene' | 'viewport-game' | 'inspector' | 'console' | 'build';
17
- /** Mirrors `@vgai/sdk`'s `project.scene.apply` operation input — the same shape `vgai apply-diff` builds from its `<scene> <patch>` file arguments. `patch` is a `SceneDiff` JSON object (validated by the operation's own Zod schema — a malformed patch throws, never silently no-ops). */
18
- export interface ApplyDiffInput {
19
- /** Project-relative path to the `.vscn.json` scene file to modify. */
20
- scenePath: string;
21
- /** A `SceneDiff`-shaped patch object. */
22
- patch: unknown;
23
- /** Validate + report only — never write to disk. Default `false`. */
24
- dryRun?: boolean;
25
- /** Refuse a stale write when the file's content hash no longer matches (optimistic concurrency) — omit to skip the check. */
26
- baseHash?: string;
27
- }
28
- /** The `project.scene.apply` operation's result shape. */
29
- export interface ApplyDiffResult {
30
- dryRun: boolean;
31
- written: boolean;
32
- filesChanged: string[];
33
- before: {
34
- scene: unknown;
35
- };
36
- after: {
37
- scene: unknown;
38
- };
39
- }
15
+ export type PanelName = 'viewport-edit' | 'viewport-play' | 'inspector' | 'console' | 'build';
40
16
  /**
41
17
  * Lightweight extension-based `AssetKind` guess for `openAsset`'s optional
42
18
  * `kind` argument. Deliberately independent of (not shared with) the
@@ -46,18 +22,17 @@ export interface ApplyDiffResult {
46
22
  */
47
23
  export declare function inferAssetKind(path: string): AssetKind;
48
24
  export declare class LiveEditor {
49
- private readonly client;
50
- /** Absolute project root this session is bound to — used only by `applyDiff` (FILE mode; every other method goes over the wire and never needs it). */
51
- private readonly projectRoot;
52
- constructor(client: EditorClient,
53
- /** Absolute project root this session is bound to — used only by `applyDiff` (FILE mode; every other method goes over the wire and never needs it). */
54
- projectRoot: string);
25
+ #private;
26
+ constructor(client: EditorClient);
55
27
  /**
56
- * No `path` -> the currently open scene's save path (`status().savePath`).
57
- * `path` given -> opens it (`EditorClient.openScene` — the same wire call
58
- * `vgai scene <path>` sends), and the given path is returned back.
28
+ * The active authoring adapter's persistence destination where a save would
29
+ * land (`status().savePath`).
30
+ *
31
+ * WO-8: this also took a `path` and OPENED that scene through
32
+ * `EditorClient.openScene` -> the `open-scene` relay verb. That verb now rejects
33
+ * (the `.vscn.json` format is deleted), so only the read remains.
59
34
  */
60
- scene(path?: string): Promise<string | null>;
35
+ scene(): Promise<string | null>;
61
36
  /**
62
37
  * Make the connected human editor show the same subject/view as the agent.
63
38
  * The returned URL is a compact, shareable projection — not a serialized
@@ -68,16 +43,6 @@ export declare class LiveEditor {
68
43
  currentView(): Promise<EditorView>;
69
44
  /** Capture the same center document the human is currently looking at. */
70
45
  captureActiveDocument(size?: number): Promise<ActiveDocumentCapture>;
71
- /**
72
- * FILE mode — writes the scene file directly. NOT a live wire command:
73
- * there is no live-editor wire op for this yet (`vgai apply-diff`'s own
74
- * module doc names the identical gap: "LIVE MODE — NOT BUILT ... route the
75
- * patch through a RUNNING editor"). Reuses the exact same engine
76
- * machinery `vgai apply-diff` uses, through `@vgai/sdk`'s
77
- * `project.scene.apply` operation (the operation-registry's own
78
- * projection of that logic) — never hand-rolled here.
79
- */
80
- applyDiff(input: ApplyDiffInput): Promise<ApplyDiffResult>;
81
46
  play(opts?: {
82
47
  seed?: number;
83
48
  }): Promise<void>;
@@ -92,17 +57,75 @@ export declare class LiveEditor {
92
57
  deselect(): Promise<void>;
93
58
  /** No `id` -> focus the current selection (mirrors bare `vgai focus`); `id` given -> focus that entity. */
94
59
  focus(id?: string): Promise<void>;
60
+ /**
61
+ * Frame the edit viewport camera on one entity. Same framing as
62
+ * `focus(id)`, but an entity id the scene does not know THROWS, naming the
63
+ * id — where `focus` quietly does nothing. Reach for this whenever the next
64
+ * step reads the viewport (`screenshot()`, an
65
+ * `assetPreview(..., { stage: 'scene' })`): a framing that silently missed
66
+ * would otherwise be indistinguishable from one that worked.
67
+ */
68
+ frame(entityId: string): Promise<void>;
95
69
  view(preset: ViewPreset): Promise<void>;
96
- /** `vgai show <viewport <scene|game>|inspector|console|build>`'s four sub-verbs, folded into one action name. */
70
+ /**
71
+ * Show several instances of the running game split-screen — the whole point
72
+ * of multiplayer authoring: `await editor.instances(2)` puts a second view
73
+ * beside the first, watchable by a human, each drivable by its own bot
74
+ * (`game.instance(id)` / `tools.run(name, params, { instance })`). Pass a
75
+ * TOTAL count (default "Player 1"/"Player 2"/… labels) or explicit names
76
+ * (`editor.instances(['Alice', 'Bob'])`, index 0 = primary). A count of `1`
77
+ * (or a single name) collapses back to a single view. Requires a live play
78
+ * session.
79
+ */
80
+ instances(countOrNames: number | string[]): Promise<void>;
81
+ /** `vgai show <viewport <edit|play>|inspector|console|build>`'s four sub-verbs, folded into one action name. */
97
82
  showPanel(name: PanelName): Promise<void>;
98
83
  /** `kind` inferred from `path`'s extension when omitted (`inferAssetKind`) — pass it explicitly to override. */
99
84
  openAsset(path: string, kind?: AssetKind): Promise<void>;
100
- /** Captures the editor's native Asset Lab four-view preview for `path` (a project-relative asset path). */
101
- assetPreview(path: string, options?: AssetPreviewOptions): Promise<AssetPreviewCapture>;
85
+ /**
86
+ * Captures the editor's native four-view preview. A bare string is a
87
+ * project-relative asset path (the common case); an explicit source object
88
+ * targets a path, a LIVE SCENE ENTITY (`assetPreview({ entityId }, …)`) —
89
+ * which is what makes `options.stage: 'scene'`, the entity photographed
90
+ * where it stands under the scene's own lighting, reachable from here — or
91
+ * RAW GLB BYTES (`assetPreview({ glbBase64 }, …)`), for a model that exists
92
+ * only in the calling Node process's memory and has never been written to
93
+ * disk. `stage` defaults to `'lab'`, the neutral Asset Lab staging this has
94
+ * always produced, and the bytes form is lab-only.
95
+ */
96
+ assetPreview(source: string | AssetPreviewSource, options?: AssetPreviewOptions): Promise<AssetPreviewCapture>;
102
97
  grid(on: boolean): Promise<void>;
103
98
  helpers(on: boolean): Promise<void>;
104
99
  stats(on: boolean): Promise<void>;
105
100
  shading(mode: ShadingMode): Promise<void>;
101
+ /**
102
+ * READ the inspector, as data — the serialized inspection subject
103
+ * (`editor.inspect()`; design: `docs/ARCHITECTURE-CORE.md` §Editor chrome,
104
+ * "The Inspection Model"). This is the Figma-Inspect analog: whatever a
105
+ * human would see in the inspector right now — the subject's identity, its
106
+ * verbs, and every identified section in display order, with a `fields`
107
+ * section's CURRENT VALUES at their scriptable `path`s.
108
+ *
109
+ * Reach for it whenever the next step depends on what an object actually
110
+ * IS: `await editor.select(id)` then `await editor.inspect()` answers "what
111
+ * properties does this thing have, and what are they set to" in one call,
112
+ * against the same model the panel renders — no scene-graph reads, no
113
+ * guessing at property names.
114
+ *
115
+ * When the inspector is showing NOTHING — nothing selected on a surface
116
+ * with no empty-state subject of its own, which is most of them — the answer
117
+ * is `{none: true}`, so "the human sees no inspector" and "the read failed"
118
+ * are never the same value. A surface whose empty space IS a real thing (an
119
+ * open Asset Lab document) still answers with that subject, and never with
120
+ * another surface's.
121
+ *
122
+ * A `custom` section body is a named opaque: the editor renders it with
123
+ * React, so the wire reports its identity rather than pretending to describe
124
+ * its rendering — plus, when the section can say what it DISPLAYS, a `data`
125
+ * payload in its own vocabulary (`transform` carries
126
+ * `{position, rotation, scale}`, rotation in Euler XYZ degrees).
127
+ */
128
+ inspect(): Promise<InspectedInspection>;
106
129
  /** Mirrors `vgai status` — the full live editor state as JSON. */
107
130
  status(): Promise<EditorState>;
108
131
  /** A live viewport PNG (`EditorClient.captureViewport`) — no direct CLI verb exists; this is the closest wire read. */
package/dist/editor.js CHANGED
@@ -1,17 +1,15 @@
1
1
  /**
2
2
  * `LiveEditor` — the editor-control half of `@vgai/live`'s `{ editor, game,
3
- * page }` (SHARED-SESSION-SPEC.md, Wave 2). Methods are named after the
3
+ * page }` (Wave 2). Methods are named after the
4
4
  * ACTION, not the CLI flag spelling (e.g. `select('all')` rather than a
5
- * separate `selectAll`, `showPanel('viewport-game')` rather than
6
- * `vgai show viewport game`'s two-token shape) — see each method's own doc
5
+ * separate `selectAll`, `showPanel('viewport-play')` rather than
6
+ * `vgai show viewport play`'s two-token shape) — see each method's own doc
7
7
  * comment for the exact CLI verb / `EditorClient` call it mirrors.
8
8
  *
9
- * Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module
10
- * never hand-rolls a `fetch` to `/__editor/command` itself. The ONE
11
- * exception is `applyDiff`, which is FILE mode (writes the scene file
12
- * directly), not a live wire command — see its own doc comment.
9
+ * Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module never
10
+ * hand-rolls a `fetch` to `/__editor/command` itself. (WO-8 removed the one
11
+ * exception, `applyDiff`, which was FILE mode rather than a live wire command.)
13
12
  */
14
- import { operations } from '@vgai/sdk';
15
13
  const EXTENSION_KIND = {
16
14
  '.glb': 'model',
17
15
  '.gltf': 'model',
@@ -20,6 +18,7 @@ const EXTENSION_KIND = {
20
18
  '.jpeg': 'image',
21
19
  '.webp': 'image',
22
20
  '.gif': 'image',
21
+ '.svg': 'image',
23
22
  '.mp3': 'audio',
24
23
  '.ogg': 'audio',
25
24
  '.wav': 'audio',
@@ -33,8 +32,10 @@ const EXTENSION_KIND = {
33
32
  * on. Callers with an unusual extension can always pass `kind` explicitly.
34
33
  */
35
34
  export function inferAssetKind(path) {
36
- if (path.endsWith('.vscn.json'))
37
- return 'scene';
35
+ // WO-8: `.vscn.json` mapped to 'scene' here. That format is deleted, and
36
+ // `.prefab.json` is likewise retired (PRs #578/#581/#589) but the `'prefab'`
37
+ // AssetKind itself still exists in `@vgai/editor-sdk`, so the mapping is left
38
+ // for that retirement's own follow-up rather than half-removed here.
38
39
  if (path.endsWith('.prefab.json'))
39
40
  return 'prefab';
40
41
  if (path.endsWith('.mat.json'))
@@ -44,25 +45,24 @@ export function inferAssetKind(path) {
44
45
  return EXTENSION_KIND[ext] ?? 'json';
45
46
  }
46
47
  export class LiveEditor {
47
- client;
48
- projectRoot;
49
- constructor(client,
50
- /** Absolute project root this session is bound to — used only by `applyDiff` (FILE mode; every other method goes over the wire and never needs it). */
51
- projectRoot) {
52
- this.client = client;
53
- this.projectRoot = projectRoot;
48
+ /** `#`-private, not `private`: `vgai eval --list` enumerates this object's
49
+ * real runtime members, and TypeScript's erased `private` would leave the
50
+ * raw `EditorClient` advertised beside them. See `./game-client/`'s
51
+ * `client.ts` (GameClient's field block) for the full reasoning. */
52
+ #client;
53
+ constructor(client) {
54
+ this.#client = client;
54
55
  }
55
56
  /**
56
- * No `path` -> the currently open scene's save path (`status().savePath`).
57
- * `path` given -> opens it (`EditorClient.openScene` — the same wire call
58
- * `vgai scene <path>` sends), and the given path is returned back.
57
+ * The active authoring adapter's persistence destination where a save would
58
+ * land (`status().savePath`).
59
+ *
60
+ * WO-8: this also took a `path` and OPENED that scene through
61
+ * `EditorClient.openScene` -> the `open-scene` relay verb. That verb now rejects
62
+ * (the `.vscn.json` format is deleted), so only the read remains.
59
63
  */
60
- async scene(path) {
61
- if (path !== undefined) {
62
- await this.client.openScene(path);
63
- return path;
64
- }
65
- const state = await this.client.getState();
64
+ async scene() {
65
+ const state = await this.#client.getState();
66
66
  return state.savePath;
67
67
  }
68
68
  /**
@@ -71,121 +71,174 @@ export class LiveEditor {
71
71
  * workspace or document payload.
72
72
  */
73
73
  async present(view) {
74
- return this.client.present(view);
74
+ return this.#client.present(view);
75
75
  }
76
76
  /** The human editor's actual active document, selection, camera and utility. */
77
77
  async currentView() {
78
- return this.client.currentView();
78
+ return this.#client.currentView();
79
79
  }
80
80
  /** Capture the same center document the human is currently looking at. */
81
81
  async captureActiveDocument(size) {
82
- return this.client.captureActiveDocument(size);
83
- }
84
- /**
85
- * FILE mode writes the scene file directly. NOT a live wire command:
86
- * there is no live-editor wire op for this yet (`vgai apply-diff`'s own
87
- * module doc names the identical gap: "LIVE MODE NOT BUILT ... route the
88
- * patch through a RUNNING editor"). Reuses the exact same engine
89
- * machinery `vgai apply-diff` uses, through `@vgai/sdk`'s
90
- * `project.scene.apply` operation (the operation-registry's own
91
- * projection of that logic) — never hand-rolled here.
82
+ return this.#client.captureActiveDocument(size);
83
+ }
84
+ /*
85
+ * `applyDiff` lived here. It dispatched `project.scene.apply`, one of the
86
+ * twelve `.vscn` document tools deleted with the format, so there is nothing
87
+ * left for it to call. `ApplyDiffInput` / `ApplyDiffResult` went with it, and
88
+ * so did `projectRoot` this was the ONE method that used it (every other
89
+ * method goes over the wire).
92
90
  */
93
- async applyDiff(input) {
94
- const outcome = await operations.dispatch('project.scene.apply', input, {
95
- projectRoot: this.projectRoot,
96
- });
97
- if (!outcome.ok) {
98
- throw new Error(`@vgai/live: applyDiff failed [${outcome.error.code}]: ${outcome.error.message}`);
99
- }
100
- return outcome.data;
101
- }
102
91
  async play(opts) {
103
- await this.client.play(opts);
92
+ await this.#client.play(opts);
104
93
  }
105
94
  async stop() {
106
- await this.client.stop();
95
+ await this.#client.stop();
107
96
  }
108
97
  async pause() {
109
- await this.client.pause();
98
+ await this.#client.pause();
110
99
  }
111
100
  async resume() {
112
- await this.client.resume();
101
+ await this.#client.resume();
113
102
  }
114
103
  /** Advance `n` frames (default 1) — `EditorClient.step()` sent `n` times, mirroring `vgai step` run repeatedly. */
115
104
  async step(n = 1) {
116
105
  for (let i = 0; i < n; i++) {
117
- await this.client.step();
106
+ await this.#client.step();
118
107
  }
119
108
  }
120
109
  /** `'all'` -> `EditorClient.selectAll()` (mirrors `vgai select --all`); otherwise `EditorClient.select(id)` (mirrors `vgai select <entityId>`). */
121
110
  async select(id) {
122
111
  if (id === 'all') {
123
- await this.client.selectAll();
112
+ await this.#client.selectAll();
124
113
  return;
125
114
  }
126
- await this.client.select(id);
115
+ await this.#client.select(id);
127
116
  }
128
117
  /** Mirrors `vgai deselect`. */
129
118
  async deselect() {
130
- await this.client.select(null);
119
+ await this.#client.select(null);
131
120
  }
132
121
  /** No `id` -> focus the current selection (mirrors bare `vgai focus`); `id` given -> focus that entity. */
133
122
  async focus(id) {
134
123
  if (id !== undefined) {
135
- await this.client.focusEntity(id);
124
+ await this.#client.focusEntity(id);
136
125
  return;
137
126
  }
138
- await this.client.focusSelection();
127
+ await this.#client.focusSelection();
128
+ }
129
+ /**
130
+ * Frame the edit viewport camera on one entity. Same framing as
131
+ * `focus(id)`, but an entity id the scene does not know THROWS, naming the
132
+ * id — where `focus` quietly does nothing. Reach for this whenever the next
133
+ * step reads the viewport (`screenshot()`, an
134
+ * `assetPreview(..., { stage: 'scene' })`): a framing that silently missed
135
+ * would otherwise be indistinguishable from one that worked.
136
+ */
137
+ async frame(entityId) {
138
+ await this.#client.frameEntity(entityId);
139
139
  }
140
140
  async view(preset) {
141
- await this.client.viewPreset(preset);
141
+ await this.#client.viewPreset(preset);
142
142
  }
143
- /** `vgai show <viewport <scene|game>|inspector|console|build>`'s four sub-verbs, folded into one action name. */
143
+ /**
144
+ * Show several instances of the running game split-screen — the whole point
145
+ * of multiplayer authoring: `await editor.instances(2)` puts a second view
146
+ * beside the first, watchable by a human, each drivable by its own bot
147
+ * (`game.instance(id)` / `tools.run(name, params, { instance })`). Pass a
148
+ * TOTAL count (default "Player 1"/"Player 2"/… labels) or explicit names
149
+ * (`editor.instances(['Alice', 'Bob'])`, index 0 = primary). A count of `1`
150
+ * (or a single name) collapses back to a single view. Requires a live play
151
+ * session.
152
+ */
153
+ async instances(countOrNames) {
154
+ await this.#client.setInstanceCount(countOrNames);
155
+ }
156
+ /** `vgai show <viewport <edit|play>|inspector|console|build>`'s four sub-verbs, folded into one action name. */
144
157
  async showPanel(name) {
145
158
  switch (name) {
146
- case 'viewport-scene':
147
- await this.client.showViewport('scene');
159
+ case 'viewport-edit':
160
+ await this.#client.showViewport('edit');
148
161
  return;
149
- case 'viewport-game':
150
- await this.client.showViewport('game');
162
+ case 'viewport-play':
163
+ await this.#client.showViewport('play');
151
164
  return;
152
165
  case 'inspector':
153
- await this.client.showInspector();
166
+ await this.#client.showInspector();
154
167
  return;
155
168
  case 'console':
156
- await this.client.toggleConsole();
169
+ await this.#client.toggleConsole();
157
170
  return;
158
171
  case 'build':
159
- await this.client.showBuild();
172
+ await this.#client.showBuild();
160
173
  return;
161
174
  }
162
175
  }
163
176
  /** `kind` inferred from `path`'s extension when omitted (`inferAssetKind`) — pass it explicitly to override. */
164
177
  async openAsset(path, kind) {
165
- await this.client.openAsset(path, kind ?? inferAssetKind(path));
178
+ await this.#client.openAsset(path, kind ?? inferAssetKind(path));
166
179
  }
167
- /** Captures the editor's native Asset Lab four-view preview for `path` (a project-relative asset path). */
168
- async assetPreview(path, options) {
169
- return this.client.captureAssetPreview({ assetPath: path }, options);
180
+ /**
181
+ * Captures the editor's native four-view preview. A bare string is a
182
+ * project-relative asset path (the common case); an explicit source object
183
+ * targets a path, a LIVE SCENE ENTITY (`assetPreview({ entityId }, …)`) —
184
+ * which is what makes `options.stage: 'scene'`, the entity photographed
185
+ * where it stands under the scene's own lighting, reachable from here — or
186
+ * RAW GLB BYTES (`assetPreview({ glbBase64 }, …)`), for a model that exists
187
+ * only in the calling Node process's memory and has never been written to
188
+ * disk. `stage` defaults to `'lab'`, the neutral Asset Lab staging this has
189
+ * always produced, and the bytes form is lab-only.
190
+ */
191
+ async assetPreview(source, options) {
192
+ return this.#client.captureAssetPreview(typeof source === 'string' ? { assetPath: source } : source, options);
170
193
  }
171
194
  async grid(on) {
172
- await this.client.setGrid(on);
195
+ await this.#client.setGrid(on);
173
196
  }
174
197
  async helpers(on) {
175
- await this.client.setHelpers(on);
198
+ await this.#client.setHelpers(on);
176
199
  }
177
200
  async stats(on) {
178
- await this.client.setStats(on);
201
+ await this.#client.setStats(on);
179
202
  }
180
203
  async shading(mode) {
181
- await this.client.setShadingMode(mode);
204
+ await this.#client.setShadingMode(mode);
205
+ }
206
+ /**
207
+ * READ the inspector, as data — the serialized inspection subject
208
+ * (`editor.inspect()`; design: `docs/ARCHITECTURE-CORE.md` §Editor chrome,
209
+ * "The Inspection Model"). This is the Figma-Inspect analog: whatever a
210
+ * human would see in the inspector right now — the subject's identity, its
211
+ * verbs, and every identified section in display order, with a `fields`
212
+ * section's CURRENT VALUES at their scriptable `path`s.
213
+ *
214
+ * Reach for it whenever the next step depends on what an object actually
215
+ * IS: `await editor.select(id)` then `await editor.inspect()` answers "what
216
+ * properties does this thing have, and what are they set to" in one call,
217
+ * against the same model the panel renders — no scene-graph reads, no
218
+ * guessing at property names.
219
+ *
220
+ * When the inspector is showing NOTHING — nothing selected on a surface
221
+ * with no empty-state subject of its own, which is most of them — the answer
222
+ * is `{none: true}`, so "the human sees no inspector" and "the read failed"
223
+ * are never the same value. A surface whose empty space IS a real thing (an
224
+ * open Asset Lab document) still answers with that subject, and never with
225
+ * another surface's.
226
+ *
227
+ * A `custom` section body is a named opaque: the editor renders it with
228
+ * React, so the wire reports its identity rather than pretending to describe
229
+ * its rendering — plus, when the section can say what it DISPLAYS, a `data`
230
+ * payload in its own vocabulary (`transform` carries
231
+ * `{position, rotation, scale}`, rotation in Euler XYZ degrees).
232
+ */
233
+ async inspect() {
234
+ return this.#client.inspect();
182
235
  }
183
236
  /** Mirrors `vgai status` — the full live editor state as JSON. */
184
237
  async status() {
185
- return this.client.getState();
238
+ return this.#client.getState();
186
239
  }
187
240
  /** A live viewport PNG (`EditorClient.captureViewport`) — no direct CLI verb exists; this is the closest wire read. */
188
241
  async screenshot(size) {
189
- return this.client.captureViewport(size);
242
+ return this.#client.captureViewport(size);
190
243
  }
191
244
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * run-3 dogfood friction #2 — a legitimately-progressing script built from
3
+ * many short `game.input.hold()` calls looked WEDGED to anything watching its
4
+ * stdout for liveness, even though it was making real progress. Root cause: `GameInput.hold` (client.ts) collapsed
5
+ * what used to be a `waitSimTime` POLL loop into a single bridge call per
6
+ * hold (see its own doc comment — deliberately, to cut transport round
7
+ * trips), so a spec built from many holds and little else can go silent on
8
+ * stdout between them for minutes at a time.
9
+ *
10
+ * `wait-for.ts`'s `maybeHeartbeat` already solves the analogous problem for
11
+ * the `waitFor`/`waitSimTime` POLL loop, but it is deliberately SIM-TICK
12
+ * aware (a frozen sim clock must stay heartbeat-silent so that loop's own
13
+ * ~30s stall guard, `WAIT_FOR_STALL_POLL_LIMIT`, can still diagnose a
14
+ * genuinely-stuck game rather than a heartbeat papering over it). This is
15
+ * the companion for `GameClient`'s generic bridge dispatch
16
+ * (`callBridge`/`callBridgeAsync`), which has no poll loop and no sim tick
17
+ * to check at all: "wire activity" here just means a bridge call was
18
+ * actually DISPATCHED — sequential awaits mean a new dispatch can only
19
+ * happen once the previous one resolved, so a genuinely wedged page (bridge
20
+ * calls that never resolve at all) still goes — and stays — silent. The
21
+ * throttle exists only to bound
22
+ * stdout volume during a burst of fast calls, not to filter out "fake"
23
+ * activity.
24
+ */
25
+ export declare const BRIDGE_HEARTBEAT_INTERVAL_MS = 30000;
26
+ /** Mutable-by-replacement bookkeeping a caller threads through successive
27
+ * `maybeBridgeHeartbeat` calls — mirrors `wait-for.ts`'s `HeartbeatState`
28
+ * shape/style, minus the tick field (no sim-progress condition here). */
29
+ export interface BridgeHeartbeatState {
30
+ lastEmitWallMs: number;
31
+ }
32
+ /** The liveness line this prints to stdout — greppable, and prefixed
33
+ * distinctly from `wait-for.ts`'s `vgai-heartbeat` so the two liveness
34
+ * sources are distinguishable in a run's log. */
35
+ export declare function formatBridgeHeartbeatLine(testTitle: string, method: string): string;
36
+ /**
37
+ * Pure decision, called once per bridge dispatch: should a heartbeat print
38
+ * now, and what's the updated bookkeeping? Returns the SAME `state` object
39
+ * (referentially) when nothing should emit, so a caller can cheaply no-op.
40
+ */
41
+ export declare function maybeBridgeHeartbeat(opts: {
42
+ nowMs: number;
43
+ method: string;
44
+ testTitle: string;
45
+ state: BridgeHeartbeatState;
46
+ }): {
47
+ line: string | null;
48
+ state: BridgeHeartbeatState;
49
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * run-3 dogfood friction #2 — a legitimately-progressing script built from
3
+ * many short `game.input.hold()` calls looked WEDGED to anything watching its
4
+ * stdout for liveness, even though it was making real progress. Root cause: `GameInput.hold` (client.ts) collapsed
5
+ * what used to be a `waitSimTime` POLL loop into a single bridge call per
6
+ * hold (see its own doc comment — deliberately, to cut transport round
7
+ * trips), so a spec built from many holds and little else can go silent on
8
+ * stdout between them for minutes at a time.
9
+ *
10
+ * `wait-for.ts`'s `maybeHeartbeat` already solves the analogous problem for
11
+ * the `waitFor`/`waitSimTime` POLL loop, but it is deliberately SIM-TICK
12
+ * aware (a frozen sim clock must stay heartbeat-silent so that loop's own
13
+ * ~30s stall guard, `WAIT_FOR_STALL_POLL_LIMIT`, can still diagnose a
14
+ * genuinely-stuck game rather than a heartbeat papering over it). This is
15
+ * the companion for `GameClient`'s generic bridge dispatch
16
+ * (`callBridge`/`callBridgeAsync`), which has no poll loop and no sim tick
17
+ * to check at all: "wire activity" here just means a bridge call was
18
+ * actually DISPATCHED — sequential awaits mean a new dispatch can only
19
+ * happen once the previous one resolved, so a genuinely wedged page (bridge
20
+ * calls that never resolve at all) still goes — and stays — silent. The
21
+ * throttle exists only to bound
22
+ * stdout volume during a burst of fast calls, not to filter out "fake"
23
+ * activity.
24
+ */
25
+ export const BRIDGE_HEARTBEAT_INTERVAL_MS = 30_000;
26
+ /** The liveness line this prints to stdout — greppable, and prefixed
27
+ * distinctly from `wait-for.ts`'s `vgai-heartbeat` so the two liveness
28
+ * sources are distinguishable in a run's log. */
29
+ export function formatBridgeHeartbeatLine(testTitle, method) {
30
+ return `vgai-bridge-heartbeat ${testTitle} method=${method}`;
31
+ }
32
+ /**
33
+ * Pure decision, called once per bridge dispatch: should a heartbeat print
34
+ * now, and what's the updated bookkeeping? Returns the SAME `state` object
35
+ * (referentially) when nothing should emit, so a caller can cheaply no-op.
36
+ */
37
+ export function maybeBridgeHeartbeat(opts) {
38
+ const wallElapsed = opts.nowMs - opts.state.lastEmitWallMs;
39
+ if (wallElapsed >= BRIDGE_HEARTBEAT_INTERVAL_MS) {
40
+ return {
41
+ line: formatBridgeHeartbeatLine(opts.testTitle, opts.method),
42
+ state: { lastEmitWallMs: opts.nowMs },
43
+ };
44
+ }
45
+ return { line: null, state: opts.state };
46
+ }