@vgai/live 0.5.11 → 0.5.13

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.
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `editor.document` — the session binding for the scoped editor-chrome door.
3
+ *
4
+ * WHY IT IS A SEPARATE OBJECT, and why the verbs are these four, is recorded
5
+ * once in the implementation's header
6
+ * (`packages/editor/src/editor-document-probe.ts`); the short version is that
7
+ * `game.page()` is play-mode-gated and rooted at the GAME container, so an
8
+ * editor surface that is not a running game could be neither read nor driven
9
+ * through the product. This door is the answer for exactly one subject: the
10
+ * ACTIVE center document. It is not page automation — a selector resolving
11
+ * outside that document's container is refused by a message naming the scope.
12
+ *
13
+ * It reaches editor chrome only in the sense that a project-tool document IS
14
+ * chrome; the dock, hierarchy and inspector keep their own doors
15
+ * (`editor.hierarchy()`, `editor.inspect()`), which report what those panels
16
+ * RENDERED rather than what a DOM scrape can find.
17
+ *
18
+ * A field on `LiveEditor` rather than methods on it, so `vgai eval --list`
19
+ * shows the four verbs as one named surface — the same reason `game.input`
20
+ * and `game.events` are instance fields.
21
+ */
22
+ import type { DocumentProbeResult, EditorClient } from '@vgai/editor-sdk';
23
+ /** Modifier/targeting options shared by the gesture verbs. */
24
+ export interface DocumentGestureOptions {
25
+ /** Which match to drive when the selector matches several (default 0). */
26
+ index?: number;
27
+ }
28
+ export interface DocumentKeyOptions extends DocumentGestureOptions {
29
+ /** Target one element instead of whatever inside the document has focus. */
30
+ selector?: string;
31
+ code?: string;
32
+ ctrlKey?: boolean;
33
+ metaKey?: boolean;
34
+ shiftKey?: boolean;
35
+ altKey?: boolean;
36
+ }
37
+ export interface DocumentPasteOptions extends DocumentGestureOptions {
38
+ selector?: string;
39
+ }
40
+ export declare class LiveEditorDocument {
41
+ #private;
42
+ constructor(client: EditorClient);
43
+ /** Read matching elements inside the active document: tag, text, attributes,
44
+ * value/checked/disabled and rect. `matched` is the total before `limit`. */
45
+ query(selector: string, options?: {
46
+ limit?: number;
47
+ }): Promise<DocumentProbeResult>;
48
+ /** A REAL pointer gesture (pointerdown/mousedown/focus/pointerup/mouseup/click)
49
+ * — not `element.click()`, which a `pointerdown` listener never sees. */
50
+ click(selector: string, options?: DocumentGestureOptions): Promise<DocumentProbeResult>;
51
+ /** A real keydown/keyup on the target, or on whatever inside the document has focus. */
52
+ key(key: string, options?: DocumentKeyOptions): Promise<DocumentProbeResult>;
53
+ /** A real `ClipboardEvent` carrying `text/plain` — the gesture nothing else
54
+ * in the product can produce. */
55
+ paste(text: string, options?: DocumentPasteOptions): Promise<DocumentProbeResult>;
56
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * `editor.document` — the session binding for the scoped editor-chrome door.
3
+ *
4
+ * WHY IT IS A SEPARATE OBJECT, and why the verbs are these four, is recorded
5
+ * once in the implementation's header
6
+ * (`packages/editor/src/editor-document-probe.ts`); the short version is that
7
+ * `game.page()` is play-mode-gated and rooted at the GAME container, so an
8
+ * editor surface that is not a running game could be neither read nor driven
9
+ * through the product. This door is the answer for exactly one subject: the
10
+ * ACTIVE center document. It is not page automation — a selector resolving
11
+ * outside that document's container is refused by a message naming the scope.
12
+ *
13
+ * It reaches editor chrome only in the sense that a project-tool document IS
14
+ * chrome; the dock, hierarchy and inspector keep their own doors
15
+ * (`editor.hierarchy()`, `editor.inspect()`), which report what those panels
16
+ * RENDERED rather than what a DOM scrape can find.
17
+ *
18
+ * A field on `LiveEditor` rather than methods on it, so `vgai eval --list`
19
+ * shows the four verbs as one named surface — the same reason `game.input`
20
+ * and `game.events` are instance fields.
21
+ */
22
+ export class LiveEditorDocument {
23
+ #client;
24
+ constructor(client) {
25
+ this.#client = client;
26
+ }
27
+ /** Read matching elements inside the active document: tag, text, attributes,
28
+ * value/checked/disabled and rect. `matched` is the total before `limit`. */
29
+ async query(selector, options) {
30
+ return this.#probe({
31
+ action: 'query',
32
+ selector,
33
+ ...(options?.limit === undefined ? {} : { limit: options.limit }),
34
+ });
35
+ }
36
+ /** A REAL pointer gesture (pointerdown/mousedown/focus/pointerup/mouseup/click)
37
+ * — not `element.click()`, which a `pointerdown` listener never sees. */
38
+ async click(selector, options) {
39
+ return this.#probe({
40
+ action: 'click',
41
+ selector,
42
+ ...(options?.index === undefined ? {} : { index: options.index }),
43
+ });
44
+ }
45
+ /** A real keydown/keyup on the target, or on whatever inside the document has focus. */
46
+ async key(key, options) {
47
+ return this.#probe({ action: 'key', key, ...(options ?? {}) });
48
+ }
49
+ /** A real `ClipboardEvent` carrying `text/plain` — the gesture nothing else
50
+ * in the product can produce. */
51
+ async paste(text, options) {
52
+ return this.#probe({ action: 'paste', text, ...(options ?? {}) });
53
+ }
54
+ #probe(step) {
55
+ return this.#client.documentProbe(step);
56
+ }
57
+ }
package/dist/editor.d.ts CHANGED
@@ -10,7 +10,8 @@
10
10
  * hand-rolls a `fetch` to `/__editor/command` itself. (WO-8 removed the one
11
11
  * exception, `applyDiff`, which was FILE mode rather than a live wire command.)
