@volter/live 0.5.211 → 0.5.213

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 (73) hide show
  1. package/BUNDLED_NOTICES +13 -13
  2. package/README.md +10 -11
  3. package/dist/asset-library.d.ts +98 -0
  4. package/dist/asset-library.js +7 -0
  5. package/dist/asset-library.js.map +7 -0
  6. package/dist/chunk-7WKVJGHZ.js +15 -0
  7. package/dist/chunk-7WKVJGHZ.js.map +7 -0
  8. package/dist/chunk-B2HUWFTT.js +25 -0
  9. package/dist/chunk-B2HUWFTT.js.map +7 -0
  10. package/dist/chunk-IZJOOON2.js +805 -0
  11. package/dist/chunk-IZJOOON2.js.map +7 -0
  12. package/dist/chunk-JDOIPALX.js +158 -0
  13. package/dist/chunk-JDOIPALX.js.map +7 -0
  14. package/dist/chunk-JFAHP5CC.js +218 -0
  15. package/dist/chunk-JFAHP5CC.js.map +7 -0
  16. package/dist/chunk-PC56TDHV.js +189 -0
  17. package/dist/chunk-PC56TDHV.js.map +7 -0
  18. package/dist/chunk-WIAH4XZE.js +82 -0
  19. package/dist/chunk-WIAH4XZE.js.map +7 -0
  20. package/dist/client.d.ts +376 -0
  21. package/dist/client.js +10 -0
  22. package/dist/client.js.map +7 -0
  23. package/dist/discovery.d.ts +32 -0
  24. package/dist/discovery.js +19 -0
  25. package/dist/discovery.js.map +7 -0
  26. package/dist/editor-document.d.ts +12 -47
  27. package/dist/editor.d.ts +45 -183
  28. package/dist/http-transport.d.ts +6 -0
  29. package/dist/index.d.ts +3 -6
  30. package/dist/index.js +83 -1762
  31. package/dist/index.js.map +4 -4
  32. package/dist/installed-package.d.ts +6 -0
  33. package/dist/installed-package.js +7 -0
  34. package/dist/installed-package.js.map +7 -0
  35. package/dist/lazy-proxy.d.ts +11 -11
  36. package/dist/member-hints.d.ts +6 -9
  37. package/dist/product-create.d.ts +29 -0
  38. package/dist/product-create.js +13 -0
  39. package/dist/product-create.js.map +7 -0
  40. package/dist/product-locator.d.ts +100 -0
  41. package/dist/product-locator.js +25 -0
  42. package/dist/product-locator.js.map +7 -0
  43. package/dist/registry-format.d.ts +72 -0
  44. package/dist/registry-format.js +31 -0
  45. package/dist/registry-format.js.map +7 -0
  46. package/dist/session.d.ts +11 -15
  47. package/dist/singleton.d.ts +1 -13
  48. package/dist/volter-manifest.d.ts +55 -0
  49. package/dist/volter-manifest.js +9 -0
  50. package/dist/volter-manifest.js.map +7 -0
  51. package/dist/workbench-locator.d.ts +84 -0
  52. package/dist/workbench-locator.js +416 -0
  53. package/dist/workbench-locator.js.map +7 -0
  54. package/package.json +39 -3
  55. package/src/asset-library.ts +104 -0
  56. package/src/client.ts +1093 -0
  57. package/src/discovery.ts +105 -0
  58. package/src/editor-document.ts +12 -49
  59. package/src/editor.ts +56 -215
  60. package/src/http-transport.ts +19 -0
  61. package/src/index.ts +5 -8
  62. package/src/installed-package.ts +15 -0
  63. package/src/lazy-proxy.ts +11 -11
  64. package/src/member-hints.ts +8 -12
  65. package/src/product-create.ts +38 -0
  66. package/src/product-locator.ts +327 -0
  67. package/src/registry-format.ts +248 -0
  68. package/src/session.ts +36 -144
  69. package/src/singleton.ts +1 -16
  70. package/src/volter-manifest.ts +97 -0
  71. package/src/workbench-locator.ts +625 -0
  72. package/dist/tools.d.ts +0 -23
  73. package/src/tools.ts +0 -45
