@volter/live 0.5.210 → 0.5.212

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 (67) hide show
  1. package/BUNDLED_NOTICES +13 -13
  2. package/README.md +7 -11
  3. package/dist/chunk-7SZ3C4CC.js +768 -0
  4. package/dist/chunk-7SZ3C4CC.js.map +7 -0
  5. package/dist/chunk-7WKVJGHZ.js +15 -0
  6. package/dist/chunk-7WKVJGHZ.js.map +7 -0
  7. package/dist/chunk-SDIWRX5X.js +189 -0
  8. package/dist/chunk-SDIWRX5X.js.map +7 -0
  9. package/dist/chunk-USXOIBVX.js +81 -0
  10. package/dist/chunk-USXOIBVX.js.map +7 -0
  11. package/dist/chunk-Y5W77HYT.js +123 -0
  12. package/dist/chunk-Y5W77HYT.js.map +7 -0
  13. package/dist/chunk-YPAOTPIF.js +202 -0
  14. package/dist/chunk-YPAOTPIF.js.map +7 -0
  15. package/dist/client.d.ts +370 -0
  16. package/dist/client.js +9 -0
  17. package/dist/client.js.map +7 -0
  18. package/dist/discovery.d.ts +32 -0
  19. package/dist/discovery.js +19 -0
  20. package/dist/discovery.js.map +7 -0
  21. package/dist/editor-document.d.ts +12 -47
  22. package/dist/editor.d.ts +34 -181
  23. package/dist/http-transport.d.ts +6 -0
  24. package/dist/index.d.ts +1 -5
  25. package/dist/index.js +76 -1722
  26. package/dist/index.js.map +4 -4
  27. package/dist/installed-package.d.ts +6 -0
  28. package/dist/installed-package.js +7 -0
  29. package/dist/installed-package.js.map +7 -0
  30. package/dist/lazy-proxy.d.ts +11 -11
  31. package/dist/member-hints.d.ts +6 -9
  32. package/dist/product-create.d.ts +29 -0
  33. package/dist/product-create.js +13 -0
  34. package/dist/product-create.js.map +7 -0
  35. package/dist/product-locator.d.ts +100 -0
  36. package/dist/product-locator.js +25 -0
  37. package/dist/product-locator.js.map +7 -0
  38. package/dist/registry-format.d.ts +44 -0
  39. package/dist/registry-format.js +21 -0
  40. package/dist/registry-format.js.map +7 -0
  41. package/dist/session.d.ts +10 -3
  42. package/dist/singleton.d.ts +1 -13
  43. package/dist/volter-manifest.d.ts +55 -0
  44. package/dist/volter-manifest.js +9 -0
  45. package/dist/volter-manifest.js.map +7 -0
  46. package/dist/workbench-locator.d.ts +84 -0
  47. package/dist/workbench-locator.js +416 -0
  48. package/dist/workbench-locator.js.map +7 -0
  49. package/package.json +35 -3
  50. package/src/client.ts +1052 -0
  51. package/src/discovery.ts +105 -0
  52. package/src/editor-document.ts +12 -49
  53. package/src/editor.ts +45 -212
  54. package/src/http-transport.ts +19 -0
  55. package/src/index.ts +3 -7
  56. package/src/installed-package.ts +15 -0
  57. package/src/lazy-proxy.ts +11 -11
  58. package/src/member-hints.ts +8 -12
  59. package/src/product-create.ts +38 -0
  60. package/src/product-locator.ts +327 -0
  61. package/src/registry-format.ts +185 -0
  62. package/src/session.ts +38 -66
  63. package/src/singleton.ts +1 -16
  64. package/src/volter-manifest.ts +97 -0
  65. package/src/workbench-locator.ts +625 -0
  66. package/dist/tools.d.ts +0 -23
  67. package/src/tools.ts +0 -45