12
12
  */
13
- import type { ActiveDocumentCapture, AssetKind, AssetPreviewCapture, AssetPreviewOptions, AssetPreviewSource, EditorClient, EditorState, EditorView, InspectedInspection, PresentedEditorView, ShadingMode, ViewPreset, ViewportCapture } from '@vgai/editor-sdk';
13
+ import type { ActiveDocumentCapture, AssetKind, AssetPreviewCapture, AssetPreviewOptions, AssetPreviewSource, EditorClient, EditorState, EditorView, HistoryStep, InspectedHierarchy, InspectedInspection, PresentedEditorView, ShadingMode, ViewPreset, ViewportCapture } from '@vgai/editor-sdk';
14
+ import { LiveEditorDocument } from './editor-document.js';
14
15
  /** `vgai show <...>`'s four sub-verbs folded into one action name — see `showPanel`. */
15
16
  export type PanelName = 'viewport-edit' | 'viewport-play' | 'inspector' | 'console' | 'build';
16
17
  /**
@@ -23,6 +24,15 @@ export type PanelName = 'viewport-edit' | 'viewport-play' | 'inspector' | 'conso
23
24
  export declare function inferAssetKind(path: string): AssetKind;
24
25
  export declare class LiveEditor {
25
26
  #private;
27
+ /**
28
+ * The ACTIVE center document's own DOM: read it, click it, key it, paste
29
+ * into it. The one door onto editor chrome that is not play-mode gated, and
30
+ * deliberately scoped to that document alone —
31
+ * `packages/editor/src/editor-document-probe.ts` carries the design and the
32
+ * refusal contract. Screenshotting the same subject is
33
+ * {@link LiveEditor.captureActiveDocument}, not a fifth verb here.
34
+ */
35
+ readonly document: LiveEditorDocument;
26
36
  constructor(client: EditorClient);
27
37
  /**
28
38
  * The active authoring adapter's persistence destination — where a save would
@@ -98,6 +108,24 @@ export declare class LiveEditor {
98
108
  helpers(on: boolean): Promise<void>;
99
109
  stats(on: boolean): Promise<void>;
100
110
  shading(mode: ShadingMode): Promise<void>;
111
+ /**
112
+ * CONSENT to edits being written into the game's own source files, for this
113
+ * session — the Game document's "Persist to game source" checkbox, reachable
114
+ * from a script.
115
+ *
116
+ * Off by default every session, on purpose: it is a statement about what you
117
+ * are doing right now, never a property of the game. With it off, an edit
118
+ * lives on the running object and says so; with it on, an edit that can be
119
+ * honestly anchored to the line that CREATED the object is written there,
120
+ * and one that cannot still says so. Answers with the server's own phrase for
121
+ * who records the resulting diff — your version control, or a vendored
122
+ * game's own lock — and refuses, with the reason, where the checkbox is
123
+ * disabled.
124
+ */
125
+ persistToGameSource(on: boolean): Promise<{
126
+ enabled: boolean;
127
+ recorder: string | null;
128
+ }>;
101
129
  /**
102
130
  * READ the inspector, as data — the serialized inspection subject
103
131
  * (`editor.inspect()`; design: `docs/ARCHITECTURE-CORE.md` §Editor chrome,
@@ -126,6 +154,43 @@ export declare class LiveEditor {
126
154
  * `{position, rotation, scale}`, rotation in Euler XYZ degrees).
127
155
  */