@@ -0,0 +1,105 @@
1
+ /** Registered-session discovery; never selects another project's session. */
2
+ import { readLiveRegisteredSessions, servedProjectAnswer, sessionFetch } from './registry-format.js';
3
+ export { servedProjectAnswer } from './registry-format.js';
4
+
5
+ export const EDITOR_SESSION_DISCOVERY_TIMEOUT_MS = 2000;
6
+
7
+ export const EDITOR_PROBE_TIMEOUT_MS = 1500;
8
+
9
+ export class EditorTimeoutError extends Error {
10
+ constructor(label: string, ms: number) {
11
+ super(`${label} timed out after ${ms}ms`);
12
+ this.name = 'EditorTimeoutError';
13
+ }
14
+ }
15
+
16
+ export function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
17
+ return new Promise<T>((resolvePromise, reject) => {
18
+ const timer = setTimeout(() => reject(new EditorTimeoutError(label, ms)), ms);
19
+ promise.then(
20
+ (v) => {
21
+ clearTimeout(timer);
22
+ resolvePromise(v);
23
+ },
24
+ (err) => {
25
+ clearTimeout(timer);
26
+ reject(err instanceof Error ? err : new Error(String(err)));
27
+ },
28
+ );
29
+ });
30
+ }
31
+
32
+ export interface EditorSessionInfo {
33
+ /** The port this session's dev server is bound to. */
34
+ port: number;
35
+ /** Canonical project root path currently open, or null when none. */
36
+ project: string | null;
37
+ /** Dev-server process id, when known from the local session registry (null for an unregistered server the caller only probed by port). */
38
+ pid: number | null;
39
+ /** Exact explicitly targeted editor origin/base URL, including protocol and host. */
40
+ url?: string;
41
+ /**
42
+ * Why this server cannot DESCRIBE the `project` it is serving — its
43
+ * manifest's own parse/validation failure, naming the failing key. `null`
44
+ * when the project reads fine, and `undefined` from a probe that did not ask
45
+ * See {@link servedProjectAnswer}: a session in
46
+ * this state is still THIS project's session, and callers must report the
47
+ * reason rather than treat it as no session at all.
48
+ */
49
+ manifestError?: string | null;
50
+ }
51
+
52
+ async function fetchJson(url: string, timeoutMs: number): Promise<unknown> {
53
+ const res = await sessionFetch(url, { signal: AbortSignal.timeout(timeoutMs) });
54
+ if (!res.ok) throw new Error(`GET /__editor/project returned HTTP ${res.status}`);
55
+ return await res.json();
56
+ }
57
+
58
+ function baseUrl(session: EditorSessionInfo): string {
59
+ return session.url ?? `http://127.0.0.1:${session.port}`;
60
+ }
61
+
62
+ export interface SessionListingTransport { listSessions(timeoutMs: number): Promise<EditorSessionInfo[]>; }
63
+ export class HttpSessionDiscovery implements SessionListingTransport {
64
+ async listSessions(timeoutMs: number): Promise<EditorSessionInfo[]> {
65
+ const registered = readLiveRegisteredSessions();
66
+ const perProbeTimeout = Math.min(EDITOR_PROBE_TIMEOUT_MS, Math.max(200, timeoutMs));
67
+ const failures: string[] = [];
68
+ const probes = await Promise.all(
69
+ registered.map(async (s) => {
70
+ try {
71
+ const body = await fetchJson(
72
+ `${baseUrl({ port: s.port, project: null, pid: s.pid })}/__editor/project`,
73
+ perProbeTimeout,
74
+ );
75
+ const served = servedProjectAnswer(body);
76
+ const info: EditorSessionInfo = {
77
+ port: s.port,
78
+ project: served.path,
79
+ pid: s.pid,
80
+ manifestError: served.manifestError,
81
+ };
82
+ return info;
83
+ } catch (error) {
84
+ // Name the step that did not answer, not a guess at why: an aborted fetch here means
85
+ // the server took the request to /__editor/project and sent nothing back in time.
86
+ const timedOut = (error as { name?: string } | null)?.name === 'TimeoutError';
87
+ const detail = timedOut
88
+ ? `GET /__editor/project sent no answer within ${perProbeTimeout} ms (pid ${s.pid})`
89
+ : error instanceof Error ? `${error.name}: ${error.message}` : String(error);
90
+ failures.push(`port ${s.port}: ${detail}`);
91
+ return undefined;
92
+ }
93
+ }),
94
+ );
95
+ const sessions = probes.filter((p): p is EditorSessionInfo => p !== undefined);
96
+ // A readable registry with registered live processes is not an empty
97
+ // registry just because every HTTP discovery request failed. Keep any
98
+ // verified sessions usable, but refuse an entirely unanswered inventory
99
+ // rather than telling the caller to launch a second editor.
100
+ if (registered.length > 0 && sessions.length === 0) {
101
+ throw new Error(`Registered editor sessions could not be inspected: ${failures.join('; ')}`);
102
+ }
103
+ return sessions;
104
+ }
105
+ }
@@ -1,46 +1,15 @@
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, is recorded once
5
- * in the implementation's header
6
- * (`packages/sdk/src/kit/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.
10
- *
11
- * WHAT IT ADDRESSES is a closed VOCABULARY of named surfaces, passed as
12
- * `{ scope }` on every verb:
13
- *
14
- * `document` (default) the active centre document's whole box
15
- * `header` / `shelf` that document's toolbar strip / tool rail
16
- * `rail` the PROPERTIES view — its tabs, sections and fields
17
- * `outliner` the OUTLINER view — its rows and their controls
18
- * `content` the CONTENT view — its categories and asset rows
19
- * `menubar` the APPLICATION MENUS (Edit, View, …) and the menus they open
20
- *
21
- * await editor.document.click('[data-testid=app-menu-edit]', { scope: 'menubar' });
22
- * await editor.document.query('[role=tab]', { scope: 'rail' });
23
- * await editor.document.click('[data-testid=properties-tab-modifiers]', { scope: 'rail' });
24
- * await editor.document.click('[data-ingest-name=Cube]', { scope: 'outliner', clicks: 2 });
25
- * await editor.document.type('Wheel', { scope: 'outliner' });
26
- *
27
- * It is not page automation: a selector resolving outside the named surface is
28
- * refused by a message naming the scope and listing the others. `rail` and
29
- * `outliner`, and `content` are editor chrome and stay readable while the Game document is
30
- * active; `editor.hierarchy()` / `editor.inspect()` keep answering what those
31
- * panels RESOLVED, where this door answers what they DREW.
32
- *
33
- * A field on `LiveEditor` rather than methods on it, so the editor's `eval --list` command
34
- * shows the verbs as one named surface — the same reason `game.input`
35
- * and `game.events` are instance fields.
36
- */
1
+ /** Session binding for reading and driving the editor's named surfaces.
2
+ * Each verb accepts a document-probe scope: document, header, shelf, rail,
3
+ * outliner, utility or area. Targets outside that scope are refused. Chrome
4
+ * scopes remain available without an open document. The eval member listing
5
+ * groups these verbs under editor.document. */
37
6
 
38
7
  import type {
39
8
  DocumentProbeResult,
40
9
  DocumentProbeScope,
41
10
  DocumentProbeStep,
42
- EditorClient,
43
11
  } from '@volter/sdk';
12
+ import type { EditorClient } from './client.js';
44
13
 
45
14
  /** Which named surface a verb runs against; `document` when omitted. */
46
15
  export interface DocumentScopeOption {
@@ -128,8 +97,7 @@ export class LiveEditorDocument {
128
97
  shiftKey?: boolean;
129
98
  },
130
99
  ): Promise<DocumentProbeResult> {
131
- // An option this door does not know is refused, never dropped: a dropped `position` clicked
132
- // the element's centre and answered as if it had clicked where it was asked.
100
+ // Refuse unknown options before dispatching the gesture.
133
101
  const known = ['scope', 'index', 'clicks', 'at', 'altKey', 'ctrlKey', 'metaKey', 'shiftKey'];
134
102
  const unknown = Object.keys(options ?? {}).filter((key) => !known.includes(key));
135
103
  if (unknown.length > 0) throw new Error(`click has no option ${unknown.map((key) => `\`${key}\``).join(', ')}; its options are scope, index, clicks, at ([x, y] fractions of the element's box) and the modifier keys altKey, ctrlKey, metaKey and shiftKey.`);
@@ -164,9 +132,7 @@ export class LiveEditorDocument {
164
132
 
165
133
  /**
166
134
  * A real pointer DRAG across one matched element — press at `from`, move,
167
- * release at `to`. The gesture a direct-manipulation canvas needs; a
168
- * zero-length drag is a click at that fraction, which `click` (always the
169
- * center) cannot place.
135
+ * release at `to`. A zero-length drag presses and releases at one fraction.
170
136
  *
171
137
  * `from`, `to` and every point in `via` are `[x, y]` FRACTIONS OF THE
172
138
  * MATCHED ELEMENT'S BOX, 0..1 from its top-left — NEVER pixels and never
@@ -250,13 +216,10 @@ export class LiveEditorDocument {
250
216
  }
251
217
 
252
218
  /**
253
- * THE REPL over the open document: run `step` in the editor page against the
254
- * object the ACTIVE document published as its context (the mesh document
255
- * publishes its `MeshEditSession`, whose `ctx` is the bpy-shaped edit
256
- * context — `ctx.ops.mesh.bevel({ offset: 0.1 })`, `ctx.selection`,
257
- * `ctx.history`, `session.commit()`). Edit mode, no play. Serialized like
258
- * `game.page`: the step's own source travels, so inline every value it
259
- * needs and return plain data.
219
+ * Run `step` in the editor page against the active document's published
220
+ * context, with its document id in `info`. The document owns the context's
221
+ * API. The step's source is serialized, so inline every value it needs and
222
+ * return plain data.
260
223
  */
261
224
  async run<T = unknown>(
262
225
  step: (ctx: unknown, info: { documentId: string }) => T | Promise<T>,
package/src/editor.ts CHANGED
@@ -13,17 +13,13 @@ import type {
13
13
  StageFrameCostReading,
14
14
  EditorChromeCapture,
15
15
  EditorChromeCaptureOptions,
16
- EditorClient,
17
16
  EditorState,
18
17
  EditorView,
19
18
  EditorWorkspaceName,
20
- HistoryStep,
21
19
  InspectedFieldWrite,
22
20
  InspectedHierarchy,
23
21
  InspectedInspection,
24
22
  LabeledShotSetCapture,
25
- ModelPlayLogReading,
26
- OpenedDocument,
27
23
  PresentedEditorView,
28
24
  ShadingMode,
29
25
  StructureOp,
@@ -32,14 +28,15 @@ import type {
32
28
  ViewPreset,
33
29
  ViewportCapture,
34
30
  } from '@volter/sdk';
31
+ import type { EditorClient, HistoryStep, OpenedDocument } from './client.js';
32
+ import { LiveAssetLibrary } from './asset-library.js';
35
33
  import { LiveEditorDocument } from './editor-document.js';
36
34
 
37
- /** A panel `showPanel` can focus: the viewport tabs, the console, the build
38
- * surface, or any key the editor's own static-panel registry holds
39
- * (`hierarchy`, `assets`, `asset-library`, `inspector`, `history`, …) — which
40
- * is why this is open: the registry, not this union, is the vocabulary. */
35
+ /** A panel `showPanel` can focus: the console, the build surface, or any key
36
+ * the editor's own static-panel registry holds (`hierarchy`, `asset-library`,
37
+ * `inspector`, `history`, …) — which is why this is open: the registry, not
38
+ * this union, is the vocabulary. */
41
39
  export type PanelName =
42
- | 'viewport-edit'
43
40
  | 'console'
44
41
  // Keeps the literals above in autocomplete while admitting every key the
45
42
  // editor's registry holds — the registry answers, this union only hints.
@@ -65,15 +62,8 @@ const EXTENSION_KIND: Record<string, AssetKind> = {
65
62
  '.glsl': 'source',
66
63
  '.vert': 'source',
67
64
  '.frag': 'source',
68
- // PROJECT SCRIPTS ARE SOURCE. Without these the guess below falls through to
69
- // `'json'`, the asset-document router sends the file to the generic JSON
70
- // viewer (`asset-documents.tsx#assetDocumentViewerRoute`: `spec.kind ===
71
- // 'json'` is decided before any content routing), and the LIVE MODELING
72
- // DOCUMENT never mounts — `editor.openAsset('src/lib/fox/fox.model.ts')`
73
- // silently shows a text pane instead of the model. Only `kind: 'source'`
74
- // reaches `SourceAssetViewer`, which is what content-routes a project script
75
- // to `LiveModuleDocument`. The set matches that viewer's own
76
- // `isProjectScriptPath` regex, `/\.(?:[cm]?[jt]sx?)$/`.
65
+ // Source kinds select SourceAssetViewer, which routes project scripts by content to
66
+ // LiveModuleDocument. Keep these extensions aligned with its isProjectScriptPath predicate.
77
67
  '.ts': 'source',
78
68
  '.tsx': 'source',
79
69
  '.mts': 'source',
@@ -86,14 +76,11 @@ const EXTENSION_KIND: Record<string, AssetKind> = {
86
76
 
87
77
  /**
88
78
  * Lightweight extension-based `AssetKind` guess for `openAsset`'s optional
89
- * `kind` argument. Deliberately independent of (not shared with) the
90
- * editor's own `AssetBrowser.tsx#getAssetKind` — that function is a private,
91
- * React-component-local helper of a package `@volter/live` has no dependency
92
- * on. Callers with an unusual extension can always pass `kind` explicitly.
79
+ * `kind` argument. Callers with an unusual extension can always pass `kind`
80
+ * explicitly.
93
81
  */
94
82
  export function inferAssetKind(path: string): AssetKind {
95
- // `.prefab.json` is retired (PRs #578/#581/#589) but the `'prefab'`
96
- // AssetKind itself remains in `@volter/sdk` for view-link compatibility.
83
+ // `'prefab'` is an `@volter/sdk` AssetKind that view links name.
97
84
  if (path.endsWith('.prefab.json')) return 'prefab';
98
85
  const dot = path.lastIndexOf('.');
99
86
  const ext = dot >= 0 ? path.slice(dot).toLowerCase() : '';
@@ -101,69 +88,37 @@ export function inferAssetKind(path: string): AssetKind {
101
88
  }
102
89
 
103
90
  export class LiveEditor {
104
- /** `#`-private, not `private`: `cyclotron eval --list` enumerates this object's
91
+ /** `#`-private, not `private`: `<command> eval --list` enumerates this object's
105
92
  * real runtime members, and TypeScript's erased `private` would leave the
106
93
  * raw `EditorClient` advertised beside them. */
107
94
  readonly #client: EditorClient;
108
95
 
109
- /**
110
- * The ACTIVE center document's own DOM: read it, click it, key it, paste
111
- * into it. The one door onto editor chrome that is not play-mode gated, and
112
- * deliberately scoped to that document alone —
113
- * `packages/sdk/src/kit/editor-document-probe.ts` carries the design and the
114
- * refusal contract. Screenshotting the same subject is
115
- * {@link LiveEditor.captureActiveDocument}, not a fifth verb here.
116
- */
96
+ /** Read and drive DOM within a named scope: the active document by default, or editor chrome
97
+ * such as header, shelf, rail, outliner, utility and area. Targets outside the scope are refused.
98
+ * See `@volter/sdk`'s editor-document-probe for scope and gesture contracts. */
117
99
  readonly document: LiveEditorDocument;
118
100
 
101
+ /** The asset library: search a source, list an asset's files, download one into the project with its license
102
+ * recorded (`asset-library.ts`). */
103
+ readonly assetLibrary: LiveAssetLibrary;
104
+
119
105
  constructor(client: EditorClient) {
120
106
  this.#client = client;
121
107
  this.document = new LiveEditorDocument(client);
108
+ this.assetLibrary = new LiveAssetLibrary(client);
122
109
  }
123
110
 
124
- /**
125
- * THE BLENDER LANE'S VERBS, from `cyclotron eval`.
126
- *
127
- * Blender runs headless in the editor tab's worker (ARCHITECTURE-CORE, "THE
128
- * BLENDER IN THE TAB IS BLENDER") and answers `blender-start`,
129
- * `blender-execute`, `blender-scene-info`, `blender-object-info`,
130
- * `blender-screenshot-view`, `blender-read-file`, `blender-write-file`,
131
- * `blender-list-files`, `blender-stop` and `blender-status`. They were
132
- * reachable from `@volter/sdk` and through `volter blender-mcp` but from
133
- * no GENERAL door, so driving a session meant writing an MCP client script
134
- * per question — the same discovery failure `eval-surface.ts`'s header
135
- * records, in a lane that had not noticed it yet.
136
- *
137
- * cyclotron eval "await editor.blender('blender-execute', { code: 'import bpy; print(len(bpy.data.objects))' })"
138
- *
139
- * `blender-status` is the only verb that creates nothing: it answers whether
140
- * this tab already has a session without starting one.
141
- */
142
- async blender<T extends object = Record<string, unknown>>(
143
- type: `blender-${string}`,
111
+ /** A verb a composed package contributes (`<package>-<verb>`), waited on for its own row's budget. */
112
+ async contributedVerb<T extends object = Record<string, unknown>>(
113
+ type: string,
144
114
  fields: Record<string, unknown> = {},
145
115
  ): Promise<T> {
146
- return this.#client.blender<T>(type, fields);
147
- }
148
-
149
- /**
150
- * MODEL PLAY'S LOG, from `cyclotron eval` — the same read as
151
- * `cyclotron play-log`. A play script writes it with
152
- * `play.log(kind, facts)`; the runner adds `play-start`, `script-reload`,
153
- * `script-error` and `play-stop`. Every entry carries `simT` (simulation
154
- * seconds) and `tick` (the frame). Each model document keeps its own: the
155
- * current run, or the last one after Stop; Play from a fresh copy empties it.
156
- * `documentId` picks one (`documents` lists them); the active Play's by default.
157
- *
158
- * cyclotron eval "(await editor.modelPlayLog({ kind: 'death' })).entries"
159
- */
160
- async modelPlayLog(query?: { readonly documentId?: string; readonly since?: number; readonly kind?: string }): Promise<ModelPlayLogReading> {
161
- return this.#client.modelPlayLog(query);
116
+ return this.#client.contributedVerb<T>(type, fields);
162
117
  }
163
118
 
164
119
  /**
165
120
  * The active authoring adapter's persistence destination — where a save would
166
- * land (`status().savePath`). A read only: a three root has no scene document
121
+ * land (`status().savePath`). A read only: a stage root has no scene document
167
122
  * to open, and its root is activated instead.
168
123
  */
169
124
  async scene(): Promise<string | null> {
@@ -185,19 +140,10 @@ export class LiveEditor {
185
140
  return this.#client.currentView();
186
141
  }
187
142
 
188
- /**
189
- * Capture the same center document the human is currently looking at.
190
- *
191
- * A number is a SQUARE of that size — the default, and the right shape for
192
- * an unstaged look at a model. `{width, height}` asks for a shaped frame, so
193
- * a video-aspect look needs no crop afterwards. Both are bounded by the
194
- * relay budget (64-1024 per side, total no larger than a 1024 square); see
195
- * `@volter/sdk`'s `CaptureDimensions`.
196
- * Supply a view to present and photograph it in one editor request.
197
- */
198
- /** The active document's RENDER alone, at the size asked for: what the document draws,
199
- * without what the viewport draws over it (its navigation gizmo, its readouts). To see a
200
- * stage as the person does, use `captureEditorChrome({ region: 'document' })`. */
143
+ /** Capture the active document's render without viewport navigation gizmos or readouts.
144
+ * A number requests a square; `{width, height}` requests a shaped frame, bounded by
145
+ * `CaptureDimensions`. Supply a view to present and capture it in one request.
146
+ * Use `captureEditorChrome({ region: 'document' })` to include viewport chrome. */
201
147
  async captureActiveDocument(
202
148
  size?: CaptureDimensions,
203
149
  view?: EditorView,
@@ -252,7 +198,7 @@ export class LiveEditor {
252
198
  */
253
199
  async frame(entityId: string): Promise<void>;
254
200
  /**
255
- * Bare `frame()` frames the OPEN Object3D document's subject instead — its
201
+ * Bare `frame()` frames the OPEN asset document's subject instead — its
256
202
  * selection if it has one, else the whole model: the toolbar's own Frame
257
203
  * button, reachable from a script. `fit` scales the fitted distance (1 is
258
204
  * that button's tight fit, 1.5 stands back a little for a shot).
@@ -269,7 +215,7 @@ export class LiveEditor {
269
215
  /**
270
216
  * WATCH THE AGENT LOOK AROUND THE MODEL.
271
217
  *
272
- * Swings the open Object3D document's camera — the camera the human's tab is
218
+ * Swings the open asset document's camera — the camera the human's tab is
273
219
  * showing — around the framed subject by `azimuth`/`elevation` RADIANS,
274
220
  * animated over `duration` seconds (default 0.6), and resolves when the move
275
221
  * ends. This is deliberately not a jump cut: the point of the verb is that a
@@ -292,12 +238,7 @@ export class LiveEditor {
292
238
  /** SECONDS the move takes (default 0.6). */
293
239
  readonly duration?: number;
294
240
  }): Promise<DocumentLookOutcome> {
295
- // AN UNKNOWN KEY IS REFUSED BY NAME. Every member here is optional, so a
296
- // misspelling — `yaw` for `azimuth`, `pitch` for `elevation` — used to
297
- // orbit by nothing at all while the outcome still reported a plausible
298
- // ABSOLUTE azimuth, which reads exactly like a move that happened
299
- // (measured 2026-09-21, and it cost a round). The keys and their unit are
300
- // in the refusal because that is the moment the caller needs them.
241
+ // Refuse unknown keys by name and include the supported keys and units.
301
242
  const known = ['azimuth', 'elevation', 'duration'];
302
243
  const unknown = Object.keys(options ?? {}).filter((key) => !known.includes(key));
303
244
  if (unknown.length > 0)
@@ -336,23 +277,9 @@ export class LiveEditor {
336
277
  await this.#client.viewPreset(preset);
337
278
  }
338
279
 
339
- /**
340
- * Switch the editor's NAMED WORKSPACE — `await editor.workspace('model')`.
341
- *
342
- * A workspace is a task-named LAYOUT MEMORY over the one dock
343
- * (ARCHITECTURE-CORE §Editor chrome): `game` (the default, the editor's
344
- * standing arrangement), `model`, `sculpt`, `texture`, `animate`, `look`.
345
- * Switching is an EXPLICIT act — nothing in the editor moves chrome on its
346
- * own, opening a document included — and this is the session door to it,
347
- * beside `Window → Workspace` and the registered actions.
348
- *
349
- * Resolves once the dock has finished rebuilding, so a capture taken
350
- * immediately after photographs the arrangement that was asked for. Each
351
- * workspace remembers the user's own hand-tuning per project, so switching
352
- * away and back is lossless.
353
- */
354
- async workspace(id: EditorWorkspaceName): Promise<void> {
355
- await this.#client.setWorkspace(id);
280
+
281
+ async workspace(id: EditorWorkspaceName): Promise<{ readonly workspace: EditorWorkspaceName; readonly applied: boolean }> {
282
+ return this.#client.setWorkspace(id);
356
283
  }
357
284
 
358
285
  /**
@@ -367,11 +294,9 @@ export class LiveEditor {
367
294
 
368
295
  /**
369
296
  * A document stage's viewport PRESENTATION — its draw mode, lighting, backdrop and overlays
370
- * (`@volter/sdk/kit/viewport-presentation`) — resolved. With `layer`, that choice is
297
+ * (`@volter/sdk/viewport-presentation`) — resolved. With `layer`, that choice is
371
298
  * recorded for the view first, as a person's toolbar change would be, e.g.
372
299
  * `presentation('model:src/models/cube.blend', { all: { lighting: { studioPreset: 'kit' } } })`.
373
- * With a STRING, the named view of that id (`*.view.ts`, e.g. `'godot'`) is put on the view
374
- * whole; the answer's `viewPresets` lists the ones registered.
375
300
  */
376
301
  async presentation(
377
302
  documentId: string,
@@ -380,28 +305,11 @@ export class LiveEditor {
380
305
  return this.#client.viewportPresentation(documentId, layer);
381
306
  }
382
307
 
383
- /**
384
- * Set the MATERIAL apart from the bundle that usually carries it.
385
- * Appearance is palette × material, independent axes by ruling, so
386
- * `style()` alone can never say whether a cost belongs to the blur or to
387
- * the palette. This is the door that measures them apart; it answers with
388
- * what the chrome wears afterwards (`style` is `null` when the mix matches
389
- * no registered bundle).
390
- */
391
- async appearance(appearance: {
392
- readonly material?: string;
393
- }): Promise<{ material: string; style: string | null }> {
394
- return this.#client.setAppearance(appearance);
395
- }
396
-
397
- /** Focus an editor panel: a viewport tab, the console, the build surface, or
308
+ /** Focus an editor panel: the console, the build surface, or
398
309
  * any key the editor's static-panel registry holds — an unknown key refuses
399
310
  * naming the ones it does. */
400
311
  async showPanel(name: PanelName): Promise<void> {
401
312
  switch (name) {
402
- case 'viewport-edit':
403
- await this.#client.showViewport('edit');
404
- return;
405
313
  case 'console':
406
314
  await this.#client.toggleConsole();
407
315
  return;
@@ -415,24 +323,18 @@ export class LiveEditor {
415
323
 
416
324
  /** `kind` inferred from `path`'s extension when omitted (`inferAssetKind`) — pass it explicitly to override. */
417
325
  async openAsset(path: string, kind?: AssetKind): Promise<void> {
418
- // A `.blend` is not an asset but a Model document the project's finder lists; opened as an
419
- // asset it fell to the extension guess's `json` and showed its bytes.
420
- if (path.toLowerCase().endsWith('.blend'))
421
- throw new Error(
422
- `${path} is a Blender Model document, not an asset: open it with editor.open("model:${path}").`,
423
- );
326
+ // Finder-listed documents use their document opener rather than asset-kind inference.
327
+ const document = (await this.#client.documentTable()).entries.find((entry) => entry.source?.path === path);
328
+ if (document)
329
+ throw new Error(`${path} is a ${document.kind} document, not an asset: open it with editor.open("${document.id}").`);
424
330
  await this.#client.openAsset(path, kind ?? inferAssetKind(path));
425
331
  }
426
332
 
427
333
  /**
428
334
  * SELECT a project asset — the browser's single click, which fills the
429
335
  * Inspector without opening a document. `openAsset` is the double click.
430
- *
431
- * This is how a project's own `asset.inspector` section is reached: select
432
- * the file it matches, then `inspect()` lists the verbs that section
433
- * declares and `runAction(id)` runs one. Selecting a path nothing matches
434
- * is not an error — the Inspector shows what it has, exactly as it does
435
- * for a human.
336
+ * Selecting a path nothing matches is not an error — the Inspector shows
337
+ * what it has, exactly as it does for a human.
436
338
  */
437
339
  async selectAsset(path: string): Promise<void> {
438
340
  await this.#client.selectAsset(path);
@@ -446,8 +348,8 @@ export class LiveEditor {
446
348
  * where it stands under the scene's own lighting, reachable from here — or
447
349
  * RAW GLB BYTES (`assetPreview({ glbBase64 }, …)`), for a model that exists
448
350
  * only in the calling Node process's memory and has never been written to
449
- * disk. `stage` defaults to `'lab'`, the neutral Asset Lab staging this has
450
- * always produced, and the bytes form is lab-only.
351
+ * disk. `stage` defaults to `'lab'`, the neutral Asset Lab staging, and the
352
+ * bytes form is lab-only.
451
353
  */
452
354
  async assetPreview(
453
355
  source: string | AssetPreviewSource,
@@ -465,8 +367,6 @@ export class LiveEditor {
465
367
  * crops, rendered against the asset's own skeleton, with a contact sheet.
466
368
  * Every source {@link assetPreview} takes works here, GLB bytes included:
467
369
  * a shot set stages its own subject, so it needs no place to stand.
468
- *
469
- * Sole in-repo caller today: `project.bake.preview`'s `--orbit` lane.
470
370
  */
471
371
  async assetPreviewShots(
472
372
  source: string | AssetPreviewSource,
@@ -496,33 +396,7 @@ export class LiveEditor {
496
396
  await this.#client.setShadingMode(mode);
497
397
  }
498
398
 
499
- /**
500
- * READ the inspector, as data — the serialized inspection subject
501
- * (`editor.inspect()`; design: `docs/ARCHITECTURE-CORE.md` §Editor chrome,
502
- * "The Inspection Model"). This is the Figma-Inspect analog: whatever a
503
- * human would see in the inspector right now — the subject's identity, its
504
- * verbs, and every identified section in display order, with a `fields`
505
- * section's CURRENT VALUES at their scriptable `path`s.
506
- *
507
- * Reach for it whenever the next step depends on what an object actually
508
- * IS: `await editor.select(id)` then `await editor.inspect()` answers "what
509
- * properties does this thing have, and what are they set to" in one call,
510
- * against the same model the panel renders — no scene-graph reads, no
511
- * guessing at property names.
512
- *
513
- * When the inspector is showing NOTHING — nothing selected on a surface
514
- * with no empty-state subject of its own, which is most of them — the answer
515
- * is `{none: true}`, so "the human sees no inspector" and "the read failed"
516
- * are never the same value. A surface whose empty space IS a real thing (an
517
- * open Asset Lab document) still answers with that subject, and never with
518
- * another surface's.
519
- *
520
- * A `custom` section body is a named opaque: the editor renders it with
521
- * React, so the wire reports its identity rather than pretending to describe
522
- * its rendering — plus, when the section can say what it DISPLAYS, a `data`
523
- * payload in its own vocabulary (`transform` carries
524
- * `{position, rotation, scale}`, rotation in Euler XYZ degrees).
525
- */
399
+
526
400
  async inspect(): Promise<InspectedInspection> {
527
401
  return this.#client.inspect();
528
402
  }
@@ -534,32 +408,11 @@ export class LiveEditor {
534
408
  }
535
409
 
536
410
  /**
537
- * Run ONE command by id — the door to everything the command palette lists.
538
- *
539
- * ONE NAME (orchestrator ruling 2026-09-19). There were briefly TWO doors
540
- * onto the one view-verb table — this one and `editor.viewVerb(view, verb)`,
541
- * which addressed the same registry by its two halves. A second addressing
542
- * of one table is a second name for one thing, and an agent reading
543
- * `--list` had to choose between them with nothing to choose on. This door
544
- * stays because it is strictly wider: it addresses a COMMAND ID, so under
545
- * the frame it reaches everything the workbench knows — a `volter.action.<id>`
546
- * editor action, one of VS Code's own — and not only a view. A VIEW is
547
- * reached by spelling its verb's command id:
411
+ * Run a command by id. Under the native frame this uses ICommandService and
412
+ * can invoke editor or VS Code commands. Standalone sessions resolve registered
413
+ * view verbs of the form `volter.<view>.<verb>` and refuse other ids.
548
414
  *
549
- * await editor.command('volter.blender-uv-view.state')
550
- * await editor.command('volter.blender-uv-view.zoom', { to: 600 })
551
- *
552
- * await editor.command('volter.blender-node-view.view-all')
553
- * await editor.command('volter.blender-node-view.look', { node: 'Principled BSDF' })
554
- *
555
- * Under the Code-OSS frame this is the workbench's own command service, so
556
- * any command id works — ours and VS Code's alike. Standalone the editor's `edit` command
557
- * has no command service and answers the `volter.<view>.<verb>` shape off the
558
- * SAME verb table the frame's commands call, refusing any other id by name.
559
- * One table, two doors, exactly like the keymap's.
560
- *
561
- * Answers with whatever the command returned — a view verb's own state, or
562
- * `null` for a command that returns nothing.
415
+ * Returns the command’s result, or `null` when it returns nothing.
563
416
  */
564
417
  async command(commandId: string, args?: unknown): Promise<unknown> {
565
418
  return this.#client.runCommand(commandId, args);
@@ -575,11 +428,6 @@ export class LiveEditor {
575
428
  * helpers the menu items call, so there is one implementation of each op and
576
429
  * not a second that can disagree with what a human gets.
577
430
  *
578
- * It exists because the menu is a POINTER surface: every one of these ops was
579
- * reachable only by right-clicking a hierarchy row, which is nothing an agent
580
- * can do — so for an ingest root, whose only authoring surface IS the editor,
581
- * structure was closed entirely.
582
- *
583
431
  * `id`/`ids` default to the current selection. The answer carries the same
584
432
  * per-edit `write` ack `setField` does, so `write.persisted` tells a saved
585
433
  * restructure from a live-only one. An op the active adapter does not provide
@@ -618,23 +466,14 @@ export class LiveEditor {
618
466
  *
619
467
  * The companion to {@link inspect}: that one answers "what IS the selected
620
468
  * thing", this one answers "what does the tree LOOK LIKE". It is the panel's
621
- * own output, not a fresh walk of the scene — the adapter's tree after the
622
- * component marks fold implementation subtrees (bones, particle renderers,
623
- * instanced pools), after the internals reveal, the document promotion, the
624
- * child cap, the collapse state, the search filter and the selection scope.
625
- *
626
- * Works in play mode and edit mode; the answer says which (`playState`,
627
- * `activeViewportTab`), because the two are different adapters and a tree
628
- * that looks wrong is very often the wrong adapter's tree.
469
+ * own projection of component marks, document promotion, child caps,
470
+ * collapse state, search and selection scope onto the adapter tree.
629
471
  *
630
472
  * Prefer this over `status().entities`, which is deliberately a different
631
473
  * question — the RAW adapter tree, unprojected. A panel that renders the
632
474
  * wrong rows looks perfectly healthy in that facet.
633
475
  *
634
- * Each row carries `childCount` (what its caret opens), `internalChildCount`
635
- * (what is folded behind "Reveal Internals") and `expandable` (whether the
636
- * panel draws a caret at all), so "this subtree exists but nothing in the UI
637
- * opens it" is a fact you can read rather than one you have to notice.
476
+ * Each row reports the projected child count and whether the panel draws a disclosure control.
638
477
  *
639
478
  * Rejects, naming the panel, when no hierarchy panel is mounted — an empty
640
479
  * tree would be a fabricated answer about a surface nobody is being shown.
@@ -688,7 +527,9 @@ export class LiveEditor {
688
527
  return this.#client.removeInspectionField(path);
689
528
  }
690
529
 
691
- /** Open a document by its adapter-declared id, through its registered owner. */
530
+ /** Open a document by its adapter-declared id, through its registered owner. An id the adapter would list for a
531
+ * file not written yet (a new model, `model:prefabs/coin/coin.blend`) opens as that new document, written by its
532
+ * first save. */
692
533
  async open(id: string): Promise<OpenedDocument> {
693
534
  return this.#client.open(id);
694
535
  }