@@ -0,0 +1,768 @@
1
+ // src/http-transport.ts
2
+ import { Agent, fetch as nodeFetch } from "undici";
3
+ function createDispatcher(timeoutMs) {
4
+ return new Agent({ headersTimeout: timeoutMs, bodyTimeout: timeoutMs });
5
+ }
6
+ async function dispatchFetch(url, init, dispatcher) {
7
+ if (!dispatcher) return fetch(url, init);
8
+ return await nodeFetch(url, {
9
+ ...init,
10
+ dispatcher
11
+ });
12
+ }
13
+
14
+ // ../sdk/src/commands.ts
15
+ function verbCommandId(type) {
16
+ return `volter.${type}`;
17
+ }
18
+
19
+ // src/client.ts
20
+ var DEFAULT_URL = "http://127.0.0.1:20173";
21
+ var EditorCommandError = class extends Error {
22
+ code;
23
+ constructor(message, code) {
24
+ super(message);
25
+ this.name = "EditorCommandError";
26
+ this.code = code;
27
+ }
28
+ };
29
+ var COMMAND_DEADLINE_MS = 15e4;
30
+ var UNDICI_DEFAULT_HEADERS_TIMEOUT_MS = 3e5;
31
+ var CONSOLE_DRAIN_TIMEOUT_MS = 1500;
32
+ function describeFetchFailure(error) {
33
+ const messages = [];
34
+ let current = error;
35
+ for (let depth = 0; depth < 8; depth++) {
36
+ if (!(current instanceof Error)) break;
37
+ if (current.message) messages.push(current.message);
38
+ const code = current.code;
39
+ if (typeof code === "string" && code !== "") {
40
+ return { code, detail: messages.join(" <- ") };
41
+ }
42
+ const aggregate = current.errors;
43
+ if (Array.isArray(aggregate) && aggregate.length > 0) {
44
+ const inner = describeFetchFailure(aggregate[0]);
45
+ return { code: inner.code, detail: [...messages, inner.detail].join(" <- ") };
46
+ }
47
+ current = current.cause;
48
+ }
49
+ return { code: "UNKNOWN", detail: messages.join(" <- ") || String(error) };
50
+ }
51
+ var RETRYABLE_TRANSPORT_CODES = /* @__PURE__ */ new Set([
52
+ "ECONNREFUSED",
53
+ "ECONNRESET",
54
+ "EPIPE",
55
+ "ETIMEDOUT",
56
+ "EHOSTUNREACH",
57
+ "UND_ERR_SOCKET",
58
+ "UND_ERR_CONNECT_TIMEOUT"
59
+ ]);
60
+ var TRANSPORT_RETRY_DELAY_MS = 400;
61
+ var EditorClient = class {
62
+ baseUrl;
63
+ fetchOverride;
64
+ transport;
65
+ constructor(opts) {
66
+ if (opts !== void 0 && (typeof opts !== "object" || opts === null || Array.isArray(opts))) {
67
+ throw new TypeError(
68
+ 'EditorClient options must be an object. Use new EditorClient({ url: "http://127.0.0.1:20173" }), not new EditorClient("...").'
69
+ );
70
+ }
71
+ const unknownOptions = Object.keys(opts ?? {}).filter(
72
+ (key) => key !== "url" && key !== "transport" && key !== "fetch"
73
+ );
74
+ if (unknownOptions.length > 0) {
75
+ throw new Error(
76
+ `EditorClient: unknown option${unknownOptions.length === 1 ? "" : "s"} ${unknownOptions.map((key) => `"${key}"`).join(", ")}. Use { url: "http://127.0.0.1:<port>" } to target an editor.`
77
+ );
78
+ }
79
+ this.baseUrl = (opts?.url ?? DEFAULT_URL).replace(/\/$/, "");
80
+ this.fetchOverride = opts?.fetch;
81
+ this.transport = opts?.transport ?? null;
82
+ }
83
+ /**
84
+ * `retryTransport` opts a command into ONE automatic retry after a transport
85
+ * failure (see {@link RETRYABLE_TRANSPORT_CODES}). It is deliberately
86
+ * OPT-IN and off by default: a socket that died after the request was written
87
+ * cannot prove the editor did not already run the command, so a blanket retry
88
+ * would risk playing/stopping/writing twice. Read-only relays — the captures —
89
+ * have no such hazard and turn it on.
90
+ */
91
+ async command(body, options) {
92
+ const type = String(body["type"] ?? "command");
93
+ const deadlineMs = options?.deadlineMs ?? COMMAND_DEADLINE_MS;
94
+ if (this.transport) {
95
+ const answered = await this.transport(body);
96
+ if (!answered.ok) {
97
+ throw new EditorCommandError(
98
+ answered.error ?? `Editor command "${type}" failed`,
99
+ answered.code
100
+ );
101
+ }
102
+ return answered;
103
+ }
104
+ const url = `${this.baseUrl}/__editor/command`;
105
+ const request = JSON.stringify(body);
106
+ let res;
107
+ let retried = false;
108
+ let commandDispatcher;
109
+ for (; ; ) {
110
+ try {
111
+ const init = {
112
+ method: "POST",
113
+ headers: { "Content-Type": "application/json" },
114
+ body: request,
115
+ signal: AbortSignal.timeout(deadlineMs)
116
+ };
117
+ commandDispatcher = deadlineMs > UNDICI_DEFAULT_HEADERS_TIMEOUT_MS ? createDispatcher(deadlineMs + 3e4) : void 0;
118
+ res = this.fetchOverride ? await this.fetchOverride(url, init) : await dispatchFetch(url, init, commandDispatcher);
119
+ break;
120
+ } catch (error) {
121
+ await commandDispatcher?.destroy();
122
+ commandDispatcher = void 0;
123
+ if (error?.name === "TimeoutError") {
124
+ throw new EditorCommandError(
125
+ `The editor at ${this.baseUrl} never answered "${type}" within ${Math.round(deadlineMs / 1e3)}s \u2014 past every server-side budget, so the server itself is not answering. Check the terminal that started this editor session.`
126
+ );
127
+ }
128
+ const { code, detail } = describeFetchFailure(error);
129
+ if (options?.retryTransport === true && !retried && RETRYABLE_TRANSPORT_CODES.has(code)) {
130
+ retried = true;
131
+ await new Promise((resolve) => setTimeout(resolve, TRANSPORT_RETRY_DELAY_MS));
132
+ continue;
133
+ }
134
+ throw new EditorCommandError(
135
+ `POST ${url} ("${type}") never reached the editor: ${code}${detail ? ` (${detail})` : ""}.` + (retried ? ` Retried once after ${TRANSPORT_RETRY_DELAY_MS}ms; it failed the same way.` : "") + (options?.retryTransport === true ? "" : " Not retried automatically: this command can change editor state, and a socket that died after the request was written cannot prove the editor did not already run it.") + " A transport failure means the port stopped answering, not that the editor refused \u2014 the dev server restarts on any watched source edit, and it shuts itself down after an idle window. Check the terminal that started this editor session and confirm the port this client resolved.",
136
+ code
137
+ );
138
+ }
139
+ }
140
+ let data;
141
+ try {
142
+ data = await this.readJson(res);
143
+ } finally {
144
+ await commandDispatcher?.destroy();
145
+ }
146
+ if (!data.ok) {
147
+ throw new EditorCommandError(
148
+ data.error ?? `Editor command failed: ${res.status}`,
149
+ data.code
150
+ );
151
+ }
152
+ return data;
153
+ }
154
+ /** Refuse shutdown if a document cannot be saved. Does not stop the server. */
155
+ async prepareClose() {
156
+ await this.command({ type: "session-prepare-close" });
157
+ }
158
+ // --- Selection ---
159
+ async select(id) {
160
+ await this.command({ type: "select", id });
161
+ }
162
+ async selectMultiple(ids) {
163
+ await this.command({ type: "select-multiple", ids });
164
+ }
165
+ async selectAll() {
166
+ await this.command({ type: "select-all" });
167
+ }
168
+ // --- Viewport ---
169
+ async focusEntity(id) {
170
+ await this.command({ type: "focus-entity", id });
171
+ }
172
+ async focusSelection() {
173
+ await this.command({ type: "focus-selection" });
174
+ }
175
+ /**
176
+ * Frame the EDIT viewport camera on one entity — the strict sibling of
177
+ * {@link focusEntity}. Same framing; an id the scene does not know is a
178
+ * refusal naming the id (`EditorCommandError`, code `ENTITY_NOT_FOUND`)
179
+ * rather than `focusEntity`'s silent no-op, so a caller that frames an
180
+ * entity before capturing it cannot photograph the wrong thing.
181
+ */
182
+ async frameEntity(id) {
183
+ await this.command({ type: "frame-entity", id });
184
+ }
185
+ async viewPreset(preset) {
186
+ await this.command({ type: "view-preset", preset });
187
+ }
188
+ /**
189
+ * LOOK AROUND THE OPEN MODEL, visibly. Swings the active asset
190
+ * document's camera — the one on the human's screen — by `azimuth`/
191
+ * `elevation` radians, animated over `duration` seconds, and resolves when
192
+ * the move ends. A human drag during the move cancels it where it stands
193
+ * (`cancelledBy: 'human'`); the promise still resolves.
194
+ */
195
+ async orbitDocument(options) {
196
+ return this.command({ type: "document-orbit", ...options });
197
+ }
198
+ /** What a frame of a 3D document's stage costs, uncapped (`document-frame-cost`). */
199
+ async frameCostDocument(options) {
200
+ return this.command({ type: "document-frame-cost", ...options });
201
+ }
202
+ /** A slow full revolution around the open document's subject, at a constant rate. */
203
+ async turntableDocument(options) {
204
+ return this.command({ type: "document-turntable", ...options });
205
+ }
206
+ /**
207
+ * Frame the open document's subject (its selection if it has one). `fit`
208
+ * scales the fitted distance: 1 is the toolbar Frame button's tight fit.
209
+ */
210
+ async frameDocument(fit) {
211
+ return this.command({
212
+ type: "document-frame",
213
+ ...fit === void 0 ? {} : { fit }
214
+ });
215
+ }
216
+ async setCamera(position, target, fov) {
217
+ await this.command({
218
+ type: "set-camera",
219
+ position,
220
+ target,
221
+ ...fov === void 0 ? {} : { fov }
222
+ });
223
+ }
224
+ async captureViewport(size) {
225
+ const data = await this.command({
226
+ type: "capture-viewport",
227
+ ...size === void 0 ? {} : { size }
228
+ });
229
+ return { base64: data.base64, mimeType: data.mimeType };
230
+ }
231
+ /**
232
+ * Capture an isolated, deterministic four-view preview through the editor's
233
+ * native Asset Lab. The SDK delegates rendering to the editor; it never
234
+ * loads, clones, or interprets 3D assets itself.
235
+ */
236
+ async captureAssetPreview(source, options = {}) {
237
+ const data = await this.command(
238
+ {
239
+ type: "capture-asset-preview",
240
+ ...source,
241
+ ...options
242
+ },
243
+ // Photographing changes nothing, so a momentary transport failure is
244
+ // retried rather than costing the caller its capture.
245
+ { retryTransport: true }
246
+ );
247
+ return {
248
+ width: data.width,
249
+ height: data.height,
250
+ orientation: data.orientation,
251
+ views: data.views,
252
+ contactSheet: data.contactSheet
253
+ };
254
+ }
255
+ /**
256
+ * A caller-defined labeled shot set (`live`'s `assetPreviewShots`): the
257
+ * DEFINITION travels with the command (see `AssetPreviewShotSetDefinition`),
258
+ * and the editor's generic
259
+ * capture engine renders it — see the 3D integration's
260
+ * `captureShotSetAssetPreview`. Throws (via `command`'s `{ok:false}`
261
+ * unwrap) with a clear message naming the missing joint(s) when the asset
262
+ * lacks a bone the definition requires.
263
+ */
264
+ async captureShotSetPreview(source, definition, options = {}) {
265
+ const data = await this.command(
266
+ {
267
+ type: "capture-asset-preview",
268
+ ...source,
269
+ ...options,
270
+ shotSet: definition
271
+ },
272
+ { retryTransport: true }
273
+ );
274
+ return {
275
+ width: data.width,
276
+ height: data.height,
277
+ shots: data.shots,
278
+ warnings: data.warnings,
279
+ contactSheet: data.contactSheet
280
+ };
281
+ }
282
+ /**
283
+ * Score the asset against a reference GLB (the editor's `screenshot
284
+ * <model.glb> --compare <ref.glb>` command): matched orthographic front + side silhouettes
285
+ * (equal-height bounding-box framing, both yaw-normalized to face the
286
+ * camera), per-view IoU numbers, and overlay evidence images. The
287
+ * reference GLB's raw bytes travel base64 in the command; the editor
288
+ * renders and scores — the SDK never interprets 3D assets itself.
289
+ */
290
+ async captureAssetComparePreview(source, refGlbBase64, options = {}) {
291
+ const { refForward, ...dimensions } = options;
292
+ const data = await this.command({
293
+ type: "capture-asset-preview",
294
+ ...source,
295
+ ...dimensions,
296
+ compare: { glbBase64: refGlbBase64, ...refForward ? { forward: refForward } : {} }
297
+ });
298
+ return { width: data.width, height: data.height, views: data.views };
299
+ }
300
+ // --- Panels ---
301
+ /** Focus a static workspace panel by the key the editor's panel registry
302
+ * holds; an unknown key refuses naming the keys it does hold. */
303
+ async showPanel(panel) {
304
+ await this.command({ type: "show-panel", panel });
305
+ }
306
+ /** Show several instances of the running project split-screen — multiplayer
307
+ * authoring. Pass a total `count` (default "Player N" labels) or an array of
308
+ * `names` (its length is the count; index 0 is the primary). Requires a live
309
+ * play session. */
310
+ async setInstanceCount(countOrNames) {
311
+ await this.command(
312
+ Array.isArray(countOrNames) ? { type: "set-instance-count", names: countOrNames } : { type: "set-instance-count", count: countOrNames }
313
+ );
314
+ }
315
+ async openAsset(path, kind) {
316
+ await this.command({ type: "open-asset-tab", path, kind });
317
+ }
318
+ /** SELECT a project asset — the other half of the browser's
319
+ * selection-vs-open contract (single click selects and fills the
320
+ * Inspector; double click opens a document). */
321
+ async selectAsset(path) {
322
+ await this.command({ type: "select-asset", path });
323
+ }
324
+ async closeAsset(key) {
325
+ await this.command({ type: "close-asset-tab", key });
326
+ }
327
+ async toggleCommandPalette() {
328
+ await this.command({ type: "toggle-command-palette" });
329
+ }
330
+ async toggleConsole() {
331
+ await this.command({ type: "toggle-console" });
332
+ }
333
+ /** Switch the editor's NAMED WORKSPACE — the task-named layout memory
334
+ * (`design`/`animate`/`look`, or one a package contributes). Resolves once the
335
+ * dock has finished rebuilding, so a following capture photographs the
336
+ * arrangement that was asked for. */
337
+ async setWorkspace(workspace) {
338
+ await this.command({ type: "set-workspace", workspace });
339
+ }
340
+ /** Choose a STYLE (`volter.style`) — its material, icon set and type, and the colour theme it
341
+ * was drawn with (`classic`/`glass`/`maya`/`substance`, or one a package the project or
342
+ * product composes carries). */
343
+ async setStyle(style) {
344
+ await this.command({ type: "set-style", style });
345
+ }
346
+ /** A view's presentation, answered resolved. With a LAYER the person's choice is recorded
347
+ * first. */
348
+ async viewportPresentation(documentId, layer) {
349
+ return this.command({
350
+ type: "viewport-presentation",
351
+ documentId,
352
+ ...layer ? { layer } : {}
353
+ });
354
+ }
355
+ async showBuild() {
356
+ await this.command({ type: "show-build" });
357
+ }
358
+ /** Atomically present a durable editor view and return its shareable URL. */
359
+ async present(view) {
360
+ const presented = await this.command({ type: "present-view", view });
361
+ return { view: presented.view, url: presented.url, warnings: presented.warnings };
362
+ }
363
+ async inspect() {
364
+ const data = await this.command({ type: "inspect" });
365
+ return data.subject;
366
+ }
367
+ /** Run one verb exposed by the active Inspector subject, by its id. */
368
+ async runInspectionAction(actionId) {
369
+ const data = await this.command({
370
+ type: "run-inspection-action",
371
+ actionId
372
+ });
373
+ return data.subject;
374
+ }
375
+ /**
376
+ * Run ONE command by id — the door to everything the command palette lists.
377
+ *
378
+ * Under the Code-OSS frame this is the workbench's own `ICommandService`, so
379
+ * any command id works: a view's `volter.<view>.<verb>`, an editor action's
380
+ * `volter.action.<id>`, or one of VS Code's own. A call made before the
381
+ * command service lands waits for it.
382
+ *
383
+ * The result is whatever the command answered — a view verb's state, or
384
+ * `null` for a command that returns nothing.
385
+ */
386
+ async runCommand(commandId, args) {
387
+ const data = await this.command({
388
+ type: "run-command",
389
+ commandId,
390
+ ...args === void 0 ? {} : { args }
391
+ });
392
+ return data.result;
393
+ }
394
+ /**
395
+ * One STRUCTURE op on the authored tree — the hierarchy context menu's own
396
+ * verbs, on the same helpers, for a caller with no pointer to right-click
397
+ * with. `id`/`ids` default to the current selection.
398
+ */
399
+ async structureOp(op, options = {}) {
400
+ return this.command({ type: "structure-op", op, ...options });
401
+ }
402
+ /** "Extract Component…" — the hierarchy row's action, as a command. Answers
403
+ * the action's own sentence, which NAMES the files it created. */
404
+ async extractComponent(options = {}) {
405
+ return this.command({ type: "extract-component", ...options });
406
+ }
407
+ /** "Fork Component…" — extract's twin: one new file, one callsite retargeted. */
408
+ async forkComponent(options = {}) {
409
+ return this.command({ type: "fork-component", ...options });
410
+ }
411
+ /**
412
+ * The HIERARCHY PANEL's actual rendered row tree, as data.
413
+ *
414
+ * The panel projects component marks, document promotion, child caps,
415
+ * collapse state, search and selection scope onto the adapter tree.
416
+ *
417
+ * Deliberately NOT `status().entities`, which walks the raw adapter tree and
418
+ * therefore answers a different question: a panel defect is invisible in it.
419
+ *
420
+ * Each row reports the projected child count and whether the panel draws a disclosure control.
421
+ *
422
+ * Rejects, naming the panel, when no hierarchy panel is mounted: an empty
423
+ * tree would be a fabricated answer about a surface nobody is being shown.
424
+ */
425
+ async hierarchy() {
426
+ const data = await this.command({ type: "hierarchy" });
427
+ return data.hierarchy;
428
+ }
429
+ /** Run the Hierarchy panel's own Expand All action. */
430
+ async expandHierarchyAll() {
431
+ await this.command({ type: "expand-hierarchy-all" });
432
+ }
433
+ /** Run the Hierarchy panel's own Collapse All action — Expand All's other
434
+ * half, and the only way back to the tree's rest state through the product
435
+ * (see `HierarchyPanelSnapshot.collapseAll`). */
436
+ async collapseHierarchyAll() {
437
+ await this.command({ type: "collapse-hierarchy-all" });
438
+ }
439
+ /**
440
+ * Write one editable path through the active Inspector's own IO.
441
+ *
442
+ * The answer carries `write` as well as the subject, because an ack alone
443
+ * cannot be believed: a write with no persistence route open succeeds and
444
+ * changes no byte, and `write.persisted` is how the caller tells the two
445
+ * apart without diffing the tree (`InspectedWriteDestination` in `types.ts`).
446
+ */
447
+ async setInspectionField(path, value) {
448
+ const data = await this.command({
449
+ type: "set-inspection-field",
450
+ path,
451
+ value
452
+ });
453
+ return { subject: data.subject, write: data.write };
454
+ }
455
+ /**
456
+ * REMOVE one editable path's authored override — the other half of the write
457
+ * door, and the only one that can express byte-ABSENCE.
458
+ *
459
+ * {@link setInspectionField} writes a VALUE, so reverting a property an
460
+ * authoring gesture ADDED puts the default back EXPLICITLY and leaves the
461
+ * source one attribute heavier than it started. This drops the property, so
462
+ * whatever governs it in its absence takes over — the same `io.remove` the
463
+ * Inspector's revert arrow calls, the same persistence pipe, the same awaited
464
+ * `{ destination, persisted }` ack.
465
+ *
466
+ * Rejects with `code: 'REMOVAL_UNAVAILABLE'` when the field does not declare
467
+ * itself removable or the lane implements no removal door. That refusal is a
468
+ * MISSING SEAM, not a failed removal, and it is coded rather than phrased
469
+ * precisely so a caller can grade the two differently.
470
+ */
471
+ async removeInspectionField(path) {
472
+ const data = await this.command({
473
+ type: "remove-inspection-field",
474
+ path
475
+ });
476
+ return { subject: data.subject, write: data.write };
477
+ }
478
+ /**
479
+ * OPEN one piece of the adapter's SCENE TABLE by id — a scene, a prefab, or
480
+ * a story state, because the table makes them siblings (they differ only in
481
+ * instance site). The ids are exactly what `getState().adapter.scenes.entries`
482
+ * reports, so the table is both the menu and the address space. It opens
483
+ * the entry's document in Play as in Edit.
484
+ *
485
+ * Rejects with a coded reason rather than prose: `SCENE_NOT_FOUND` (and it
486
+ * names the ids that DO exist), `SCENE_NOT_OPENABLE` carrying the reason an
487
+ * entry owns no document (the adapter's own, when it declared one),
488
+ * `SCENE_TABLE_UNAVAILABLE` before the adapter has loaded, and
489
+ * `SCENE_DOCUMENT_NOT_MOUNTED` when the host has no document for a piece the
490
+ * table says is openable — a host gap, not a table statement.
491
+ */
492
+ async open(id) {
493
+ return this.command({ type: "open", id });
494
+ }
495
+ /**
496
+ * Undo / redo one project transaction — the same queue the keyboard shortcut
497
+ * drives. `moved` is false when there was nothing left in that direction.
498
+ */
499
+ async undo() {
500
+ return this.command({ type: "undo" });
501
+ }
502
+ async redo() {
503
+ return this.command({ type: "redo" });
504
+ }
505
+ /** Read the editor's actual current durable projection. */
506
+ async currentView() {
507
+ const data = await this.command({ type: "current-view" });
508
+ return data.view;
509
+ }
510
+ /** Photograph the editor PAGE itself — every panel as the person sees it, at
511
+ * `scale` output pixels per CSS pixel (default `devicePixelRatio`), which is
512
+ * what a 1 px border or a glyph edge is judged through. */
513
+ async captureEditorChrome(options) {
514
+ return this.command({
515
+ type: "capture-editor-chrome",
516
+ ...options?.scale === void 0 ? {} : { scale: options.scale },
517
+ ...options?.region === void 0 ? {} : { region: options.region },
518
+ ...options?.name === void 0 ? {} : { name: options.name }
519
+ });
520
+ }
521
+ /** Capture the active document's render, excluding editor chrome. A number
522
+ * requests a square; `{width, height}` requests that shape, bounded by
523
+ * {@link CaptureDimensions}. With a view, present and capture it in one request
524
+ * so discovery cannot retarget the capture between client calls. */
525
+ async captureActiveDocument(size, view) {
526
+ return this.command({
527
+ type: "capture-active-document",
528
+ ...view ? { view } : {},
529
+ ...typeof size === "number" ? { size } : {},
530
+ ...typeof size === "object" && size !== null ? { width: size.width, height: size.height } : {}
531
+ });
532
+ }
533
+ /**
534
+ * Read or drive the ACTIVE center document's own DOM — the scoped
535
+ * editor-chrome door, and the read/gesture half of the same subject
536
+ * {@link captureActiveDocument} photographs. NOT play-mode gated, and NOT
537
+ * page automation: a target outside the active document's container is
538
+ * refused by name. Design and scope contract:
539
+ * `packages/sdk/src/editor-document-probe.ts`.
540
+ */
541
+ async documentProbe(step) {
542
+ return this.command({ type: "document-probe", step });
543
+ }
544
+ /**
545
+ * A verb a package contributes: the Code-OSS command `volter.<type>` its page half registers,
546
+ * waited on as long as its declaration says (`contributes.commands`' `volter.timeoutMs`, which
547
+ * the relay reads from the same manifest), so a package's long verb (a modeling chunk, minutes
548
+ * under Wasm) has its budget in one place. A verb no package declares is refused by name
549
+ * before anything is sent.
550
+ */
551
+ async contributedVerb(type, fields = {}) {
552
+ const id = verbCommandId(type);
553
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/commands/${encodeURIComponent(id)}`);
554
+ const declared = await this.readJson(res);
555
+ if (!res.ok) throw new Error(declared.error ?? `No package declares the command "${id}" (${res.status}).`);
556
+ return this.command({ type, ...fields }, { deadlineMs: declared.timeoutMs + 3e4 });
557
+ }
558
+ /**
559
+ * Run a wire-carried step against the ACTIVE document's published context
560
+ * (`packages/editor-core/src/document-context-registry.ts`) — the REPL door over
561
+ * an open document. `src` is JavaScript source evaluated in the page; references to the caller's
562
+ * closures are unavailable.
563
+ */
564
+ async documentScript(src) {
565
+ const outcome = await this.command({ type: "document-script", src });
566
+ return outcome.result;
567
+ }
568
+ // --- Display (set semantics) ---
569
+ async setGrid(enabled) {
570
+ await this.command({ type: "set-grid", enabled });
571
+ }
572
+ async setHelpers(enabled) {
573
+ await this.command({ type: "set-helpers", enabled });
574
+ }
575
+ async setStats(enabled) {
576
+ await this.command({ type: "set-stats", enabled });
577
+ }
578
+ async setShadingMode(mode) {
579
+ await this.command({ type: "set-shading-mode", mode });
580
+ }
581
+ async setHelperType(helperType, enabled) {
582
+ await this.command({ type: "set-helper-type", helperType, enabled });
583
+ }
584
+ // --- Transform tools (set semantics) ---
585
+ async setTransformMode(mode) {
586
+ await this.command({ type: "set-transform-mode", mode });
587
+ }
588
+ async setTransformSpace(space) {
589
+ await this.command({ type: "set-transform-space", space });
590
+ }
591
+ async setSnap(enabled) {
592
+ await this.command({ type: "set-snap", enabled });
593
+ }
594
+ // --- Project management ---
595
+ /** A new project from one of the running product's templates or examples (its `/__editor/templates`), or its default
596
+ * where neither is named. */
597
+ async createProject(name, location, from = {}) {
598
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/create-project`, {
599
+ method: "POST",
600
+ headers: { "Content-Type": "application/json" },
601
+ body: JSON.stringify({ name, location, ...from })
602
+ });
603
+ const data = await this.readJson(res);
604
+ if (!res.ok) {
605
+ throw new Error(data.error ?? `Create project failed: ${res.status}`);
606
+ }
607
+ return { path: data.path, config: data.config };
608
+ }
609
+ async openProject(path) {
610
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/open-project`, {
611
+ method: "POST",
612
+ headers: { "Content-Type": "application/json" },
613
+ body: JSON.stringify({ path })
614
+ });
615
+ const body = await this.readJson(res);
616
+ if (!res.ok) {
617
+ throw new Error(body.error ?? `Open project failed: ${res.status}`);
618
+ }
619
+ }
620
+ async getProject() {
621
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/project`);
622
+ const data = await this.readJson(res);
623
+ if (!res.ok) throw new Error(data.error ?? `Failed to get project: ${res.status}`);
624
+ return data.project;
625
+ }
626
+ async listRecentProjects() {
627
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/recent-projects`);
628
+ const data = await this.readJson(res);
629
+ if (!res.ok) throw new Error(data.error ?? `Failed to list projects: ${res.status}`);
630
+ return data.projects;
631
+ }
632
+ // --- State ---
633
+ /**
634
+ * The document table the host resolved — every scene, prefab, page, model,
635
+ * shot, take … the project's finders produced (`getState().adapter.scenes`
636
+ * is the same projection). A command, so it answers wherever the control
637
+ * channel reaches, not only where `/__editor/state` is served.
638
+ */
639
+ async documentTable() {
640
+ return this.command({ type: "document-table" });
641
+ }
642
+ async reloadPage() {
643
+ const timeoutMs = 6e4;
644
+ const epochs = async () => {
645
+ const { tabs } = await this.getState().catch(() => ({ tabs: [] }));
646
+ return new Map(tabs.map((tab) => [tab.tabId8, tab]));
647
+ };
648
+ const pause = () => new Promise((settle) => setTimeout(settle, 250));
649
+ const before = await epochs();
650
+ const deadline = Date.now() + timeoutMs;
651
+ await this.command({ type: "page-reload" });
652
+ let reloaded = [];
653
+ while (reloaded.length === 0) {
654
+ if (Date.now() > deadline) throw new Error("The reload was ordered and no tab of this session reported a new page load.");
655
+ await pause();
656
+ const now = await epochs();
657
+ reloaded = [...now].filter(([tabId, tab]) => tab.epochCount > (before.get(tabId)?.epochCount ?? 0)).map(([tabId]) => tabId);
658
+ }
659
+ for (; ; ) {
660
+ if (Date.now() > deadline) throw new Error("A new page loaded, but its command listener did not become ready.");
661
+ const now = await epochs();
662
+ if (reloaded.some((tabId) => {
663
+ const listener = now.get(tabId)?.commandListener;
664
+ return typeof listener !== "string" || listener === "ready";
665
+ })) break;
666
+ await pause();
667
+ }
668
+ for (; ; ) {
669
+ try {
670
+ await this.command({ type: "current-view" });
671
+ return;
672
+ } catch (error) {
673
+ if (Date.now() > deadline) throw error;
674
+ await pause();
675
+ }
676
+ }
677
+ }
678
+ async getState() {
679
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/state`);
680
+ const state = await this.readJson(res);
681
+ if (!res.ok) throw new Error(`Failed to get editor state: ${res.status} ${res.statusText}`);
682
+ return state;
683
+ }
684
+ /**
685
+ * The complete unresolved console set the session is holding right now.
686
+ *
687
+ * The console endpoint returns named conditions; command envelopes carry
688
+ * only their counts. The request has a bounded header deadline.
689
+ */
690
+ async getUnresolvedConsole(opts) {
691
+ const res = await this.httpFetch(
692
+ `${this.baseUrl}/__editor/console${opts?.all === true ? "?all=1" : ""}`,
693
+ { signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS) }
694
+ );
695
+ if (!res.ok) {
696
+ throw new Error(`Failed to read unresolved console: ${res.status}`);
697
+ }
698
+ return await this.readJson(res);
699
+ }
700
+ /** Acknowledge one named console condition. */
701
+ async acknowledgeConsole(input) {
702
+ const res = await this.httpFetch(`${this.baseUrl}/__editor/console/ack`, {
703
+ method: "POST",
704
+ headers: { "Content-Type": "application/json" },
705
+ body: JSON.stringify(input),
706
+ signal: AbortSignal.timeout(CONSOLE_DRAIN_TIMEOUT_MS * 4)
707
+ });
708
+ const body = await this.readJson(res);
709
+ return body;
710
+ }
711
+ /**
712
+ * `fetch` for this client's plain routes, with the ONE thing Node's `fetch`
713
+ * will not do: name why it failed.
714
+ *
715
+ * `readJson` below already owns "the server answered the wrong thing"; this
716
+ * owns "nothing answered at all", naming the real code Node buries on
717
+ * `.cause`. No retry
718
+ * here — these routes create projects, run tools and acknowledge console
719
+ * conditions, so repeating one is the caller's decision. The relayed
720
+ * `command` path above has its own opt-in retry for the read-only captures.
721
+ */
722
+ async httpFetch(url, init) {
723
+ try {
724
+ return this.fetchOverride ? await this.fetchOverride(url, init) : await dispatchFetch(url, init);
725
+ } catch (error) {
726
+ if (error?.name === "TimeoutError") throw error;
727
+ const { code, detail } = describeFetchFailure(error);
728
+ throw new EditorCommandError(
729
+ `${init?.method ?? "GET"} ${url} never reached the editor: ${code}${detail ? ` (${detail})` : ""}. Nothing answered on that port \u2014 check the terminal that started this editor session and confirm the port this client resolved.`,
730
+ code
731
+ );
732
+ }
733
+ }
734
+ /** Require a JSON response from the editor server. */
735
+ async readJson(res) {
736
+ const contentType = res.headers.get("content-type") ?? "";
737
+ if (!contentType.includes("application/json")) {
738
+ throw new Error(
739
+ `The editor at ${this.baseUrl} answered with its page fallback (${contentType || "no content-type"}) rather than JSON, so no editor server handled the request. Check that this URL is a running editor session.`
740
+ );
741
+ }
742
+ return await res.json();
743
+ }
744
+ /**
745
+ * Whether an editor browser tab is connected to the server *right now*.
746
+ * Unlike {@link getState}, this reflects live SSE connections, not cached
747
+ * state — use it to check whether commands will actually reach an editor.
748
+ */
749
+ async isConnected() {
750
+ const state = await this.getState();
751
+ return state.connected === true;
752
+ }
753
+ async waitForState(predicate, timeoutMs = 1e4) {
754
+ const start = Date.now();
755
+ while (Date.now() - start < timeoutMs) {
756
+ const state = await this.getState();
757
+ if (predicate(state)) return state;
758
+ await new Promise((r) => setTimeout(r, 100));
759
+ }
760
+ throw new Error(`waitForState timed out after ${timeoutMs}ms`);
761
+ }
762
+ };
763
+
764
+ export {
765
+ EditorCommandError,
766
+ EditorClient
767
+ };
768
+ //# sourceMappingURL=chunk-7SZ3C4CC.js.map