128
156
  inspect(): Promise<InspectedInspection>;
157
+ /**
158
+ * READ the hierarchy panel, as data — the rows a human is looking at right
159
+ * now, nested exactly as the panel nests them.
160
+ *
161
+ * The companion to {@link inspect}: that one answers "what IS the selected
162
+ * thing", this one answers "what does the tree LOOK LIKE". It is the panel's
163
+ * own output, not a fresh walk of the scene — the adapter's tree after the
164
+ * component marks fold implementation subtrees (bones, particle renderers,
165
+ * instanced pools), after the internals reveal, the document promotion, the
166
+ * child cap, the collapse state, the search filter and the selection scope.
167
+ *
168
+ * Works in play mode and edit mode; the answer says which (`playState`,
169
+ * `activeViewportTab`), because the two are different adapters and a tree
170
+ * that looks wrong is very often the wrong adapter's tree.
171
+ *
172
+ * Prefer this over `status().entities`, which is deliberately a different
173
+ * question — the RAW adapter tree, unprojected. A panel that renders the
174
+ * wrong rows looks perfectly healthy in that facet.
175
+ *
176
+ * Each row carries `childCount` (what its caret opens), `internalChildCount`
177
+ * (what is folded behind "Reveal Internals") and `expandable` (whether the
178
+ * panel draws a caret at all), so "this subtree exists but nothing in the UI
179
+ * opens it" is a fact you can read rather than one you have to notice.
180
+ *
181
+ * Rejects, naming the panel, when no hierarchy panel is mounted — an empty
182
+ * tree would be a fabricated answer about a surface nobody is being shown.
183
+ */
184
+ hierarchy(): Promise<InspectedHierarchy>;
185
+ /**
186
+ * Write one editable field from `inspect()` by its stable path, through the
187
+ * same Inspector IO and persistence boundary the human control uses.
188
+ */
189
+ setField(path: string, value: unknown): Promise<InspectedInspection>;
190
+ /** Undo / redo one project transaction, through the session's own history
191
+ * queue — the same one the keyboard shortcut drives. */
192
+ undo(): Promise<HistoryStep>;
193
+ redo(): Promise<HistoryStep>;
129
194
  /** Mirrors `vgai status` — the full live editor state as JSON. */
130
195
  status(): Promise<EditorState>;
131
196
  /** A live viewport PNG (`EditorClient.captureViewport`) — no direct CLI verb exists; this is the closest wire read. */
package/dist/editor.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * hand-rolls a `fetch` to `/__editor/command` itself. (WO-8 removed the one
11
11
  * exception, `applyDiff`, which was FILE mode rather than a live wire command.)
12
12
  */
13
+ import { LiveEditorDocument } from './editor-document.js';
13
14
  const EXTENSION_KIND = {
14
15
  '.glb': 'model',
15
16
  '.gltf': 'model',
@@ -19,10 +20,15 @@ const EXTENSION_KIND = {
19
20
  '.webp': 'image',
20
21
  '.gif': 'image',
21
22
  '.svg': 'image',
23
+ '.hdr': 'image',
24
+ '.exr': 'image',
22
25
  '.mp3': 'audio',
23
26
  '.ogg': 'audio',
24
27
  '.wav': 'audio',
25
28
  '.flac': 'audio',
29
+ '.glsl': 'source',
30
+ '.vert': 'source',
31
+ '.frag': 'source',
26
32
  };
27
33
  /**
28
34
  * Lightweight extension-based `AssetKind` guess for `openAsset`'s optional
@@ -32,14 +38,10 @@ const EXTENSION_KIND = {
32
38
  * on. Callers with an unusual extension can always pass `kind` explicitly.
33
39
  */
34
40
  export function inferAssetKind(path) {
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.
41
+ // `.prefab.json` is retired (PRs #578/#581/#589) but the `'prefab'`
42
+ // AssetKind itself remains in `@vgai/editor-sdk` for view-link compatibility.
39
43
  if (path.endsWith('.prefab.json'))
40
44
  return 'prefab';
41
- if (path.endsWith('.mat.json'))
42
- return 'material';
43
45
  const dot = path.lastIndexOf('.');
44
46
  const ext = dot >= 0 ? path.slice(dot).toLowerCase() : '';
45
47
  return EXTENSION_KIND[ext] ?? 'json';
@@ -50,8 +52,18 @@ export class LiveEditor {
50
52
  * raw `EditorClient` advertised beside them. See `./game-client/`'s
51
53
  * `client.ts` (GameClient's field block) for the full reasoning. */
52
54
  #client;
55
+ /**
56
+ * The ACTIVE center document's own DOM: read it, click it, key it, paste
57
+ * into it. The one door onto editor chrome that is not play-mode gated, and
58
+ * deliberately scoped to that document alone —
59
+ * `packages/editor/src/editor-document-probe.ts` carries the design and the
60
+ * refusal contract. Screenshotting the same subject is
61
+ * {@link LiveEditor.captureActiveDocument}, not a fifth verb here.
62
+ */
63
+ document;
53
64
  constructor(client) {
54
65
  this.#client = client;
66
+ this.document = new LiveEditorDocument(client);
55
67
  }
56
68
  /**
57
69
  * The active authoring adapter's persistence destination — where a save would
@@ -203,6 +215,23 @@ export class LiveEditor {
203
215
  async shading(mode) {
204
216
  await this.#client.setShadingMode(mode);
205
217
  }
218
+ /**
219
+ * CONSENT to edits being written into the game's own source files, for this
220
+ * session — the Game document's "Persist to game source" checkbox, reachable
221
+ * from a script.
222
+ *
223
+ * Off by default every session, on purpose: it is a statement about what you
224
+ * are doing right now, never a property of the game. With it off, an edit
225
+ * lives on the running object and says so; with it on, an edit that can be
226
+ * honestly anchored to the line that CREATED the object is written there,
227
+ * and one that cannot still says so. Answers with the server's own phrase for
228
+ * who records the resulting diff — your version control, or a vendored
229
+ * game's own lock — and refuses, with the reason, where the checkbox is
230
+ * disabled.
231
+ */
232
+ async persistToGameSource(on) {
233
+ return this.#client.setSourcePersistConsent(on);
234
+ }
206
235
  /**
207
236
  * READ the inspector, as data — the serialized inspection subject
208
237
  * (`editor.inspect()`; design: `docs/ARCHITECTURE-CORE.md` §Editor chrome,
@@ -233,6 +262,51 @@ export class LiveEditor {
233
262
  async inspect() {
234
263
  return this.#client.inspect();
235
264
  }
265
+ /**
266
+ * READ the hierarchy panel, as data — the rows a human is looking at right
267
+ * now, nested exactly as the panel nests them.
268
+ *
269
+ * The companion to {@link inspect}: that one answers "what IS the selected
270
+ * thing", this one answers "what does the tree LOOK LIKE". It is the panel's
271
+ * own output, not a fresh walk of the scene — the adapter's tree after the
272
+ * component marks fold implementation subtrees (bones, particle renderers,
273
+ * instanced pools), after the internals reveal, the document promotion, the
274
+ * child cap, the collapse state, the search filter and the selection scope.
275
+ *
276
+ * Works in play mode and edit mode; the answer says which (`playState`,
277
+ * `activeViewportTab`), because the two are different adapters and a tree
278
+ * that looks wrong is very often the wrong adapter's tree.
279
+ *
280
+ * Prefer this over `status().entities`, which is deliberately a different
281
+ * question — the RAW adapter tree, unprojected. A panel that renders the
282
+ * wrong rows looks perfectly healthy in that facet.
283
+ *
284
+ * Each row carries `childCount` (what its caret opens), `internalChildCount`
285
+ * (what is folded behind "Reveal Internals") and `expandable` (whether the
286
+ * panel draws a caret at all), so "this subtree exists but nothing in the UI
287
+ * opens it" is a fact you can read rather than one you have to notice.
288
+ *
289
+ * Rejects, naming the panel, when no hierarchy panel is mounted — an empty
290
+ * tree would be a fabricated answer about a surface nobody is being shown.
291
+ */
292
+ async hierarchy() {
293
+ return this.#client.hierarchy();
294
+ }
295
+ /**
296
+ * Write one editable field from `inspect()` by its stable path, through the
297
+ * same Inspector IO and persistence boundary the human control uses.
298
+ */
299
+ async setField(path, value) {
300
+ return this.#client.setInspectionField(path, value);
301
+ }
302
+ /** Undo / redo one project transaction, through the session's own history
303
+ * queue — the same one the keyboard shortcut drives. */
304
+ async undo() {
305
+ return this.#client.undo();
306
+ }
307
+ async redo() {
308
+ return this.#client.redo();
309
+ }
236
310
  /** Mirrors `vgai status` — the full live editor state as JSON. */
237
311
  async status() {
238
312
  return this.#client.getState();
package/dist/index.d.ts CHANGED
@@ -27,6 +27,13 @@
27
27
  * await editor.grid(true);
28
28
  * await game.input.tap('jump');
29
29
  *
30
+ * `editor.document` is the OTHER surface door and the one that is NOT
31
+ * play-mode gated: `page(step)` is rooted at the running GAME container, so
32
+ * `editor.document.{query,click,key,paste}` is how an editor surface that is
33
+ * not a game — a capability's workspace document, the Data sheet — gets read
34
+ * and driven through the product. It is scoped to the ACTIVE document and
35
+ * refuses anything outside it by name (`editor-document.ts`).
36
+ *
30
37
  * `page(step)` = `GameClient.page(step)` (Wave-2's playwright-shim surface,
31
38
  * PR #166) — write `step` as a literal `async (page) => {...}` and inline
32
39
  * every value it needs. KNOWN WIRE LIMITATION: under the relay transport this
@@ -44,6 +51,8 @@ import { LiveTools } from './tools.js';
44
51
  export type { ActiveDocumentCapture, EditorClient, EditorView, EditorViewDocument, PresentedEditorView, } from '@vgai/editor-sdk';
45
52
  export type { PanelName } from './editor.js';
46
53
  export { inferAssetKind, LiveEditor } from './editor.js';
54
+ export type { DocumentGestureOptions, DocumentKeyOptions, DocumentPasteOptions, } from './editor-document.js';
55
+ export { LiveEditorDocument } from './editor-document.js';
47
56
  export { createGameClient, createLiveGame, type LiveGame } from './game.js';
48
57
  export * from './game-client/index.js';
49
58
  export type { ProjectSessionHint, ResolvedSession, SessionListingTransport, SessionResolutionDeps, } from './session.js';
package/dist/index.js CHANGED
@@ -27,6 +27,13 @@
27
27
  * await editor.grid(true);
28
28
  * await game.input.tap('jump');
29
29
  *
30
+ * `editor.document` is the OTHER surface door and the one that is NOT
31
+ * play-mode gated: `page(step)` is rooted at the running GAME container, so
32
+ * `editor.document.{query,click,key,paste}` is how an editor surface that is
33
+ * not a game — a capability's workspace document, the Data sheet — gets read
34
+ * and driven through the product. It is scoped to the ACTIVE document and
35
+ * refuses anything outside it by name (`editor-document.ts`).
36
+ *
30
37
  * `page(step)` = `GameClient.page(step)` (Wave-2's playwright-shim surface,
31
38
  * PR #166) — write `step` as a literal `async (page) => {...}` and inline
32
39
  * every value it needs. KNOWN WIRE LIMITATION: under the relay transport this
@@ -45,6 +52,7 @@ import { resolveSession } from './session.js';
45
52
  import { createLazySession } from './singleton.js';
46
53
  import { LiveTools } from './tools.js';
47
54
  export { inferAssetKind, LiveEditor } from './editor.js';
55
+ export { LiveEditorDocument } from './editor-document.js';
48
56
  export { createGameClient, createLiveGame } from './game.js';
49
57
  export * from './game-client/index.js';
50
58
  export { findProjectRootFrom, resolveSession } from './session.js';
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@vgai/live",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.11",
5
+ "version": "0.5.13",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
@@ -31,8 +31,8 @@
31
31
  "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput"
32
32
  },
33
33
  "dependencies": {
34
- "@vgai/editor-sdk": "0.5.11",
35
- "@vgai/sdk": "0.5.11"
34
+ "@vgai/editor-sdk": "0.5.13",
35
+ "@vgai/sdk": "0.5.13"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@playwright/test": ">=1.58.2 <2"
@@ -0,0 +1,86 @@
1
+ /**
2
+ * `editor.document` — the session binding for the scoped editor-chrome door.
3
+ *
4
+ * WHY IT IS A SEPARATE OBJECT, and why the verbs are these four, is recorded
5
+ * once in the implementation's header
6
+ * (`packages/editor/src/editor-document-probe.ts`); the short version is that
7
+ * `game.page()` is play-mode-gated and rooted at the GAME container, so an
8
+ * editor surface that is not a running game could be neither read nor driven
9
+ * through the product. This door is the answer for exactly one subject: the
10
+ * ACTIVE center document. It is not page automation — a selector resolving
11
+ * outside that document's container is refused by a message naming the scope.
12
+ *
13
+ * It reaches editor chrome only in the sense that a project-tool document IS
14
+ * chrome; the dock, hierarchy and inspector keep their own doors
15
+ * (`editor.hierarchy()`, `editor.inspect()`), which report what those panels
16
+ * RENDERED rather than what a DOM scrape can find.
17
+ *
18
+ * A field on `LiveEditor` rather than methods on it, so `vgai eval --list`
19
+ * shows the four verbs as one named surface — the same reason `game.input`
20
+ * and `game.events` are instance fields.
21
+ */
22
+
23
+ import type { DocumentProbeResult, DocumentProbeStep, EditorClient } from '@vgai/editor-sdk';
24
+
25
+ /** Modifier/targeting options shared by the gesture verbs. */
26
+ export interface DocumentGestureOptions {
27
+ /** Which match to drive when the selector matches several (default 0). */
28
+ index?: number;
29
+ }
30
+
31
+ export interface DocumentKeyOptions extends DocumentGestureOptions {
32
+ /** Target one element instead of whatever inside the document has focus. */
33
+ selector?: string;
34
+ code?: string;
35
+ ctrlKey?: boolean;
36
+ metaKey?: boolean;
37
+ shiftKey?: boolean;
38
+ altKey?: boolean;
39
+ }
40
+
41
+ export interface DocumentPasteOptions extends DocumentGestureOptions {
42
+ selector?: string;
43
+ }
44
+
45
+ export class LiveEditorDocument {
46
+ readonly #client: EditorClient;
47
+
48
+ constructor(client: EditorClient) {
49
+ this.#client = client;
50
+ }
51
+
52
+ /** Read matching elements inside the active document: tag, text, attributes,
53
+ * value/checked/disabled and rect. `matched` is the total before `limit`. */
54
+ async query(selector: string, options?: { limit?: number }): Promise<DocumentProbeResult> {
55
+ return this.#probe({
56
+ action: 'query',
57
+ selector,
58
+ ...(options?.limit === undefined ? {} : { limit: options.limit }),
59
+ });
60
+ }
61
+
62
+ /** A REAL pointer gesture (pointerdown/mousedown/focus/pointerup/mouseup/click)
63
+ * — not `element.click()`, which a `pointerdown` listener never sees. */
64
+ async click(selector: string, options?: DocumentGestureOptions): Promise<DocumentProbeResult> {
65
+ return this.#probe({
66
+ action: 'click',
67
+ selector,
68
+ ...(options?.index === undefined ? {} : { index: options.index }),
69
+ });
70
+ }
71
+
72
+ /** A real keydown/keyup on the target, or on whatever inside the document has focus. */
73
+ async key(key: string, options?: DocumentKeyOptions): Promise<DocumentProbeResult> {
74
+ return this.#probe({ action: 'key', key, ...(options ?? {}) });
75
+ }
76
+
77
+ /** A real `ClipboardEvent` carrying `text/plain` — the gesture nothing else
78
+ * in the product can produce. */
79
+ async paste(text: string, options?: DocumentPasteOptions): Promise<DocumentProbeResult> {
80
+ return this.#probe({ action: 'paste', text, ...(options ?? {}) });
81
+ }
82
+
83
+ #probe(step: DocumentProbeStep): Promise<DocumentProbeResult> {
84
+ return this.#client.documentProbe(step);
85
+ }
86
+ }
package/src/editor.ts CHANGED
@@ -20,12 +20,15 @@ import type {
20
20
  EditorClient,
21
21
  EditorState,
22
22
  EditorView,
23
+ HistoryStep,
24
+ InspectedHierarchy,
23
25
  InspectedInspection,
24
26
  PresentedEditorView,
25
27
  ShadingMode,
26
28
  ViewPreset,
27
29
  ViewportCapture,
28
30
  } from '@vgai/editor-sdk';
31
+ import { LiveEditorDocument } from './editor-document.js';
29
32
 
30
33
  /** `vgai show <...>`'s four sub-verbs folded into one action name — see `showPanel`. */
31
34
  export type PanelName = 'viewport-edit' | 'viewport-play' | 'inspector' | 'console' | 'build';
@@ -39,10 +42,15 @@ const EXTENSION_KIND: Record<string, AssetKind> = {
39
42
  '.webp': 'image',
40
43
  '.gif': 'image',
41
44
  '.svg': 'image',
45
+ '.hdr': 'image',
46
+ '.exr': 'image',
42
47
  '.mp3': 'audio',
43
48
  '.ogg': 'audio',
44
49
  '.wav': 'audio',
45
50
  '.flac': 'audio',
51
+ '.glsl': 'source',
52
+ '.vert': 'source',
53
+ '.frag': 'source',
46
54
  };
47
55
 
48
56
  /**
@@ -53,12 +61,9 @@ const EXTENSION_KIND: Record<string, AssetKind> = {
53
61
  * on. Callers with an unusual extension can always pass `kind` explicitly.
54
62
  */
55
63
  export function inferAssetKind(path: string): AssetKind {
56
- // WO-8: `.vscn.json` mapped to 'scene' here. That format is deleted, and
57
- // `.prefab.json` is likewise retired (PRs #578/#581/#589) but the `'prefab'`
58
- // AssetKind itself still exists in `@vgai/editor-sdk`, so the mapping is left
59
- // for that retirement's own follow-up rather than half-removed here.
64
+ // `.prefab.json` is retired (PRs #578/#581/#589) but the `'prefab'`
65
+ // AssetKind itself remains in `@vgai/editor-sdk` for view-link compatibility.
60
66
  if (path.endsWith('.prefab.json')) return 'prefab';
61
- if (path.endsWith('.mat.json')) return 'material';
62
67
  const dot = path.lastIndexOf('.');
63
68
  const ext = dot >= 0 ? path.slice(dot).toLowerCase() : '';
64
69
  return EXTENSION_KIND[ext] ?? 'json';
@@ -71,8 +76,19 @@ export class LiveEditor {
71
76
  * `client.ts` (GameClient's field block) for the full reasoning. */
72
77
  readonly #client: EditorClient;
73
78
 
79
+ /**
80
+ * The ACTIVE center document's own DOM: read it, click it, key it, paste
81
+ * into it. The one door onto editor chrome that is not play-mode gated, and
82
+ * deliberately scoped to that document alone —
83
+ * `packages/editor/src/editor-document-probe.ts` carries the design and the
84
+ * refusal contract. Screenshotting the same subject is
85
+ * {@link LiveEditor.captureActiveDocument}, not a fifth verb here.
86
+ */
87
+ readonly document: LiveEditorDocument;
88
+
74
89
  constructor(client: EditorClient) {
75
90
  this.#client = client;
91
+ this.document = new LiveEditorDocument(client);
76
92
  }
77
93
 
78
94
  /**
@@ -254,6 +270,24 @@ export class LiveEditor {
254
270
  await this.#client.setShadingMode(mode);
255
271
  }
256
272
 
273
+ /**
274
+ * CONSENT to edits being written into the game's own source files, for this
275
+ * session — the Game document's "Persist to game source" checkbox, reachable
276
+ * from a script.
277
+ *
278
+ * Off by default every session, on purpose: it is a statement about what you
279
+ * are doing right now, never a property of the game. With it off, an edit
280
+ * lives on the running object and says so; with it on, an edit that can be
281
+ * honestly anchored to the line that CREATED the object is written there,
282
+ * and one that cannot still says so. Answers with the server's own phrase for
283
+ * who records the resulting diff — your version control, or a vendored
284
+ * game's own lock — and refuses, with the reason, where the checkbox is
285
+ * disabled.
286
+ */
287
+ async persistToGameSource(on: boolean): Promise<{ enabled: boolean; recorder: string | null }> {
288
+ return this.#client.setSourcePersistConsent(on);
289
+ }
290
+
257
291
  /**
258
292
  * READ the inspector, as data — the serialized inspection subject
259
293
  * (`editor.inspect()`; design: `docs/ARCHITECTURE-CORE.md` §Editor chrome,
@@ -285,6 +319,55 @@ export class LiveEditor {
285
319
  return this.#client.inspect();
286
320
  }
287
321
 
322
+ /**
323
+ * READ the hierarchy panel, as data — the rows a human is looking at right
324
+ * now, nested exactly as the panel nests them.
325
+ *
326
+ * The companion to {@link inspect}: that one answers "what IS the selected
327
+ * thing", this one answers "what does the tree LOOK LIKE". It is the panel's
328
+ * own output, not a fresh walk of the scene — the adapter's tree after the
329
+ * component marks fold implementation subtrees (bones, particle renderers,
330
+ * instanced pools), after the internals reveal, the document promotion, the
331
+ * child cap, the collapse state, the search filter and the selection scope.
332
+ *
333
+ * Works in play mode and edit mode; the answer says which (`playState`,
334
+ * `activeViewportTab`), because the two are different adapters and a tree
335
+ * that looks wrong is very often the wrong adapter's tree.
336
+ *
337
+ * Prefer this over `status().entities`, which is deliberately a different
338
+ * question — the RAW adapter tree, unprojected. A panel that renders the
339
+ * wrong rows looks perfectly healthy in that facet.
340
+ *
341
+ * Each row carries `childCount` (what its caret opens), `internalChildCount`
342
+ * (what is folded behind "Reveal Internals") and `expandable` (whether the
343
+ * panel draws a caret at all), so "this subtree exists but nothing in the UI
344
+ * opens it" is a fact you can read rather than one you have to notice.
345
+ *
346
+ * Rejects, naming the panel, when no hierarchy panel is mounted — an empty
347
+ * tree would be a fabricated answer about a surface nobody is being shown.
348
+ */
349
+ async hierarchy(): Promise<InspectedHierarchy> {
350
+ return this.#client.hierarchy();
351
+ }
352
+
353
+ /**
354
+ * Write one editable field from `inspect()` by its stable path, through the
355
+ * same Inspector IO and persistence boundary the human control uses.
356
+ */
357
+ async setField(path: string, value: unknown): Promise<InspectedInspection> {
358
+ return this.#client.setInspectionField(path, value);
359
+ }
360
+
361
+ /** Undo / redo one project transaction, through the session's own history
362
+ * queue — the same one the keyboard shortcut drives. */
363
+ async undo(): Promise<HistoryStep> {
364
+ return this.#client.undo();
365
+ }
366
+
367
+ async redo(): Promise<HistoryStep> {
368
+ return this.#client.redo();
369
+ }
370
+
288
371
  /** Mirrors `vgai status` — the full live editor state as JSON. */
289
372
  async status(): Promise<EditorState> {
290
373
  return this.#client.getState();
package/src/index.ts CHANGED
@@ -27,6 +27,13 @@
27
27
  * await editor.grid(true);
28
28
  * await game.input.tap('jump');
29
29
  *
30
+ * `editor.document` is the OTHER surface door and the one that is NOT
31
+ * play-mode gated: `page(step)` is rooted at the running GAME container, so
32
+ * `editor.document.{query,click,key,paste}` is how an editor surface that is
33
+ * not a game — a capability's workspace document, the Data sheet — gets read
34
+ * and driven through the product. It is scoped to the ACTIVE document and
35
+ * refuses anything outside it by name (`editor-document.ts`).
36
+ *
30
37
  * `page(step)` = `GameClient.page(step)` (Wave-2's playwright-shim surface,
31
38
  * PR #166) — write `step` as a literal `async (page) => {...}` and inline
32
39
  * every value it needs. KNOWN WIRE LIMITATION: under the relay transport this
@@ -56,6 +63,12 @@ export type {
56
63
  } from '@vgai/editor-sdk';
57
64
  export type { PanelName } from './editor.js';
58
65
  export { inferAssetKind, LiveEditor } from './editor.js';
66
+ export type {
67
+ DocumentGestureOptions,
68
+ DocumentKeyOptions,
69
+ DocumentPasteOptions,
70
+ } from './editor-document.js';
71
+ export { LiveEditorDocument } from './editor-document.js';
59
72
  export { createGameClient, createLiveGame, type LiveGame } from './game.js';
60
73
  export * from './game-client/index.js';
61
74
  export type {